@saidsef/tracing-node 6.0.0 → 7.0.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
@@ -26,15 +26,30 @@ Full documentation: [tracing-node.readthedocs.io](https://tracing-node.readthedo
26
26
  npm install @saidsef/tracing-node --save
27
27
  ```
28
28
 
29
+ Elasticsearch spans carrying the query, the operation and the index name need an optional peer dependency:
30
+
31
+ ```shell
32
+ npm install opentelemetry-instrumentation-elasticsearch --save
33
+ ```
34
+
35
+ That package pins `@opentelemetry/core` to the 1.x line, so installing it brings [GHSA-8988-4f7v-96qf](https://github.com/advisories/GHSA-8988-4f7v-96qf) into the dependency tree. Without it, an Elasticsearch call is still traced as an HTTP client span and still appears on the service graph. See [Instrumentation](https://tracing-node.readthedocs.io/en/latest/instrumentation/) for what each option records.
36
+
29
37
  ## Usage
30
38
 
31
39
  ```javascript
40
+ // instrument.mjs
32
41
  import { setupTracing } from '@saidsef/tracing-node';
33
42
 
34
43
  setupTracing({hostname: 'hostname', serviceName: 'service_name', url: 'endpoint'});
35
44
  ```
36
45
 
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.
46
+ ```shell
47
+ node --import ./instrument.mjs ./app.mjs
48
+ ```
49
+
50
+ `serviceName` and `url` are required, and both fall back to the `SERVICE_NAME` and `ENDPOINT` environment variables.
51
+
52
+ `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.
38
53
 
39
54
  ## Collector and backend
40
55
 
@@ -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,13 +14,15 @@
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';
20
23
  import {diag, DiagConsoleLogger, DiagLogLevel, metrics} from '@opentelemetry/api';
21
24
  import {HttpInstrumentation} from '@opentelemetry/instrumentation-http';
22
25
  import {DnsInstrumentation} from '@opentelemetry/instrumentation-dns';
23
- import {ElasticsearchInstrumentation} from 'opentelemetry-instrumentation-elasticsearch';
24
26
  import {ExpressInstrumentation, ExpressLayerType} from '@opentelemetry/instrumentation-express';
25
27
  import {logs} from '@opentelemetry/api-logs';
26
28
  import {NodeTracerProvider} from '@opentelemetry/sdk-trace-node';
@@ -41,6 +43,27 @@ import {ATTR_CONTAINER_NAME} from '@opentelemetry/semantic-conventions/incubatin
41
43
 
42
44
  diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.INFO);
43
45
 
46
+ // Reported here rather than where it happens, because the hook registers before
47
+ // this logger exists and the warning would go nowhere.
48
+ if (esmHookFailure) {
49
+ diag.warn(`ESM loader hook not registered, so ES module imports are not instrumented: ${esmHookFailure.message}`);
50
+ }
51
+
52
+ // An optional peer dependency: it pins @opentelemetry/core 1.x, which carries a
53
+ // published advisory. Absent, an Elasticsearch call still gets a client span from
54
+ // the http or undici instrumentation. See #576.
55
+ let ElasticsearchInstrumentation = null;
56
+ try {
57
+ ({ElasticsearchInstrumentation} = await import('opentelemetry-instrumentation-elasticsearch'));
58
+ } catch (error) {
59
+ // Absence is the expected case. An installed but unloadable package would
60
+ // otherwise vanish silently, and tracing must not take the application down.
61
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND') {
62
+ diag.warn('opentelemetry-instrumentation-elasticsearch failed to load:', error);
63
+ }
64
+ }
65
+
66
+
44
67
  // Set a non-negative integer span attribute from a header value; ignore invalid input.
45
68
  const setIntAttribute = (span, name, value) => {
46
69
  if (!value) return;
@@ -107,9 +130,10 @@ let loggerProvider = null;
107
130
  *
108
131
  * This function configures a NodeTracerProvider with various instrumentations
109
132
  * and span processors to enable tracing for the application. It supports
110
- * tracing for HTTP, Express, AWS, Pino, DNS, Elasticsearch, and IORedis.
111
- * The IORedis instrumentation includes peer.service attributes for proper
112
- * service map visualization in distributed tracing tools like Tempo.
133
+ * tracing for HTTP, Express, AWS, Pino, DNS, and IORedis, and for Elasticsearch
134
+ * when the optional opentelemetry-instrumentation-elasticsearch package is
135
+ * installed. The IORedis instrumentation includes peer.service attributes for
136
+ * proper service map visualization in distributed tracing tools like Tempo.
113
137
  *
114
138
  * A MeterProvider is registered alongside it, which is what makes the
115
139
  * instrumentations record the request duration histograms they already
@@ -347,7 +371,7 @@ export function setupTracing(options = {}) {
347
371
  return `${cmdName} ${args.join(' ')}`;
348
372
  },
349
373
  }),
350
- new ElasticsearchInstrumentation(),
374
+ ...(ElasticsearchInstrumentation ? [new ElasticsearchInstrumentation()] : []),
351
375
  // Event loop delay, GC pauses and heap occupancy are metric-only, and they
352
376
  // are what explains a whole service slowing at once. Constructed only with
353
377
  // metrics on, since the collectors start sampling on construction.
@@ -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": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
@@ -54,14 +54,21 @@
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
- "opentelemetry-instrumentation-elasticsearch": "^0.41.0"
57
+ "import-in-the-middle": "^3.0.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "eslint": "^10.10.0"
61
61
  },
62
+ "peerDependencies": {
63
+ "opentelemetry-instrumentation-elasticsearch": "^0.41.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "opentelemetry-instrumentation-elasticsearch": {
67
+ "optional": true
68
+ }
69
+ },
62
70
  "overrides": {
63
- "protobufjs": "^7.5.3",
64
- "@opentelemetry/core": "^2.11.0"
71
+ "protobufjs": "^7.5.3"
65
72
  },
66
73
  "allowScripts": {
67
74
  "protobufjs@7.6.6": true