DEV Community

Modulis
Modulis

Posted on Originally published at modulis.ai AI-assisted

OpenTelemetry with Next.js, Node.js and Kubernetes: A Practical Production Setup

When a production issue occurs, finding the actual cause can be harder than fixing it.

A request might start in a Next.js frontend, pass through multiple Node.js services, hit a database, and eventually fail somewhere along the way.

You might have logs in one place, traces in another, and error reports somewhere else.

Without proper instrumentation and correlation, debugging becomes a manual exercise.

This is where OpenTelemetry (OTel) becomes useful.

OpenTelemetry provides a vendor-neutral way to collect traces, metrics, and logs from your applications. Once your applications are instrumented, you can change telemetry backends without rewriting your instrumentation.

In this guide, we'll set up OpenTelemetry for a practical production stack:

  • Next.js frontend
  • Node.js / Express backend
  • Distributed tracing
  • Logs connected to traces
  • Kubernetes OpenTelemetry Collector
  • Filtering and sensitive-data scrubbing

By the end, you'll have a foundation for understanding what's happening across your services.

1. Start with consistent service naming

Before installing any packages, establish consistent naming across your services.

OpenTelemetry uses resource attributes to describe which service is generating telemetry.

Here are four attributes worth configuring:

Attribute Example Purpose
service.name checkout-api Identifies the service
service.namespace storefront Groups related services
service.version 1.42.0 Connects errors with deployments
deployment.environment.name production Separates production and staging

Set these through environment variables:

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
Enter fullscreen mode Exit fullscreen mode

A useful practice: Set service.version to your Git SHA or release tag through your CI/CD pipeline.

When an error starts appearing after a deployment, you'll have a much easier time connecting the incident to the relevant release.

2. Instrument your Next.js application

Next.js supports OpenTelemetry through its instrumentation file.

One straightforward approach is using @vercel/otel. Despite the package name, it also works when running Next.js on your own infrastructure.

Install the dependencies:

npm install @vercel/otel @opentelemetry/api
Enter fullscreen mode Exit fullscreen mode

Create an instrumentation.ts file in your project root (or inside src/ if your application uses that directory).

import { registerOTel } from '@vercel/otel';

export function register() {
  registerOTel({
    serviceName: process.env.OTEL_SERVICE_NAME ?? 'web-frontend',
  });
}
Enter fullscreen mode Exit fullscreen mode

This provides instrumentation for:

  • Incoming requests
  • Server-side rendering
  • Route handlers
  • Server-side fetch calls

The exporter uses the standard OTEL_EXPORTER_OTLP_* environment variables configured earlier.

Add custom spans

Automatic instrumentation is useful, but sometimes you need additional context around specific business operations.

For example, consider a recommendation service:

import { trace } from '@opentelemetry/api';

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

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

        return await fetchRecommendations(userId);
      } catch (err) {
        span.recordException(err as Error);
        throw err;
      } finally {
        span.end();
      }
    }
  );
}
Enter fullscreen mode Exit fullscreen mode

This creates a custom span around the operation and records exceptions when something goes wrong.

Important: Avoid adding sensitive or high-cardinality values such as raw user IDs to span attributes unless there is a justified reason and appropriate data handling.

Also remember that server-side instrumentation and browser-side tracing are separate concerns. Edge runtime instrumentation has limitations, so consider your runtime requirements when designing the setup.

3. Instrument your Node.js / Express API

For a Node.js service, the OpenTelemetry Node SDK and auto-instrumentation packages provide a practical starting point.

They can automatically instrument supported libraries such as HTTP clients, Express, database clients, Redis and gRPC.

Install:

npm install \
  @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/exporter-trace-otlp-http \
  @opentelemetry/exporter-metrics-otlp-http \
  @opentelemetry/sdk-metrics
Enter fullscreen mode Exit fullscreen mode

Create an instrumentation.js file:

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', () => {
  sdk.shutdown().finally(() => process.exit(0));
});
Enter fullscreen mode Exit fullscreen mode

Load instrumentation before your application

This is one of the most important implementation details.

OpenTelemetry auto-instrumentation needs to initialize before the application modules load. Otherwise, it might not correctly hook into Express, database clients or other libraries.

For a CommonJS application, start your service like this:

node --require ./instrumentation.js server.js
Enter fullscreen mode Exit fullscreen mode

In your Dockerfile:

CMD ["node", "--require", "./instrumentation.js", "server.js"]
Enter fullscreen mode Exit fullscreen mode

For a zero-code approach, the auto-instrumentation package can also register itself:

node --require @opentelemetry/auto-instrumentations-node/register server.js
Enter fullscreen mode Exit fullscreen mode

If you're using ES modules ("type": "module"), check the OpenTelemetry JavaScript documentation for the appropriate ESM loader configuration.

4. Connect logs with traces

Collecting traces and logs independently is useful.

Connecting them is much more powerful.

Imagine an API request that takes several seconds to complete.

Your trace tells you which operation was slow, but your application logs might contain the actual error or business context.

The goal is to have every relevant log line carry the trace_id of the request that generated it.

For example, with Pino:

const pino = require('pino');

const logger = pino();

app.post('/checkout', async (req, res) => {
  logger.info(
    { cartSize: req.body.items.length },
    'checkout started'
  );
});
Enter fullscreen mode Exit fullscreen mode

With the appropriate OpenTelemetry log instrumentation and request context, the resulting log record can include:

{
  "msg": "checkout started",
  "cartSize": 3,
  "trace_id": "4bf92f...",
  "span_id": "00f067..."
}
Enter fullscreen mode Exit fullscreen mode

There are two practical ways to collect these logs:

  1. Continue writing logs to stdout and let a log collector, such as the OpenTelemetry Collector's filelog receiver, collect them.
  2. Export logs directly through OTLP from the SDK.

Either approach can work.

The important part is maintaining trace context so that you can move from a trace to its corresponding logs.

5. Deploy an OpenTelemetry Collector on Kubernetes

Your applications can export telemetry directly to a backend.

However, putting an OpenTelemetry Collector between your applications and the backend gives you much better control over the telemetry pipeline.

A Collector allows you to:

  • Filter noisy health-check traffic.
  • Remove sensitive attributes before export.
  • Batch telemetry.
  • Retry exports when a backend becomes temporarily unavailable.
  • Change telemetry backends without modifying every application.

For Kubernetes deployments, you can use the OpenTelemetry Helm chart or OpenTelemetry Operator.

The following is a starting configuration using the otelcol-contrib distribution.

Collector configuration

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
      - key: enduser.id
        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]
Enter fullscreen mode Exit fullscreen mode

Replace the exporter endpoint with your actual backend endpoint.

Why this configuration matters

Memory limiter

Helps protect the Collector from excessive memory usage. It should be placed early in each pipeline.

Health-check filtering

Removes /healthz and /readyz spans from the trace pipeline.

Sensitive-data scrubbing

Removes authorization and cookie headers and hashes selected user attributes before export.

Batch processing

Groups telemetry records before exporting them.

One important consideration: filtering and scrubbing policies should be reviewed against your application's actual data and security requirements.

Additional improvements

Once the basic setup is working, consider adding:

  • Tail sampling to retain important error and slow traces while sampling healthy traffic.
  • The k8sattributes processor to enrich telemetry with Kubernetes pod, namespace and deployment metadata.

These additions can help make the telemetry pipeline more useful as your infrastructure grows.

6. Verify that your instrumentation works

Don't assume that a running Collector means your instrumentation is correct.

Generate some application traffic and check the following:

  • [ ] Every service appears with its actual service.name.
  • [ ] A request through Next.js and the API appears as one distributed trace.
  • [ ] Log lines contain the same trace_id.
  • [ ] Health-check spans are filtered out.
  • [ ] Authorization headers and raw emails are not present in exported span attributes.

Common problems

Symptom Likely cause What to check
unknown_service Service name missing Set OTEL_SERVICE_NAME
No Express or database spans Instrumentation loaded too late Use --require
Traces split between services Context propagation failure Check traceparent
Increasing telemetry costs High-cardinality attributes Remove unnecessary IDs and full URLs from metric attributes
Data missing during shutdown SDK not flushed Call sdk.shutdown() on SIGTERM
Collector restarts Memory pressure Configure memory_limiter early in pipelines

If traces are breaking between services, check whether the outgoing request is using an instrumented client and whether an intermediate proxy is preserving the traceparent header.

7. What happens after OpenTelemetry is configured?

At this point, you have established the telemetry foundation:

  • Your applications emit traces and metrics.
  • Logs carry trace context.
  • The Collector filters and processes telemetry.
  • Sensitive attributes are handled before export.
  • Your backend receives correlated signals.

But collecting telemetry is only one part of production debugging.

When an incident occurs, engineers still need to connect the relevant error, trace, logs, user session and deployment context to understand what actually happened.

This is where having correlated engineering signals becomes valuable.

Connecting your OpenTelemetry setup to Modulis

Modulis is an engineering intelligence and incident-resolution platform built on OpenTelemetry.

It runs inside your own cloud and correlates production errors, traces, logs, session replays and Git metadata.

Once your application is instrumented, you can connect your Collector to the Modulis OTLP endpoint provided during setup.

Add the following resource attribute:

OTEL_RESOURCE_ATTRIBUTES=modulis.application=storefront,service.namespace=storefront,service.version=1.42.0,deployment.environment.name=production
Enter fullscreen mode Exit fullscreen mode

If modulis.application isn't configured, Modulis falls back to service.namespace.

With the telemetry connected, Modulis brings the related engineering context together. Its AI resolution workflow uses your own model key to identify likely root causes and open PR-ready fixes for your engineers to review and merge.

The deployment runs in your own cloud, and AI resolution uses your own model key.

You can explore the platform at modulis.ai.


Final thoughts

OpenTelemetry provides a solid foundation for understanding what happens across distributed applications.

For teams running Next.js, Node.js and Kubernetes, a well-configured setup can make production debugging considerably more structured.

Start with consistent resource attributes, instrument your applications, connect logs to traces, and introduce a Collector to control the telemetry pipeline.

Most importantly, verify the entire request path instead of assuming that individual services are correctly instrumented.

Collecting telemetry is the first step. Making it useful during an actual production incident is the real goal.

Original implementation guide: https://modulis.ai/blog/opentelemetry-nextjs-nodejs-setup-guide/

Top comments (0)