opentelemetry-js icon indicating copy to clipboard operation
opentelemetry-js copied to clipboard

[node-sdk] Missing @opentelemetry/exporter-jaeger raises an error on Next.js app

Open jstlaurent opened this issue 2 years ago • 12 comments
trafficstars

What happened?

Steps to Reproduce

Create a basic Next.js app and follow the steps to manually add OpenTelemetry to the application.

Use the OpenTelemetry setup code below. Don't set any environment variables, so that a ConsoleSpanExporter gets configured.

Start the dev server (npm run dev) and launch the app.

Expected Result

I see the default OpenTelemetry spans from my request in the console.

Actual Result

I get an error message from a failed import: Can't resolve '@opentelemetry/exporter-jaeger'. See the full trace below.

Additional Details

It looks like node-sdk imports TracerProviderWithEnvExporters. As a side effect of which, a Map of registered providers is built that assumes Jaeger is available and throws an error if it isn't. Since the dependency was removed in v0.44, the error gets thrown.

Edit: Note that I'm not using Jaeger. ~I'll add the dependency to solve the error, but I would rather not have to include it at all.~ I can't include @opentelemetry/exporter-jaeger in my project because #3759 happens.

OpenTelemetry Setup Code

//instrumentation.ts
import { settings } from '@/config'
import { Span, SpanOptions, SpanStatusCode, trace } from '@opentelemetry/api'

export const tracer = trace.getTracer(settings.SERVICE_NAME)

export async function register() {
  if (settings.DISABLE_OTEL) {
    console.info('OpenTelemetry is disabled')
    return
  }

  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await import('./instrumentation.node.ts')
  }
}

//instrumentation.node.ts

import { settings } from '@/config'
import * as grpc from '@grpc/grpc-js'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
import { Resource } from '@opentelemetry/resources'
import { NodeSDK } from '@opentelemetry/sdk-node'
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
  SimpleSpanProcessor
} from '@opentelemetry/sdk-trace-node'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'

function getProcessor() {
  if (settings.HONEYCOMB_API_KEY) {
    const metadata = new grpc.Metadata()
    metadata.set('x-honeycomb-team', settings.HONEYCOMB_API_KEY)

    const exporter = new OTLPTraceExporter({
      url: 'grpc://api.honeycomb.io:443/',
      credentials: grpc.credentials.createSsl(),
      metadata
    })

    // Values from https://github.com/honeycombio/intro-to-o11y-nodejs/blob/main/src/tracing.js
    return new BatchSpanProcessor(exporter, {
      maxQueueSize: 16000,
      maxExportBatchSize: 1000,
      scheduledDelayMillis: 500
    })
  }

  console.info('Using console exporter')
  return new SimpleSpanProcessor(new ConsoleSpanExporter())
}

const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: settings.SERVICE_NAME,
    [SemanticResourceAttributes.SERVICE_NAMESPACE]: settings.PROJECT_NAME,
    [SemanticResourceAttributes.SERVICE_VERSION]:
      settings.VERCEL_GIT_COMMIT_SHA,
    [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: settings.VERCEL_ENV
  }),
  spanProcessor: getProcessor()
})

sdk.start()

package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "format": "prettier --write ."
  },
  "dependencies": {
    "next": "^14.0.2",
    "react": "^18.2.0",
    "@opentelemetry/api": "^1.7.0",
    "@opentelemetry/exporter-trace-otlp-grpc": "^0.45.1",
    "@opentelemetry/resources": "^1.18.1",
    "@opentelemetry/sdk-node": "^0.45.1",
    "@opentelemetry/sdk-trace-node": "^1.18.1",
    "@opentelemetry/semantic-conventions": "^1.18.1",
    "tailwind-merge": "^2.0.0",
    "tailwindcss": "^3.3.5",
    "tailwindcss-animate": "^1.0.7",
  },
  "devDependencies": {
    "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
    "@tailwindcss/typography": "^0.5.10",
    "@types/node": "^20.9.0",
    "@types/react": "^18.2.37",
    "@types/react-dom": "^18.2.15",
    "@types/ws": "^8.5.9",
    "autoprefixer": "^10.4.16",
    "dotenv": "^16.3.1",
    "dotenv-cli": "^7.3.0",
    "eslint": "^8.53.0",
    "eslint-config-next": "^14.0.2",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-tailwindcss": "^3.13.0",
    "postcss": "^8.4.30",
    "prettier": "^3.1.0",
    "prettier-plugin-tailwindcss": "^0.5.7",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  },
  "postcss": {
    "plugins": {
      "tailwindcss": {},
      "autoprefixer": {}
    }
  }
}

Relevant log output

./node_modules/@opentelemetry/sdk-node/build/src/TracerProviderWithEnvExporter.js
Module not found: Can't resolve '@opentelemetry/exporter-jaeger' in '/Users/julien/projects/my-app/node_modules/@opentelemetry/sdk-node/build/src'

Import trace for requested module:
./node_modules/@opentelemetry/sdk-node/build/src/TracerProviderWithEnvExporter.js
./node_modules/@opentelemetry/sdk-node/build/src/sdk.js
./node_modules/@opentelemetry/sdk-node/build/src/index.js
./src/instrumentation.node.ts
./src/instrumentation.ts
./src/services/dataset.ts
./src/app/datasets/page.tsx

jstlaurent avatar Nov 15 '23 19:11 jstlaurent

Encountering the same issue with next.js app. As far as I understand https://github.com/open-telemetry/opentelemetry-js/pull/4214 was supposed to fix this, but we still have the issue.

 "@opentelemetry/api": "^1.7.0",
    "@opentelemetry/exporter-metrics-otlp-http": "^0.47.0",
    "@opentelemetry/exporter-trace-otlp-http": "^0.47.0",
    "@opentelemetry/instrumentation-http": "^0.47.0",
    "@opentelemetry/resources": "^1.20.0",
    "@opentelemetry/sdk-metrics": "^1.20.0",
    "@opentelemetry/sdk-node": "^0.47.0",
    "@opentelemetry/sdk-trace-node": "^1.20.0",
    "@opentelemetry/semantic-conventions": "^1.20.0",
Import trace for requested module:
../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/TracerProviderWithEnvExporter.js
../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/sdk.js
../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/index.js
 ⚠ ../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/TracerProviderWithEnvExporter.js
Module not found: Can't resolve '@opentelemetry/exporter-jaeger' in 'C:\Users\macie\Documents\src\sphere\node_modules\.pnpm\@[email protected]_@[email protected]\node_modules\@opentelemetry\sdk-node\build\src'

lewinskimaciej avatar Jan 25 '24 11:01 lewinskimaciej

I am encountering this as well. I think the core issue is that the attempt to lazy load exporter-jaeger doesn't work because Nextjs is trying to bundle the library via webpack. So it searches out the dynamic require() calls and can't find the module. If I manually add exporter-jaeger, I get back to the original problem listed in #4214 . I also tried fixing ansi-color (which is the offending module in #4214) but that still yields warnings due to other dynamic imports. I think testing this library against webpack will be necessary in order for it to work properly with Nextjs.

djboulia avatar Feb 13 '24 16:02 djboulia

UPDATE: I was able to work around these issues for my server components in nextjs by adding this to my nextjs.config.js file:

experimental: { serverComponentsExternalPackages: ['@acme/package'], },

In my case, all of the OpenTelemetry stuff was already isolated in an NPM package, so I just added that to the stanza above. If you are directly importing the OTel libraries, you may need to specify multiple modules here. My understanding is that this directive tells Next not to bundle the given library, avoiding webpack and avoiding all of the errors from dynamic loading.

djboulia avatar Feb 13 '24 20:02 djboulia

Encountering the same issue with next.js app. As far as I understand #4214 was supposed to fix this, but we still have the issue.

 "@opentelemetry/api": "^1.7.0",
    "@opentelemetry/exporter-metrics-otlp-http": "^0.47.0",
    "@opentelemetry/exporter-trace-otlp-http": "^0.47.0",
    "@opentelemetry/instrumentation-http": "^0.47.0",
    "@opentelemetry/resources": "^1.20.0",
    "@opentelemetry/sdk-metrics": "^1.20.0",
    "@opentelemetry/sdk-node": "^0.47.0",
    "@opentelemetry/sdk-trace-node": "^1.20.0",
    "@opentelemetry/semantic-conventions": "^1.20.0",
Import trace for requested module:
../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/TracerProviderWithEnvExporter.js
../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/sdk.js
../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/index.js
 ⚠ ../../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@opentelemetry/sdk-node/build/src/TracerProviderWithEnvExporter.js
Module not found: Can't resolve '@opentelemetry/exporter-jaeger' in 'C:\Users\macie\Documents\src\sphere\node_modules\.pnpm\@[email protected]_@[email protected]\node_modules\@opentelemetry\sdk-node\build\src'

I forgot to update: I managed to fix this issue by... actually following docs. https://nextjs.org/docs/app/building-your-application/optimizing/open-telemetry This part is really important:

Now you can initialize NodeSDK in your instrumentation.ts. Unlike @vercel/otel, NodeSDK is not compatible with edge runtime, so you need to make sure that you are importing them only when process.env.NEXT_RUNTIME === 'nodejs'.

What happened on my side is that I've put register function in a condition based on NEXT_RUNTIME and one other var we care about but... not the import. It's actually important to import conditionally as well, as some OTel stuff will initialize in the background the moment it's require'd, We've moved our OTel registration stuff to a separate file that we import conditionally based on runtime and it works fine now. Spans from next.js are really weird and I'm to this day not sure if I can get any useful info from them on my own but it does work.

lewinskimaciej avatar Feb 14 '24 08:02 lewinskimaciej

I tried @djboulia's approach, but it didn't work for me, unfortunately. However, since I submitted this issue, Vercel has updated their @vercel/otel library to allow for more configuration. This is what I needed to configure a Honeycomb.io exporter.

So I ended up with an hybrid of @lewinskimaciej's solution and Next's automated setup.

It looks something like this:

In instrumentation.ts:

import { settings } from '@/config'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { registerOTel } from '@vercel/otel'

export async function register() {
  if (settings.DISABLE_OTEL) {
    console.info('OpenTelemetry is disabled')
    return
  }

  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { getProcessor } = await import('./instrumentation.node')
    registerOTel({
      serviceName: settings.SERVICE_NAME,
      attributes: {
        [SemanticResourceAttributes.SERVICE_NAMESPACE]: settings.PROJECT_NAME
      },
      spanProcessors: [getProcessor()]
    })
  }
}

And in instrumentation.node.ts:

import { settings } from '@/config'
import * as grpc from '@grpc/grpc-js'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
  SimpleSpanProcessor,
  type SpanProcessor
} from '@opentelemetry/sdk-trace-base'

export function getProcessor(): SpanProcessor {
  if (settings.HONEYCOMB_API_KEY) {
    const metadata = new grpc.Metadata()
    metadata.set('x-honeycomb-team', settings.HONEYCOMB_API_KEY)

    const exporter = new OTLPTraceExporter({
      url: 'grpc://api.honeycomb.io:443/',
      credentials: grpc.credentials.createSsl(),
      metadata
    })

    // Values from https://github.com/honeycombio/intro-to-o11y-nodejs/blob/main/src/tracing.js
    return new BatchSpanProcessor(exporter, {
      maxQueueSize: 16000,
      maxExportBatchSize: 1000,
      scheduledDelayMillis: 500
    })
  }

  console.info('Using console exporter')
  return new SimpleSpanProcessor(new ConsoleSpanExporter())
}

I still need to Node-only conditional import, because grpcneeds Node API that are missing on the edge platform.

Hope that can help folks navigate this issue.

jstlaurent avatar Feb 15 '24 14:02 jstlaurent

This saved me! Thank you so much 🚀

ShubhamPalriwala avatar Mar 13 '24 13:03 ShubhamPalriwala

alternative solution, you might find it useful -

instumentation.ts:

export async function register() {
  console.log('registering instrumentation');
  const { NodeSDK } = await import('@opentelemetry/sdk-node');
  const { ConsoleSpanExporter } = await import('@opentelemetry/sdk-trace-node');
  const { getNodeAutoInstrumentations } = await import(
    '@opentelemetry/auto-instrumentations-node'
  );
  const { PeriodicExportingMetricReader, ConsoleMetricExporter } = await import(
    '@opentelemetry/sdk-metrics'
  );

  const sdk = new NodeSDK({
    traceExporter: new ConsoleSpanExporter(),
    instrumentations: [
      getNodeAutoInstrumentations({
        '@opentelemetry/instrumentation-grpc': { enabled: false },
      }),
    ],
    metricReader: new PeriodicExportingMetricReader({
      exporter: new ConsoleMetricExporter(),
    }),
  });

  sdk.start();
}

next.config.js

const { NormalModuleReplacementPlugin } = require('webpack');
const path = require('path');
const nextConfig = {
  experimental: {
    instrumentationHook: true,
  },
  webpack: (config, options) => {
    return {
      ...config,
 plugins: [
        ...config.plugins,
        new NormalModuleReplacementPlugin(
          /@opentelemetry\/exporter-jaeger/,
          path.resolve(path.join(__dirname, './polyfills.js'))
        ),
      ],
      resolve: {
        ...config.resolve,
        fallback: {
          ...config.resolve.fallback,
          stream: false,
          zlib: false,
          http: false,
          tls: false,
          net: false,
          http2: false,
          dns: false,
          os: false,
          fs: false,
          path: false,
          https: false,
        },
      },
    };
  },
};

polyfills.js:

module.exports = () => {};

ranshamay avatar Mar 26 '24 10:03 ranshamay

I tried @djboulia's approach, but it didn't work for me, unfortunately. However, since I submitted this issue, Vercel has updated their @vercel/otel library to allow for more configuration. This is what I needed to configure a Honeycomb.io exporter.

So I ended up with an hybrid of @lewinskimaciej's solution and Next's automated setup.

It looks something like this:

In instrumentation.ts:

import { settings } from '@/config'
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'
import { registerOTel } from '@vercel/otel'

export async function register() {
  if (settings.DISABLE_OTEL) {
    console.info('OpenTelemetry is disabled')
    return
  }

  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const { getProcessor } = await import('./instrumentation.node')
    registerOTel({
      serviceName: settings.SERVICE_NAME,
      attributes: {
        [SemanticResourceAttributes.SERVICE_NAMESPACE]: settings.PROJECT_NAME
      },
      spanProcessors: [getProcessor()]
    })
  }
}

And in instrumentation.node.ts:

import { settings } from '@/config'
import * as grpc from '@grpc/grpc-js'
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
import {
  BatchSpanProcessor,
  ConsoleSpanExporter,
  SimpleSpanProcessor,
  type SpanProcessor
} from '@opentelemetry/sdk-trace-base'

export function getProcessor(): SpanProcessor {
  if (settings.HONEYCOMB_API_KEY) {
    const metadata = new grpc.Metadata()
    metadata.set('x-honeycomb-team', settings.HONEYCOMB_API_KEY)

    const exporter = new OTLPTraceExporter({
      url: 'grpc://api.honeycomb.io:443/',
      credentials: grpc.credentials.createSsl(),
      metadata
    })

    // Values from https://github.com/honeycombio/intro-to-o11y-nodejs/blob/main/src/tracing.js
    return new BatchSpanProcessor(exporter, {
      maxQueueSize: 16000,
      maxExportBatchSize: 1000,
      scheduledDelayMillis: 500
    })
  }

  console.info('Using console exporter')
  return new SimpleSpanProcessor(new ConsoleSpanExporter())
}

I still need to Node-only conditional import, because grpcneeds Node API that are missing on the edge platform.

Hope that can help folks navigate this issue.

I'm not using vercel otel configuration but manual config. Is there a fix for this?

Nhollas avatar May 11 '24 09:05 Nhollas

Solution for me was to add sdk initialization with the condition, inside register function

image

SnaiNeR avatar May 21 '24 19:05 SnaiNeR

Solution for me was to add sdk initialization with the condition, inside register function

image

What does your opentelemetry file look like?

Nhollas avatar May 21 '24 20:05 Nhollas

This is how I fixed it. I just had to prevent auto-instrumentation and sdk-node from server components.

/** @type {import("next").NextConfig} */
const config = {
 reactStrictMode: true,
 experimental: {
   instrumentationHook: true,
   serverComponentsExternalPackages: [
     "@opentelemetry/auto-instrumentations-node",
     "@opentelemetry/sdk-node",
   ],
 },
}

kayandra avatar Aug 12 '24 23:08 kayandra

This is how I fixed it. I just had to prevent auto-instrumentation and sdk-node from server components.

/** @type {import("next").NextConfig} */
const config = {
 reactStrictMode: true,
 experimental: {
   instrumentationHook: true,
   serverComponentsExternalPackages: [
     "@opentelemetry/auto-instrumentations-node",
     "@opentelemetry/sdk-node",
   ],
 },
}

kayandra avatar Aug 12 '24 23:08 kayandra