@saidsef/tracing-node 4.0.0 → 4.1.1

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
@@ -15,6 +15,7 @@ Effortlessly supercharge your applications with world-class distributed tracing!
15
15
  | Feature | Description |
16
16
  |---------|-------------|
17
17
  | HTTP/HTTPS instrumentation | Automatic service detection |
18
+ | fetch/undici instrumentation | Outgoing `globalThis.fetch` calls |
18
19
  | Express.js support | Framework instrumentation |
19
20
  | Elasticsearch client | Database instrumentation |
20
21
  | IORedis client | Cache instrumentation |
@@ -30,38 +31,27 @@ Effortlessly supercharge your applications with world-class distributed tracing!
30
31
  - ...
31
32
  - Profit?
32
33
 
33
- ## Instalation
34
+ ## Where the traces go
34
35
 
35
- ```
36
- npm install @saidsef/tracing-node --save
37
- ```
36
+ `setupTracing` exports OTLP over gRPC, so any OpenTelemetry-compatible collector or backend will take it - point `url` at yours.
37
+
38
+ If you do not have one yet, [grafana-loki-on-k8s](https://github.com/saidsef/grafana-loki-on-k8s) is a companion project that deploys the full LGTM+ stack - Grafana, Prometheus, Mimir, Loki, Tempo, Pyroscope, Alloy and Beyla - to Kubernetes with `kubectl apply -k ./deployment`, broken into small composable manifests rather than a single opaque chart. Send traces to its Alloy OTLP receiver and they land in Tempo, with the metrics-generator turning them into RED and service-graph metrics in Mimir:
38
39
 
39
- ## Upgrading to 4.0.0
40
+ ```javascript
41
+ setupTracing({serviceName: 'my-service', url: 'http://alloy:4317'});
42
+ ```
40
43
 
41
- **Breaking change: spans now carry the stable OpenTelemetry semantic conventions.**
44
+ 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.
42
45
 
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.
46
+ ## Instalation
44
47
 
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 |
48
+ ```
49
+ npm install @saidsef/tracing-node --save
50
+ ```
61
51
 
62
- Server-side HTTP metrics also move from `http.server.duration` (milliseconds) to `http.server.request.duration` (seconds), and the client equivalents likewise.
52
+ ## Upgrading
63
53
 
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.
54
+ Breaking changes and the attribute renames they bring are recorded in the [release notes](https://github.com/saidsef/tracing-node/releases) for the version concerned.
65
55
 
66
56
  ## Usage
67
57
 
@@ -92,7 +82,7 @@ setupTracing({hostname: 'hostname', serviceName: 'service_name', url: 'endpoint'
92
82
 
93
83
  ## Source
94
84
 
95
- Our latest and greatest source of `tracing-node` can be found on [GitHub](https://github.com/saidsef/tracing-nodec/fork). Fork us!
85
+ Our latest and greatest source of `tracing-node` can be found on [GitHub](https://github.com/saidsef/tracing-node/fork). Fork us!
96
86
 
97
87
  ## Contributing
98
88
 
package/libs/index.mjs CHANGED
@@ -25,6 +25,7 @@ import {ExpressInstrumentation} from '@opentelemetry/instrumentation-express';
25
25
  import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
26
26
  import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-grpc';
27
27
  import {PinoInstrumentation} from '@opentelemetry/instrumentation-pino';
28
+ import {UndiciInstrumentation} from '@opentelemetry/instrumentation-undici';
28
29
  import {IORedisInstrumentation} from '@opentelemetry/instrumentation-ioredis';
29
30
  import {registerInstrumentations} from '@opentelemetry/instrumentation';
30
31
  import {FsInstrumentation} from '@opentelemetry/instrumentation-fs';
@@ -50,6 +51,21 @@ const truncateArg = (value, limit) => {
50
51
  return str.length > limit ? `${str.substring(0, limit)}...` : str;
51
52
  };
52
53
 
54
+ // Tempo names a service-graph node from peer.service, which no instrumentation
55
+ // emits, so both the http and undici hooks below derive it from the remote host.
56
+ const PEER_SERVICES = ['elasticsearch', 'redis'];
57
+
58
+ const setPeerService = (span, host) => {
59
+ if (!host) return;
60
+ for (const service of PEER_SERVICES) {
61
+ if (host.includes(service)) {
62
+ span.setAttribute('peer.service', service);
63
+ span.setAttribute('db.system.name', service);
64
+ return;
65
+ }
66
+ }
67
+ };
68
+
53
69
  let tracerProvider = null; // Declare provider in module scope for access in stopTracing
54
70
 
55
71
  /**
@@ -134,18 +150,7 @@ export function setupTracing(options = {}) {
134
150
 
135
151
  // Only an outgoing ClientRequest carries .host, so bailing without it keeps
136
152
  // server spans out: peer.service must name the remote service being called.
137
- const applyCustomAttributesOnSpan = (span, request) => {
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
- }
148
- };
153
+ const applyCustomAttributesOnSpan = (span, request) => setPeerService(span, request?.host);
149
154
 
150
155
  const instrumentations = [
151
156
  new HttpInstrumentation({
@@ -178,6 +183,14 @@ export function setupTracing(options = {}) {
178
183
  if (requestId) span.setAttribute('http.request_id', requestId);
179
184
  },
180
185
  }),
186
+ // globalThis.fetch runs on undici, which never touches the http/https
187
+ // modules HttpInstrumentation patches. Without this, fetch calls produce no
188
+ // client span and inject no traceparent, so the callee starts a new trace
189
+ // and the two services can never be paired into a service-graph edge.
190
+ new UndiciInstrumentation({
191
+ // UndiciRequest exposes origin, not the .host the http hook reads.
192
+ requestHook: (span, request) => setPeerService(span, request?.origin),
193
+ }),
181
194
  new ExpressInstrumentation({
182
195
  requestHook: (span, info) => {
183
196
  // info is ExpressRequestInfo: { request, route, layerType }
@@ -49,6 +49,20 @@ describe('setupTracing', () => {
49
49
  assert.ok(tracer, 'tracer should be defined');
50
50
  });
51
51
 
52
+ // globalThis.fetch runs on undici, so it is invisible to HttpInstrumentation.
53
+ // Registering it must not disturb setup, and the patch has to land on the
54
+ // global fetch itself - otherwise outgoing calls carry no traceparent.
55
+ it('should instrument global fetch', () => {
56
+ const before = globalThis.fetch;
57
+ const tracer = setupTracing({
58
+ serviceName: 'test-service',
59
+ url: 'http://localhost:4317',
60
+ });
61
+ assert.ok(tracer, 'tracer should be defined');
62
+ assert.strictEqual(typeof globalThis.fetch, 'function', 'global fetch should still be callable');
63
+ assert.ok(before, 'global fetch should exist on a supported runtime');
64
+ });
65
+
52
66
  it('should accept optional instrumentations', () => {
53
67
  const tracer = setupTracing({
54
68
  serviceName: 'test-service',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
@@ -23,7 +23,7 @@
23
23
  "author": "Said Sef <saidsef@gmail.com>",
24
24
  "license": "Apache-2.0",
25
25
  "engines": {
26
- "node": ">= 20"
26
+ "node": ">= 20.6.0"
27
27
  },
28
28
  "bugs": {
29
29
  "url": "https://github.com/saidsef/tracing-node/issues"
@@ -41,6 +41,7 @@
41
41
  "@opentelemetry/instrumentation-http": "^0.221.0",
42
42
  "@opentelemetry/instrumentation-ioredis": "^0.69.0",
43
43
  "@opentelemetry/instrumentation-pino": "^0.67.0",
44
+ "@opentelemetry/instrumentation-undici": "^0.31.0",
44
45
  "@opentelemetry/resources": "^2.10.0",
45
46
  "@opentelemetry/sdk-trace-base": "^2.10.0",
46
47
  "@opentelemetry/sdk-trace-node": "^2.10.0",