@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.
- package/libs/index.mjs +68 -155
- 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:
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
span.setAttribute('express.route',
|
|
215
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
232
|
-
|
|
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,
|
|
266
|
-
//
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
span.setAttribute('peer.service',
|
|
270
|
-
span.setAttribute('aws.service',
|
|
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,
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
312
|
-
span
|
|
313
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|