Skip to content

Commit eac93d9

Browse files
committed
feat: create bootstrap span on demand (DASH0_BOOTSTRAP_SPAN)
1 parent ffacbd9 commit eac93d9

File tree

6 files changed

+160
-52
lines changed

6 files changed

+160
-52
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ If no `OTEL_SERVICE_NAME` has been set, a service name is automatically derived
1414
(if it is present) as `${packageJson.name}@${packageJson.version}`.
1515
This can be disabled either by setting `OTEL_SERVICE_NAME` or by setting `DASH0_AUTOMATIC_SERVICE_NAME=false`.
1616

17+
### <a id="DASH0_BOOTSTRAP_SPAN">DASH0_BOOTSTRAP_SPAN</a>
18+
19+
If set to a non-empty string, the distribution will create a span immediately at startup with the span name set to the
20+
value of DASH0_BOOTSTRAP_SPAN.
21+
1722
### <a id="DASH0_DEBUG">DASH0_DEBUG</a>
1823

1924
Additional debug logs can be enabled by setting `DASH0_DEBUG=true`.

src/init.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import { SpanKind, trace } from '@opentelemetry/api';
45
import { getNodeAutoInstrumentations, getResourceDetectors } from '@opentelemetry/auto-instrumentations-node';
56
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
67
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
@@ -84,6 +85,16 @@ const sdk = new NodeSDK(configuration);
8485

8586
sdk.start();
8687

88+
if (process.env.DASH0_BOOTSTRAP_SPAN != null) {
89+
const tracer = trace.getTracer('dash0-nodejs-distribution');
90+
tracer //
91+
.startSpan(process.env.DASH0_BOOTSTRAP_SPAN, {
92+
root: true,
93+
kind: SpanKind.INTERNAL,
94+
})
95+
.end();
96+
}
97+
8798
if (process.env.DASH0_DEBUG) {
8899
console.log('Dash0 OpenTelemetry distribution for Node.js: NodeSDK started.');
89100
}

test/integration/rootHooks.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,6 @@ export const mochaHooks = {
2121
console.debug('[rootHooks] global mock collector started');
2222
},
2323

24-
async beforeEach() {
25-
collectorInstance.clear();
26-
},
27-
2824
async afterAll() {
2925
console.debug('[rootHooks] stopping global mock collector');
3026
await collectorInstance.stop();

test/integration/test.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ describe('attach', () => {
3434
expectedDistroVersion = JSON.parse(String(await readFile('package.json'))).version;
3535
});
3636

37-
describe('tracing', () => {
37+
beforeEach(async function () {
38+
collector().clear();
39+
});
40+
41+
describe('basic tracing', () => {
3842
let appUnderTest: ChildProcessWrapper;
3943

4044
before(async () => {
@@ -49,7 +53,7 @@ describe('attach', () => {
4953

5054
it('should attach via --require and capture spans', async () => {
5155
await waitUntil(async () => {
52-
const telemetry = await waitForTelemetry();
56+
const telemetry = await sendRequestAndWaitForTraceData();
5357
expectMatchingSpan(
5458
telemetry.traces,
5559
[
@@ -59,7 +63,7 @@ describe('attach', () => {
5963
resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion),
6064
],
6165
[
62-
span => expect(span.kind).to.equal(SpanKind.SERVER),
66+
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
6367
span => expectSpanAttribute(span, 'http.route', '/ohai'),
6468
],
6569
);
@@ -83,12 +87,12 @@ describe('attach', () => {
8387

8488
it('should attach via --require and detect the pod uid', async () => {
8589
await waitUntil(async () => {
86-
const telemetry = await waitForTelemetry();
90+
const telemetry = await sendRequestAndWaitForTraceData();
8791
expectMatchingSpan(
8892
telemetry.traces,
8993
[resource => expectResourceAttribute(resource, 'k8s.pod.uid', 'f57400dc-94ce-4806-a52e-d2726f448f15')],
9094
[
91-
span => expect(span.kind).to.equal(SpanKind.SERVER),
95+
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
9296
span => expectSpanAttribute(span, 'http.route', '/ohai'),
9397
],
9498
);
@@ -111,22 +115,56 @@ describe('attach', () => {
111115

112116
it('should attach via --require and derive a service name from the package.json file', async () => {
113117
await waitUntil(async () => {
114-
const telemetry = await waitForTelemetry();
118+
const telemetry = await sendRequestAndWaitForTraceData();
115119
expectMatchingSpan(
116120
telemetry.traces,
117121
[
118122
resource =>
119123
expectResourceAttribute(resource, 'service.name', '[email protected]'),
120124
],
121125
[
122-
span => expect(span.kind).to.equal(SpanKind.SERVER),
126+
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
123127
span => expectSpanAttribute(span, 'http.route', '/ohai'),
124128
],
125129
);
126130
});
127131
});
128132
});
129133

134+
describe('bootstrap span', () => {
135+
let appUnderTest: ChildProcessWrapper;
136+
137+
before(async () => {
138+
const appConfiguration = defaultAppConfiguration(appPort);
139+
appConfiguration.env!.DASH0_BOOTSTRAP_SPAN = 'Dash0 Test Bootstrap Span';
140+
appUnderTest = new ChildProcessWrapper(appConfiguration);
141+
});
142+
143+
after(async () => {
144+
await appUnderTest.stop();
145+
});
146+
147+
it('should create an internal span on bootstrap', async () => {
148+
// It is important for this test that we do not start the app in the before hook, since the beforeEach from the
149+
// top level suite clears the mock collector's spans, thus we would accidentally delete the bootstrap span
150+
// (because the top level beforeHook is executed after this suite's before hook).
151+
await appUnderTest.start();
152+
await waitUntil(async () => {
153+
const telemetry = await waitForTraceData();
154+
expectMatchingSpan(
155+
telemetry.traces,
156+
[
157+
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
158+
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
159+
resource => expectResourceAttribute(resource, 'telemetry.distro.name', 'dash0-nodejs'),
160+
resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion),
161+
],
162+
[span => expect(span.name).to.equal('Dash0 Test Bootstrap Span')],
163+
);
164+
});
165+
});
166+
});
167+
130168
describe('print spans to file', () => {
131169
let appUnderTest: ChildProcessWrapper;
132170
const spanFilename = join(__dirname, 'spans.json');
@@ -185,7 +223,7 @@ describe('attach', () => {
185223
resourceAttributes =>
186224
expect(resourceAttributes['telemetry.distro.version']).to.equal(expectedDistroVersion),
187225
],
188-
[spanAttributes => expect(spanAttributes.kind).to.equal(SpanKind.SERVER)],
226+
[spanAttributes => expect(spanAttributes.kind).to.equal(SpanKind.SERVER, 'span kind should be server')],
189227
[spanAttributes => expect(spanAttributes['http.route']).to.equal('/ohai')],
190228
);
191229
});
@@ -217,12 +255,9 @@ describe('attach', () => {
217255
});
218256
});
219257

220-
async function waitForTelemetry() {
258+
async function sendRequestAndWaitForTraceData() {
221259
await sendRequestAndVerifyResponse();
222-
if (!(await collector().hasTraces())) {
223-
throw new Error('The collector never received any spans.');
224-
}
225-
return await collector().fetchTelemetry();
260+
return waitForTraceData();
226261
}
227262

228263
async function sendRequestAndVerifyResponse() {
@@ -232,6 +267,13 @@ describe('attach', () => {
232267
expect(responsePayload).to.deep.equal({ message: 'We make Observability easy for every developer.' });
233268
}
234269

270+
async function waitForTraceData() {
271+
if (!(await collector().hasTraces())) {
272+
throw new Error('The collector never received any spans.');
273+
}
274+
return await collector().fetchTelemetry();
275+
}
276+
235277
async function verifyFileHasBeenCreated(filename: string): Promise<FileHandle> {
236278
let file;
237279
try {

test/util/expectMatchingSpan.ts

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44
import { ExportTraceServiceRequest } from '../collector/types/opentelemetry/proto/collector/trace/v1/trace_service';
55
import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource';
66
import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace';
7-
import { Expectation, findMatchingSpans, findMatchingSpansInFileDump } from './findMatchingSpans';
7+
import { Expectation, findMatchingSpans, findMatchingSpansInFileDump, MatchingSpansResult } from './findMatchingSpans';
88

99
export function expectMatchingSpan(
1010
traceDataItems: ExportTraceServiceRequest[],
1111
resourceExpectations: Expectation<Resource>[],
1212
spanExpectations: Expectation<Span>[],
1313
): Span {
14-
const { matchingSpans, lastError } = findMatchingSpans(traceDataItems, resourceExpectations, spanExpectations);
15-
return processFindSpanResult(matchingSpans, lastError);
14+
const matchResult = findMatchingSpans(traceDataItems, resourceExpectations, spanExpectations);
15+
return processFindSpanResult(matchResult);
1616
}
1717

1818
export function expectMatchingSpanInFileDump(
@@ -21,29 +21,33 @@ export function expectMatchingSpanInFileDump(
2121
spanExpectations: Expectation<any>[],
2222
spanAttributeExpectations: Expectation<any>[],
2323
): Span {
24-
const { matchingSpans, lastError } = findMatchingSpansInFileDump(
24+
const matchResult = findMatchingSpansInFileDump(
2525
spans,
2626
resourceAttributeExpectations,
2727
spanExpectations,
2828
spanAttributeExpectations,
2929
);
30-
return processFindSpanResult(matchingSpans, lastError);
30+
return processFindSpanResult(matchResult);
3131
}
3232

33-
function processFindSpanResult(matchingSpans: Span[], lastError: Error | undefined): Span {
34-
if (matchingSpans.length === 1) {
35-
return matchingSpans[0];
36-
} else if (matchingSpans.length === 0) {
37-
if (lastError) {
38-
throw new Error(`No matching span found. Most recent failing expectation: ${lastError}`);
33+
function processFindSpanResult(matchResult: MatchingSpansResult): Span {
34+
if (matchResult.matchingSpans) {
35+
const matchingSpans = matchResult.matchingSpans;
36+
if (matchingSpans.length === 1) {
37+
return matchingSpans[0];
38+
} else if (matchingSpans.length > 1) {
39+
throw new Error(
40+
`Expected exactly one matching span, found ${matchingSpans.length}.\nMatches:\n${JSON.stringify(matchingSpans, null, 2)}`,
41+
);
3942
} else {
40-
throw new Error('No matching span found.');
43+
throw new Error('Unexpected error while processing matching spans.');
4144
}
42-
} else if (matchingSpans.length > 1) {
45+
} else if (matchResult.bestCandidate) {
46+
const bestCandidate = matchResult.bestCandidate;
4347
throw new Error(
44-
`Expected exactly one matching span, found ${matchingSpans.length}.\nMatches:\n${JSON.stringify(matchingSpans, null, 2)} `,
48+
`No matching span has been found. The best candidate passed ${bestCandidate.passedChecks} and failed check ${bestCandidate.passedChecks + 1} with error ${bestCandidate.error}. This is the best candidate:\n${JSON.stringify(bestCandidate.spanLike, null, 2)}`,
4549
);
4650
} else {
47-
throw new Error('Unexpected error while processing matching spans.');
51+
throw new Error('No matching span has been found.');
4852
}
4953
}

0 commit comments

Comments
 (0)