@saidsef/tracing-node 4.4.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/libs/index.mjs CHANGED
@@ -21,7 +21,7 @@ import {diag, DiagConsoleLogger, DiagLogLevel, metrics} from '@opentelemetry/api
21
21
  import {HttpInstrumentation} from '@opentelemetry/instrumentation-http';
22
22
  import {DnsInstrumentation} from '@opentelemetry/instrumentation-dns';
23
23
  import {ElasticsearchInstrumentation} from 'opentelemetry-instrumentation-elasticsearch';
24
- import {ExpressInstrumentation} from '@opentelemetry/instrumentation-express';
24
+ import {ExpressInstrumentation, ExpressLayerType} from '@opentelemetry/instrumentation-express';
25
25
  import {logs} from '@opentelemetry/api-logs';
26
26
  import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
27
27
  import {OTLPLogExporter} from '@opentelemetry/exporter-logs-otlp-grpc';
@@ -72,6 +72,32 @@ const setPeerService = (span, host) => {
72
72
  }
73
73
  };
74
74
 
75
+ // The express hook runs once per layer span - every middleware, every router,
76
+ // and the request handler. Only the request handler carries the matched route,
77
+ // so the rest return before serialising anything.
78
+ const expressRequestHook = (span, info) => {
79
+ // info is ExpressRequestInfo: { request, route, layerType }
80
+ if (info?.layerType !== ExpressLayerType.REQUEST_HANDLER) return;
81
+
82
+ const request = info.request;
83
+ if (info.route) {
84
+ span.setAttribute('express.route', info.route);
85
+ }
86
+ if (request?.params && Object.keys(request.params).length > 0) {
87
+ span.setAttribute('express.params', JSON.stringify(request.params));
88
+ }
89
+ // Names only. Query values carry tokens and personal data, and the span
90
+ // attribute value length limit defaults to unbounded.
91
+ const queryKeys = request?.query ? Object.keys(request.query) : [];
92
+ if (queryKeys.length > 0) {
93
+ span.setAttribute('express.query_keys', queryKeys.sort());
94
+ }
95
+ // Add user context if available
96
+ if (request?.user?.id) {
97
+ span.setAttribute('user.id', request.user.id);
98
+ }
99
+ };
100
+
75
101
  let tracerProvider = null; // Declare provider in module scope for access in stopTracing
76
102
  let meterProvider = null;
77
103
  let loggerProvider = null;
@@ -247,26 +273,7 @@ export function setupTracing(options = {}) {
247
273
  requestHook: (span, request) => setPeerService(span, request?.origin),
248
274
  }),
249
275
  new ExpressInstrumentation({
250
- requestHook: (span, info) => {
251
- // info is ExpressRequestInfo: { request, route, layerType }
252
- const request = info.request;
253
- if (info.route) {
254
- span.setAttribute('express.route', info.route);
255
- if (request?.method) {
256
- span.updateName(`${request.method} ${info.route}`);
257
- }
258
- }
259
- if (request?.params && Object.keys(request.params).length > 0) {
260
- span.setAttribute('express.params', JSON.stringify(request.params));
261
- }
262
- if (request?.query && Object.keys(request.query).length > 0) {
263
- span.setAttribute('express.query', JSON.stringify(request.query));
264
- }
265
- // Add user context if available
266
- if (request?.user?.id) {
267
- span.setAttribute('user.id', request.user.id);
268
- }
269
- },
276
+ requestHook: expressRequestHook,
270
277
  }),
271
278
  new PinoInstrumentation({
272
279
  // Log sending is on by default, and every record is parsed and rebuilt as
@@ -417,6 +424,13 @@ export async function stopTracing() {
417
424
  }
418
425
  }
419
426
 
427
+ /**
428
+ * @internal
429
+ * The express request hook, exposed so it can be driven without Express.
430
+ * DO NOT use in production code.
431
+ */
432
+ export const __expressRequestHookForTesting = expressRequestHook;
433
+
420
434
  /**
421
435
  * @internal
422
436
  * Resets the tracer provider for testing purposes.
@@ -5,7 +5,7 @@ import { metrics } from '@opentelemetry/api';
5
5
  import { logs } from '@opentelemetry/api-logs';
6
6
  import { MeterProvider } from '@opentelemetry/sdk-metrics';
7
7
  import { LoggerProvider } from '@opentelemetry/sdk-logs';
8
- import { setupTracing, stopTracing, __resetTracingForTesting } from './index.mjs';
8
+ import { setupTracing, stopTracing, __resetTracingForTesting, __expressRequestHookForTesting } from './index.mjs';
9
9
 
10
10
  describe('setupTracing', () => {
11
11
  // Clear environment and reset tracing state before each test
@@ -172,3 +172,93 @@ describe('setupTracing', () => {
172
172
  assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'a later setup should register again');
173
173
  });
174
174
  });
175
+
176
+ // The instrumentation calls the hook once per layer span, so the cheap path
177
+ // through it matters as much as what it records.
178
+ describe('express request hook', () => {
179
+ const fakeSpan = () => {
180
+ const attributes = {};
181
+ return {
182
+ attributes,
183
+ names: [],
184
+ setAttribute(key, value) {
185
+ attributes[key] = value;
186
+ },
187
+ updateName(name) {
188
+ this.names.push(name);
189
+ },
190
+ };
191
+ };
192
+
193
+ const requestHandler = (request, route = '/work/:id') => ({
194
+ request,
195
+ route,
196
+ layerType: 'request_handler',
197
+ });
198
+
199
+ it('should record route and params on a request handler layer', () => {
200
+ const span = fakeSpan();
201
+ __expressRequestHookForTesting(span, requestHandler({
202
+ method: 'GET',
203
+ params: {id: '42'},
204
+ query: {},
205
+ }));
206
+ assert.strictEqual(span.attributes['express.route'], '/work/:id');
207
+ assert.strictEqual(span.attributes['express.params'], '{"id":"42"}');
208
+ });
209
+
210
+ it('should ignore middleware and router layers', () => {
211
+ for (const layerType of ['middleware', 'router']) {
212
+ const span = fakeSpan();
213
+ __expressRequestHookForTesting(span, {
214
+ request: {method: 'GET', params: {id: '42'}, query: {page: '1'}},
215
+ route: '/work/:id',
216
+ layerType,
217
+ });
218
+ assert.deepStrictEqual(span.attributes, {}, `${layerType} layer should record nothing`);
219
+ }
220
+ });
221
+
222
+ // The HTTP instrumentation renames the server span from http.route already.
223
+ // Renaming here would relabel every middleware span with the same string.
224
+ it('should not rename the span', () => {
225
+ const span = fakeSpan();
226
+ __expressRequestHookForTesting(span, requestHandler({
227
+ method: 'GET',
228
+ params: {id: '42'},
229
+ query: {},
230
+ }));
231
+ assert.deepStrictEqual(span.names, [], 'the hook should not rename a span');
232
+ });
233
+
234
+ // A query string carries tokens and personal data, and the span attribute
235
+ // value length limit is unbounded by default.
236
+ it('should record query key names without their values', () => {
237
+ const span = fakeSpan();
238
+ __expressRequestHookForTesting(span, requestHandler({
239
+ method: 'GET',
240
+ params: {},
241
+ query: {token: 'sensitive-value', page: '2'},
242
+ }));
243
+ assert.deepStrictEqual(span.attributes['express.query_keys'], ['page', 'token']);
244
+ assert.strictEqual(span.attributes['express.query'], undefined, 'query values should not be recorded');
245
+ assert.ok(!JSON.stringify(span.attributes).includes('sensitive-value'), 'no query value should reach the span');
246
+ });
247
+
248
+ it('should record the user id when the application sets one', () => {
249
+ const span = fakeSpan();
250
+ __expressRequestHookForTesting(span, requestHandler({
251
+ method: 'GET',
252
+ params: {},
253
+ query: {},
254
+ user: {id: 'user-7'},
255
+ }));
256
+ assert.strictEqual(span.attributes['user.id'], 'user-7');
257
+ });
258
+
259
+ it('should tolerate a layer with no request', () => {
260
+ const span = fakeSpan();
261
+ assert.doesNotThrow(() => __expressRequestHookForTesting(span, {layerType: 'request_handler'}));
262
+ assert.deepStrictEqual(span.attributes, {});
263
+ });
264
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "4.4.0",
3
+ "version": "5.0.0",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {