@saidsef/tracing-node 3.21.1 → 3.22.1

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.
Files changed (2) hide show
  1. package/libs/index.mjs +68 -155
  2. package/package.json +1 -2
package/libs/index.mjs CHANGED
@@ -17,9 +17,7 @@
17
17
  */
18
18
 
19
19
  import {AwsInstrumentation} from '@opentelemetry/instrumentation-aws-sdk';
20
- import {AsyncHooksContextManager} from '@opentelemetry/context-async-hooks';
21
20
  import {BatchSpanProcessor} from '@opentelemetry/sdk-trace-base';
22
- import {CompositePropagator, W3CBaggagePropagator, W3CTraceContextPropagator} from '@opentelemetry/core';
23
21
  import {ConnectInstrumentation} from '@opentelemetry/instrumentation-connect';
24
22
  import {diag, DiagConsoleLogger, DiagLogLevel} from '@opentelemetry/api';
25
23
  import {HttpInstrumentation} from '@opentelemetry/instrumentation-http';
@@ -38,6 +36,15 @@ import {ATTR_CONTAINER_NAME} from '@opentelemetry/semantic-conventions/incubatin
38
36
 
39
37
  diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
40
38
 
39
+ // Set a non-negative integer span attribute from a header value; ignore invalid input.
40
+ const setIntAttribute = (span, name, value) => {
41
+ if (!value) return;
42
+ const parsed = parseInt(value, 10);
43
+ if (!Number.isNaN(parsed) && parsed >= 0) {
44
+ span.setAttribute(name, parsed);
45
+ }
46
+ };
47
+
41
48
  /**
42
49
  * Sets up tracing for the application using OpenTelemetry.
43
50
  *
@@ -101,34 +108,26 @@ export function setupTracing(options = {}) {
101
108
  exportTimeoutMillis: 10000,
102
109
  });
103
110
 
111
+ // Explicit attributes (service/container) must win over env detection, so
112
+ // detect first and merge the explicit resource on top. Only include defined
113
+ // keys so an undefined hostname does not write container.name: undefined.
114
+ const explicitAttributes = {[ATTR_SERVICE_NAME]: serviceName};
115
+ if (hostname) {
116
+ explicitAttributes[ATTR_CONTAINER_NAME] = hostname;
117
+ }
118
+
104
119
  tracerProvider = new NodeTracerProvider({
105
120
  spanProcessors: [spanProcessor],
106
- resource: new resourceFromAttributes({
107
- [ATTR_SERVICE_NAME]: serviceName,
108
- [ATTR_CONTAINER_NAME]: hostname,
109
- }).merge(
110
- detectResources({
111
- detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
112
- })
113
- ),
114
- autoDetectResources: true,
121
+ resource: detectResources({
122
+ detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
123
+ }).merge(resourceFromAttributes(explicitAttributes)),
115
124
  });
116
125
 
117
- // Initialize the tracer provider with propagators
118
- tracerProvider.register({
119
- contextManager: new AsyncHooksContextManager().enable(),
120
- propagator: new CompositePropagator({
121
- propagators: [
122
- new W3CTraceContextPropagator(),
123
- new W3CBaggagePropagator(),
124
- ],
125
- }),
126
- });
127
-
128
- // Ignore spans from static assets.
129
- const ignoreIncomingRequestHook = (req) => {
130
- return req.url.startsWith('/metrics') || req.url.startsWith('/healthz');
131
- };
126
+ // Register globally. With no overrides, register() installs the modern
127
+ // AsyncLocalStorageContextManager and a CompositePropagator of
128
+ // W3CTraceContext + W3CBaggage - identical propagation to the previous
129
+ // explicit config, with the recommended context manager.
130
+ tracerProvider.register();
132
131
 
133
132
  // Hook to set peer service name for outgoing requests
134
133
  const applyCustomAttributesOnSpan = (span, request) => {
@@ -154,7 +153,8 @@ export function setupTracing(options = {}) {
154
153
  const instrumentations = [
155
154
  new HttpInstrumentation({
156
155
  serverName: serviceName,
157
- ignoreIncomingRequestHook,
156
+ // Ignore spans from static assets (metrics/health probes).
157
+ ignoreIncomingRequestHook: (req) => req.url.startsWith('/metrics') || req.url.startsWith('/healthz'),
158
158
  applyCustomAttributesOnSpan,
159
159
  requestHook: (span, request) => {
160
160
  // Enrich spans with additional HTTP request attributes
@@ -173,13 +173,7 @@ export function setupTracing(options = {}) {
173
173
  if (userAgent) span.setAttribute('http.user_agent', userAgent);
174
174
  if (contentType) span.setAttribute('http.request.content_type', contentType);
175
175
 
176
- // Safe integer parsing with validation
177
- if (contentLength) {
178
- const length = parseInt(contentLength, 10);
179
- if (!Number.isNaN(length) && length >= 0) {
180
- span.setAttribute('http.request.content_length', length);
181
- }
182
- }
176
+ setIntAttribute(span, 'http.request.content_length', contentLength);
183
177
 
184
178
  // Correlation headers for distributed tracing
185
179
  if (requestId) span.setAttribute('http.request_id', requestId);
@@ -196,100 +190,74 @@ export function setupTracing(options = {}) {
196
190
 
197
191
  if (contentType) span.setAttribute('http.response.content_type', contentType);
198
192
 
199
- if (contentLength) {
200
- const length = parseInt(contentLength, 10);
201
- if (!Number.isNaN(length) && length >= 0) {
202
- span.setAttribute('http.response.content_length', length);
203
- }
204
- }
193
+ setIntAttribute(span, 'http.response.content_length', contentLength);
205
194
 
206
195
  if (requestId) span.setAttribute('http.request_id', requestId);
207
196
  },
208
197
  }),
209
198
  new ExpressInstrumentation({
210
- ignoreIncomingRequestHook,
211
- requestHook: (span, request) => {
212
- // Add Express-specific attributes
213
- if (request.route?.path) {
214
- span.setAttribute('express.route', request.route.path);
215
- span.updateName(`${request.method} ${request.route.path}`);
199
+ requestHook: (span, info) => {
200
+ // info is ExpressRequestInfo: { request, route, layerType }
201
+ const request = info.request;
202
+ if (info.route) {
203
+ span.setAttribute('express.route', info.route);
204
+ if (request?.method) {
205
+ span.updateName(`${request.method} ${info.route}`);
206
+ }
216
207
  }
217
- if (request.params && Object.keys(request.params).length > 0) {
208
+ if (request?.params && Object.keys(request.params).length > 0) {
218
209
  span.setAttribute('express.params', JSON.stringify(request.params));
219
210
  }
220
- if (request.query && Object.keys(request.query).length > 0) {
211
+ if (request?.query && Object.keys(request.query).length > 0) {
221
212
  span.setAttribute('express.query', JSON.stringify(request.query));
222
213
  }
223
214
  // Add user context if available
224
- if (request.user?.id) {
215
+ if (request?.user?.id) {
225
216
  span.setAttribute('user.id', request.user.id);
226
217
  }
227
218
  },
228
219
  }),
229
220
  new PinoInstrumentation({
230
221
  logHook: (span, record) => {
231
- // Inject trace context into log records
232
- const spanContext = span.spanContext();
233
- record['trace_id'] = spanContext.traceId;
234
- record['span_id'] = spanContext.spanId;
235
- record['trace_flags'] = `0${spanContext.traceFlags.toString(16)}`;
236
-
237
- // Add service name for better log correlation
222
+ // trace_id/span_id/trace_flags are injected by the instrumentation by
223
+ // default; only add service name for better log correlation.
238
224
  if (serviceName) {
239
225
  record['service.name'] = serviceName;
240
226
  }
241
227
  },
242
- logSeverity: {
243
- error: 'ERROR',
244
- warn: 'WARN',
245
- info: 'INFO',
246
- debug: 'DEBUG',
247
- trace: 'TRACE',
248
- },
249
- }),
250
- new ConnectInstrumentation({
251
- ignoreIncomingRequestHook,
252
- requestHook: (span, request) => {
253
- // Add Connect middleware attributes
254
- if (request.url) {
255
- span.setAttribute('connect.url', request.url);
256
- }
257
- if (request.method) {
258
- span.setAttribute('connect.method', request.method);
259
- }
260
- },
261
228
  }),
229
+ // ConnectInstrumentation accepts only the base InstrumentationConfig; it has
230
+ // no request/ignore hooks, so configure it with defaults.
231
+ new ConnectInstrumentation(),
262
232
  new AwsInstrumentation({
263
233
  suppressInternalInstrumentation: false,
264
234
  sqsExtractContextPropagationFromPayload: true,
265
- preRequestHook: (span, request) => {
266
- // Add peer.service attribute for better service map visualization
267
- const serviceName = request.serviceName || request.service?.serviceIdentifier;
268
- if (serviceName) {
269
- span.setAttribute('peer.service', serviceName.toLowerCase());
270
- span.setAttribute('aws.service', serviceName.toLowerCase());
235
+ preRequestHook: (span, requestInfo) => {
236
+ // requestInfo is AwsSdkRequestHookInformation: { request: NormalizedRequest }
237
+ const awsServiceName = requestInfo.request?.serviceName;
238
+ if (awsServiceName) {
239
+ span.setAttribute('peer.service', awsServiceName.toLowerCase());
240
+ span.setAttribute('aws.service', awsServiceName.toLowerCase());
271
241
  }
272
242
  },
273
- responseHook: (span, response) => {
274
- // Add additional attributes from response if available
275
- if (response?.requestId) {
276
- span.setAttribute('aws.request_id', response.requestId);
243
+ responseHook: (span, responseInfo) => {
244
+ // responseInfo is AwsSdkResponseHookInformation: { response: NormalizedResponse }
245
+ const requestId = responseInfo.response?.requestId;
246
+ if (requestId) {
247
+ span.setAttribute('aws.request_id', requestId);
277
248
  }
278
249
  },
279
250
  }),
280
251
  new IORedisInstrumentation({
281
252
  requireParentSpan: false,
282
- requestHook: (span, cmdName, cmdArgs) => {
283
- // Set peer.service for service graph visualization - CRITICAL for Tempo
253
+ requestHook: (span, {cmdName, cmdArgs}) => {
254
+ // requestInfo is IORedisRequestHookInformation: { cmdName, cmdArgs }.
255
+ // Set peer.service for service graph visualization - CRITICAL for Tempo.
256
+ // The span is already created with SpanKind.CLIENT and net.peer.name is
257
+ // already set to the real host by the instrumentation, so we do not
258
+ // override those here.
284
259
  span.setAttribute('peer.service', 'redis');
285
260
  span.setAttribute('db.system', 'redis');
286
-
287
- // CRITICAL: Ensure span kind is CLIENT for service graph
288
- span.setAttribute('span.kind', 'CLIENT');
289
-
290
- // Add network peer attributes (helps Tempo identify the service)
291
- span.setAttribute('net.peer.name', 'redis');
292
- span.setAttribute('db.connection_string', 'redis');
293
261
 
294
262
  // Add command details for better observability
295
263
  if (cmdName) {
@@ -308,20 +276,11 @@ export function setupTracing(options = {}) {
308
276
  }
309
277
  },
310
278
  responseHook: (span, cmdName, cmdArgs, response) => {
311
- // Ensure peer.service persists through response
312
- span.setAttribute('peer.service', 'redis');
313
- span.setAttribute('db.system', 'redis');
314
-
315
- // Add command details for better observability
316
- if (cmdName) {
317
- span.setAttribute('db.operation', cmdName.toUpperCase());
318
- }
319
-
320
- // Log response size if available
279
+ // peer.service, db.system and db.operation are already set on this span
280
+ // by requestHook and persist for the span's lifetime, so they are not
281
+ // re-set here. Record only the response shape for observability.
321
282
  if (response !== undefined && response !== null) {
322
- const responseType = typeof response;
323
- span.setAttribute('db.response.type', responseType);
324
-
283
+ span.setAttribute('db.response.type', typeof response);
325
284
  if (Array.isArray(response)) {
326
285
  span.setAttribute('db.response.count', response.length);
327
286
  }
@@ -348,56 +307,10 @@ export function setupTracing(options = {}) {
348
307
  if (enableDnsInstrumentation) {
349
308
  // Enable DNS instrumentation if specified
350
309
  // This instrumentation is useful for tracing DNS operations.
310
+ // DnsInstrumentationConfig only supports ignoreHostnames; it has no
311
+ // request/response/error hooks.
351
312
  instrumentations.push(new DnsInstrumentation({
352
313
  ignoreHostnames: ['localhost', '127.0.0.1', '::1'],
353
- requestHook: (span, request) => {
354
- // Add DNS query details for better observability
355
- if (request.hostname) {
356
- span.setAttribute('dns.hostname', request.hostname);
357
- span.updateName(`DNS ${request.hostname}`);
358
- }
359
- if (request.rrtype) {
360
- span.setAttribute('dns.record_type', request.rrtype);
361
- }
362
- // Add additional context
363
- span.setAttribute('peer.service', 'dns');
364
- span.setAttribute('dns.query_count', 1);
365
- },
366
- responseHook: (span, response) => {
367
- // Add DNS response details
368
- if (Array.isArray(response)) {
369
- span.setAttribute('dns.result_count', response.length);
370
- // Log first few results for debugging (limit to avoid overwhelming spans)
371
- const resultSample = response.slice(0, 3).map(r =>
372
- typeof r === 'string' ? r : JSON.stringify(r)
373
- );
374
- if (resultSample.length > 0) {
375
- span.setAttribute('dns.results', JSON.stringify(resultSample));
376
- }
377
- } else if (response) {
378
- span.setAttribute('dns.result_count', 1);
379
- span.setAttribute('dns.result', typeof response === 'string' ? response : JSON.stringify(response));
380
- }
381
- },
382
- errorHook: (span, error) => {
383
- // Enhanced error tracking for DNS failures
384
- if (error) {
385
- span.setAttribute('dns.error', true);
386
- span.setAttribute('dns.error.code', error.code || 'UNKNOWN');
387
- span.setAttribute('dns.error.message', error.message || 'DNS lookup failed');
388
-
389
- // Categorize common DNS errors
390
- if (error.code === 'ENOTFOUND') {
391
- span.setAttribute('dns.error.type', 'NOT_FOUND');
392
- } else if (error.code === 'ETIMEOUT') {
393
- span.setAttribute('dns.error.type', 'TIMEOUT');
394
- } else if (error.code === 'ECONNREFUSED') {
395
- span.setAttribute('dns.error.type', 'CONNECTION_REFUSED');
396
- } else {
397
- span.setAttribute('dns.error.type', 'OTHER');
398
- }
399
- }
400
- },
401
314
  }));
402
315
  }
403
316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "3.21.1",
3
+ "version": "3.22.1",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
@@ -31,7 +31,6 @@
31
31
  "homepage": "https://github.com/saidsef/tracing-node#readme",
32
32
  "dependencies": {
33
33
  "@opentelemetry/api": "^1.9.0",
34
- "@opentelemetry/context-async-hooks": "^2.8.0",
35
34
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.219.0",
36
35
  "@opentelemetry/instrumentation": "^0.219.0",
37
36
  "@opentelemetry/instrumentation-aws-sdk": "^0.74.0",