@saidsef/tracing-node 4.3.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
@@ -22,6 +22,7 @@ Effortlessly supercharge your applications with world-class distributed tracing!
22
22
  | AWS SDK | Cloud service instrumentation |
23
23
  | Pino logger | Integration with trace/span IDs |
24
24
  | Node runtime metrics | Event loop, garbage collection, heap |
25
+ | Log export | Pino records over OTLP, correlated by trace |
25
26
  | RED metrics | Request duration histograms over OTLP |
26
27
  | DNS/FS instrumentation | Optional monitoring |
27
28
  | Resource detection | Host, OS, process, container |
@@ -47,6 +48,8 @@ The W3C Trace Context propagation this library registers is what lets Tempo pair
47
48
 
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.
49
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
+
50
53
  ## Instalation
51
54
 
52
55
  ```
@@ -86,6 +89,8 @@ setupTracing({hostname: 'hostname', serviceName: 'service_name', url: 'endpoint'
86
89
  | enableMetrics | boolean | export metrics as well as traces | No | `true` |
87
90
  | metricsUrl | string | metrics endpoint, when it differs from `url` | No | `url` |
88
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` |
89
94
 
90
95
  ## Documentation
91
96
 
package/libs/index.mjs CHANGED
@@ -22,7 +22,9 @@ 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';
26
28
  import {OTLPMetricExporter} from '@opentelemetry/exporter-metrics-otlp-grpc';
27
29
  import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc';
28
30
  import {PinoInstrumentation} from '@opentelemetry/instrumentation-pino';
@@ -31,6 +33,7 @@ import {IORedisInstrumentation} from '@opentelemetry/instrumentation-ioredis';
31
33
  import {registerInstrumentations} from '@opentelemetry/instrumentation';
32
34
  import {RuntimeNodeInstrumentation} from '@opentelemetry/instrumentation-runtime-node';
33
35
  import {MeterProvider, PeriodicExportingMetricReader} from '@opentelemetry/sdk-metrics';
36
+ import {BatchLogRecordProcessor, LoggerProvider} from '@opentelemetry/sdk-logs';
34
37
  import {FsInstrumentation} from '@opentelemetry/instrumentation-fs';
35
38
  import {resourceFromAttributes, detectResources, envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector} from '@opentelemetry/resources';
36
39
  import {ATTR_SERVICE_NAME} from '@opentelemetry/semantic-conventions';
@@ -71,6 +74,7 @@ const setPeerService = (span, host) => {
71
74
 
72
75
  let tracerProvider = null; // Declare provider in module scope for access in stopTracing
73
76
  let meterProvider = null;
77
+ let loggerProvider = null;
74
78
 
75
79
  /**
76
80
  * Sets up tracing for the application using OpenTelemetry.
@@ -95,6 +99,8 @@ let meterProvider = null;
95
99
  * @param {boolean} [options.enableMetrics=true] - Export metrics as well as traces.
96
100
  * @param {string} [options.metricsUrl=options.url] - Endpoint for metrics, when it differs from the trace endpoint.
97
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.
98
104
  *
99
105
  * @returns {Tracer} - The tracer for the service.
100
106
  */
@@ -115,6 +121,8 @@ export function setupTracing(options = {}) {
115
121
  enableMetrics = true,
116
122
  metricsUrl = url,
117
123
  metricExportIntervalMillis = 60000,
124
+ enableLogs = true,
125
+ logsUrl = url,
118
126
  } = options;
119
127
 
120
128
  // Validate required parameters
@@ -149,8 +157,8 @@ export function setupTracing(options = {}) {
149
157
  explicitAttributes[ATTR_CONTAINER_NAME] = hostname;
150
158
  }
151
159
 
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.
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.
154
162
  const resource = detectResources({
155
163
  detectors: [envDetector, hostDetector, osDetector, processDetector, serviceInstanceIdDetector],
156
164
  }).merge(resourceFromAttributes(explicitAttributes));
@@ -173,6 +181,22 @@ export function setupTracing(options = {}) {
173
181
  metrics.setGlobalMeterProvider(meterProvider);
174
182
  }
175
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
+
176
200
  // Register globally. With no overrides, register() installs the modern
177
201
  // AsyncLocalStorageContextManager and a CompositePropagator of
178
202
  // W3CTraceContext + W3CBaggage - identical propagation to the previous
@@ -245,6 +269,10 @@ export function setupTracing(options = {}) {
245
269
  },
246
270
  }),
247
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,
248
276
  logHook: (span, record) => {
249
277
  // trace_id/span_id/trace_flags are injected by the instrumentation by
250
278
  // default; only add service name for better log correlation.
@@ -329,6 +357,7 @@ export function setupTracing(options = {}) {
329
357
  registerInstrumentations({
330
358
  tracerProvider,
331
359
  meterProvider,
360
+ loggerProvider,
332
361
  instrumentations,
333
362
  });
334
363
 
@@ -337,11 +366,11 @@ export function setupTracing(options = {}) {
337
366
  }
338
367
 
339
368
  /**
340
- * Gracefully stops the tracing by shutting down the tracer and meter providers.
369
+ * Gracefully stops the tracing by shutting down every provider it registered.
341
370
  *
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.
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.
345
374
  *
346
375
  * @returns {Promise<void>} - A promise that resolves when shutdown is complete.
347
376
  */
@@ -372,6 +401,20 @@ export async function stopTracing() {
372
401
  diag.error('Error during metrics shutdown:', error);
373
402
  }
374
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
+ }
375
418
  }
376
419
 
377
420
  /**
@@ -382,4 +425,5 @@ export async function stopTracing() {
382
425
  export function __resetTracingForTesting() {
383
426
  tracerProvider = null;
384
427
  meterProvider = null;
428
+ loggerProvider = null;
385
429
  }
@@ -2,7 +2,9 @@
2
2
  import { describe, it, beforeEach, afterEach } from 'node:test';
3
3
  import assert from 'node:assert';
4
4
  import { metrics } from '@opentelemetry/api';
5
+ import { logs } from '@opentelemetry/api-logs';
5
6
  import { MeterProvider } from '@opentelemetry/sdk-metrics';
7
+ import { LoggerProvider } from '@opentelemetry/sdk-logs';
6
8
  import { setupTracing, stopTracing, __resetTracingForTesting } from './index.mjs';
7
9
 
8
10
  describe('setupTracing', () => {
@@ -105,6 +107,53 @@ describe('setupTracing', () => {
105
107
  assert.ok(metrics.getMeterProvider() instanceof MeterProvider, 'global meter provider should be the SDK one');
106
108
  });
107
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
+
108
157
  // Without the unregister in stopTracing the API refuses the second
109
158
  // registration and the global keeps pointing at the shut-down provider.
110
159
  it('should unregister the meter provider on shutdown', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "4.3.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,8 @@
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",
36
38
  "@opentelemetry/exporter-metrics-otlp-grpc": "^0.222.0",
37
39
  "@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
38
40
  "@opentelemetry/instrumentation": "^0.222.0",
@@ -47,6 +49,7 @@
47
49
  "@opentelemetry/instrumentation-runtime-node": "^0.35.0",
48
50
  "@opentelemetry/instrumentation-undici": "^0.32.0",
49
51
  "@opentelemetry/resources": "^2.11.0",
52
+ "@opentelemetry/sdk-logs": "^0.222.0",
50
53
  "@opentelemetry/sdk-metrics": "^2.11.0",
51
54
  "@opentelemetry/sdk-trace-base": "^2.11.0",
52
55
  "@opentelemetry/sdk-trace-node": "^2.11.0",