CtrlK
BlogDocsLog inGet started
Tessl Logo

kopai/otel-instrumentation

Instrument applications with the OpenTelemetry SDK and prove the telemetry is good by validating it against a local Kopai backend. Use when setting up observability, adding tracing/logging/metrics, deciding what to instrument or which attributes to add, retrofitting OTel into an existing codebase, threading context through call chains, configuring sampling, or when traces/logs/metrics aren't appearing after setup. Also use when users say things like "my traces aren't showing up", "I don't see any data", or "how do I add observability to my app". Do NOT use to investigate existing telemetry for a root cause (use root-cause-analysis), to build dashboards (use create-dashboard), or to instrument LLM and agent calls (use otel-genai-instrumentation).

Quality

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

lang-nextjs.mdreferences/

titleimpacttags
Next.js InstrumentationHIGHlang, nextjs, react, traces, browser, server

Next.js Instrumentation

Set up OpenTelemetry for Next.js App Router — two approaches: @vercel/otel (simple) or manual SDK (full control, browser+server).

Approach 1: @vercel/otel (Recommended Start)

Minimal setup, server-side traces only.

Install:

pnpm add @vercel/otel

src/instrumentation.ts:

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

export function register() {
  registerOTel({ serviceName: "my-nextjs-app" });
}

No next.config.ts changes needed.

Approach 2: Manual SDK (Server + Browser)

Full control over both server-side and client-side instrumentation with distributed tracing.

Install:

pnpm add @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-trace-node \
  @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base \
  @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources \
  @opentelemetry/semantic-conventions @opentelemetry/context-zone \
  @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch \
  @opentelemetry/instrumentation-document-load

next.config.ts — externalize Node SDK packages:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  serverExternalPackages: [
    "@opentelemetry/sdk-node",
    "@opentelemetry/sdk-trace-node",
  ],
};

export default nextConfig;

src/instrumentation.ts — runtime check + dynamic import:

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    await import("./instrumentation.node");
  }
}

src/instrumentation.node.ts — server-side NodeSDK:

import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-node";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";

const sdk = new NodeSDK({
  resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: "server-side",
  }),
  spanProcessor: new SimpleSpanProcessor(new OTLPTraceExporter()),
});

try {
  sdk.start();
} catch (err) {
  console.error("OTel SDK start failed", err);
}

process.on("SIGTERM", () => {
  sdk.shutdown().catch(() => process.exit(0));
});

src/app/otel-provider.tsx — browser-side WebTracerProvider (client component):

"use client";

import { useEffect, useRef } from "react";
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
import { ZoneContextManager } from "@opentelemetry/context-zone";
import { registerInstrumentations } from "@opentelemetry/instrumentation";
import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";

export default function OtelProvider({ children }: { children: React.ReactNode }) {
  const initialized = useRef(false);

  useEffect(() => {
    if (initialized.current) return;
    initialized.current = true;

    const exporter = new OTLPTraceExporter({ url: "/api/otel" });
    const provider = new WebTracerProvider({
      resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "client-side" }),
      spanProcessors: [new BatchSpanProcessor(exporter)],
    });

    provider.register({ contextManager: new ZoneContextManager() });

    registerInstrumentations({
      instrumentations: [
        new DocumentLoadInstrumentation(),
        new FetchInstrumentation({
          propagateTraceHeaderCorsUrls: [/.*/],
          ignoreUrls: [/\/api\/otel/],
        }),
      ],
    });
  }, []);

  return <>{children}</>;
}

src/app/layout.tsx — wrap app with OtelProvider:

import OtelProvider from "./otel-provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <OtelProvider>{children}</OtelProvider>
      </body>
    </html>
  );
}

src/app/api/otel/route.ts — OTLP proxy (browser can't reach collector directly due to CORS):

import { NextResponse } from "next/server";

export async function POST(request: Request) {
  const endpoint =
    process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://localhost:4318";
  const body = await request.arrayBuffer();

  const res = await fetch(`${endpoint}/v1/traces`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body,
  });

  return new NextResponse(null, { status: res.status });
}

Key Concepts

  • src/instrumentation.ts is a Next.js convention — runs at server startup
  • NEXT_RUNTIME check prevents Node SDK from loading in edge runtime
  • serverExternalPackages prevents Next.js from bundling Node-only OTel packages
  • Browser → /api/otel → collector proxy pattern avoids CORS issues
  • propagateTraceHeaderCorsUrls injects traceparent headers in fetch calls, linking browser and server spans into distributed traces
  • ignoreUrls: [/\/api\/otel/] prevents infinite loop (don't trace the trace-export request)

What Gets Instrumented

ApproachSignalDescription
@vercel/otelServer tracesHTTP spans, route handlers
Manual SDKServer tracesHTTP spans via @opentelemetry/sdk-node
Manual SDKBrowser tracesPage load + fetch spans via @opentelemetry/sdk-trace-web

Browser fetch calls inject traceparent headers, so server spans appear as children of browser spans — creating end-to-end distributed traces.

Validate

  1. Start the Kopai backend:
npx @kopai/app start
  1. Set environment and run the app:
export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
pnpm dev
  1. Generate traffic by opening http://localhost:3000 and interacting with the app.

  2. Validate traces are received:

# Search for traces from your service
npx @kopai/cli traces search --service server-side --json
npx @kopai/cli traces search --service client-side --json

# Inspect a specific trace
npx @kopai/cli traces get <trace-id>

Example

See the complete working examples:

Reference

Next

SDK setup is step 3 of six. It gets bytes flowing; it does not make the telemetry good.

  1. Decide what earns a span — instrument-spans.md
  2. Add the context that makes spans answerable — instrument-attributes.md
  3. Instrument the error paths — instrument-errors.md
  4. Drive traffic yourself — drive-traffic.md
  5. Assert on what arrived — validate-traces.md

Confirm before moving on: the SDK starts before any application code that could create a span, and shutdown flushes on SIGTERM (validate-shutdown.md). Both fail silently.

references

_sections.md

architectural-patterns.md

attributes.md

cli-reference.md

context-propagation.md

custom-instrumentation.md

drive-traffic.md

instrument-attributes.md

instrument-errors.md

instrument-spans.md

lang-cpp.md

lang-dotnet.md

lang-erlang.md

lang-fastify.md

lang-go.md

lang-java.md

lang-nextjs.md

lang-nodejs.md

lang-php.md

lang-python.md

lang-ruby.md

lang-rust.md

layered-telemetry.md

nextjs-examples.md

otel-docs.md

sampling.md

setup-backend.md

setup-environment.md

troubleshoot-missing-attrs.md

troubleshoot-missing-spans.md

troubleshoot-no-data.md

troubleshoot-wrong-port.md

validate-logs.md

validate-metrics.md

validate-shutdown.md

validate-traces.md

CHANGELOG.md

SKILL.md

tile.json