Skip to content

Commit

Permalink
feat: create bootstrap span on demand (DASH0_BOOTSTRAP_SPAN.)
Browse files Browse the repository at this point in the history
  • Loading branch information
basti1302 committed Jun 12, 2024
1 parent 1abe2b0 commit c496b36
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 48 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ If no `OTEL_SERVICE_NAME` has been set, a service name is automatically derived
(if it is present) as `${packageJson.name}@${packageJson.version}`.
This can be disabled either by setting `OTEL_SERVICE_NAME` or by setting `DASH0_AUTOMATIC_SERVICE_NAME=false`.

### <a id="DASH0_BOOTSTRAP_SPAN">DASH0_BOOTSTRAP_SPAN</a>

If set to a non-empty string, the distribution will create a span immediately at startup with the span name set to the
value of DASH0_BOOTSTRAP_SPAN.

### <a id="DASH0_DEBUG">DASH0_DEBUG</a>

Additional debug logs can be enabled by setting `DASH0_DEBUG=true`.
Expand Down
12 changes: 12 additions & 0 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: Copyright 2024 Dash0 Inc.
// SPDX-License-Identifier: Apache-2.0

import { SpanKind, trace } from '@opentelemetry/api';
import { getNodeAutoInstrumentations, getResourceDetectors } from '@opentelemetry/auto-instrumentations-node';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-proto';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto';
Expand Down Expand Up @@ -87,3 +88,14 @@ sdk.start();
if (process.env.DASH0_DEBUG) {
console.log('Dash0 OpenTelemetry distribution for Node.js: NodeSDK started.');
}

setTimeout(() => {
if (process.env.DASH0_BOOTSTRAP_SPAN != null) {
trace
.getTracer('dash0-nodejs-distribution')
.startSpan(process.env.DASH0_BOOTSTRAP_SPAN, { kind: SpanKind.INTERNAL })
.end();
}
// Note: Calling startSpan immediately after sdk.start() will result in the span not actually being created, probably
// because the tracer is not ready yet. For now we use a delay of 1s to make this somewhat reliable.
}, 1000);
61 changes: 48 additions & 13 deletions test/integration/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('attach', () => {
expectedDistroVersion = JSON.parse(String(await readFile('package.json'))).version;
});

describe('tracing', () => {
describe('basic tracing', () => {
let appUnderTest: ChildProcessWrapper;

before(async () => {
Expand All @@ -49,7 +49,7 @@ describe('attach', () => {

it('should attach via --require and capture spans', async () => {
await waitUntil(async () => {
const telemetry = await waitForTelemetry();
const telemetry = await sendRequestAndWaitForTraceData();
expectMatchingSpan(
telemetry.traces,
[
Expand All @@ -59,7 +59,7 @@ describe('attach', () => {
resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion),
],
[
span => expect(span.kind).to.equal(SpanKind.SERVER),
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
span => expectSpanAttribute(span, 'http.route', '/ohai'),
],
);
Expand All @@ -83,12 +83,12 @@ describe('attach', () => {

it('should attach via --require and detect the pod uid', async () => {
await waitUntil(async () => {
const telemetry = await waitForTelemetry();
const telemetry = await sendRequestAndWaitForTraceData();
expectMatchingSpan(
telemetry.traces,
[resource => expectResourceAttribute(resource, 'k8s.pod.uid', 'f57400dc-94ce-4806-a52e-d2726f448f15')],
[
span => expect(span.kind).to.equal(SpanKind.SERVER),
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
span => expectSpanAttribute(span, 'http.route', '/ohai'),
],
);
Expand All @@ -111,22 +111,53 @@ describe('attach', () => {

it('should attach via --require and derive a service name from the package.json file', async () => {
await waitUntil(async () => {
const telemetry = await waitForTelemetry();
const telemetry = await sendRequestAndWaitForTraceData();
expectMatchingSpan(
telemetry.traces,
[
resource =>
expectResourceAttribute(resource, 'service.name', '[email protected]'),
],
[
span => expect(span.kind).to.equal(SpanKind.SERVER),
span => expect(span.kind).to.equal(SpanKind.SERVER, 'span kind should be server'),
span => expectSpanAttribute(span, 'http.route', '/ohai'),
],
);
});
});
});

describe('bootstrap span', () => {
let appUnderTest: ChildProcessWrapper;

before(async () => {
const appConfiguration = defaultAppConfiguration(appPort);
appConfiguration.env!.DASH0_BOOTSTRAP_SPAN = 'Dash0 Test Bootstrap Span';
appUnderTest = new ChildProcessWrapper(appConfiguration);
await appUnderTest.start();
});

after(async () => {
await appUnderTest.stop();
});

it('should create an internal span on bootstrap', async () => {
await waitUntil(async () => {
const telemetry = await waitForTraceData();
expectMatchingSpan(
telemetry.traces,
[
resource => expectResourceAttribute(resource, 'telemetry.sdk.name', 'opentelemetry'),
resource => expectResourceAttribute(resource, 'telemetry.sdk.language', 'nodejs'),
resource => expectResourceAttribute(resource, 'telemetry.distro.name', 'dash0-nodejs'),
resource => expectResourceAttribute(resource, 'telemetry.distro.version', expectedDistroVersion),
],
[span => expect(span.name).to.equal('Dash0 Test Bootstrap Span')],
);
});
});
});

describe('print spans to file', () => {
let appUnderTest: ChildProcessWrapper;
const spanFilename = join(__dirname, 'spans.json');
Expand Down Expand Up @@ -185,7 +216,7 @@ describe('attach', () => {
resourceAttributes =>
expect(resourceAttributes['telemetry.distro.version']).to.equal(expectedDistroVersion),
],
[spanAttributes => expect(spanAttributes.kind).to.equal(SpanKind.SERVER)],
[spanAttributes => expect(spanAttributes.kind).to.equal(SpanKind.SERVER, 'span kind should be server')],
[spanAttributes => expect(spanAttributes['http.route']).to.equal('/ohai')],
);
});
Expand Down Expand Up @@ -217,12 +248,9 @@ describe('attach', () => {
});
});

async function waitForTelemetry() {
async function sendRequestAndWaitForTraceData() {
await sendRequestAndVerifyResponse();
if (!(await collector().hasTraces())) {
throw new Error('The collector never received any spans.');
}
return await collector().fetchTelemetry();
return waitForTraceData();
}

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

async function waitForTraceData() {
if (!(await collector().hasTraces())) {
throw new Error('The collector never received any spans.');
}
return await collector().fetchTelemetry();
}

async function verifyFileHasBeenCreated(filename: string): Promise<FileHandle> {
let file;
try {
Expand Down
34 changes: 19 additions & 15 deletions test/util/expectMatchingSpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
import { ExportTraceServiceRequest } from '../collector/types/opentelemetry/proto/collector/trace/v1/trace_service';
import { Resource } from '../collector/types/opentelemetry/proto/resource/v1/resource';
import { Span } from '../collector/types/opentelemetry/proto/trace/v1/trace';
import { Expectation, findMatchingSpans, findMatchingSpansInFileDump } from './findMatchingSpans';
import { Expectation, findMatchingSpans, findMatchingSpansInFileDump, MatchingSpansResult } from './findMatchingSpans';

export function expectMatchingSpan(
traceDataItems: ExportTraceServiceRequest[],
resourceExpectations: Expectation<Resource>[],
spanExpectations: Expectation<Span>[],
): Span {
const { matchingSpans, lastError } = findMatchingSpans(traceDataItems, resourceExpectations, spanExpectations);
return processFindSpanResult(matchingSpans, lastError);
const matchResult = findMatchingSpans(traceDataItems, resourceExpectations, spanExpectations);
return processFindSpanResult(matchResult);
}

export function expectMatchingSpanInFileDump(
Expand All @@ -21,29 +21,33 @@ export function expectMatchingSpanInFileDump(
spanExpectations: Expectation<any>[],
spanAttributeExpectations: Expectation<any>[],
): Span {
const { matchingSpans, lastError } = findMatchingSpansInFileDump(
const matchResult = findMatchingSpansInFileDump(
spans,
resourceAttributeExpectations,
spanExpectations,
spanAttributeExpectations,
);
return processFindSpanResult(matchingSpans, lastError);
return processFindSpanResult(matchResult);
}

function processFindSpanResult(matchingSpans: Span[], lastError: Error | undefined): Span {
if (matchingSpans.length === 1) {
return matchingSpans[0];
} else if (matchingSpans.length === 0) {
if (lastError) {
throw new Error(`No matching span found. Most recent failing expectation: ${lastError}`);
function processFindSpanResult(matchResult: MatchingSpansResult): Span {
if (matchResult.matchingSpans) {
const matchingSpans = matchResult.matchingSpans;
if (matchingSpans.length === 1) {
return matchingSpans[0];
} else if (matchingSpans.length > 1) {
throw new Error(
`Expected exactly one matching span, found ${matchingSpans.length}.\nMatches:\n${JSON.stringify(matchingSpans, null, 2)}`,
);
} else {
throw new Error('No matching span found.');
throw new Error('Unexpected error while processing matching spans.');
}
} else if (matchingSpans.length > 1) {
} else if (matchResult.bestCandidate) {
const bestCandidate = matchResult.bestCandidate;
throw new Error(
`Expected exactly one matching span, found ${matchingSpans.length}.\nMatches:\n${JSON.stringify(matchingSpans, null, 2)} `,
`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)}`,
);
} else {
throw new Error('Unexpected error while processing matching spans.');
throw new Error('No matching span has been found.');
}
}
Loading

0 comments on commit c496b36

Please sign in to comment.