Skip to content

Commit 0f90b3d

Browse files
aabmasspichlermarc
andauthored
fix(auto-instrumentations-node): shutdown the SDK when the process exits normally (#2394)
Co-authored-by: Marc Pichler <marc.pichler@dynatrace.com>
1 parent 2105609 commit 0f90b3d

File tree

3 files changed

+125
-30
lines changed

3 files changed

+125
-30
lines changed

metapackages/auto-instrumentations-node/src/register.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,16 @@ try {
4040
);
4141
}
4242

43-
process.on('SIGTERM', () => {
44-
sdk
45-
.shutdown()
46-
.then(() => diag.debug('OpenTelemetry SDK terminated'))
47-
.catch(error => diag.error('Error terminating OpenTelemetry SDK', error));
48-
});
43+
async function shutdown(): Promise<void> {
44+
try {
45+
await sdk.shutdown();
46+
diag.debug('OpenTelemetry SDK terminated');
47+
} catch (error) {
48+
diag.error('Error terminating OpenTelemetry SDK', error);
49+
}
50+
}
51+
52+
// Gracefully shutdown SDK if a SIGTERM is received
53+
process.on('SIGTERM', shutdown);
54+
// Gracefully shutdown SDK if Node.js is exiting normally
55+
process.once('beforeExit', shutdown);

metapackages/auto-instrumentations-node/test/register.test.ts

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,92 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { spawnSync } from 'child_process';
17+
import { execFile, PromiseWithChild } from 'child_process';
1818
import * as assert from 'assert';
19+
import { promisify } from 'util';
20+
import { Readable } from 'stream';
1921

20-
describe('Register', function () {
21-
it('can load auto instrumentation from command line', () => {
22-
const proc = spawnSync(
23-
process.execPath,
24-
['--require', '../build/src/register.js', './test-app/app.js'],
25-
{
26-
cwd: __dirname,
27-
timeout: 5000,
28-
killSignal: 'SIGKILL', // SIGTERM is not sufficient to terminate some hangs
29-
env: Object.assign({}, process.env, {
30-
OTEL_NODE_RESOURCE_DETECTORS: 'none',
31-
OTEL_TRACES_EXPORTER: 'console',
32-
// nx (used by lerna run) defaults `FORCE_COLOR=true`, which in
33-
// node v18.17.0, v20.3.0 and later results in ANSI color escapes
34-
// in the ConsoleSpanExporter output that is checked below.
35-
FORCE_COLOR: '0',
36-
}),
22+
const execFilePromise = promisify(execFile);
23+
24+
function runWithRegister(path: string): PromiseWithChild<{
25+
stdout: string;
26+
stderr: string;
27+
}> {
28+
return execFilePromise(
29+
process.execPath,
30+
['--require', '../build/src/register.js', path],
31+
{
32+
cwd: __dirname,
33+
timeout: 1500,
34+
killSignal: 'SIGKILL', // SIGTERM is not sufficient to terminate some hangs
35+
env: Object.assign({}, process.env, {
36+
OTEL_NODE_RESOURCE_DETECTORS: 'none',
37+
OTEL_TRACES_EXPORTER: 'console',
38+
OTEL_LOG_LEVEL: 'debug',
39+
// nx (used by lerna run) defaults `FORCE_COLOR=true`, which in
40+
// node v18.17.0, v20.3.0 and later results in ANSI color escapes
41+
// in the ConsoleSpanExporter output that is checked below.
42+
FORCE_COLOR: '0',
43+
}),
44+
}
45+
);
46+
}
47+
48+
function waitForString(stream: Readable, str: string): Promise<void> {
49+
return new Promise((resolve, reject) => {
50+
function check(chunk: Buffer): void {
51+
if (chunk.includes(str)) {
52+
resolve();
53+
stream.off('data', check);
3754
}
55+
}
56+
stream.on('data', check);
57+
stream.on('close', () =>
58+
reject(`Stream closed without ever seeing "${str}"`)
59+
);
60+
});
61+
}
62+
63+
describe('Register', function () {
64+
it('can load auto instrumentation from command line', async () => {
65+
const runPromise = runWithRegister('./test-app/app.js');
66+
const { child } = runPromise;
67+
const { stdout } = await runPromise;
68+
assert.equal(child.exitCode, 0, `child.exitCode (${child.exitCode})`);
69+
assert.equal(
70+
child.signalCode,
71+
null,
72+
`child.signalCode (${child.signalCode})`
3873
);
39-
assert.ifError(proc.error);
40-
assert.equal(proc.status, 0, `proc.status (${proc.status})`);
41-
assert.equal(proc.signal, null, `proc.signal (${proc.signal})`);
4274

4375
assert.ok(
44-
proc.stdout.includes(
76+
stdout.includes(
4577
'OpenTelemetry automatic instrumentation started successfully'
4678
)
4779
);
4880

81+
assert.ok(
82+
stdout.includes('OpenTelemetry SDK terminated'),
83+
`Process output was missing message indicating successful shutdown, got stdout:\n${stdout}`
84+
);
85+
4986
// Check a span has been generated for the GET request done in app.js
87+
assert.ok(stdout.includes("name: 'GET'"), 'console span output in stdout');
88+
});
89+
90+
it('shuts down the NodeSDK when SIGTERM is received', async () => {
91+
const runPromise = runWithRegister('./test-app/app-server.js');
92+
const { child } = runPromise;
93+
await waitForString(child.stdout!, 'Finshed request');
94+
child.kill('SIGTERM');
95+
const { stdout } = await runPromise;
96+
5097
assert.ok(
51-
proc.stdout.includes("name: 'GET'"),
52-
'console span output in stdout'
98+
stdout.includes('OpenTelemetry SDK terminated'),
99+
`Process output was missing message indicating successful shutdown, got stdout:\n${stdout}`
53100
);
101+
102+
// Check a span has been generated for the GET request done in app.js
103+
assert.ok(stdout.includes("name: 'GET'"), 'console span output in stdout');
54104
});
55105
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
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+
//Used in register.test.ts to mimic a JS app that stays alive like a server.
18+
const http = require('http');
19+
20+
const options = {
21+
hostname: 'example.com',
22+
port: 80,
23+
path: '/',
24+
method: 'GET',
25+
};
26+
27+
const req = http.request(options);
28+
req.end();
29+
req.on('close', () => {
30+
console.log('Finshed request');
31+
});
32+
33+
// Make sure there is work on the event loop
34+
const handle = setInterval(() => {}, 1);
35+
// Gracefully shut down
36+
process.on('SIGTERM', () => {
37+
clearInterval(handle);
38+
});

0 commit comments

Comments
 (0)