@saidsef/tracing-node 5.0.1 → 6.1.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
@@ -15,7 +15,7 @@
15
15
  Full documentation: [tracing-node.readthedocs.io](https://tracing-node.readthedocs.io/).
16
16
 
17
17
  ## Prerequisites
18
- - NodeJS
18
+ - NodeJS >= 24.0.0
19
19
  - Observability
20
20
  - ...
21
21
  - Profit?
@@ -29,12 +29,32 @@ npm install @saidsef/tracing-node --save
29
29
  ## Usage
30
30
 
31
31
  ```javascript
32
+ // instrument.mjs
32
33
  import { setupTracing } from '@saidsef/tracing-node';
33
34
 
34
35
  setupTracing({hostname: 'hostname', serviceName: 'service_name', url: 'endpoint'});
35
36
  ```
36
37
 
37
- `serviceName` and `url` are required, and both fall back to the `SERVICE_NAME` and `ENDPOINT` environment variables. `setupTracing` has to run before the application imports the libraries being traced.
38
+ ```shell
39
+ node --import ./instrument.mjs ./app.mjs
40
+ ```
41
+
42
+ `serviceName` and `url` are required, and both fall back to the `SERVICE_NAME` and `ENDPOINT` environment variables.
43
+
44
+ `setupTracing` has to run before the application imports the libraries being traced, which is what the `--import` preload guarantees. The library registers the `import-in-the-middle` loader hook on import, so ES modules and CommonJS modules are both patched. Importing an instrumented package statically in the same file as the library loads it too early to be patched, so the preload is the form to reach for. [Initialisation order](https://tracing-node.readthedocs.io/en/latest/usage/#initialisation-order) covers the alternatives.
45
+
46
+ ## Collector and backend
47
+
48
+ The exporter speaks OTLP over gRPC, so any OpenTelemetry-compatible collector accepts all three signals. Point `url` at yours.
49
+
50
+ [**grafana-loki-on-k8s**](https://github.com/saidsef/grafana-loki-on-k8s) is the companion stack, and the one the end to end harness in [`test/e2e/`](./test/e2e) targets. It deploys Grafana, Prometheus, Mimir, Loki, Tempo, Pyroscope, Alloy and Beyla to Kubernetes as small composable manifests.
51
+
52
+ ```shell
53
+ git clone https://github.com/saidsef/grafana-loki-on-k8s
54
+ kubectl apply -k grafana-loki-on-k8s/deployment
55
+ ```
56
+
57
+ Traces sent to its Alloy OTLP receiver on port 4317 land in Tempo, log records in Loki and metrics in Mimir. Tempo's metrics generator turns the spans into RED and service graph metrics, which is what the `peer.service` attribute this library sets exists to feed.
38
58
 
39
59
  ## Documentation
40
60
 
@@ -12,7 +12,7 @@ spec:
12
12
  preemptionPolicy: PreemptLowerPriority
13
13
  containers:
14
14
  - name: tracing-node
15
- image: docker.io/node:20-alpine3.19
15
+ image: docker.io/node:24-alpine
16
16
  workingDir: "/app"
17
17
  command:
18
18
  - /bin/sh
@@ -0,0 +1,42 @@
1
+ /*
2
+ * Copyright Said Sef
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * https://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import {register} from 'node:module';
18
+
19
+ // A loader hook only reaches modules imported after it registers, so this lives
20
+ // in its own module and is the first import of index.mjs, ahead of every
21
+ // instrumentation. import.meta.url resolves the hook against this package:
22
+ // import-in-the-middle sits in the library's own tree, not the consumer's.
23
+
24
+ const FALSEY = ['false', '0'];
25
+
26
+ const enabled = !FALSEY.includes((process.env.TRACING_NODE_ESM_HOOK ?? '').toLowerCase());
27
+
28
+ let failure = null;
29
+
30
+ if (enabled) {
31
+ try {
32
+ register('import-in-the-middle/hook.mjs', import.meta.url);
33
+ } catch (error) {
34
+ failure = error;
35
+ }
36
+ }
37
+
38
+ /** Whether the ESM loader hook is in place, so ES module imports get patched. */
39
+ export const esmHookRegistered = enabled && failure === null;
40
+
41
+ /** The error that stopped registration, reported by index.mjs once diag has a logger. */
42
+ export const esmHookFailure = failure;
package/libs/index.mjs CHANGED
@@ -14,6 +14,9 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
+ // First, and deliberately: registering the ESM loader hook has to happen before
18
+ // any instrumented module is imported.
19
+ import {esmHookFailure} from './esm-hook.mjs';
17
20
  import {AwsInstrumentation} from '@opentelemetry/instrumentation-aws-sdk';
18
21
  import {BatchSpanProcessor} from '@opentelemetry/sdk-trace-base';
19
22
  import {ConnectInstrumentation} from '@opentelemetry/instrumentation-connect';
@@ -41,6 +44,12 @@ import {ATTR_CONTAINER_NAME} from '@opentelemetry/semantic-conventions/incubatin
41
44
 
42
45
  diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
43
46
 
47
+ // Reported here rather than where it happens, because the hook registers before
48
+ // this logger exists and the warning would go nowhere.
49
+ if (esmHookFailure) {
50
+ diag.warn(`ESM loader hook not registered, so ES module imports are not instrumented: ${esmHookFailure.message}`);
51
+ }
52
+
44
53
  // Set a non-negative integer span attribute from a header value; ignore invalid input.
45
54
  const setIntAttribute = (span, name, value) => {
46
55
  if (!value) return;
@@ -1,6 +1,7 @@
1
1
  // index.test.mjs
2
2
  import { describe, it, beforeEach, afterEach } from 'node:test';
3
3
  import assert from 'node:assert';
4
+ import { execFileSync } from 'node:child_process';
4
5
  import { metrics } from '@opentelemetry/api';
5
6
  import { logs } from '@opentelemetry/api-logs';
6
7
  import { MeterProvider } from '@opentelemetry/sdk-metrics';
@@ -262,3 +263,36 @@ describe('express request hook', () => {
262
263
  assert.deepStrictEqual(span.attributes, {});
263
264
  });
264
265
  });
266
+
267
+ // A loader hook registers once per process, so the opt-out cannot be exercised
268
+ // in this one. Each case reads the module in a child process instead.
269
+ describe('ESM loader hook', () => {
270
+ const hookModule = new URL('./esm-hook.mjs', import.meta.url).href;
271
+
272
+ const registeredWith = (value) => {
273
+ const env = {...process.env};
274
+ delete env.TRACING_NODE_ESM_HOOK;
275
+ if (value !== undefined) {
276
+ env.TRACING_NODE_ESM_HOOK = value;
277
+ }
278
+ return execFileSync(process.execPath, [
279
+ '--input-type=module',
280
+ '--eval',
281
+ `import {esmHookRegistered} from ${JSON.stringify(hookModule)}; console.log(esmHookRegistered);`,
282
+ ], {env, encoding: 'utf8'}).trim();
283
+ };
284
+
285
+ it('should register by default', () => {
286
+ assert.strictEqual(registeredWith(undefined), 'true');
287
+ });
288
+
289
+ it('should skip registration when opted out', () => {
290
+ assert.strictEqual(registeredWith('false'), 'false');
291
+ assert.strictEqual(registeredWith('0'), 'false');
292
+ });
293
+
294
+ it('should register for any other value', () => {
295
+ assert.strictEqual(registeredWith('true'), 'true');
296
+ assert.strictEqual(registeredWith(''), 'true');
297
+ });
298
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saidsef/tracing-node",
3
- "version": "5.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
@@ -25,7 +25,7 @@
25
25
  "author": "Said Sef <saidsef@gmail.com>",
26
26
  "license": "Apache-2.0",
27
27
  "engines": {
28
- "node": ">= 20.6.0"
28
+ "node": ">= 24.0.0"
29
29
  },
30
30
  "bugs": {
31
31
  "url": "https://github.com/saidsef/tracing-node/issues"
@@ -54,6 +54,7 @@
54
54
  "@opentelemetry/sdk-trace-base": "^2.11.0",
55
55
  "@opentelemetry/sdk-trace-node": "^2.11.0",
56
56
  "@opentelemetry/semantic-conventions": "^1.43.0",
57
+ "import-in-the-middle": "^3.0.0",
57
58
  "opentelemetry-instrumentation-elasticsearch": "^0.41.0"
58
59
  },
59
60
  "devDependencies": {