Skip to content

@opentelemetry/instrumentation-aws-sdk doesn't work with NextJS #2734

Open
@alexandr2110pro

Description

@alexandr2110pro

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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions