diff --git a/package-lock.json b/package-lock.json index bc3a6589c7..d774eb69d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34845,6 +34845,7 @@ "devDependencies": { "@opentelemetry/api": "^1.3.0", "@opentelemetry/context-async-hooks": "^1.8.0", + "@opentelemetry/contrib-test-utils": "^0.36.0", "@opentelemetry/sdk-trace-base": "^1.8.0", "@opentelemetry/sdk-trace-node": "^1.8.0", "@types/express": "4.17.18", @@ -43174,6 +43175,7 @@ "requires": { "@opentelemetry/api": "^1.3.0", "@opentelemetry/context-async-hooks": "^1.8.0", + "@opentelemetry/contrib-test-utils": "^0.36.0", "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.49.1", "@opentelemetry/sdk-trace-base": "^1.8.0", diff --git a/plugins/node/opentelemetry-instrumentation-express/package.json b/plugins/node/opentelemetry-instrumentation-express/package.json index cc0ccf5dbd..4593ec546c 100644 --- a/plugins/node/opentelemetry-instrumentation-express/package.json +++ b/plugins/node/opentelemetry-instrumentation-express/package.json @@ -46,6 +46,7 @@ "devDependencies": { "@opentelemetry/api": "^1.3.0", "@opentelemetry/context-async-hooks": "^1.8.0", + "@opentelemetry/contrib-test-utils": "^0.36.0", "@opentelemetry/sdk-trace-base": "^1.8.0", "@opentelemetry/sdk-trace-node": "^1.8.0", "@types/express": "4.17.18", diff --git a/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts b/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts index 80c0251815..a442ddbaa0 100644 --- a/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts +++ b/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SpanStatusCode, context, trace } from '@opentelemetry/api'; +import { SpanStatusCode, context, SpanKind, trace } from '@opentelemetry/api'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { @@ -26,6 +26,7 @@ import { AttributeNames } from '../src/enums/AttributeNames'; import { ExpressInstrumentation } from '../src'; import { createServer, httpRequest, serverWithMiddleware } from './utils'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import * as testUtils from '@opentelemetry/contrib-test-utils'; const instrumentation = new ExpressInstrumentation(); instrumentation.enable(); @@ -493,4 +494,43 @@ describe('ExpressInstrumentation', () => { ); }); }); + it('should work with ESM usage', async () => { + await testUtils.runTestFixture({ + cwd: __dirname, + argv: ['fixtures/use-express.mjs'], + env: { + NODE_OPTIONS: + '--experimental-loader=@opentelemetry/instrumentation/hook.mjs', + NODE_NO_WARNINGS: '1', + }, + checkResult: (err, stdout, stderr) => { + assert.ifError(err); + }, + checkCollector: (collector: testUtils.TestCollector) => { + // use-express.mjs creates an express app with a 'GET /post/:id' endpoint and + // a `simpleMiddleware`, then makes a single 'GET /post/0' request. We + // expect to see spans like this: + // span 'GET /post/:id' + // `- span 'middleware - query' + // `- span 'middleware - expressInit' + // `- span 'middleware - simpleMiddleware' + // `- span 'request handler - /post/:id' + const spans = collector.sortedSpans; + assert.strictEqual(spans[0].name, 'GET /post/:id'); + assert.strictEqual(spans[0].kind, SpanKind.CLIENT); + assert.strictEqual(spans[1].name, 'middleware - query'); + assert.strictEqual(spans[1].kind, SpanKind.SERVER); + assert.strictEqual(spans[1].parentSpanId, spans[0].spanId); + assert.strictEqual(spans[2].name, 'middleware - expressInit'); + assert.strictEqual(spans[2].kind, SpanKind.SERVER); + assert.strictEqual(spans[2].parentSpanId, spans[0].spanId); + assert.strictEqual(spans[3].name, 'middleware - simpleMiddleware'); + assert.strictEqual(spans[3].kind, SpanKind.SERVER); + assert.strictEqual(spans[3].parentSpanId, spans[0].spanId); + assert.strictEqual(spans[4].name, 'request handler - /post/:id'); + assert.strictEqual(spans[4].kind, SpanKind.SERVER); + assert.strictEqual(spans[4].parentSpanId, spans[0].spanId); + }, + }); + }); }); diff --git a/plugins/node/opentelemetry-instrumentation-express/test/fixtures/use-express.mjs b/plugins/node/opentelemetry-instrumentation-express/test/fixtures/use-express.mjs new file mode 100644 index 0000000000..6dd75360f2 --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-express/test/fixtures/use-express.mjs @@ -0,0 +1,67 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Use express from an ES module: +// node --experimental-loader=@opentelemetry/instrumentation/hook.mjs use-express.mjs + +import { promisify } from 'util'; +import { createTestNodeSdk } from '@opentelemetry/contrib-test-utils'; + +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import { ExpressInstrumentation } from '../../build/src/index.js'; + +const sdk = createTestNodeSdk({ + serviceName: 'use-express', + instrumentations: [ + new HttpInstrumentation(), + new ExpressInstrumentation() + ] +}) +sdk.start(); + +import express from 'express'; +import * as http from 'http'; + +const app = express(); + +app.use(async function simpleMiddleware(req, res, next) { + // Wait a short delay to ensure this "middleware - ..." span clearly starts + // before the "router - ..." span. The test rely on a start-time-based sort + // of the produced spans. If they start in the same millisecond, then tests + // can be flaky. + await promisify(setTimeout)(10); + next(); +}); + +app.get('/post/:id', (req, res) => { + res.send(`Post id: ${req.params.id}`); +}); + +const server = http.createServer(app); +await new Promise(resolve => server.listen(0, resolve)); +const port = server.address().port; + +await new Promise(resolve => { + http.get(`http://localhost:${port}/post/0`, (res) => { + res.resume(); + res.on('end', () => { + resolve(); + }); + }) +}); + +await new Promise(resolve => server.close(resolve)); +await sdk.shutdown();