@saidsef/tracing-node 6.0.0 → 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
@@ -29,12 +29,19 @@ 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.
38
45
 
39
46
  ## Collector and backend
40
47
 
@@ -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": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "tracing NodeJS - Wrapper for OpenTelemetry instrumentation packages",
5
5
  "main": "libs/index.mjs",
6
6
  "scripts": {
@@ -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": {