<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Modulis Engineering]]></title><description><![CDATA[Learn how to implement OpenTelemetry in Next.js and Node.js, configure distributed tracing, correlate logs, and deploy an OpenTelemetry Collector on Kubernetes.]]></description><link>https://modulis-engineering.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aeaf8e30eabac1451004f81/7ea43dcb-556e-46a8-87bd-001909f374e2.png</url><title>Modulis Engineering</title><link>https://modulis-engineering.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 12:54:03 GMT</lastBuildDate><atom:link href="https://modulis-engineering.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A Complete OpenTelemetry Setup for Next.js, Node.js and Kubernetes]]></title><description><![CDATA[Learn how to implement OpenTelemetry in Next.js and Node.js, configure distributed tracing, correlate logs, and deploy an Otel Collector on Kubernetes]]></description><link>https://modulis-engineering.hashnode.dev/a-complete-opentelemetry-setup-for-next-js-node-js-and-kubernetes</link><guid isPermaLink="true">https://modulis-engineering.hashnode.dev/a-complete-opentelemetry-setup-for-next-js-node-js-and-kubernetes</guid><category><![CDATA[OpenTelemetry]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Devops]]></category><dc:creator><![CDATA[Suprith Modulis]]></dc:creator><pubDate>Thu, 05 Nov 2026 11:25:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aeaf8e30eabac1451004f81/6ecd2424-9987-4de4-acf5-2f1a04aff339.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<p>Production debugging becomes complicated when your application is distributed across multiple services.</p>
<p>A request starts in Next.js, reaches a Node.js API, queries a database, and eventually returns an error. You might have logs in one tool, traces in another, and errors somewhere else.</p>
<p>The challenge is connecting all these signals to understand what actually happened.</p>
<p><strong>OpenTelemetry provides a vendor-neutral standard for collecting traces, metrics and logs across distributed applications.</strong></p>
<p>In this tutorial, we'll implement OpenTelemetry across a practical production stack:</p>
<ul>
<li>Next.js frontend</li>
<li>Node.js / Express backend</li>
<li>Distributed tracing</li>
<li>Trace-correlated logs</li>
<li>Kubernetes OpenTelemetry Collector</li>
<li>Telemetry filtering and sensitive-data scrubbing</li>
</ul>
<h2>1. Configure OpenTelemetry resource attributes</h2>
<p>Start by defining consistent service attributes.</p>
<pre><code class="language-bash">OTEL_SERVICE_NAME=checkout-api

OTEL_RESOURCE_ATTRIBUTES=service.namespace=storefront,service.version=1.42.0,deployment.environment.name=production

OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector.observability.svc.cluster.local:4318

OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
</code></pre>
<p>These attributes help identify which service generated the telemetry.</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><a href="http://service.name">service.name</a></td>
<td>Identifies the service</td>
</tr>
<tr>
<td>service.namespace</td>
<td>Groups related services</td>
</tr>
<tr>
<td>service.version</td>
<td>Identifies the deployed version</td>
</tr>
<tr>
<td><a href="http://deployment.environment.name">deployment.environment.name</a></td>
<td>Identifies production, staging, etc.</td>
</tr>
</tbody></table>
<p>A useful practice is to populate <code>service.version</code> from your Git SHA or release tag.</p>
<h2>2. Instrument Next.js</h2>
<p>Install the OpenTelemetry integration:</p>
<pre><code class="language-bash">npm install @vercel/otel @opentelemetry/api
</code></pre>
<p>Create <code>instrumentation.ts</code>:</p>
<pre><code class="language-typescript">import { registerOTel } from '@vercel/otel';

export function register() {
  registerOTel({
    serviceName: process.env.OTEL_SERVICE_NAME ?? 'web-frontend',
  });
}
</code></pre>
<p>This provides instrumentation for supported server-side Next.js operations.</p>
<p>You can also create custom spans around important application operations.</p>
<pre><code class="language-typescript">import { trace } from '@opentelemetry/api';

const tracer = trace.getTracer('web-frontend');

export async function getRecommendations(userId: string) {
  return tracer.startActiveSpan(
    'getRecommendations',
    async (span) =&gt; {
      try {
        span.setAttribute('app.recommendation.source', 'ml-service');

        return await fetchRecommendations(userId);
      } catch (error) {
        span.recordException(error as Error);
        throw error;
      } finally {
        span.end();
      }
    }
  );
}
</code></pre>
<p>Avoid putting sensitive user information into span attributes.</p>
<h2>3. Instrument Node.js and Express</h2>
<p>Install the SDK and auto-instrumentation packages:</p>
<pre><code class="language-bash">npm install \
  @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/sdk-metrics
</code></pre>
<p>Create <code>instrumentation.js</code>:</p>
<pre><code class="language-javascript">const { NodeSDK } = require('@opentelemetry/sdk-node');

const {
  getNodeAutoInstrumentations
} = require('@opentelemetry/auto-instrumentations-node');

const {
  OTLPTraceExporter
} = require('@opentelemetry/exporter-trace-otlp-http');

const {
  OTLPMetricExporter
} = require('@opentelemetry/exporter-metrics-otlp-http');

const {
  PeriodicExportingMetricReader
} = require('@opentelemetry/sdk-metrics');

const sdk = new NodeSDK({
  traceExporter: new OTLPTraceExporter(),

  metricReader: new PeriodicExportingMetricReader({
    exporter: new OTLPMetricExporter(),
    exportIntervalMillis: 30000,
  }),

  instrumentations: [
    getNodeAutoInstrumentations({
      '@opentelemetry/instrumentation-fs': {
        enabled: false,
      },
    }),
  ],
});

sdk.start();

process.on('SIGTERM', () =&gt; {
  sdk.shutdown().finally(() =&gt; process.exit(0));
});
</code></pre>
<h3>Load instrumentation before your application</h3>
<p>This is critical for auto-instrumentation.</p>
<p>Start your application using:</p>
<pre><code class="language-bash">node --require ./instrumentation.js server.js
</code></pre>
<p>For Docker:</p>
<pre><code class="language-dockerfile">CMD ["node", "--require", "./instrumentation.js", "server.js"]
</code></pre>
<p>If instrumentation loads after application modules, some libraries might not be instrumented correctly.</p>
<h2>4. Correlate logs with traces</h2>
<p>Traces explain the execution path. Logs provide additional application context.</p>
<p>The goal is to include <code>trace_id</code> and <code>span_id</code> in relevant log records.</p>
<p>For example:</p>
<pre><code class="language-json">{
  "msg": "checkout started",
  "cartSize": 3,
  "trace_id": "4bf92f...",
  "span_id": "00f067..."
}
</code></pre>
<p>You can collect application logs through stdout using a log collector or export them directly using OTLP.</p>
<p>The important part is preserving the active trace context.</p>
<h2>5. Deploy the OpenTelemetry Collector on Kubernetes</h2>
<p>Instead of sending telemetry directly from every application to your backend, introduce an OpenTelemetry Collector.</p>
<p>It provides a central place to:</p>
<ul>
<li>Filter unnecessary telemetry.</li>
<li>Remove sensitive attributes.</li>
<li>Batch records.</li>
<li>Manage exporters.</li>
<li>Apply consistent processing policies.</li>
</ul>
<p>Example Collector configuration:</p>
<pre><code class="language-yaml">receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25

  filter/healthchecks:
    error_mode: ignore
    traces:
      span:
        - 'attributes["url.path"] == "/healthz"'
        - 'attributes["url.path"] == "/readyz"'

  attributes/scrub:
    actions:
      - key: http.request.header.authorization
        action: delete
      - key: http.request.header.cookie
        action: delete
      - key: user.email
        action: hash

  batch:
    send_batch_size: 8192
    timeout: 5s

exporters:
  otlphttp:
    endpoint: https://YOUR-OTLP-BACKEND:4318

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors:
        - memory_limiter
        - filter/healthchecks
        - attributes/scrub
        - batch
      exporters: [otlphttp]

    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlphttp]

    logs:
      receivers: [otlp]
      processors: [memory_limiter, attributes/scrub, batch]
      exporters: [otlphttp]
</code></pre>
<p>Replace the exporter endpoint with your backend's actual OTLP endpoint.</p>
<p>For production, validate the filtering rules and scrubbing policies against your actual telemetry schema.</p>
<h2>6. Verify your setup</h2>
<p>Generate application traffic and verify:</p>
<ul>
<li>Every service has the correct <code>service.name</code>.</li>
<li>Frontend and backend requests appear in distributed traces.</li>
<li>Logs contain the corresponding trace IDs.</li>
<li>Health-check spans are filtered.</li>
<li>Sensitive headers aren't exported.</li>
<li>SDKs flush telemetry during graceful shutdown.</li>
</ul>
<p>Common issues:</p>
<table>
<thead>
<tr>
<th>Problem</th>
<th>What to check</th>
</tr>
</thead>
<tbody><tr>
<td><code>unknown_service</code></td>
<td>Service name configuration</td>
</tr>
<tr>
<td>Missing Express spans</td>
<td>Instrumentation initialization order</td>
</tr>
<tr>
<td>Broken distributed traces</td>
<td>Trace context propagation</td>
</tr>
<tr>
<td>High telemetry volume</td>
<td>Cardinality and filtering</td>
</tr>
<tr>
<td>Missing shutdown telemetry</td>
<td>SDK shutdown handling</td>
</tr>
</tbody></table>
<h2>7. From telemetry collection to incident resolution</h2>
<p>Collecting telemetry is only the first step.</p>
<p>When a production incident occurs, engineers still need to correlate errors, logs, traces, user sessions and deployment changes to identify the root cause.</p>
<p>This is the problem Modulis is designed to address.</p>
<p><a href="https://modulis.ai">Modulis</a> is an AI-powered engineering intelligence and incident-resolution platform.</p>
<p>It:</p>
<ul>
<li>Correlates logs, traces, errors, sessions and Git context.</li>
<li>Runs inside the customer's own cloud.</li>
<li>Uses the customer's own AI model key.</li>
<li>Identifies likely root causes.</li>
<li>Opens PR-ready fixes for engineers to review and merge.</li>
</ul>
<p>Rather than just showing another dashboard, the goal is to move from production incident to a reviewable fix.</p>
<p>You can explore Modulis at <a href="https://modulis.ai">https://modulis.ai</a>.</p>
<h2>Conclusion</h2>
<p>OpenTelemetry gives engineering teams a consistent foundation for understanding distributed applications.</p>
<p>For teams running Next.js, Node.js and Kubernetes, start with consistent resource attributes, instrument your applications, correlate logs and traces, and introduce a Collector to manage telemetry.</p>
<p>The next challenge is making all that collected information useful when production breaks.</p>
<p>That's where engineering intelligence and automated incident resolution become important.</p>
<p><strong>Original guide:</strong> <a href="https://modulis.ai/blog/opentelemetry-nextjs-nodejs-setup-guide/">https://modulis.ai/blog/opentelemetry-nextjs-nodejs-setup-guide/</a></p>
]]></content:encoded></item></channel></rss>