From 7510757aeeee47a7f0c4bb31de45be3a71bb673e Mon Sep 17 00:00:00 2001 From: Siim Kallas Date: Tue, 25 Jan 2022 12:02:09 +0200 Subject: [PATCH] feat(express): allow rewriting span names (#463) --- .../README.md | 6 + .../package.json | 2 +- .../src/instrumentation.ts | 46 ++++- .../src/types.ts | 19 ++ .../test/custom-config.test.ts | 27 +-- .../test/express.test.ts | 193 +++++++----------- .../test/hooks.test.ts | 172 ++++++++++++++++ .../test/ignore-all.test.ts | 29 +-- .../test/utils.ts | 76 +++++++ 9 files changed, 398 insertions(+), 172 deletions(-) create mode 100644 plugins/node/opentelemetry-instrumentation-express/test/hooks.test.ts create mode 100644 plugins/node/opentelemetry-instrumentation-express/test/utils.ts diff --git a/plugins/node/opentelemetry-instrumentation-express/README.md b/plugins/node/opentelemetry-instrumentation-express/README.md index ae714434dce..2eb5cf98d3f 100644 --- a/plugins/node/opentelemetry-instrumentation-express/README.md +++ b/plugins/node/opentelemetry-instrumentation-express/README.md @@ -62,6 +62,7 @@ Express instrumentation has few options available to choose from. You can set th | ------- | ---- | ------- | ----------- | | `ignoreLayers` | `IgnoreMatcher[]` | `[/^\/_internal\//]` | Ignore layers that by match. | | `ignoreLayersType`| `ExpressLayerType[]` | `['request_handler']` | Ignore layers of specified type. | +| `spanNameHook` | `SpanNameHook` | `() => 'my-span-name'` | Can be used to customize span names by returning a new name from the hook. | `ignoreLayers` accepts an array of elements of types: @@ -75,6 +76,11 @@ Express instrumentation has few options available to choose from. You can set th - `middleware`, - `request_handler` is the name for anything that's not a router or a middleware. +`spanNameHook` is invoked with 2 arguments: + +- `info: ExpressRequestInfo` containing the incoming Express.js request, the current route handler creating a span and `ExpressLayerType` - the type of the handling layer or undefined when renaming the root HTTP instrumentation span. +- `defaultName: string` - original name proposed by the instrumentation. + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/plugins/node/opentelemetry-instrumentation-express/package.json b/plugins/node/opentelemetry-instrumentation-express/package.json index 86562c65096..28e8d704c22 100644 --- a/plugins/node/opentelemetry-instrumentation-express/package.json +++ b/plugins/node/opentelemetry-instrumentation-express/package.json @@ -6,7 +6,7 @@ "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js-contrib", "scripts": { - "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts'", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", diff --git a/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts b/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts index e8a9b6199a9..37a1ea37633 100644 --- a/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts +++ b/plugins/node/opentelemetry-instrumentation-express/src/instrumentation.ts @@ -21,13 +21,14 @@ import { RPCType, } from '@opentelemetry/core'; import { trace, context, diag, SpanAttributes } from '@opentelemetry/api'; -import * as express from 'express'; +import type * as express from 'express'; import { ExpressLayer, ExpressRouter, PatchedRequest, _LAYERS_STORE_PROPERTY, ExpressInstrumentationConfig, + ExpressRequestInfo, } from './types'; import { ExpressLayerType } from './enums/ExpressLayerType'; import { AttributeNames } from './enums/AttributeNames'; @@ -58,6 +59,14 @@ export class ExpressInstrumentation extends InstrumentationBase< ); } + override setConfig(config: ExpressInstrumentationConfig = {}) { + this._config = Object.assign({}, config); + } + + override getConfig(): ExpressInstrumentationConfig { + return this._config as ExpressInstrumentationConfig; + } + init() { return [ new InstrumentationNodeModuleDefinition( @@ -202,9 +211,14 @@ export class ExpressInstrumentation extends InstrumentationBase< ExpressLayerType.REQUEST_HANDLER && rpcMetadata?.type === RPCType.HTTP ) { - rpcMetadata.span.updateName( + const name = instrumentation._getSpanName( + { + request: req, + route, + }, `${req.method} ${route.length > 0 ? route : '/'}` ); + rpcMetadata.span.updateName(name); } // verify against the config if the layer should be ignored @@ -218,7 +232,15 @@ export class ExpressInstrumentation extends InstrumentationBase< return original.apply(this, arguments); } - const span = instrumentation.tracer.startSpan(metadata.name, { + const spanName = instrumentation._getSpanName( + { + request: req, + layerType: type, + route, + }, + metadata.name + ); + const span = instrumentation.tracer.startSpan(spanName, { attributes: Object.assign(attributes, metadata.attributes), }); const startTime = hrTime(); @@ -277,4 +299,22 @@ export class ExpressInstrumentation extends InstrumentationBase< }; }); } + + _getSpanName(info: ExpressRequestInfo, defaultName: string) { + const hook = this.getConfig().spanNameHook; + + if (!(hook instanceof Function)) { + return defaultName; + } + + try { + return hook(info, defaultName) ?? defaultName; + } catch (err) { + diag.error( + 'express instrumentation: error calling span name rewrite hook', + err + ); + return defaultName; + } + } } diff --git a/plugins/node/opentelemetry-instrumentation-express/src/types.ts b/plugins/node/opentelemetry-instrumentation-express/src/types.ts index 30aa38edddf..16c0468cb37 100644 --- a/plugins/node/opentelemetry-instrumentation-express/src/types.ts +++ b/plugins/node/opentelemetry-instrumentation-express/src/types.ts @@ -70,6 +70,24 @@ export type LayerMetadata = { export type IgnoreMatcher = string | RegExp | ((name: string) => boolean); +export type ExpressRequestInfo = { + request: Request; + route: string; + /** + * If layerType is undefined, SpanNameHook is being invoked to rename the original root HTTP span. + */ + layerType?: ExpressLayerType; +}; + +export type SpanNameHook = ( + info: ExpressRequestInfo, + /** + * If no decision is taken based on RequestInfo, the default name + * supplied by the instrumentation can be used instead. + */ + defaultName: string +) => string; + /** * Options available for the Express Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-Instrumentation-express#express-Instrumentation-options)) */ @@ -78,4 +96,5 @@ export interface ExpressInstrumentationConfig extends InstrumentationConfig { ignoreLayers?: IgnoreMatcher[]; /** Ignore specific layers based on their type */ ignoreLayersType?: ExpressLayerType[]; + spanNameHook?: SpanNameHook; } diff --git a/plugins/node/opentelemetry-instrumentation-express/test/custom-config.test.ts b/plugins/node/opentelemetry-instrumentation-express/test/custom-config.test.ts index 30298da8c07..7cff1c11740 100644 --- a/plugins/node/opentelemetry-instrumentation-express/test/custom-config.test.ts +++ b/plugins/node/opentelemetry-instrumentation-express/test/custom-config.test.ts @@ -27,6 +27,7 @@ import { RPCType, setRPCMetadata } from '@opentelemetry/core'; import { ExpressLayerType } from '../src/enums/ExpressLayerType'; import { AttributeNames } from '../src/enums/AttributeNames'; import { ExpressInstrumentation, ExpressInstrumentationConfig } from '../src'; +import { createServer, httpRequest } from './utils'; const instrumentation = new ExpressInstrumentation({ ignoreLayersType: [ExpressLayerType.MIDDLEWARE], @@ -36,26 +37,6 @@ instrumentation.disable(); import * as express from 'express'; import * as http from 'http'; -import { AddressInfo } from 'net'; - -const httpRequest = { - get: (options: http.ClientRequestArgs | string) => { - return new Promise((resolve, reject) => { - return http.get(options, resp => { - let data = ''; - resp.on('data', chunk => { - data += chunk; - }); - resp.on('end', () => { - resolve(data); - }); - resp.on('error', err => { - reject(err); - }); - }); - }); - }, -}; describe('ExpressInstrumentation', () => { const provider = new NodeTracerProvider(); @@ -84,9 +65,9 @@ describe('ExpressInstrumentation', () => { beforeEach(async () => { app = express(); - server = http.createServer(app); - await new Promise(resolve => server.listen(0, resolve)); - port = (server.address() as AddressInfo).port; + const httpServer = await createServer(app); + server = httpServer.server; + port = httpServer.port; }); afterEach(() => { diff --git a/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts b/plugins/node/opentelemetry-instrumentation-express/test/express.test.ts index a0033d0bfc0..cfbe225db54 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 { context, trace, Span, Tracer } from '@opentelemetry/api'; +import { context, trace } from '@opentelemetry/api'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; import { @@ -22,9 +22,9 @@ import { SimpleSpanProcessor, } from '@opentelemetry/sdk-trace-base'; import * as assert from 'assert'; -import { setRPCMetadata, RPCType } from '@opentelemetry/core'; import { AttributeNames } from '../src/enums/AttributeNames'; import { ExpressInstrumentation } from '../src'; +import { createServer, httpRequest, serverWithMiddleware } from './utils'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; const instrumentation = new ExpressInstrumentation(); @@ -32,64 +32,6 @@ instrumentation.enable(); instrumentation.disable(); import * as express from 'express'; -import * as http from 'http'; -import { AddressInfo } from 'net'; - -const httpRequest = { - get: (options: http.ClientRequestArgs | string) => { - return new Promise((resolve, reject) => { - return http.get(options, resp => { - let data = ''; - resp.on('data', chunk => { - data += chunk; - }); - resp.on('end', () => { - resolve(data); - }); - resp.on('error', err => { - reject(err); - }); - }); - }); - }, -}; - -const serverWithMiddleware = async ( - tracer: Tracer, - rootSpan: Span, - addMiddlewares: (app: express.Express) => void -): Promise => { - const app = express(); - if (tracer) { - app.use((req, res, next) => { - const rpcMetadata = { type: RPCType.HTTP, span: rootSpan }; - return context.with( - setRPCMetadata(trace.setSpan(context.active(), rootSpan), rpcMetadata), - next - ); - }); - } - - app.use(express.json()); - - addMiddlewares(app); - - const router = express.Router(); - app.use('/toto', router); - router.get('/:id', (req, res) => { - setImmediate(() => { - res.status(200).end(req.params.id); - }); - }); - const server = http.createServer(app); - await new Promise(resolve => - server.listen(0, () => { - resolve(); - }) - ); - - return server; -}; describe('ExpressInstrumentation', () => { const provider = new NodeTracerProvider(); @@ -129,18 +71,22 @@ describe('ExpressInstrumentation', () => { const router = express.Router(); app.use('/toto', router); let finishListenerCount: number | undefined; - const server = await serverWithMiddleware(tracer, rootSpan, app => { - app.use((req, res, next) => { - res.on('finish', () => { - finishListenerCount = res.listenerCount('finish'); + const { server, port } = await serverWithMiddleware( + tracer, + rootSpan, + app => { + app.use(express.json()); + app.use((req, res, next) => { + res.on('finish', () => { + finishListenerCount = res.listenerCount('finish'); + }); + next(); }); - next(); - }); - for (let index = 0; index < 15; index++) { - app.use(customMiddleware); + for (let index = 0; index < 15; index++) { + app.use(customMiddleware); + } } - }); - const port = (server.address() as AddressInfo).port; + ); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), @@ -187,24 +133,27 @@ describe('ExpressInstrumentation', () => { it('supports sync middlewares directly responding', async () => { const rootSpan = tracer.startSpan('rootSpan'); let finishListenerCount: number | undefined; - const server = await serverWithMiddleware(tracer, rootSpan, app => { - app.use((req, res, next) => { - res.on('finish', () => { - finishListenerCount = res.listenerCount('finish'); + const { server, port } = await serverWithMiddleware( + tracer, + rootSpan, + app => { + app.use((req, res, next) => { + res.on('finish', () => { + finishListenerCount = res.listenerCount('finish'); + }); + next(); }); - next(); - }); - const syncMiddleware: express.RequestHandler = (req, res, next) => { - for (let i = 0; i < 1000000; i++) { - continue; + const syncMiddleware: express.RequestHandler = (req, res, next) => { + for (let i = 0; i < 1000000; i++) { + continue; + } + res.status(200).end('middleware'); + }; + for (let index = 0; index < 15; index++) { + app.use(syncMiddleware); } - res.status(200).end('middleware'); - }; - for (let index = 0; index < 15; index++) { - app.use(syncMiddleware); } - }); - const port = (server.address() as AddressInfo).port; + ); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), @@ -230,23 +179,26 @@ describe('ExpressInstrumentation', () => { it('supports async middlewares', async () => { const rootSpan = tracer.startSpan('rootSpan'); let finishListenerCount: number | undefined; - const server = await serverWithMiddleware(tracer, rootSpan, app => { - app.use((req, res, next) => { - res.on('finish', () => { - finishListenerCount = res.listenerCount('finish'); - }); - next(); - }); - const asyncMiddleware: express.RequestHandler = (req, res, next) => { - setTimeout(() => { + const { server, port } = await serverWithMiddleware( + tracer, + rootSpan, + app => { + app.use((req, res, next) => { + res.on('finish', () => { + finishListenerCount = res.listenerCount('finish'); + }); next(); - }, 50); - }; - for (let index = 0; index < 15; index++) { - app.use(asyncMiddleware); + }); + const asyncMiddleware: express.RequestHandler = (req, res, next) => { + setTimeout(() => { + next(); + }, 50); + }; + for (let index = 0; index < 15; index++) { + app.use(asyncMiddleware); + } } - }); - const port = (server.address() as AddressInfo).port; + ); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), @@ -272,23 +224,26 @@ describe('ExpressInstrumentation', () => { it('supports async middlewares directly responding', async () => { const rootSpan = tracer.startSpan('rootSpan'); let finishListenerCount: number | undefined; - const server = await serverWithMiddleware(tracer, rootSpan, app => { - app.use((req, res, next) => { - res.on('finish', () => { - finishListenerCount = res.listenerCount('finish'); + const { server, port } = await serverWithMiddleware( + tracer, + rootSpan, + app => { + app.use((req, res, next) => { + res.on('finish', () => { + finishListenerCount = res.listenerCount('finish'); + }); + next(); }); - next(); - }); - const asyncMiddleware: express.RequestHandler = (req, res, next) => { - setTimeout(() => { - res.status(200).end('middleware'); - }, 50); - }; - for (let index = 0; index < 15; index++) { - app.use(asyncMiddleware); + const asyncMiddleware: express.RequestHandler = (req, res, next) => { + setTimeout(() => { + res.status(200).end('middleware'); + }, 50); + }; + for (let index = 0; index < 15; index++) { + app.use(asyncMiddleware); + } } - }); - const port = (server.address() as AddressInfo).port; + ); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), @@ -323,9 +278,7 @@ describe('ExpressInstrumentation', () => { router.get('/:id', (req, res, next) => { return res.status(200).end('test'); }); - const server = http.createServer(app); - await new Promise(resolve => server.listen(0, resolve)); - const port = (server.address() as AddressInfo).port; + const { server, port } = await createServer(app); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); const res = await httpRequest.get(`http://localhost:${port}/toto/tata`); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); @@ -345,9 +298,7 @@ describe('ExpressInstrumentation', () => { } return next(); }); - const server = http.createServer(app); - await new Promise(resolve => server.listen(0, resolve)); - const port = (server.address() as AddressInfo).port; + const { server, port } = await createServer(app); assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), diff --git a/plugins/node/opentelemetry-instrumentation-express/test/hooks.test.ts b/plugins/node/opentelemetry-instrumentation-express/test/hooks.test.ts new file mode 100644 index 00000000000..7e03255cf7b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-express/test/hooks.test.ts @@ -0,0 +1,172 @@ +/* + * 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. + */ + +import { context, trace, Span } from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import { AsyncHooksContextManager } from '@opentelemetry/context-async-hooks'; +import { + InMemorySpanExporter, + SimpleSpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import * as assert from 'assert'; +import type * as http from 'http'; +import { ExpressInstrumentation } from '../src'; +import { SpanNameHook } from '../src/types'; + +const instrumentation = new ExpressInstrumentation(); +instrumentation.enable(); +instrumentation.disable(); + +import { httpRequest, serverWithMiddleware } from './utils'; + +describe('ExpressInstrumentation hooks', () => { + const provider = new NodeTracerProvider(); + const memoryExporter = new InMemorySpanExporter(); + const spanProcessor = new SimpleSpanProcessor(memoryExporter); + provider.addSpanProcessor(spanProcessor); + const tracer = provider.getTracer('default'); + const contextManager = new AsyncHooksContextManager().enable(); + + before(() => { + instrumentation.setTracerProvider(provider); + context.setGlobalContextManager(contextManager); + instrumentation.enable(); + }); + + afterEach(() => { + contextManager.disable(); + contextManager.enable(); + memoryExporter.reset(); + }); + + describe('span name hooks', () => { + let server: http.Server; + let port: number; + let rootSpan: Span; + + beforeEach(async () => { + rootSpan = tracer.startSpan('rootSpan'); + + const httpServer = await serverWithMiddleware(tracer, rootSpan, app => { + app.get('*', (req, res) => { + res.send('ok'); + }); + }); + server = httpServer.server; + port = httpServer.port; + }); + + afterEach(() => { + server.close(); + }); + + it('should rename spans', async () => { + instrumentation.setConfig({ + spanNameHook: ({ request, route, layerType }, defaultName) => { + if (layerType) { + return `hook - ${route}`; + } + + if (route === '*') { + return `parent - ${request.method} ${request.url}`; + } + + return defaultName; + }, + }); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + await httpRequest.get(`http://localhost:${port}/foo/3`); + rootSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + + assert.notStrictEqual( + spans.find(span => span.name === 'parent - GET /foo/3'), + undefined + ); + + assert.notStrictEqual( + spans.find(span => span.name === 'hook - *'), + undefined + ); + } + ); + }); + + it('should use the default name when hook throws an error', async () => { + instrumentation.setConfig({ + spanNameHook: () => { + throw new Error(); + }, + }); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + await httpRequest.get(`http://localhost:${port}/foo/3`); + rootSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + + assert.notStrictEqual( + spans.find(span => span.name === 'GET *'), + undefined + ); + + assert.notStrictEqual( + spans.find(span => span.name === 'request handler - *'), + undefined + ); + } + ); + }); + + it('should use the default name when returning undefined from hook', async () => { + const spanNameHook: SpanNameHook = () => { + return undefined as unknown as string; + }; + instrumentation.setConfig({ + spanNameHook, + }); + + await context.with( + trace.setSpan(context.active(), rootSpan), + async () => { + await httpRequest.get(`http://localhost:${port}/foo/3`); + rootSpan.end(); + + const spans = memoryExporter.getFinishedSpans(); + assert.strictEqual(spans.length, 2); + + assert.notStrictEqual( + spans.find(span => span.name === 'GET *'), + undefined + ); + + assert.notStrictEqual( + spans.find(span => span.name === 'request handler - *'), + undefined + ); + } + ); + }); + }); +}); diff --git a/plugins/node/opentelemetry-instrumentation-express/test/ignore-all.test.ts b/plugins/node/opentelemetry-instrumentation-express/test/ignore-all.test.ts index 85b24aec735..ae07b1efcfd 100644 --- a/plugins/node/opentelemetry-instrumentation-express/test/ignore-all.test.ts +++ b/plugins/node/opentelemetry-instrumentation-express/test/ignore-all.test.ts @@ -25,6 +25,7 @@ import * as assert from 'assert'; import { RPCType, setRPCMetadata } from '@opentelemetry/core'; import { AttributeNames } from '../src/enums/AttributeNames'; import { ExpressInstrumentation, ExpressLayerType } from '../src'; +import { createServer, httpRequest } from './utils'; const instrumentation = new ExpressInstrumentation({ ignoreLayersType: [ @@ -38,26 +39,6 @@ instrumentation.disable(); import * as express from 'express'; import * as http from 'http'; -import { AddressInfo } from 'net'; - -const httpRequest = { - get: (options: http.ClientRequestArgs | string) => { - return new Promise((resolve, reject) => { - return http.get(options, resp => { - let data = ''; - resp.on('data', chunk => { - data += chunk; - }); - resp.on('end', () => { - resolve(data); - }); - resp.on('error', err => { - reject(err); - }); - }); - }); - }, -}; describe('ExpressInstrumentation', () => { const provider = new NodeTracerProvider(); @@ -81,6 +62,7 @@ describe('ExpressInstrumentation', () => { describe('when route exists', () => { let server: http.Server; + let port: number; let rootSpan: Span; beforeEach(async () => { @@ -110,8 +92,9 @@ describe('ExpressInstrumentation', () => { }); }); - server = http.createServer(app); - await new Promise(resolve => server.listen(0, resolve)); + const httpServer = await createServer(app); + server = httpServer.server; + port = httpServer.port; }); afterEach(() => { @@ -119,7 +102,6 @@ describe('ExpressInstrumentation', () => { }); it('should ignore all ExpressLayerType based on config', async () => { - const port = (server.address() as AddressInfo).port; assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), @@ -145,7 +127,6 @@ describe('ExpressInstrumentation', () => { }); it('root span name should be modified to GET /todo/:id', async () => { - const port = (server.address() as AddressInfo).port; assert.strictEqual(memoryExporter.getFinishedSpans().length, 0); await context.with( trace.setSpan(context.active(), rootSpan), diff --git a/plugins/node/opentelemetry-instrumentation-express/test/utils.ts b/plugins/node/opentelemetry-instrumentation-express/test/utils.ts new file mode 100644 index 00000000000..92a3313e62b --- /dev/null +++ b/plugins/node/opentelemetry-instrumentation-express/test/utils.ts @@ -0,0 +1,76 @@ +/* + * 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. + */ + +import { context, trace, Span, Tracer } from '@opentelemetry/api'; +import { setRPCMetadata, RPCType } from '@opentelemetry/core'; +import * as http from 'http'; +import type { AddressInfo } from 'net'; +import * as express from 'express'; + +export const httpRequest = { + get: (options: http.ClientRequestArgs | string) => { + return new Promise((resolve, reject) => { + return http.get(options, resp => { + let data = ''; + resp.on('data', chunk => { + data += chunk; + }); + resp.on('end', () => { + resolve(data); + }); + resp.on('error', err => { + reject(err); + }); + }); + }); + }, +}; + +export async function createServer(app: express.Express) { + const server = http.createServer(app); + await new Promise(resolve => server.listen(0, resolve)); + const port = (server.address() as AddressInfo).port; + return { server, port }; +} + +export async function serverWithMiddleware( + tracer: Tracer, + rootSpan: Span, + addMiddlewares: (app: express.Express) => void +) { + const app = express(); + if (tracer) { + app.use((req, res, next) => { + const rpcMetadata = { type: RPCType.HTTP, span: rootSpan }; + return context.with( + setRPCMetadata(trace.setSpan(context.active(), rootSpan), rpcMetadata), + next + ); + }); + } + + addMiddlewares(app); + + const router = express.Router(); + app.use('/toto', router); + router.get('/:id', (req, res) => { + setImmediate(() => { + res.status(200).end(req.params.id); + }); + }); + + return createServer(app); +}