@saidsef/tracing-node 4.2.0 → 4.4.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
@@ -21,6 +21,9 @@ Effortlessly supercharge your applications with world-class distributed tracing!
21
21
  | IORedis client | Cache instrumentation |
22
22
  | AWS SDK | Cloud service instrumentation |
23
23
  | Pino logger | Integration with trace/span IDs |
24
+ | Node runtime metrics | Event loop, garbage collection, heap |
25
+ | Log export | Pino records over OTLP, correlated by trace |
26
+ | RED metrics | Request duration histograms over OTLP |
24
27
  | DNS/FS instrumentation | Optional monitoring |
25
28
  | Resource detection | Host, OS, process, container |
26
29
  | W3C Trace Context | Standard propagation |
@@ -43,6 +46,10 @@ setupTracing({serviceName: 'my-service', url: 'http://alloy:4317'});
43
46
 
44
47
  The W3C Trace Context propagation this library registers is what lets Tempo pair a caller's client span with the callee's server span, which is what a service graph is built from.
45
48
 
49
+ Metrics go to the same endpoint by default and land in Mimir. They are recorded before the sampler runs, so they stay complete however far trace volume is turned down.
50
+
51
+ Pino log records go to the same endpoint and land in Loki, each carrying the trace and span id of the request that wrote it. No log agent or file scraping sits in between.
52
+
46
53
  ## Instalation
47
54
 
48
55
  ```
@@ -79,6 +86,11 @@ setupTracing({hostname: 'hostname', serviceName: 'service_name', url: 'endpoint'
79
86
  | url | string | tracing endpoint i.e. `<schema>://<host>:<port>` | Yes | `n/a` |
80
87
  | enableFsInstrumentation | boolean | enable FS instrumentation | No | `false` |
81
88
  | enableDnsInstrumentation | boolean | enable DNS instrumentation | No | `false` |
89
+ | enableMetrics | boolean | export metrics as well as traces | No | `true` |
90
+ | metricsUrl | string | metrics endpoint, when it differs from `url` | No | `url` |
91
+ | metricExportIntervalMillis | number | how often metrics are exported | No | `60000` |
92
+ | enableLogs | boolean | send Pino log records over OTLP | No | `true` |
93
+ | logsUrl | string | logs endpoint, when it differs from `url` | No | `url` |
82
94
 
83
95
  ## Documentation
84
96
 
package/libs/index.mjs CHANGED
@@ -17,17 +17,23 @@
17
17
  import {AwsInstrumentation} from '@opentelemetry/instrumentation-aws-sdk';
18
18
  import {BatchSpanProcessor} from '@opentelemetry/sdk-trace-base';
19
19
  import {ConnectInstrumentation} from '@opentelemetry/instrumentation-connect';
20
- import {diag, DiagConsoleLogger, DiagLogLevel} from '@opentelemetry/api';
20
+ 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
24
  import {ExpressInstrumentation} from '@opentelemetry/instrumentation-express';
25
+ import {logs} from '@opentelemetry/api-logs';
25
26
  import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
27
+ import {OTLPLogExporter} from '@opentelemetry/exporter-logs-otlp-grpc';
28
+ import {OTLPMetricExporter} from '@opentelemetry/exporter-metrics-otlp-grpc';
26
29
  import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc';
27
30
  import {PinoInstrumentation} from '@opentelemetry/instrumentation-pino';
28
31
  import {UndiciInstrumentation} from '@opentelemetry/instrumentation-undici';
29
32
  import {IORedisInstrumentation} from '@opentelemetry/instrumentation-ioredis';
30
33
  import {registerInstrumentations} from '@opentelemetry/instrumentation';
34
+ import {RuntimeNodeInstrumentation} from '@opentelemetry/instrumentation-runtime-node';
35
+ import {MeterProvider, PeriodicExportingMetricReader} from '@opentelemetry/sdk-metrics';
36
+ import {BatchLogRecordProcessor, LoggerProvider} from '@opentelemetry/sdk-logs';
31
37
  import {FsInstrumentation} from '@opentelemetry/instrumentation-fs';
32
38
  import {resourceFromAttributes, detectResources, envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector} from '@opentelemetry/resources';
33
39
  import {ATTR_SERVICE_NAME} from '@opentelemetry/semantic-conventions';
@@ -67,6 +73,8 @@ const setPeerService = (span, host) => {
67
73
  };
68
74
 
69
75
  let tracerProvider = null; // Declare provider in module scope for access in stopTracing
76
+ let meterProvider = null;
77
+ let loggerProvider = null;
70
78
 
71
79
  /**
72
80
  * Sets up tracing for the application using OpenTelemetry.
@@ -77,6 +85,10 @@ let tracerProvider = null; // Declare provider in module scope for access in sto
77
85
  * The IORedis instrumentation includes peer.service attributes for proper
78
86
  * service map visualization in distributed tracing tools like Tempo.
79
87
  *
88
+ * A MeterProvider is registered alongside it, which is what makes the
89
+ * instrumentations record the request duration histograms they already
90
+ * compute, and adds the Node runtime metrics.
91
+ *
80
92
  * @param {Object} options - Configuration options for tracing.
81
93
  * @param {string} [options.hostname=process.env.CONTAINER_NAME || process.env.HOSTNAME] - The hostname of the service.
82
94
  * @param {string} [options.serviceName=process.env.SERVICE_NAME] - The name of the service.
@@ -84,6 +96,11 @@ let tracerProvider = null; // Declare provider in module scope for access in sto
84
96
  * @param {number} [options.concurrencyLimit=10] - The concurrency limit for the exporter.
85
97
  * @param {boolean} [options.enableFsInstrumentation=false] - Enable file system instrumentation.
86
98
  * @param {boolean} [options.enableDnsInstrumentation=false] - Enable DNS instrumentation.
99
+ * @param {boolean} [options.enableMetrics=true] - Export metrics as well as traces.
100
+ * @param {string} [options.metricsUrl=options.url] - Endpoint for metrics, when it differs from the trace endpoint.
101
+ * @param {number} [options.metricExportIntervalMillis=60000] - How often metrics are exported.
102
+ * @param {boolean} [options.enableLogs=true] - Send Pino log records over OTLP.
103
+ * @param {string} [options.logsUrl=options.url] - Endpoint for logs, when it differs from the trace endpoint.
87
104
  *
88
105
  * @returns {Tracer} - The tracer for the service.
89
106
  */
@@ -101,6 +118,11 @@ export function setupTracing(options = {}) {
101
118
  concurrencyLimit = 10,
102
119
  enableFsInstrumentation = false,
103
120
  enableDnsInstrumentation = false,
121
+ enableMetrics = true,
122
+ metricsUrl = url,
123
+ metricExportIntervalMillis = 60000,
124
+ enableLogs = true,
125
+ logsUrl = url,
104
126
  } = options;
105
127
 
106
128
  // Validate required parameters
@@ -135,13 +157,46 @@ export function setupTracing(options = {}) {
135
157
  explicitAttributes[ATTR_CONTAINER_NAME] = hostname;
136
158
  }
137
159
 
160
+ // One resource for every signal. Grafana pairs a metric and a log line with
161
+ // a trace on service.name, so the providers carry an identical resource.
162
+ const resource = detectResources({
163
+ detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
164
+ }).merge(resourceFromAttributes(explicitAttributes));
165
+
138
166
  tracerProvider = new NodeTracerProvider({
139
167
  spanProcessors: [spanProcessor],
140
- resource: detectResources({
141
- detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
142
- }).merge(resourceFromAttributes(explicitAttributes)),
168
+ resource,
143
169
  });
144
170
 
171
+ if (enableMetrics) {
172
+ meterProvider = new MeterProvider({
173
+ resource,
174
+ readers: [
175
+ new PeriodicExportingMetricReader({
176
+ exporter: new OTLPMetricExporter({...exportOptions, url: metricsUrl}),
177
+ exportIntervalMillis: metricExportIntervalMillis,
178
+ }),
179
+ ],
180
+ });
181
+ metrics.setGlobalMeterProvider(meterProvider);
182
+ }
183
+
184
+ if (enableLogs) {
185
+ loggerProvider = new LoggerProvider({
186
+ resource,
187
+ processors: [
188
+ new BatchLogRecordProcessor({
189
+ exporter: new OTLPLogExporter({...exportOptions, url: logsUrl}),
190
+ maxQueueSize: 4096,
191
+ maxExportBatchSize: 1024,
192
+ scheduledDelayMillis: 2000,
193
+ exportTimeoutMillis: 10000,
194
+ }),
195
+ ],
196
+ });
197
+ logs.setGlobalLoggerProvider(loggerProvider);
198
+ }
199
+
145
200
  // Register globally. With no overrides, register() installs the modern
146
201
  // AsyncLocalStorageContextManager and a CompositePropagator of
147
202
  // W3CTraceContext + W3CBaggage - identical propagation to the previous
@@ -214,6 +269,10 @@ export function setupTracing(options = {}) {
214
269
  },
215
270
  }),
216
271
  new PinoInstrumentation({
272
+ // Log sending is on by default, and every record is parsed and rebuilt as
273
+ // a LogRecord before it reaches a logger. With no logger provider that
274
+ // work is done for a no-op, so turn it off rather than pay for nothing.
275
+ disableLogSending: !enableLogs,
217
276
  logHook: (span, record) => {
218
277
  // trace_id/span_id/trace_flags are injected by the instrumentation by
219
278
  // default; only add service name for better log correlation.
@@ -282,6 +341,10 @@ export function setupTracing(options = {}) {
282
341
  },
283
342
  }),
284
343
  new ElasticsearchInstrumentation(),
344
+ // Event loop delay, GC pauses and heap occupancy are metric-only, and they
345
+ // are what explains a whole service slowing at once. Constructed only with
346
+ // metrics on, since the collectors start sampling on construction.
347
+ ...(enableMetrics ? [new RuntimeNodeInstrumentation()] : []),
285
348
  // Spread so the optional instrumentations are constructed only when enabled:
286
349
  // FsInstrumentation patches fs on construction.
287
350
  ...(enableFsInstrumentation ? [new FsInstrumentation()] : []),
@@ -289,9 +352,12 @@ export function setupTracing(options = {}) {
289
352
  ...(enableDnsInstrumentation ? [new DnsInstrumentation({ignoreHostnames: ['localhost', '127.0.0.1', '::1']})] : []),
290
353
  ];
291
354
 
292
- // Register instrumentations
355
+ // Register instrumentations. Without meterProvider the instrumentations get
356
+ // the no-op meter, and the histograms they already record are discarded.
293
357
  registerInstrumentations({
294
358
  tracerProvider,
359
+ meterProvider,
360
+ loggerProvider,
295
361
  instrumentations,
296
362
  });
297
363
 
@@ -300,11 +366,11 @@ export function setupTracing(options = {}) {
300
366
  }
301
367
 
302
368
  /**
303
- * Gracefully stops the tracing by shutting down the tracer provider.
369
+ * Gracefully stops the tracing by shutting down every provider it registered.
304
370
  *
305
- * This function ensures that all pending spans are exported and resources are
306
- * cleaned up properly. It is recommended to call this function during the
307
- * application's shutdown process.
371
+ * This function ensures that all pending spans, metrics and log records are
372
+ * exported and resources are cleaned up properly. It is recommended to call
373
+ * this function during the application's shutdown process.
308
374
  *
309
375
  * @returns {Promise<void>} - A promise that resolves when shutdown is complete.
310
376
  */
@@ -320,6 +386,35 @@ export async function stopTracing() {
320
386
  } else {
321
387
  diag.warn('Tracer provider is not initialized.');
322
388
  }
389
+
390
+ // Separate from the trace shutdown, so a failing exporter on one signal
391
+ // still lets the other flush.
392
+ if (meterProvider) {
393
+ try {
394
+ await meterProvider.shutdown();
395
+ meterProvider = null;
396
+ // The API refuses a second setGlobalMeterProvider, so unregister here or
397
+ // a later setupTracing leaves the global pointing at a dead provider.
398
+ metrics.disable();
399
+ diag.info('Metrics have been successfully shut down.');
400
+ } catch (error) {
401
+ diag.error('Error during metrics shutdown:', error);
402
+ }
403
+ }
404
+
405
+ if (loggerProvider) {
406
+ try {
407
+ await loggerProvider.shutdown();
408
+ diag.info('Logs have been successfully shut down.');
409
+ } catch (error) {
410
+ diag.error('Error during logs shutdown:', error);
411
+ } finally {
412
+ // A second setGlobalLoggerProvider is ignored, so unregister whatever the
413
+ // flush did, or a later setupTracing keeps writing to a dead provider.
414
+ loggerProvider = null;
415
+ logs.disable();
416
+ }
417
+ }
323
418
  }
324
419
 
325
420
  /**
@@ -329,4 +424,6 @@ export async function stopTracing() {
329
424
  */
330
425
  export function __resetTracingForTesting() {
331
426
  tracerProvider = null;
427
+ meterProvider = null;
428
+ loggerProvider = null;
332
429
  }
@@ -1,6 +1,10 @@
1
1
  // index.test.mjs
2
2
  import { describe, it, beforeEach, afterEach } from 'node:test';
3
3
  import assert from 'node:assert';
4
+ import { metrics } from '@opentelemetry/api';
5
+ import { logs } from '@opentelemetry/api-logs';
6
+ import { MeterProvider } from '@opentelemetry/sdk-metrics';
7
+ import { LoggerProvider } from '@opentelemetry/sdk-logs';
4
8
  import { setupTracing, stopTracing, __resetTracingForTesting } from './index.mjs';
5
9
 
6
10
  describe('setupTracing', () => {
@@ -72,4 +76,99 @@ describe('setupTracing', () => {
72
76
  });
73
77
  assert.ok(tracer, 'tracer should be defined');
74
78
  });
79
+
80
+ // The http and undici instrumentations record their duration histograms
81
+ // whether or not a meter provider exists. Without one the API hands them the
82
+ // no-op meter and every measurement is dropped.
83
+ it('should register a global meter provider by default', () => {
84
+ setupTracing({
85
+ serviceName: 'test-service',
86
+ url: 'http://localhost:4317',
87
+ });
88
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'global meter provider should be the SDK one');
89
+ });
90
+
91
+ it('should leave the no-op meter provider in place when metrics are disabled', () => {
92
+ setupTracing({
93
+ serviceName: 'test-service',
94
+ url: 'http://localhost:4317',
95
+ enableMetrics: false,
96
+ });
97
+ assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider), 'no meter provider should be registered');
98
+ });
99
+
100
+ it('should accept a separate metrics endpoint', () => {
101
+ const tracer = setupTracing({
102
+ serviceName: 'test-service',
103
+ url: 'http://localhost:4317',
104
+ metricsUrl: 'http://localhost:4318',
105
+ });
106
+ assert.ok(tracer, 'tracer should be defined');
107
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'global meter provider should be the SDK one');
108
+ });
109
+
110
+ // The Pino instrumentation sends every log record to the Logs API whether or
111
+ // not a provider is registered. Without one the record is built and dropped.
112
+ it('should register a global logger provider by default', () => {
113
+ setupTracing({
114
+ serviceName: 'test-service',
115
+ url: 'http://localhost:4317',
116
+ });
117
+ assert.ok(logs.getLoggerProvider() instanceof LoggerProvider, 'global logger provider should be the SDK one');
118
+ });
119
+
120
+ it('should leave the no-op logger provider in place when logs are disabled', () => {
121
+ setupTracing({
122
+ serviceName: 'test-service',
123
+ url: 'http://localhost:4317',
124
+ enableLogs: false,
125
+ });
126
+ assert.ok(!(logs.getLoggerProvider() instanceof LoggerProvider), 'no logger provider should be registered');
127
+ });
128
+
129
+ it('should accept a separate logs endpoint', () => {
130
+ const tracer = setupTracing({
131
+ serviceName: 'test-service',
132
+ url: 'http://localhost:4317',
133
+ logsUrl: 'http://localhost:4318',
134
+ });
135
+ assert.ok(tracer, 'tracer should be defined');
136
+ assert.ok(logs.getLoggerProvider() instanceof LoggerProvider, 'global logger provider should be the SDK one');
137
+ });
138
+
139
+ // Without the unregister in stopTracing the API keeps the first provider and
140
+ // silently ignores the second registration.
141
+ it('should unregister the logger provider on shutdown', async () => {
142
+ setupTracing({
143
+ serviceName: 'test-service',
144
+ url: 'http://localhost:4317',
145
+ });
146
+ await stopTracing();
147
+ assert.ok(!(logs.getLoggerProvider() instanceof LoggerProvider), 'logger provider should be unregistered');
148
+
149
+ __resetTracingForTesting();
150
+ setupTracing({
151
+ serviceName: 'test-service',
152
+ url: 'http://localhost:4317',
153
+ });
154
+ assert.ok(logs.getLoggerProvider() instanceof LoggerProvider, 'a later setup should register again');
155
+ });
156
+
157
+ // Without the unregister in stopTracing the API refuses the second
158
+ // registration and the global keeps pointing at the shut-down provider.
159
+ it('should unregister the meter provider on shutdown', async () => {
160
+ setupTracing({
161
+ serviceName: 'test-service',
162
+ url: 'http://localhost:4317',
163
+ });
164
+ await stopTracing();
165
+ assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider), 'meter provider should be unregistered');
166
+
167
+ __resetTracingForTesting();
168
+ setupTracing({
169
+ serviceName: 'test-service',
170
+ url: 'http://localhost:4317',
171
+ });
172
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'a later setup should register again');
173
+ });
75
174
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
@@ -33,6 +33,9 @@
33
33
  "homepage": "https://github.com/saidsef/tracing-node#readme",
34
34
  "dependencies": {
35
35
  "@opentelemetry/api": "^1.9.1",
36
+ "@opentelemetry/api-logs": "^0.222.0",
37
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.222.0",
38
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.222.0",
36
39
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
37
40
  "@opentelemetry/instrumentation": "^0.222.0",
38
41
  "@opentelemetry/instrumentation-aws-sdk": "^0.77.0",
@@ -43,8 +46,11 @@
43
46
  "@opentelemetry/instrumentation-http": "^0.222.0",
44
47
  "@opentelemetry/instrumentation-ioredis": "^0.70.0",
45
48
  "@opentelemetry/instrumentation-pino": "^0.68.0",
49
+ "@opentelemetry/instrumentation-runtime-node": "^0.35.0",
46
50
  "@opentelemetry/instrumentation-undici": "^0.32.0",
47
51
  "@opentelemetry/resources": "^2.11.0",
52
+ "@opentelemetry/sdk-logs": "^0.222.0",
53
+ "@opentelemetry/sdk-metrics": "^2.11.0",
48
54
  "@opentelemetry/sdk-trace-base": "^2.11.0",
49
55
  "@opentelemetry/sdk-trace-node": "^2.11.0",
50
56
  "@opentelemetry/semantic-conventions": "^1.43.0",