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

Express doesn't get instrumented with ESM

Open kevinmarks opened this issue 1 year ago • 13 comments

What version of OpenTelemetry are you using?

"@opentelemetry/api": "^1.6.0",
"@opentelemetry/auto-instrumentations-node": "^0.40.1",

What version of Node are you using?

v16.20.1

What did you do?

Instrumented a minimal node app with honeycombio

What did you expect to see?

Traces that included the express auto instrumentation

What did you see instead?

Traces that only included http and net instrumentation

Additional context

Example project here - needs a honeycomb api key in telemetry.mjs to run https://github.com/kevinmarks/OT-testbed

kevinmarks avatar Dec 07 '23 15:12 kevinmarks

Seems you use ECMA script modules. Support is experimental and requires some extra love on how you call node. see here

Flarna avatar Dec 07 '23 15:12 Flarna

If I run it with that prefix, I get a logline after the debug output saying:

Applying patch for [email protected]
OT-testbed Listening on 8989

but no events sent to honeycomb at all. Removing that again, http events show up.

kevinmarks avatar Dec 07 '23 18:12 kevinmarks

I'm taking a look into this. The Applying patch for [email protected] log is a good sign of express instrumentation patching, but I'll report back what I find.

JamieDanielson avatar Dec 07 '23 21:12 JamieDanielson

I guess this is similar to some of our other ESM problems. I can do a very small reproducer using just the ExpressInstrumentation (not fully set up to instrument, just to show the problem) that I believe is a good indicator of the problem, and I think it's related to how modules are loaded in ESM vs CJS.

import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api';
diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG);

import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
new ExpressInstrumentation();

import express from 'express';
const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(3000, () => console.log('Example app listening on port 3000!'));

This ESM app test.mjs shows a loading error in output, with and without the loader hook

✗ node --experimental-loader=@opentelemetry/instrumentation/hook.mjs test.mjs
(node:16336) ExperimentalWarning: Custom ESM Loaders is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
@opentelemetry/api: Registered a global for diag v1.6.0.
@opentelemetry/instrumentation-express Module express has been loaded before @opentelemetry/instrumentation-express so it might not work, please initialize it before requiring express
Example app listening on port 3000!

An identical app called test.cjs that uses const and require does not have the same error

✗ node test.cjs
@opentelemetry/api: Registered a global for diag v1.6.0.
Applying patch for [email protected]
Example app listening on port 3000!

In both, the order of everything in the code is identical. This will need more time to sort out.

JamieDanielson avatar Jan 10 '24 21:01 JamieDanielson

I am encountering the same issue. I created a reduced test case before discovering this issue. Sharing in case it's helpful: https://github.com/OliverJAsh/esm-otel.

As the reduced test case demonstrates, this issue also affects the Pino instrumentation.

OliverJAsh avatar Jan 12 '24 15:01 OliverJAsh

@kevinmarks sorry for the delay on this, I had gotten stuck on it and picked up some other things in the meantime. I've come back around to it and believe I have a solution as I was able to use your example repro.

The key issue is that express is getting loaded before the instrumentation can monkey-patch it. So we can use top-level awaits available in ESM to solve this.

The main changes I made to make your app work were to make the tracing an async function with an awaited default export, and then await the import of express in the app. I also ran the start command with the loader hook.

// tracing.mjs
...
export const startTracing = async () => {
  const node_sdk = new NodeSDK({
  ...
  });

  node_sdk.start()
}
export default await startTracing;
// index.mjs
import startTracing from './tracing.mjs';
startTracing();

const expressModule = await import('express');
const express = expressModule.default;
...

Now with debug enabled, my log includes the patch logs I expect, and hitting the endpoint results in spans for http and express.

➜  OT-testbed git:(main) ✗ npm run start:mjs

> [email protected] start:mjs
> node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./index.mjs
...
@opentelemetry/instrumentation-http Applying patch for [email protected]
Applying patch for net module
Applying patch for [email protected]
OT-testbed Listening on 3000
...

Would you be able to give this a try with your app and confirm it works properly?

JamieDanielson avatar Apr 18 '24 21:04 JamieDanielson

Actually you might even be able to keep your app and tracing module as is, and instead change the start script to ensure the tracing file is loaded first. node --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import ./tracing.mjs ./index.mjs. Doing this I also got instrumentation as expected for express and http, though note I'm on Node 18.18.2.

JamieDanielson avatar May 03 '24 22:05 JamieDanielson

Doing some testing as @JamieDanielson said and this also seems to do the trick, just in case for those who want to use @opentelemetry/auto-instrumentations-node register script instead of developing it's own otel bootstrap file (the good part of this is to configure otel directly into the env vars):

node --loader @opentelemetry/instrumentation/hook.mjs --import @opentelemetry/auto-instrumentations-node/register index

Tested in node 20.12.1 with Express 4.18.2

jeffbicca avatar Jun 05 '24 19:06 jeffbicca

--experimental-loader=@opentelemetry/instrumentation/hook.mjs doesn't work with astro for example.

vchirikov avatar Jun 12 '24 07:06 vchirikov

doesn't work with astro for example.

@vchirikov If you are able to help provide more details, it would be best to open a separate issue to for getting things working with Astro. Ideally that would include a small and complete reproduction so maintainers don't have to learn how to build an astro-based app. My total guess is that astro is a web framework that is doing bundling. If so, then bundling currently breaks any auto-instrumentation because included packages are no longer loaded using require or import.

trentm avatar Jun 12 '24 15:06 trentm

Offtopic

Astro is based on vite, vite uses ESM. The first error was `SyntaxError: The requested module './bundle-full.mjs' does not provide an export named 'codeToHast'` from `shiki` and some of astro internals (`dist/cookies` as far as I can remember). It's an issue with `import-in-the-middle`. I found a workaround - hook ONLY the desired modules (`node:*` in my case), I have to create a git patch to do it (with `patch-package`), but at least it works.

Anyway, I think the usage of opentelemetry-js packages should be much easier & better documented.

vchirikov avatar Jun 12 '24 17:06 vchirikov

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 14 days.

github-actions[bot] avatar Aug 12 '24 06:08 github-actions[bot]

@vchirikov

still Offtopic

I found a workaround - hook ONLY the desired modules

I hope that OTel will get to doing that by default now that import-in-the-middle supports it via https://github.com/nodejs/import-in-the-middle/pull/146

trentm avatar Aug 12 '24 22:08 trentm