@saidsef/tracing-node 3.22.6 → 4.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/README.md +27 -0
- package/libs/index.mjs +48 -84
- package/libs/index.test.mjs +9 -21
- package/package.json +16 -16
package/README.md
CHANGED
|
@@ -36,6 +36,33 @@ Effortlessly supercharge your applications with world-class distributed tracing!
|
|
|
36
36
|
npm install @saidsef/tracing-node --save
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
## Upgrading to 4.0.0
|
|
40
|
+
|
|
41
|
+
**Breaking change: spans now carry the stable OpenTelemetry semantic conventions.**
|
|
42
|
+
|
|
43
|
+
The upstream instrumentations ([open-telemetry/opentelemetry-js-contrib#3585](https://github.com/open-telemetry/opentelemetry-js-contrib/pull/3585)) dropped the legacy attributes and removed the `OTEL_SEMCONV_STABILITY_OPT_IN` escape hatch, so there is no way to keep the old names. The public API of `setupTracing` / `stopTracing` is unchanged - no code changes are required - but any dashboard, alert or processor keyed on the old attribute names must be updated.
|
|
44
|
+
|
|
45
|
+
| Removed | Replacement | Affected spans |
|
|
46
|
+
|---------|-------------|----------------|
|
|
47
|
+
| `http.method` | `http.request.method` | HTTP |
|
|
48
|
+
| `http.status_code` | `http.response.status_code` | HTTP, AWS SDK |
|
|
49
|
+
| `http.url` | `url.full` | HTTP |
|
|
50
|
+
| `http.target` | `url.path` + `url.query` | HTTP |
|
|
51
|
+
| `http.scheme` | `url.scheme` | HTTP |
|
|
52
|
+
| `http.user_agent` | `user_agent.original` | HTTP |
|
|
53
|
+
| `http.client_ip` | `client.address` | HTTP |
|
|
54
|
+
| `http.flavor` | `network.protocol.version` | HTTP |
|
|
55
|
+
| `net.peer.name` | `server.address` | HTTP, IORedis |
|
|
56
|
+
| `net.peer.port` | `server.port` | HTTP, IORedis |
|
|
57
|
+
| `db.system` | `db.system.name` | IORedis, DynamoDB |
|
|
58
|
+
| `db.statement` | `db.query.text` | IORedis, DynamoDB |
|
|
59
|
+
| `db.operation` | `db.operation.name` | IORedis, DynamoDB |
|
|
60
|
+
| `db.connection_string` | none | IORedis |
|
|
61
|
+
|
|
62
|
+
Server-side HTTP metrics also move from `http.server.duration` (milliseconds) to `http.server.request.duration` (seconds), and the client equivalents likewise.
|
|
63
|
+
|
|
64
|
+
`peer.service` is unchanged, so Tempo/Grafana service graphs keep working as before. IORedis spans also gain `db.operation.name`, which distinguishes `MULTI`/`PIPELINE` commands.
|
|
65
|
+
|
|
39
66
|
## Usage
|
|
40
67
|
|
|
41
68
|
You can set required params via env variables or function:
|
package/libs/index.mjs
CHANGED
|
@@ -43,6 +43,15 @@ const setIntAttribute = (span, name, value) => {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
// Slice before converting: String(buf) decodes a whole 1MB Buffer only to throw
|
|
47
|
+
// it away. 4 bytes per UTF-16 unit plus slack keeps the result byte-identical.
|
|
48
|
+
const truncateArg = (value, limit) => {
|
|
49
|
+
const str = Buffer.isBuffer(value) ? value.subarray(0, limit * 4 + 8).toString() : String(value);
|
|
50
|
+
return str.length > limit ? `${str.substring(0, limit)}...` : str;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let tracerProvider = null; // Declare provider in module scope for access in stopTracing
|
|
54
|
+
|
|
46
55
|
/**
|
|
47
56
|
* Sets up tracing for the application using OpenTelemetry.
|
|
48
57
|
*
|
|
@@ -53,7 +62,7 @@ const setIntAttribute = (span, name, value) => {
|
|
|
53
62
|
* service map visualization in distributed tracing tools like Tempo.
|
|
54
63
|
*
|
|
55
64
|
* @param {Object} options - Configuration options for tracing.
|
|
56
|
-
* @param {string} [options.hostname=process.env.HOSTNAME] - The hostname of the service.
|
|
65
|
+
* @param {string} [options.hostname=process.env.CONTAINER_NAME || process.env.HOSTNAME] - The hostname of the service.
|
|
57
66
|
* @param {string} [options.serviceName=process.env.SERVICE_NAME] - The name of the service.
|
|
58
67
|
* @param {string} [options.url=process.env.ENDPOINT] - The endpoint URL for the tracing collector.
|
|
59
68
|
* @param {number} [options.concurrencyLimit=10] - The concurrency limit for the exporter.
|
|
@@ -62,12 +71,10 @@ const setIntAttribute = (span, name, value) => {
|
|
|
62
71
|
*
|
|
63
72
|
* @returns {Tracer} - The tracer for the service.
|
|
64
73
|
*/
|
|
65
|
-
let tracerProvider = null; // Declare provider in module scope for access in stopTracing
|
|
66
|
-
|
|
67
74
|
export function setupTracing(options = {}) {
|
|
68
75
|
// Prevent multiple initializations - return existing provider if already set up
|
|
69
76
|
if (tracerProvider) {
|
|
70
|
-
|
|
77
|
+
diag.warn('Tracing is already initialized. Returning existing tracer.');
|
|
71
78
|
return tracerProvider.getTracer(options.serviceName || process.env.SERVICE_NAME);
|
|
72
79
|
}
|
|
73
80
|
|
|
@@ -95,10 +102,8 @@ export function setupTracing(options = {}) {
|
|
|
95
102
|
timeoutMillis: 10000,
|
|
96
103
|
};
|
|
97
104
|
|
|
98
|
-
// Register the span processor with the tracer provider
|
|
99
105
|
const exporter = new OTLPTraceExporter(exportOptions);
|
|
100
106
|
|
|
101
|
-
// Configure BatchSpanProcessor for production workloads
|
|
102
107
|
const spanProcessor = new BatchSpanProcessor(exporter, {
|
|
103
108
|
maxQueueSize: 4096,
|
|
104
109
|
maxExportBatchSize: 1024,
|
|
@@ -127,69 +132,49 @@ export function setupTracing(options = {}) {
|
|
|
127
132
|
// explicit config, with the recommended context manager.
|
|
128
133
|
tracerProvider.register();
|
|
129
134
|
|
|
130
|
-
//
|
|
135
|
+
// Only an outgoing ClientRequest carries .host, so bailing without it keeps
|
|
136
|
+
// server spans out: peer.service must name the remote service being called.
|
|
131
137
|
const applyCustomAttributesOnSpan = (span, request) => {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Detect Redis endpoints
|
|
143
|
-
if (hostname.includes('redis') || url.includes('redis') ||
|
|
144
|
-
hostname.includes(':6379') || url.includes(':6379')) {
|
|
145
|
-
span.setAttribute('peer.service', 'redis');
|
|
146
|
-
span.setAttribute('db.system', 'redis');
|
|
138
|
+
const host = request?.host;
|
|
139
|
+
if (!host) return;
|
|
140
|
+
|
|
141
|
+
for (const service of ['elasticsearch', 'redis']) {
|
|
142
|
+
if (host.includes(service)) {
|
|
143
|
+
span.setAttribute('peer.service', service);
|
|
144
|
+
span.setAttribute('db.system.name', service);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
147
|
}
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
-
// Register instrumentations
|
|
151
150
|
const instrumentations = [
|
|
152
151
|
new HttpInstrumentation({
|
|
153
|
-
serverName: serviceName,
|
|
154
152
|
// Ignore spans from static assets (metrics/health probes).
|
|
155
153
|
ignoreIncomingRequestHook: (req) => req.url.startsWith('/metrics') || req.url.startsWith('/healthz'),
|
|
156
154
|
applyCustomAttributesOnSpan,
|
|
157
155
|
requestHook: (span, request) => {
|
|
158
|
-
//
|
|
159
|
-
|
|
156
|
+
// Outgoing ClientRequest exposes getHeaders(); incoming IncomingMessage has .headers.
|
|
157
|
+
const headers = request.getHeaders?.() ?? request.headers;
|
|
158
|
+
if (!headers) return;
|
|
160
159
|
|
|
161
|
-
const
|
|
160
|
+
const contentType = headers['content-type'];
|
|
161
|
+
const requestId = headers['x-request-id'];
|
|
162
|
+
const correlationId = headers['x-correlation-id'];
|
|
162
163
|
|
|
163
|
-
// Safe header extraction with case-insensitive fallback
|
|
164
|
-
const userAgent = headers['user-agent'] || headers['User-Agent'];
|
|
165
|
-
const contentType = headers['content-type'] || headers['Content-Type'];
|
|
166
|
-
const contentLength = headers['content-length'] || headers['Content-Length'];
|
|
167
|
-
const requestId = headers['x-request-id'] || headers['X-Request-ID'];
|
|
168
|
-
const correlationId = headers['x-correlation-id'] || headers['X-Correlation-ID'];
|
|
169
|
-
|
|
170
|
-
// Only set attributes if values exist
|
|
171
|
-
if (userAgent) span.setAttribute('http.user_agent', userAgent);
|
|
172
164
|
if (contentType) span.setAttribute('http.request.content_type', contentType);
|
|
173
|
-
|
|
174
|
-
setIntAttribute(span, 'http.request.content_length', contentLength);
|
|
175
|
-
|
|
176
|
-
// Correlation headers for distributed tracing
|
|
165
|
+
setIntAttribute(span, 'http.request.content_length', headers['content-length']);
|
|
177
166
|
if (requestId) span.setAttribute('http.request_id', requestId);
|
|
178
167
|
if (correlationId) span.setAttribute('http.correlation_id', correlationId);
|
|
179
168
|
},
|
|
180
169
|
responseHook: (span, response) => {
|
|
181
|
-
// Add response attributes for better observability
|
|
182
170
|
if (!response.headers) return;
|
|
183
171
|
|
|
184
172
|
const headers = response.headers;
|
|
185
|
-
const contentType = headers['content-type']
|
|
186
|
-
const
|
|
187
|
-
const requestId = headers['x-request-id'] || headers['X-Request-ID'];
|
|
173
|
+
const contentType = headers['content-type'];
|
|
174
|
+
const requestId = headers['x-request-id'];
|
|
188
175
|
|
|
189
176
|
if (contentType) span.setAttribute('http.response.content_type', contentType);
|
|
190
|
-
|
|
191
|
-
setIntAttribute(span, 'http.response.content_length', contentLength);
|
|
192
|
-
|
|
177
|
+
setIntAttribute(span, 'http.response.content_length', headers['content-length']);
|
|
193
178
|
if (requestId) span.setAttribute('http.request_id', requestId);
|
|
194
179
|
},
|
|
195
180
|
}),
|
|
@@ -249,17 +234,12 @@ export function setupTracing(options = {}) {
|
|
|
249
234
|
new IORedisInstrumentation({
|
|
250
235
|
requireParentSpan: false,
|
|
251
236
|
requestHook: (span, {cmdName, cmdArgs}) => {
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
// already set to the real host by the instrumentation, so we do not
|
|
256
|
-
// override those here.
|
|
237
|
+
// peer.service drives the Tempo service graph and is never emitted by
|
|
238
|
+
// the instrumentation, so it has to be set here. db.system.name,
|
|
239
|
+
// db.operation.name and server.* already come from the instrumentation.
|
|
257
240
|
span.setAttribute('peer.service', 'redis');
|
|
258
|
-
span.setAttribute('db.system', 'redis');
|
|
259
241
|
|
|
260
|
-
// Add command details for better observability
|
|
261
242
|
if (cmdName) {
|
|
262
|
-
span.setAttribute('db.operation', cmdName.toUpperCase());
|
|
263
243
|
span.updateName(`redis.${cmdName.toUpperCase()}`);
|
|
264
244
|
}
|
|
265
245
|
|
|
@@ -274,9 +254,8 @@ export function setupTracing(options = {}) {
|
|
|
274
254
|
}
|
|
275
255
|
},
|
|
276
256
|
responseHook: (span, cmdName, cmdArgs, response) => {
|
|
277
|
-
// peer.service
|
|
278
|
-
//
|
|
279
|
-
// re-set here. Record only the response shape for observability.
|
|
257
|
+
// peer.service is already set by requestHook and persists for the
|
|
258
|
+
// span's lifetime, so only the response shape is recorded here.
|
|
280
259
|
if (response !== undefined && response !== null) {
|
|
281
260
|
span.setAttribute('db.response.type', typeof response);
|
|
282
261
|
if (Array.isArray(response)) {
|
|
@@ -285,37 +264,22 @@ export function setupTracing(options = {}) {
|
|
|
285
264
|
}
|
|
286
265
|
},
|
|
287
266
|
dbStatementSerializer: (cmdName, cmdArgs) => {
|
|
288
|
-
|
|
289
|
-
const args = cmdArgs.map(arg => {
|
|
290
|
-
const str = String(arg);
|
|
291
|
-
return str.length > 100 ? `${str.substring(0, 100)}...` : str;
|
|
292
|
-
});
|
|
267
|
+
const args = cmdArgs.map(arg => truncateArg(arg, 100));
|
|
293
268
|
return `${cmdName} ${args.join(' ')}`;
|
|
294
269
|
},
|
|
295
270
|
}),
|
|
296
271
|
new ElasticsearchInstrumentation(),
|
|
272
|
+
// Spread so the optional instrumentations are constructed only when enabled:
|
|
273
|
+
// FsInstrumentation patches fs on construction.
|
|
274
|
+
...(enableFsInstrumentation ? [new FsInstrumentation()] : []),
|
|
275
|
+
// DnsInstrumentationConfig accepts only ignoreHostnames; it has no hooks.
|
|
276
|
+
...(enableDnsInstrumentation ? [new DnsInstrumentation({ignoreHostnames: ['localhost', '127.0.0.1', '::1']})] : []),
|
|
297
277
|
];
|
|
298
278
|
|
|
299
|
-
if (enableFsInstrumentation) {
|
|
300
|
-
// Enable fs instrumentation if specified
|
|
301
|
-
// This instrumentation is useful for tracing file system operations.
|
|
302
|
-
instrumentations.push(new FsInstrumentation());
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
if (enableDnsInstrumentation) {
|
|
306
|
-
// Enable DNS instrumentation if specified
|
|
307
|
-
// This instrumentation is useful for tracing DNS operations.
|
|
308
|
-
// DnsInstrumentationConfig only supports ignoreHostnames; it has no
|
|
309
|
-
// request/response/error hooks.
|
|
310
|
-
instrumentations.push(new DnsInstrumentation({
|
|
311
|
-
ignoreHostnames: ['localhost', '127.0.0.1', '::1'],
|
|
312
|
-
}));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
279
|
// Register instrumentations
|
|
316
280
|
registerInstrumentations({
|
|
317
|
-
tracerProvider
|
|
318
|
-
instrumentations
|
|
281
|
+
tracerProvider,
|
|
282
|
+
instrumentations,
|
|
319
283
|
});
|
|
320
284
|
|
|
321
285
|
// Return the tracer for the service
|
|
@@ -336,12 +300,12 @@ export async function stopTracing() {
|
|
|
336
300
|
try {
|
|
337
301
|
await tracerProvider.shutdown();
|
|
338
302
|
tracerProvider = null;
|
|
339
|
-
|
|
303
|
+
diag.info('Tracing has been successfully shut down.');
|
|
340
304
|
} catch (error) {
|
|
341
|
-
|
|
305
|
+
diag.error('Error during tracing shutdown:', error);
|
|
342
306
|
}
|
|
343
307
|
} else {
|
|
344
|
-
|
|
308
|
+
diag.warn('Tracer provider is not initialized.');
|
|
345
309
|
}
|
|
346
310
|
}
|
|
347
311
|
|
package/libs/index.test.mjs
CHANGED
|
@@ -1,48 +1,38 @@
|
|
|
1
1
|
// index.test.mjs
|
|
2
2
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
3
3
|
import assert from 'node:assert';
|
|
4
|
+
import { setupTracing, stopTracing, __resetTracingForTesting } from './index.mjs';
|
|
4
5
|
|
|
5
6
|
describe('setupTracing', () => {
|
|
6
|
-
let stopTracing;
|
|
7
|
-
|
|
8
7
|
// Clear environment and reset tracing state before each test
|
|
9
|
-
beforeEach(
|
|
8
|
+
beforeEach(() => {
|
|
10
9
|
delete process.env.SERVICE_NAME;
|
|
11
10
|
delete process.env.ENDPOINT;
|
|
12
11
|
delete process.env.HOSTNAME;
|
|
13
12
|
delete process.env.CONTAINER_NAME;
|
|
14
13
|
|
|
15
|
-
// Import and store stopTracing for cleanup
|
|
16
|
-
const tracing = await import('./index.mjs');
|
|
17
|
-
stopTracing = tracing.stopTracing;
|
|
18
|
-
|
|
19
14
|
// Reset singleton for test isolation
|
|
20
|
-
|
|
15
|
+
__resetTracingForTesting();
|
|
21
16
|
});
|
|
22
17
|
|
|
23
18
|
// Clean up tracing after each test
|
|
24
19
|
afterEach(async () => {
|
|
25
|
-
|
|
26
|
-
await stopTracing();
|
|
27
|
-
}
|
|
20
|
+
await stopTracing();
|
|
28
21
|
});
|
|
29
22
|
|
|
30
|
-
it('should throw error when serviceName is not provided',
|
|
31
|
-
const { setupTracing } = await import('./index.mjs');
|
|
23
|
+
it('should throw error when serviceName is not provided', () => {
|
|
32
24
|
assert.throws(() => {
|
|
33
25
|
setupTracing({ url: 'http://localhost:4317' });
|
|
34
26
|
}, /serviceName is required/);
|
|
35
27
|
});
|
|
36
28
|
|
|
37
|
-
it('should throw error when url is not provided',
|
|
38
|
-
const { setupTracing } = await import('./index.mjs');
|
|
29
|
+
it('should throw error when url is not provided', () => {
|
|
39
30
|
assert.throws(() => {
|
|
40
31
|
setupTracing({ serviceName: 'test-service' });
|
|
41
32
|
}, /url is required/);
|
|
42
33
|
});
|
|
43
34
|
|
|
44
|
-
it('should create a tracer with required parameters',
|
|
45
|
-
const { setupTracing } = await import('./index.mjs');
|
|
35
|
+
it('should create a tracer with required parameters', () => {
|
|
46
36
|
const tracer = setupTracing({
|
|
47
37
|
serviceName: 'test-service',
|
|
48
38
|
url: 'http://localhost:4317',
|
|
@@ -50,8 +40,7 @@ describe('setupTracing', () => {
|
|
|
50
40
|
assert.ok(tracer, 'tracer should be defined');
|
|
51
41
|
});
|
|
52
42
|
|
|
53
|
-
it('should accept hostname parameter',
|
|
54
|
-
const { setupTracing } = await import('./index.mjs');
|
|
43
|
+
it('should accept hostname parameter', () => {
|
|
55
44
|
const tracer = setupTracing({
|
|
56
45
|
serviceName: 'test-service',
|
|
57
46
|
url: 'http://localhost:4317',
|
|
@@ -60,8 +49,7 @@ describe('setupTracing', () => {
|
|
|
60
49
|
assert.ok(tracer, 'tracer should be defined');
|
|
61
50
|
});
|
|
62
51
|
|
|
63
|
-
it('should accept optional instrumentations',
|
|
64
|
-
const { setupTracing } = await import('./index.mjs');
|
|
52
|
+
it('should accept optional instrumentations', () => {
|
|
65
53
|
const tracer = setupTracing({
|
|
66
54
|
serviceName: 'test-service',
|
|
67
55
|
url: 'http://localhost:4317',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saidsef/tracing-node",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
|
|
5
5
|
"main": "libs/index.mjs",
|
|
6
6
|
"scripts": {
|
|
@@ -31,30 +31,30 @@
|
|
|
31
31
|
"homepage": "https://github.com/saidsef/tracing-node#readme",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@opentelemetry/api": "^1.9.0",
|
|
34
|
-
"@opentelemetry/exporter-trace-otlp-grpc": "^0.
|
|
35
|
-
"@opentelemetry/instrumentation": "^0.
|
|
36
|
-
"@opentelemetry/instrumentation-aws-sdk": "^0.
|
|
37
|
-
"@opentelemetry/instrumentation-connect": "^0.
|
|
34
|
+
"@opentelemetry/exporter-trace-otlp-grpc": "^0.221.0",
|
|
35
|
+
"@opentelemetry/instrumentation": "^0.221.0",
|
|
36
|
+
"@opentelemetry/instrumentation-aws-sdk": "^0.76.0",
|
|
37
|
+
"@opentelemetry/instrumentation-connect": "^0.64.0",
|
|
38
38
|
"@opentelemetry/instrumentation-dns": "^0.64.0",
|
|
39
|
-
"@opentelemetry/instrumentation-express": "^0.
|
|
39
|
+
"@opentelemetry/instrumentation-express": "^0.69.0",
|
|
40
40
|
"@opentelemetry/instrumentation-fs": "^0.40.0",
|
|
41
|
-
"@opentelemetry/instrumentation-http": "^0.
|
|
42
|
-
"@opentelemetry/instrumentation-ioredis": "^0.
|
|
43
|
-
"@opentelemetry/instrumentation-pino": "^0.
|
|
44
|
-
"@opentelemetry/resources": "^2.
|
|
45
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
46
|
-
"@opentelemetry/sdk-trace-node": "^2.
|
|
47
|
-
"@opentelemetry/semantic-conventions": "^1.
|
|
41
|
+
"@opentelemetry/instrumentation-http": "^0.221.0",
|
|
42
|
+
"@opentelemetry/instrumentation-ioredis": "^0.69.0",
|
|
43
|
+
"@opentelemetry/instrumentation-pino": "^0.67.0",
|
|
44
|
+
"@opentelemetry/resources": "^2.10.0",
|
|
45
|
+
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
46
|
+
"@opentelemetry/sdk-trace-node": "^2.10.0",
|
|
47
|
+
"@opentelemetry/semantic-conventions": "^1.43.0",
|
|
48
48
|
"opentelemetry-instrumentation-elasticsearch": "^0.41.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"eslint": "^10.
|
|
51
|
+
"eslint": "^10.8.0"
|
|
52
52
|
},
|
|
53
53
|
"overrides": {
|
|
54
54
|
"protobufjs": "^7.5.3",
|
|
55
|
-
"@opentelemetry/core": "^2.
|
|
55
|
+
"@opentelemetry/core": "^2.10.0"
|
|
56
56
|
},
|
|
57
57
|
"allowScripts": {
|
|
58
|
-
"protobufjs@7.6.
|
|
58
|
+
"protobufjs@7.6.5": true
|
|
59
59
|
}
|
|
60
60
|
}
|