@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 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
- console.warn('Tracing is already initialized. Returning existing tracer.');
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
- // Hook to set peer service name for outgoing requests
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 url = request?.url || request?.uri || '';
133
- const hostname = request?.hostname || request?.host || '';
134
-
135
- // Detect Elasticsearch endpoints
136
- if (hostname.includes('elasticsearch') || url.includes('elasticsearch') ||
137
- hostname.includes(':9200') || url.includes(':9200')) {
138
- span.setAttribute('peer.service', 'elasticsearch');
139
- span.setAttribute('db.system', 'elasticsearch');
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
- // Enrich spans with additional HTTP request attributes
159
- if (!request.headers) return;
156
+ // Outgoing ClientRequest exposes getHeaders(); incoming IncomingMessage has .headers.
157
+ const headers = request.getHeaders?.() ?? request.headers;
158
+ if (!headers) return;
160
159
 
161
- const headers = request.headers;
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'] || headers['Content-Type'];
186
- const contentLength = headers['content-length'] || headers['Content-Length'];
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
- // requestInfo is IORedisRequestHookInformation: { cmdName, cmdArgs }.
253
- // Set peer.service for service graph visualization - CRITICAL for Tempo.
254
- // The span is already created with SpanKind.CLIENT and net.peer.name is
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, db.system and db.operation are already set on this span
278
- // by requestHook and persist for the span's lifetime, so they are not
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
- // Serialize command for better observability (limit arg length to avoid huge spans)
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: tracerProvider,
318
- instrumentations: 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
- console.info('Tracing has been successfully shut down.');
303
+ diag.info('Tracing has been successfully shut down.');
340
304
  } catch (error) {
341
- console.error('Error during tracing shutdown:', error);
305
+ diag.error('Error during tracing shutdown:', error);
342
306
  }
343
307
  } else {
344
- console.warn('Tracer provider is not initialized.');
308
+ diag.warn('Tracer provider is not initialized.');
345
309
  }
346
310
  }
347
311
 
@@ -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(async () => {
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
- tracing.__resetTracingForTesting();
15
+ __resetTracingForTesting();
21
16
  });
22
17
 
23
18
  // Clean up tracing after each test
24
19
  afterEach(async () => {
25
- if (stopTracing) {
26
- await stopTracing();
27
- }
20
+ await stopTracing();
28
21
  });
29
22
 
30
- it('should throw error when serviceName is not provided', async () => {
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', async () => {
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', async () => {
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', async () => {
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', async () => {
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.22.6",
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.220.0",
35
- "@opentelemetry/instrumentation": "^0.220.0",
36
- "@opentelemetry/instrumentation-aws-sdk": "^0.75.0",
37
- "@opentelemetry/instrumentation-connect": "^0.63.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.67.0",
39
+ "@opentelemetry/instrumentation-express": "^0.69.0",
40
40
  "@opentelemetry/instrumentation-fs": "^0.40.0",
41
- "@opentelemetry/instrumentation-http": "^0.220.0",
42
- "@opentelemetry/instrumentation-ioredis": "^0.68.0",
43
- "@opentelemetry/instrumentation-pino": "^0.66.0",
44
- "@opentelemetry/resources": "^2.8.0",
45
- "@opentelemetry/sdk-trace-base": "^2.8.0",
46
- "@opentelemetry/sdk-trace-node": "^2.8.0",
47
- "@opentelemetry/semantic-conventions": "^1.41.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.5.0"
51
+ "eslint": "^10.8.0"
52
52
  },
53
53
  "overrides": {
54
54
  "protobufjs": "^7.5.3",
55
- "@opentelemetry/core": "^2.8.0"
55
+ "@opentelemetry/core": "^2.10.0"
56
56
  },
57
57
  "allowScripts": {
58
- "protobufjs@7.6.4": true
58
+ "protobufjs@7.6.5": true
59
59
  }
60
60
  }