@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 +16 -1
- package/libs/esm-hook.mjs +42 -0
- package/libs/index.mjs +29 -5
- package/libs/index.test.mjs +34 -0
- package/package.json +11 -4
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
|
-
|
|
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,
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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.
|
package/libs/index.test.mjs
CHANGED
|
@@ -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": "
|
|
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
|
-
"
|
|
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
|