Open
Description
What version of OpenTelemetry are you using?
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.57.2",
"@opentelemetry/exporter-metrics-otlp-proto": "^0.57.2",
"@opentelemetry/exporter-trace-otlp-proto": "^0.57.2",
"@opentelemetry/instrumentation": "^0.57.2",
"@opentelemetry/instrumentation-aws-sdk": "^0.49.1",
"@opentelemetry/instrumentation-http": "^0.57.2",
"@opentelemetry/propagator-aws-xray": "^1.26.2",
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-logs": "^0.57.2",
"@opentelemetry/sdk-metrics": "^1.30.1",
"@opentelemetry/sdk-node": "^0.57.2",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
What version of Node are you using?
23
What did you do?
instrumentation.node.ts
import { DiagConsoleLogger, DiagLogLevel, diag } from '@opentelemetry/api';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { Resource } from '@opentelemetry/resources';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { NodeSDK } from '@opentelemetry/sdk-node';
import {
AlwaysOnSampler,
BatchSpanProcessor,
ParentBasedSampler,
} from '@opentelemetry/sdk-trace-node';
import { FetchInstrumentation } from '@vercel/otel';
import { NextJSSampler } from '@com/shared-otel';
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR);
const sdk = new NodeSDK({
resource: new Resource({}),
instrumentations: [
new FetchInstrumentation(),
new HttpInstrumentation(),
new AwsInstrumentation({
suppressInternalInstrumentation: true,
}),
],
spanProcessors: [
new BatchSpanProcessor(new OTLPTraceExporter(), {
exportTimeoutMillis: 15000,
}),
],
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter(),
}),
sampler: new ParentBasedSampler({
root: new NextJSSampler({
base: new AlwaysOnSampler(),
// base: new TraceIdRatioBasedSampler(0.1),
}),
}),
});
sdk.start();
instrumentation.ts
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./instrumentation.node');
}
}
Just in case, NextJSSampler
is
import type { Attributes } from '@opentelemetry/api';
import {
AlwaysOnSampler,
type Sampler,
SamplingDecision,
type SamplingResult,
} from '@opentelemetry/sdk-trace-node';
import {
ATTR_HTTP_ROUTE,
SEMATTRS_HTTP_TARGET,
} from '@opentelemetry/semantic-conventions';
export class NextJSSampler implements Sampler {
static readonly DEFAULT_IGNORE_PATTERNS = [
/^\/_next\//,
/^\/images\//,
// /\?_rsc=/,
/^\/health/,
];
private readonly baseSampler: Sampler;
private readonly ignorePatterns: RegExp[];
constructor(props: { base?: Sampler; ignorePatterns?: RegExp[] } = {}) {
this.baseSampler = props.base ?? new AlwaysOnSampler();
this.ignorePatterns =
props.ignorePatterns || NextJSSampler.DEFAULT_IGNORE_PATTERNS;
}
shouldSample(...args: Parameters<Sampler['shouldSample']>): SamplingResult {
const attributes: Attributes = args[4];
// TODO: keep an eye on https://github.com/vercel/otel/issues/143#issue-2874289175
const route =
// this is not populated at the moment of execution of the sampler
this.getNextRoute(attributes) ??
// this is not populated at the moment of execution of the sampler
this.getHttpRoute(attributes) ??
// this is populated
this.getHttpTarget(attributes);
return this.isIgnoredRoute(route)
? { decision: SamplingDecision.NOT_RECORD }
: this.baseSampler.shouldSample(...args);
}
private isIgnoredRoute(route?: string): boolean {
if (!route) return false;
return this.ignorePatterns.some(re => re.test(route));
}
private getNextRoute(attributes: Attributes): string | undefined {
const value = attributes['next.route'];
if (typeof value === 'string') return value;
return undefined;
}
private getHttpRoute(attributes: Attributes): string | undefined {
const value = attributes[ATTR_HTTP_ROUTE];
if (typeof value === 'string') return value;
return undefined;
}
private getHttpTarget(attributes: Attributes): string | undefined {
// TODO: replace SEMATTRS_HTTP_TARGET when better alternative is available
// https://github.com/vercel/otel/issues/143#issuecomment-2678223912
const value = attributes[SEMATTRS_HTTP_TARGET];
if (typeof value === 'string') return value;
return undefined;
}
toString(): string {
return 'Next JS Sampler - Exclude Health Route';
}
}
What did you expect to see?
SNS, SQS, and DynamoDB calls via AWS SDK v3 are traced alongside everything else.
What did you see instead?
SNS, SQS, and DynamoDB calls via AWS SDK v3 are not traced.
Http requests are traced, manual trace spans are recorded as expected, NexJS internally emitted spans are recorded as expected.
Additional context
I am on NextJS v15.1.7.
Tried both server actions and api route handlers for executing AWS related business logic on the backend - same result.
Tried with @vercel/otel
- passing instrumentations
array to registerOTel
fn. Same result.