Skip to content

@opentelemetry/instrumentation-nestjs-core - does not work with Microservice #2510

Open
@neilime

Description

@neilime

What version of OpenTelemetry are you using?

  • "@opentelemetry/instrumentation": "^0.54.0"
  • "@opentelemetry/instrumentation-fastify": "^0.41.0"
  • "@opentelemetry/instrumentation-http": "^0.54.0"
  • "@opentelemetry/instrumentation-nestjs-core": "^0.41.0"

What version of Node are you using?

20.18

What did you do?

Attempted to integrate @opentelemetry/instrumentation-nestjs-core with a NestJS-based microservice (https://docs.nestjs.com/microservices/basics) setup to capture distributed traces across microservice interactions.

What did you expect to see?

Expected the instrumentation to support NestJS microservices, allowing automatic trace propagation and capturing relevant metadata across service calls.

What did you see instead?

The current instrumentation does not fully support NestJS microservices, resulting in incomplete trace propagation or missing metadata across microservice interactions.

Additional context

Here is an implementation workaround:

import * as api from '@opentelemetry/api';
import {
  InstrumentationBase,
  InstrumentationConfig,
  InstrumentationNodeModuleDefinition,
  InstrumentationNodeModuleFile,
  isWrapped,
} from '@opentelemetry/instrumentation';
import type { NestFactory } from '@nestjs/core/nest-factory.js';
import {
  PACKAGE_VERSION,
} from '@opentelemetry/instrumentation-nestjs-core/build/src/version';

import {
  AttributeNames,
  NestType,
} from '@opentelemetry/instrumentation-nestjs-core/build/src/enums';

const supportedVersions = ['>=4.0.0 <11'];
const PACKAGE_NAME = '@opentelemetry/instrumentation-nestjs-microservice';

export class NestMicroserviceInstrumentation extends InstrumentationBase {
  static readonly COMPONENT = '@nestjs/core';
  static readonly COMMON_ATTRIBUTES = {
    component: NestMicroserviceInstrumentation.COMPONENT,
  };

  constructor(config: InstrumentationConfig = {}) {
    super(PACKAGE_NAME, PACKAGE_VERSION, config);
  }

  init() {
    const module = new InstrumentationNodeModuleDefinition(
      NestMicroserviceInstrumentation.COMPONENT,
      supportedVersions,
    );

    module.files.push(
      this.getNestFactoryFileInstrumentation(supportedVersions),
    );

    return module;
  }

  getNestFactoryFileInstrumentation(versions: string[]) {
    return new InstrumentationNodeModuleFile(
      '@nestjs/core/nest-factory.js',
      versions,
      (NestFactoryStatic: any, moduleVersion?: string) => {
        this.ensureWrapped(
          NestFactoryStatic.NestFactoryStatic.prototype,
          'createMicroservice',
          createWrapNestFactoryCreate(this.tracer, moduleVersion),
        );
        return NestFactoryStatic;
      },
      (NestFactoryStatic: any) => {
        this._unwrap(
          NestFactoryStatic.NestFactoryStatic.prototype,
          'createMicroservice',
        );
      },
    );
  }

  private ensureWrapped(
    obj: any,
    methodName: string,
    wrapper: (original: any) => any,
  ) {
    if (isWrapped(obj[methodName])) {
      this._unwrap(obj, methodName);
    }
    this._wrap(obj, methodName, wrapper);
  }
}

function createWrapNestFactoryCreate(
  tracer: api.Tracer,
  moduleVersion?: string,
) {
  return function wrapCreate(original: typeof NestFactory.create) {
    return function createWithTrace(
      this: typeof NestFactory,
      nestModule: any,
      /* NestMicroserviceOptions */
      ...args: any[]
    ) {
      const span = tracer.startSpan('Create Nest Microservice', {
        attributes: {
          ...NestMicroserviceInstrumentation.COMMON_ATTRIBUTES,
          [AttributeNames.TYPE]: NestType.APP_CREATION,
          [AttributeNames.VERSION]: moduleVersion,
          [AttributeNames.MODULE]: nestModule.name,
        },
      });
      const spanContext = api.trace.setSpan(api.context.active(), span);

      return api.context.with(spanContext, async () => {
        try {
          return await original.apply(this, [nestModule, ...args]);
        } catch (e: any) {
          throw addError(span, e);
        } finally {
          span.end();
        }
      });
    };
  };
}

const addError = (span: api.Span, error: Error) => {
  span.recordException(error);
  span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message });
  return error;
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature-requestup-for-grabsGood for taking. Extra help will be provided by maintainers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions