@saidsef/tracing-node 4.1.1 → 4.3.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,8 @@ 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
+ | RED metrics | Request duration histograms over OTLP |
24
26
  | DNS/FS instrumentation | Optional monitoring |
25
27
  | Resource detection | Host, OS, process, container |
26
28
  | W3C Trace Context | Standard propagation |
@@ -43,6 +45,8 @@ setupTracing({serviceName: 'my-service', url: 'http://alloy:4317'});
43
45
 
44
46
  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
47
 
48
+ 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.
49
+
46
50
  ## Instalation
47
51
 
48
52
  ```
@@ -79,6 +83,26 @@ setupTracing({hostname: 'hostname', serviceName: 'service_name', url: 'endpoint'
79
83
  | url | string | tracing endpoint i.e. `<schema>://<host>:<port>` | Yes | `n/a` |
80
84
  | enableFsInstrumentation | boolean | enable FS instrumentation | No | `false` |
81
85
  | enableDnsInstrumentation | boolean | enable DNS instrumentation | No | `false` |
86
+ | enableMetrics | boolean | export metrics as well as traces | No | `true` |
87
+ | metricsUrl | string | metrics endpoint, when it differs from `url` | No | `url` |
88
+ | metricExportIntervalMillis | number | how often metrics are exported | No | `60000` |
89
+
90
+ ## Documentation
91
+
92
+ Full documentation is in the [`docs/`](./docs) directory and built with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/).
93
+
94
+ Live docs: [tracing-node.readthedocs.io](https://tracing-node.readthedocs.io/)
95
+
96
+ | Page | Contents |
97
+ |------|----------|
98
+ | [Architecture](./docs/architecture.md) | The pipeline `setupTracing` builds, and how the service graph is fed |
99
+ | [Configuration](./docs/usage.md) | Options, environment variables, initialisation order and shutdown |
100
+ | [Instrumentation](./docs/instrumentation.md) | Each instrumentation, and the attributes it emits |
101
+ | [Deployment](./docs/deployment.md) | Running instrumented services in containers and Kubernetes |
102
+ | [Testing](./docs/testing.md) | The unit tests and the end to end harness |
103
+ | [Troubleshooting](./docs/troubleshooting.md) | Symptoms, causes and fixes |
104
+
105
+ Build them locally with `npm run build-docs`, which renders the site into `site/`.
82
106
 
83
107
  ## Source
84
108
 
package/libs/index.mjs CHANGED
@@ -17,17 +17,20 @@
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
25
  import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
26
+ import {OTLPMetricExporter} from '@opentelemetry/exporter-metrics-otlp-grpc';
26
27
  import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc';
27
28
  import {PinoInstrumentation} from '@opentelemetry/instrumentation-pino';
28
29
  import {UndiciInstrumentation} from '@opentelemetry/instrumentation-undici';
29
30
  import {IORedisInstrumentation} from '@opentelemetry/instrumentation-ioredis';
30
31
  import {registerInstrumentations} from '@opentelemetry/instrumentation';
32
+ import {RuntimeNodeInstrumentation} from '@opentelemetry/instrumentation-runtime-node';
33
+ import {MeterProvider, PeriodicExportingMetricReader} from '@opentelemetry/sdk-metrics';
31
34
  import {FsInstrumentation} from '@opentelemetry/instrumentation-fs';
32
35
  import {resourceFromAttributes, detectResources, envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector} from '@opentelemetry/resources';
33
36
  import {ATTR_SERVICE_NAME} from '@opentelemetry/semantic-conventions';
@@ -67,6 +70,7 @@ const setPeerService = (span, host) => {
67
70
  };
68
71
 
69
72
  let tracerProvider = null; // Declare provider in module scope for access in stopTracing
73
+ let meterProvider = null;
70
74
 
71
75
  /**
72
76
  * Sets up tracing for the application using OpenTelemetry.
@@ -77,6 +81,10 @@ let tracerProvider = null; // Declare provider in module scope for access in sto
77
81
  * The IORedis instrumentation includes peer.service attributes for proper
78
82
  * service map visualization in distributed tracing tools like Tempo.
79
83
  *
84
+ * A MeterProvider is registered alongside it, which is what makes the
85
+ * instrumentations record the request duration histograms they already
86
+ * compute, and adds the Node runtime metrics.
87
+ *
80
88
  * @param {Object} options - Configuration options for tracing.
81
89
  * @param {string} [options.hostname=process.env.CONTAINER_NAME || process.env.HOSTNAME] - The hostname of the service.
82
90
  * @param {string} [options.serviceName=process.env.SERVICE_NAME] - The name of the service.
@@ -84,6 +92,9 @@ let tracerProvider = null; // Declare provider in module scope for access in sto
84
92
  * @param {number} [options.concurrencyLimit=10] - The concurrency limit for the exporter.
85
93
  * @param {boolean} [options.enableFsInstrumentation=false] - Enable file system instrumentation.
86
94
  * @param {boolean} [options.enableDnsInstrumentation=false] - Enable DNS instrumentation.
95
+ * @param {boolean} [options.enableMetrics=true] - Export metrics as well as traces.
96
+ * @param {string} [options.metricsUrl=options.url] - Endpoint for metrics, when it differs from the trace endpoint.
97
+ * @param {number} [options.metricExportIntervalMillis=60000] - How often metrics are exported.
87
98
  *
88
99
  * @returns {Tracer} - The tracer for the service.
89
100
  */
@@ -101,6 +112,9 @@ export function setupTracing(options = {}) {
101
112
  concurrencyLimit = 10,
102
113
  enableFsInstrumentation = false,
103
114
  enableDnsInstrumentation = false,
115
+ enableMetrics = true,
116
+ metricsUrl = url,
117
+ metricExportIntervalMillis = 60000,
104
118
  } = options;
105
119
 
106
120
  // Validate required parameters
@@ -135,13 +149,30 @@ export function setupTracing(options = {}) {
135
149
  explicitAttributes[ATTR_CONTAINER_NAME] = hostname;
136
150
  }
137
151
 
152
+ // One resource for both signals. Grafana pairs a metric with a trace on
153
+ // service.name, so the two providers have to carry an identical resource.
154
+ const resource = detectResources({
155
+ detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
156
+ }).merge(resourceFromAttributes(explicitAttributes));
157
+
138
158
  tracerProvider = new NodeTracerProvider({
139
159
  spanProcessors: [spanProcessor],
140
- resource: detectResources({
141
- detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
142
- }).merge(resourceFromAttributes(explicitAttributes)),
160
+ resource,
143
161
  });
144
162
 
163
+ if (enableMetrics) {
164
+ meterProvider = new MeterProvider({
165
+ resource,
166
+ readers: [
167
+ new PeriodicExportingMetricReader({
168
+ exporter: new OTLPMetricExporter({...exportOptions, url: metricsUrl}),
169
+ exportIntervalMillis: metricExportIntervalMillis,
170
+ }),
171
+ ],
172
+ });
173
+ metrics.setGlobalMeterProvider(meterProvider);
174
+ }
175
+
145
176
  // Register globally. With no overrides, register() installs the modern
146
177
  // AsyncLocalStorageContextManager and a CompositePropagator of
147
178
  // W3CTraceContext + W3CBaggage - identical propagation to the previous
@@ -282,6 +313,10 @@ export function setupTracing(options = {}) {
282
313
  },
283
314
  }),
284
315
  new ElasticsearchInstrumentation(),
316
+ // Event loop delay, GC pauses and heap occupancy are metric-only, and they
317
+ // are what explains a whole service slowing at once. Constructed only with
318
+ // metrics on, since the collectors start sampling on construction.
319
+ ...(enableMetrics ? [new RuntimeNodeInstrumentation()] : []),
285
320
  // Spread so the optional instrumentations are constructed only when enabled:
286
321
  // FsInstrumentation patches fs on construction.
287
322
  ...(enableFsInstrumentation ? [new FsInstrumentation()] : []),
@@ -289,9 +324,11 @@ export function setupTracing(options = {}) {
289
324
  ...(enableDnsInstrumentation ? [new DnsInstrumentation({ignoreHostnames: ['localhost', '127.0.0.1', '::1']})] : []),
290
325
  ];
291
326
 
292
- // Register instrumentations
327
+ // Register instrumentations. Without meterProvider the instrumentations get
328
+ // the no-op meter, and the histograms they already record are discarded.
293
329
  registerInstrumentations({
294
330
  tracerProvider,
331
+ meterProvider,
295
332
  instrumentations,
296
333
  });
297
334
 
@@ -300,11 +337,11 @@ export function setupTracing(options = {}) {
300
337
  }
301
338
 
302
339
  /**
303
- * Gracefully stops the tracing by shutting down the tracer provider.
340
+ * Gracefully stops the tracing by shutting down the tracer and meter providers.
304
341
  *
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.
342
+ * This function ensures that all pending spans and metrics are exported and
343
+ * resources are cleaned up properly. It is recommended to call this function
344
+ * during the application's shutdown process.
308
345
  *
309
346
  * @returns {Promise<void>} - A promise that resolves when shutdown is complete.
310
347
  */
@@ -320,6 +357,21 @@ export async function stopTracing() {
320
357
  } else {
321
358
  diag.warn('Tracer provider is not initialized.');
322
359
  }
360
+
361
+ // Separate from the trace shutdown, so a failing exporter on one signal
362
+ // still lets the other flush.
363
+ if (meterProvider) {
364
+ try {
365
+ await meterProvider.shutdown();
366
+ meterProvider = null;
367
+ // The API refuses a second setGlobalMeterProvider, so unregister here or
368
+ // a later setupTracing leaves the global pointing at a dead provider.
369
+ metrics.disable();
370
+ diag.info('Metrics have been successfully shut down.');
371
+ } catch (error) {
372
+ diag.error('Error during metrics shutdown:', error);
373
+ }
374
+ }
323
375
  }
324
376
 
325
377
  /**
@@ -329,4 +381,5 @@ export async function stopTracing() {
329
381
  */
330
382
  export function __resetTracingForTesting() {
331
383
  tracerProvider = null;
384
+ meterProvider = null;
332
385
  }
@@ -1,6 +1,8 @@
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 { MeterProvider } from '@opentelemetry/sdk-metrics';
4
6
  import { setupTracing, stopTracing, __resetTracingForTesting } from './index.mjs';
5
7
 
6
8
  describe('setupTracing', () => {
@@ -72,4 +74,52 @@ describe('setupTracing', () => {
72
74
  });
73
75
  assert.ok(tracer, 'tracer should be defined');
74
76
  });
77
+
78
+ // The http and undici instrumentations record their duration histograms
79
+ // whether or not a meter provider exists. Without one the API hands them the
80
+ // no-op meter and every measurement is dropped.
81
+ it('should register a global meter provider by default', () => {
82
+ setupTracing({
83
+ serviceName: 'test-service',
84
+ url: 'http://localhost:4317',
85
+ });
86
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'global meter provider should be the SDK one');
87
+ });
88
+
89
+ it('should leave the no-op meter provider in place when metrics are disabled', () => {
90
+ setupTracing({
91
+ serviceName: 'test-service',
92
+ url: 'http://localhost:4317',
93
+ enableMetrics: false,
94
+ });
95
+ assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider), 'no meter provider should be registered');
96
+ });
97
+
98
+ it('should accept a separate metrics endpoint', () => {
99
+ const tracer = setupTracing({
100
+ serviceName: 'test-service',
101
+ url: 'http://localhost:4317',
102
+ metricsUrl: 'http://localhost:4318',
103
+ });
104
+ assert.ok(tracer, 'tracer should be defined');
105
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'global meter provider should be the SDK one');
106
+ });
107
+
108
+ // Without the unregister in stopTracing the API refuses the second
109
+ // registration and the global keeps pointing at the shut-down provider.
110
+ it('should unregister the meter provider on shutdown', async () => {
111
+ setupTracing({
112
+ serviceName: 'test-service',
113
+ url: 'http://localhost:4317',
114
+ });
115
+ await stopTracing();
116
+ assert.ok(!(metrics.getMeterProvider() instanceof MeterProvider), 'meter provider should be unregistered');
117
+
118
+ __resetTracingForTesting();
119
+ setupTracing({
120
+ serviceName: 'test-service',
121
+ url: 'http://localhost:4317',
122
+ });
123
+ assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'a later setup should register again');
124
+ });
75
125
  });
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "4.1.1",
3
+ "version": "4.3.0",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
7
7
  "test": "node --trace-warnings --test --report-uncaught-exception libs/index.test.mjs",
8
8
  "lint": "eslint .",
9
- "rebuild": "rm -rfv node_modules/ package-lock.json && npm install --prod --omit=dev"
9
+ "rebuild": "rm -rfv node_modules/ package-lock.json && npm install --prod --omit=dev",
10
+ "build-docs": "podman run --rm -v .:/docs docker.io/squidfunk/mkdocs-material:9 build",
11
+ "clean-docs": "rm -rf site"
10
12
  },
11
13
  "type": "module",
12
14
  "private": false,
@@ -30,32 +32,35 @@
30
32
  },
31
33
  "homepage": "https://github.com/saidsef/tracing-node#readme",
32
34
  "dependencies": {
33
- "@opentelemetry/api": "^1.9.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
- "@opentelemetry/instrumentation-dns": "^0.64.0",
39
- "@opentelemetry/instrumentation-express": "^0.69.0",
40
- "@opentelemetry/instrumentation-fs": "^0.40.0",
41
- "@opentelemetry/instrumentation-http": "^0.221.0",
42
- "@opentelemetry/instrumentation-ioredis": "^0.69.0",
43
- "@opentelemetry/instrumentation-pino": "^0.67.0",
44
- "@opentelemetry/instrumentation-undici": "^0.31.0",
45
- "@opentelemetry/resources": "^2.10.0",
46
- "@opentelemetry/sdk-trace-base": "^2.10.0",
47
- "@opentelemetry/sdk-trace-node": "^2.10.0",
35
+ "@opentelemetry/api": "^1.9.1",
36
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.222.0",
37
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
38
+ "@opentelemetry/instrumentation": "^0.222.0",
39
+ "@opentelemetry/instrumentation-aws-sdk": "^0.77.0",
40
+ "@opentelemetry/instrumentation-connect": "^0.65.0",
41
+ "@opentelemetry/instrumentation-dns": "^0.65.0",
42
+ "@opentelemetry/instrumentation-express": "^0.70.0",
43
+ "@opentelemetry/instrumentation-fs": "^0.41.0",
44
+ "@opentelemetry/instrumentation-http": "^0.222.0",
45
+ "@opentelemetry/instrumentation-ioredis": "^0.70.0",
46
+ "@opentelemetry/instrumentation-pino": "^0.68.0",
47
+ "@opentelemetry/instrumentation-runtime-node": "^0.35.0",
48
+ "@opentelemetry/instrumentation-undici": "^0.32.0",
49
+ "@opentelemetry/resources": "^2.11.0",
50
+ "@opentelemetry/sdk-metrics": "^2.11.0",
51
+ "@opentelemetry/sdk-trace-base": "^2.11.0",
52
+ "@opentelemetry/sdk-trace-node": "^2.11.0",
48
53
  "@opentelemetry/semantic-conventions": "^1.43.0",
49
54
  "opentelemetry-instrumentation-elasticsearch": "^0.41.0"
50
55
  },
51
56
  "devDependencies": {
52
- "eslint": "^10.8.0"
57
+ "eslint": "^10.10.0"
53
58
  },
54
59
  "overrides": {
55
60
  "protobufjs": "^7.5.3",
56
- "@opentelemetry/core": "^2.10.0"
61
+ "@opentelemetry/core": "^2.11.0"
57
62
  },
58
63
  "allowScripts": {
59
- "protobufjs@7.6.5": true
64
+ "protobufjs@7.6.6": true
60
65
  }
61
66
  }