About otel-nextjs-style
A coding skill for applying OpenTelemetry conventions in Next.js apps, including framework setup, route spans, logs, and LLM provider instrumentation. It guides developers toward native APIs and avoids duplicating provider telemetry.
Key features
- 01Next.js framework entrypoint setup
- 02Native route-handler span guidance
- 03LLM provider instrumentation guidance
- 04OTLP log configuration
- 05Configuration and smoke-check guidance
Use cases
- 01Set up telemetry in instrumentation.ts
- 02Add business spans to TypeScript routes
- 03Instrument supported LLM providers
- 04Configure structured OTLP logs
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ superloglabs/ skills --skill otel-nextjs-styleAfter installing, type/otel-nextjs-stylein Claude Code to confirm it loaded.
Details
From the project READMEOTel Next.js Style
For Next.js apps, prefer the framework entrypoint.
// instrumentation.ts
import { registerOTel } from "@vercel/otel";
export function register() {
registerOTel({
serviceName: "mugline-web",
});
}
Do not replace this with a custom NodeSDK bootstrap unless the repo is not a
normal Next/Vercel app or already has a custom provider that must be extended.
For JavaScript/TypeScript LLM providers, prefer provider instrumentation over
manual child spans. For Anthropic, add OpenInference in the same bootstrap and
keep call sites native. This example uses @vercel/[email protected]; if the installed
types are v1, use logRecordProcessor singular instead.
import Anthropic from "@anthropic-ai/sdk";
import { AnthropicInstrumentation } from "@arizeai/openinference-instrumentation-anthropic";
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
import { registerOTel } from "@vercel/otel";
const anthropicInstrumentation = new AnthropicInstrumentation({
traceConfig: {
hideInputs: true,
hideOutputs: true,
},
});
anthropicInstrumentation.manuallyInstrument(Anthropic);
export function register() {
registerOTel({
serviceName: "mugline-web",
instrumentations: [anthropicInstrumentation],
logRecordProcessors: [new BatchLogRecordProcessor(new OTLPLogExporter())],
});
}
Route Handlers
Use native OTel APIs where auto-instrumentation is blind.
import { withSpan } from "@superlog/otel-helpers";
const tracer = trace.getTracer("mugline.web");
const meter = metrics.getMeter("mugline.web");
const requests = meter.createCounter("mug.copy.generated");
export async function POST(request: Request) {
const tenantId = request.headers.get("x-tenant-id") ?? "tenant_demo";
return await withSpan("mug.copy.generate", async (span) => {
span.setAttribute("tenant.id", tenantId);
requests.add(1, { "tenant.id": tenantId, outcome: "success" });
return Response.json({ ok: true });
}, { tracer });
}
For TypeScript route handlers, use @superlog/otel-helpers withSpan for
bounded business spans and add @superlog/otel-helpers to package.json when it
is not already present. This is required when the package can be installed. It
keeps span lifecycle/error handling out of the handler body and avoids a large
indentation diff. Do not expand the whole route into
tracer.startActiveSpan(...) plus try / catch / finally unless the helper
cannot be added or the span has a true cross-callback lifecycle.
If a route has an LLM call and OpenInference/provider instrumentation supports
that SDK, do not wrap the provider call. Leave client.messages.create(...) /
equivalent in place and put business context on the active product span or
structured log. Do not duplicate provider/model/token attributes in route-level
spans, logs, or metrics when OpenInference already reports them. Do not calculate
LLM cost in route handlers; Superlog derives estimated cost in the UI/query layer
from OpenInference provider/model/token attributes.
For Anthropic in Next.js/ESM, keep the instrumentation instance and
manuallyInstrument(Anthropic) call at module scope so it runs once and before
route code.
Match the @vercel/otel logs option to the installed package/types:
@vercel/[email protected] uses logRecordProcessor singular, while
@vercel/[email protected] uses logRecordProcessors plural. For normal
Next.js/Vercel apps, do not guard
registerOTel(...) behind NEXT_RUNTIME; Next calls instrumentation.ts in
the appropriate runtime and @vercel/otel handles its own runtime differences.
console.info is not OTLP log export. If there is no existing logger bridge,
use @opentelemetry/api-logs for production log records. Remove pre-existing
console.* calls that duplicate the same structured OTel log event:
logger.emit({
severityNumber: SeverityNumber.INFO,
severityText: "INFO",
body: "generated mug copy",
attributes: {
"tenant.id": tenantId,
"gen_ai.provider.name": "anthropic",
"gen_ai.request.model": model,
"app.gen_ai.use_case": "web.mug_copy",
outcome: "success",
},
});
Configuration And Smoke
Use the source-level public Superlog configuration pattern from
otel-onboarding-style inside instrumentation.ts, then pass those constants
explicitly to registerOTel. The public project token is write-only and belongs
with the endpoint in the setup block, like a PostHog project token or Sentry
DSN.
const SUPERLOG_ENDPOINT = "https://intake.superlog.sh";
const SUPERLOG_PUBLIC_TOKEN = "sl_public_...";
// The token MUST be sent as the `x-api-key` header. Ingest only reads
// `x-api-key` or `Authorization: Bearer <token>`; any other header name 401s.
function superlogHeaders(token: string): Record<string, string> {
return { "x-api-key": token };
}
registerOTel({
serviceName: "mugline-web",
traceExporter: new OTLPTraceExporter({
url: `${SUPERLOG_ENDPOINT}/v1/traces`,
headers: superlogHeaders(SUPERLOG_PUBLIC_TOKEN),
}),
// …same shape for log + metric exporters
});
Smoke checks should use tools already in the repo, e.g. npm run typecheck or
npm run build, plus a real app request where practical. Do not invent fragile
inline Node scripts that import TypeScript source files directly, and do not
assume ts-node exists unless it is already installed.