Skip to content

Commit 7b1ff6c

Browse files
committed
test: add end-to-end tests for opentelemetry
I wasn't super confident after the recent major update and it led to me doing lots of tests across apps, symlinking in the new package version, and manually checking that metrics and traces were still being sent. I realised that this could be automated. I've added an end-to-end test where we spin up two Node.js apps - one collector and one regular app with OpenTelemetry added. We capture logs from both and this allows us to verify the basic case of "do metrics/traces get from an app to the configured collector?"
1 parent 3f45037 commit 7b1ff6c

File tree

5 files changed

+216
-0
lines changed

5 files changed

+216
-0
lines changed

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/opentelemetry/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@
2828
"@opentelemetry/instrumentation-runtime-node": "^0.13.0",
2929
"@opentelemetry/sdk-node": "^0.200.0",
3030
"@opentelemetry/semantic-conventions": "^1.30.0"
31+
},
32+
"devDependencies": {
33+
"express": "^4.21.2"
3134
}
3235
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const express = require('express');
2+
const logger = require('@dotcom-reliability-kit/logger');
3+
4+
// We set up OpenTelemetry via `--require` but this allows us to grab the same instances
5+
const { setup } = require('@dotcom-reliability-kit/opentelemetry');
6+
const { sdk } = setup();
7+
8+
const app = express();
9+
10+
app.use(async (request, response) => {
11+
logger.info({
12+
event: 'INCOMING_REQUEST',
13+
method: request.method,
14+
url: request.url
15+
});
16+
17+
// This ensures that metrics and traces are flushed
18+
await sdk.shutdown();
19+
20+
response.status(200).send('');
21+
});
22+
23+
const server = app.listen((error) => {
24+
if (error) {
25+
logger.fatal(`App could not be started: ${error.message}`);
26+
return process.exit(1);
27+
}
28+
if (process.send) {
29+
process.send({
30+
ready: true,
31+
port: server.address().port
32+
});
33+
}
34+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const express = require('express');
2+
const logger = require('@dotcom-reliability-kit/logger');
3+
4+
const app = express();
5+
6+
app.use(express.raw({ type: 'application/x-protobuf' }));
7+
8+
app.use(async (request, response) => {
9+
try {
10+
logger.info({
11+
event: 'INCOMING_REQUEST',
12+
method: request.method,
13+
url: request.url,
14+
headers: request.headers,
15+
body: request.body.toString()
16+
});
17+
response.status(200).send('');
18+
} catch (error) {
19+
logger.error(error);
20+
response.status(500).send('');
21+
}
22+
});
23+
24+
const server = app.listen((error) => {
25+
if (error) {
26+
logger.fatal(`Collector could not be started: ${error.message}`);
27+
return process.exit(1);
28+
}
29+
if (process.send) {
30+
process.send({
31+
ready: true,
32+
port: server.address().port
33+
});
34+
}
35+
});
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
const { fork } = require('node:child_process');
2+
3+
function waitForBaseUrl(childProcess) {
4+
return new Promise((resolve) => {
5+
const handler = (message) => {
6+
if (message?.ready) {
7+
resolve(`http://localhost:${message.port}`);
8+
childProcess.off('message', handler);
9+
}
10+
};
11+
childProcess.on('message', handler);
12+
});
13+
}
14+
15+
function stdoutToLogs(stdout) {
16+
return stdout.split('\n').map((logLine) => {
17+
try {
18+
return JSON.parse(logLine);
19+
} catch (_) {
20+
return logLine;
21+
}
22+
});
23+
}
24+
25+
describe('@dotcom-reliability-kit/opentelemetry end-to-end', () => {
26+
let collector;
27+
let collectorStdout = '';
28+
let collectorBaseUrl;
29+
let exporter;
30+
let exporterStdout = '';
31+
let exporterBaseUrl;
32+
33+
beforeAll(async () => {
34+
// Set up a mock collector
35+
collector = fork(`${__dirname}/fixtures/collector.js`, {
36+
env: {
37+
...process.env,
38+
NODE_ENV: 'production',
39+
SYSTEM_CODE: 'mock-system'
40+
},
41+
stdio: 'pipe'
42+
});
43+
collector.stdout.on('data', (chunk) => {
44+
collectorStdout += chunk.toString();
45+
});
46+
collector.stderr.on('data', (chunk) => {
47+
collectorStdout += chunk.toString();
48+
});
49+
collectorBaseUrl = await waitForBaseUrl(collector);
50+
51+
// Set up a Node.js app that sends Opentelemetry metrics and traces
52+
exporter = fork(`${__dirname}/fixtures/app.js`, {
53+
env: {
54+
...process.env,
55+
NODE_ENV: 'production',
56+
OPENTELEMETRY_METRICS_ENDPOINT: `${collectorBaseUrl}/metrics`,
57+
OPENTELEMETRY_TRACING_ENDPOINT: `${collectorBaseUrl}/traces`,
58+
OPENTELEMETRY_TRACING_SAMPLE_PERCENTAGE: '100',
59+
SYSTEM_CODE: 'mock-system'
60+
},
61+
execArgv: ['--require', '@dotcom-reliability-kit/opentelemetry/setup'],
62+
stdio: 'pipe'
63+
});
64+
exporter.stdout.on('data', (chunk) => {
65+
exporterStdout += chunk.toString();
66+
});
67+
exporter.stderr.on('data', (chunk) => {
68+
exporterStdout += chunk.toString();
69+
});
70+
exporterBaseUrl = await waitForBaseUrl(exporter);
71+
});
72+
73+
afterAll(() => {
74+
if (collector) {
75+
collector.kill('SIGINT');
76+
}
77+
if (exporter) {
78+
exporter.kill('SIGINT');
79+
}
80+
});
81+
82+
describe('sending an HTTP request to the exporting app', () => {
83+
let collectorLogs;
84+
let exporterLogs;
85+
86+
beforeAll(async () => {
87+
try {
88+
await fetch(`${exporterBaseUrl}/example`);
89+
collectorLogs = stdoutToLogs(collectorStdout);
90+
exporterLogs = stdoutToLogs(exporterStdout);
91+
} catch (cause) {
92+
// eslint-disable-next-line no-console
93+
console.log('COLLECTOR:', stdoutToLogs(collectorStdout));
94+
// eslint-disable-next-line no-console
95+
console.log('EXPORTER:', stdoutToLogs(exporterStdout));
96+
throw new Error('Fetch failed, see logs');
97+
}
98+
});
99+
100+
describe('exporter', () => {
101+
it('logs that OpenTelemetry metrics are enabled', () => {
102+
const log = exporterLogs.find(
103+
(log) => log?.event === 'OTEL_METRICS_STATUS' && log?.enabled === true
104+
);
105+
expect(log).toBeDefined();
106+
});
107+
it('logs that OpenTelemetry tracing is enabled', () => {
108+
const log = exporterLogs.find(
109+
(log) => log?.event === 'OTEL_TRACE_STATUS' && log?.enabled === true
110+
);
111+
expect(log).toBeDefined();
112+
});
113+
});
114+
115+
describe('collector', () => {
116+
it('receives metrics', () => {
117+
const log = collectorLogs.find(
118+
(log) =>
119+
log?.event === 'INCOMING_REQUEST' &&
120+
log?.method === 'POST' &&
121+
log?.url === '/metrics'
122+
);
123+
expect(log).toBeDefined();
124+
expect(log.headers['content-type']).toBe('application/x-protobuf');
125+
expect(log.body).toContain('mock-system');
126+
});
127+
it('receives traces', () => {
128+
const log = collectorLogs.find(
129+
(log) =>
130+
log?.event === 'INCOMING_REQUEST' &&
131+
log?.method === 'POST' &&
132+
log?.url === '/traces'
133+
);
134+
expect(log).toBeDefined();
135+
expect(log.headers['content-type']).toBe('application/x-protobuf');
136+
expect(log.body).toContain('mock-system');
137+
});
138+
});
139+
});
140+
});

0 commit comments

Comments
 (0)