Skip to content

Commit 6ff7f89

Browse files
committed
Zipkin support and tests
1 parent b6bf554 commit 6ff7f89

File tree

7 files changed

+258
-12
lines changed

7 files changed

+258
-12
lines changed

src/exporters/zipkin.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Span, Trace } from 'src/tracing';
22
import { generateSpanId } from 'src/utils/rand';
3+
import { isHex } from 'src/utils/hex';
34
import { Exporter } from './exporter';
5+
import type { SpanContext } from 'src/types';
46

57
export type ZipkinJson = ZipkinSpan[];
68

@@ -65,15 +67,36 @@ export class ZipkinExporter extends Exporter {
6567
return spans;
6668
}
6769

68-
getContextHeaders(span: Span): Record<string, string> {
70+
injectContextHeaders(span: Span): Record<string, string> {
6971
// https://github.com/openzipkin/b3-propagation
72+
// https://github.com/openzipkin/b3-propagation#why-is-parentspanid-propagated
7073
return {
7174
'X-B3-TraceId': span.getTraceId(),
7275
'X-B3-ParentSpanId': span.getSpanId(),
73-
'X-B3-SpanId': generateSpanId(), // TODO: Figure out what I want to do here
76+
'X-B3-SpanId': generateSpanId(),
7477
'X-B3-Sampled': '1', // TODO: Implement sampling
7578
};
7679
}
80+
81+
readContextHeaders(headers: Headers): SpanContext | null {
82+
const traceId = headers.get('X-B3-TraceId');
83+
const spanId = headers.get('X-B3-ParentSpanId');
84+
85+
if (!traceId || !spanId) {
86+
return null;
87+
}
88+
89+
// Validation
90+
if ((traceId.length !== 32 && traceId.length !== 16)
91+
|| !isHex(traceId)
92+
|| spanId.length !== 16
93+
|| !isHex(spanId)
94+
) {
95+
return null;
96+
}
97+
98+
return { traceId, spanId };
99+
}
77100
}
78101

79102
export class ZipkinTransformer extends ZipkinExporter {}

src/trace.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ export function createTrace(
3030
parentContext = context;
3131
}
3232

33+
// Set the `exporter` to `transformer` if defined to keep backwards compat
34+
// Likewise, set the `transformer` if `exporter` is set
35+
// TODO: Remove
36+
if (tracerOptions.collector.transformer && !tracerOptions.collector.exporter) {
37+
tracerOptions.collector.exporter = tracerOptions.collector.transformer;
38+
} else if (tracerOptions.collector.exporter && !tracerOptions.collector.transformer) {
39+
tracerOptions.collector.transformer = tracerOptions.collector.exporter;
40+
}
41+
3342
const trace = new Trace(ctx, {
3443
traceContext: parentContext,
3544
...tracerOptions,

test/otlp-exporter.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('Test OTLP Exporter', () => {
2929
}
3030
});
3131

32-
test.skip('Basic trace should transform correctly', async () => {
32+
test('Basic trace should transform correctly', async () => {
3333
devWorker = await startWorker('test/scripts/otlp/basic.ts');
3434

3535
const res = await devWorker.fetch('http://worker/test');
@@ -56,7 +56,7 @@ describe('Test OTLP Exporter', () => {
5656
expect(span.name).toBe('Request');
5757
});
5858

59-
describe.skip('Resource', () => {
59+
describe('Resource', () => {
6060
test('Default attributes are put on resource', async () => {
6161
devWorker = await startWorker('test/scripts/otlp/basic.ts');
6262

@@ -136,7 +136,7 @@ describe('Test OTLP Exporter', () => {
136136
});
137137
});
138138

139-
describe.skip('Single span', () => {
139+
describe('Single span', () => {
140140
test('You can add a single span', async () => {
141141
devWorker = await startWorker('test/scripts/otlp/single-span.ts');
142142

@@ -325,7 +325,7 @@ describe('Test OTLP Exporter', () => {
325325
});
326326
});
327327

328-
describe.skip('Multiple spans', () => {
328+
describe('Multiple spans', () => {
329329
test('You can add multiple spans', async () => {
330330
devWorker = await startWorker('test/scripts/otlp/multiple-spans.ts', {
331331
kv: [ { binding: 'KV', id: '' } ],
@@ -571,7 +571,7 @@ describe('Test OTLP Exporter', () => {
571571
});
572572
});
573573

574-
describe.skip('Child of child span', () => {
574+
describe('Child of child span', () => {
575575
test('You can add a child to a child span', async () => {
576576
devWorker = await startWorker('test/scripts/otlp/span-span.ts', {
577577
kv: [ { binding: 'KV', id: '' } ],
@@ -915,5 +915,7 @@ describe('Test OTLP Exporter', () => {
915915

916916
await worker.stop();
917917
});
918+
919+
test.todo('Can pass context in service binding');
918920
});
919921
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createTrace } from 'src/trace';
2+
import { ZipkinExporter } from 'src/exporters/zipkin';
3+
4+
interface Env {}
5+
6+
export default {
7+
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
8+
const exporter = new ZipkinExporter();
9+
const trace = createTrace(req, env, ctx, {
10+
serviceName: 'basic-fetch',
11+
collector: {
12+
url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint
13+
exporter: exporter,
14+
},
15+
});
16+
17+
const url = new URL(req.url);
18+
const address = url.searchParams.get('address');
19+
const port = url.searchParams.get('port');
20+
21+
const span = trace.startSpan('fetch');
22+
const res = await fetch(`http://${address}:${port}/test`, {
23+
headers: {
24+
...exporter.injectContextHeaders(span),
25+
},
26+
});
27+
span.end();
28+
29+
const context = await res.json<{ traceId: string, parentId: string }>();
30+
31+
await trace.send();
32+
return new Response('ok', {
33+
headers: {
34+
'trace-id': trace.getTraceId(),
35+
'span-id': span.getSpanId(),
36+
'fetched-trace-id': context.traceId,
37+
'fetched-parent-id': context.parentId,
38+
},
39+
});
40+
},
41+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createTrace } from 'src/trace';
2+
import { ZipkinExporter } from 'src/exporters/zipkin';
3+
4+
interface Env {}
5+
6+
export default {
7+
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
8+
const trace = createTrace(req, env, ctx, {
9+
serviceName: 'test-ids',
10+
collector: {
11+
url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint
12+
exporter: new ZipkinExporter(),
13+
},
14+
});
15+
16+
return Response.json({
17+
traceId: trace.getTraceId(),
18+
parentId: trace.getData().parentId,
19+
});
20+
},
21+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createTrace } from 'src/trace';
2+
import { ZipkinExporter } from 'src/exporters/zipkin';
3+
4+
interface Env {}
5+
6+
export default {
7+
async fetch(req: Request, env: Env, ctx: ExecutionContext) {
8+
const exporter = new ZipkinExporter();
9+
const trace = createTrace(req, env, ctx, {
10+
serviceName: 'traced-fetch',
11+
collector: {
12+
url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint
13+
exporter: exporter,
14+
},
15+
});
16+
17+
const url = new URL(req.url);
18+
const address = url.searchParams.get('address');
19+
const port = url.searchParams.get('port');
20+
21+
const res = await trace.tracedFetch(`http://${address}:${port}/test`);
22+
const context = await res.json<{ traceId: string, parentId: string }>();
23+
24+
await trace.send();
25+
return new Response('ok', {
26+
headers: {
27+
'trace-id': trace.getTraceId(),
28+
'span-id': trace.getChildSpans()[0].getSpanId(),
29+
'fetched-trace-id': context.traceId,
30+
'fetched-parent-id': context.parentId,
31+
},
32+
});
33+
},
34+
};

test/zipkin-exporter.test.ts

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { startCollector, startWorker } from './utils/worker';
88
let devWorker: UnstableDevWorker;
99
let collectorWorker: UnstableDevWorker;
1010

11+
const SCRIPT_PATH = 'test/scripts/zipkin';
12+
1113
describe('Test Zipkin Exporter', () => {
1214
beforeAll(async () => {
1315
collectorWorker = await startCollector({ port: 9411 });
@@ -27,7 +29,7 @@ describe('Test Zipkin Exporter', () => {
2729
}
2830
});
2931

30-
test('Basic trace should transform correctly', async () => {
32+
test.skip('Basic trace should transform correctly', async () => {
3133
devWorker = await startWorker('test/scripts/zipkin/basic.ts');
3234

3335
const res = await devWorker.fetch('http://worker/test');
@@ -48,7 +50,7 @@ describe('Test Zipkin Exporter', () => {
4850
expect(span.localEndpoint.serviceName).toBe('zipkin-basic');
4951
});
5052

51-
describe('Root span', () => {
53+
describe.skip('Root span', () => {
5254
test('Default attributes are put on root span', async () => {
5355
devWorker = await startWorker('test/scripts/zipkin/basic.ts');
5456

@@ -109,7 +111,7 @@ describe('Test Zipkin Exporter', () => {
109111
});
110112
});
111113

112-
describe('Single span', () => {
114+
describe.skip('Single span', () => {
113115
test('You can add a single span', async () => {
114116
devWorker = await startWorker('test/scripts/zipkin/single-span.ts');
115117

@@ -255,7 +257,7 @@ describe('Test Zipkin Exporter', () => {
255257
});
256258
});
257259

258-
describe('Multiple spans', () => {
260+
describe.skip('Multiple spans', () => {
259261
test('You can add multiple spans', async () => {
260262
devWorker = await startWorker('test/scripts/zipkin/multiple-spans.ts', {
261263
kv: [ { binding: 'KV', id: '' } ],
@@ -449,7 +451,7 @@ describe('Test Zipkin Exporter', () => {
449451
});
450452
});
451453

452-
describe('Child of child span', () => {
454+
describe.skip('Child of child span', () => {
453455
test('You can add a child to a child span', async () => {
454456
devWorker = await startWorker('test/scripts/zipkin/span-span.ts', {
455457
kv: [ { binding: 'KV', id: '' } ],
@@ -646,4 +648,118 @@ describe('Test Zipkin Exporter', () => {
646648
expect(secondChildSpan.annotations?.[0].timestamp).not.toBe(0);
647649
});
648650
});
651+
652+
describe('Propagation', () => {
653+
test('Can pass context in fetch', async () => {
654+
devWorker = await startWorker(`${SCRIPT_PATH}/propagation/test-id.ts`);
655+
656+
const worker = await startWorker(`${SCRIPT_PATH}/propagation/basic-fetch.ts`);
657+
658+
const res = await worker.fetch(`http://worker/?address=${devWorker.address}&port=${devWorker.port}`);
659+
660+
expect(res.status).toBe(200);
661+
662+
const traceId = res.headers.get('trace-id');
663+
if (traceId === null) {
664+
expect(traceId).not.toBeNull();
665+
return;
666+
}
667+
const spanId = res.headers.get('span-id');
668+
if (spanId === null) {
669+
expect(spanId).not.toBeNull();
670+
return;
671+
}
672+
const fetchedTraceId = res.headers.get('fetched-trace-id');
673+
if (fetchedTraceId === null) {
674+
expect(fetchedTraceId).not.toBeNull();
675+
return;
676+
}
677+
const fetchedParentId = res.headers.get('fetched-parent-id');
678+
if (fetchedParentId === null) {
679+
expect(fetchedParentId).not.toBeNull();
680+
return;
681+
}
682+
683+
const trace = await getTrace<ZipkinJson>(collectorWorker, traceId);
684+
685+
// Validate the context was passed down
686+
expect(fetchedTraceId).toBe(traceId);
687+
expect(fetchedParentId).toBe(spanId);
688+
689+
// Root + 1 child
690+
expect(trace.length).toBe(2);
691+
692+
// Root span
693+
const rootSpan = trace[0];
694+
expect(rootSpan.traceId).toBe(traceId);
695+
expect(rootSpan.name).toBe('Request');
696+
expect(rootSpan.localEndpoint.serviceName).toBe('basic-fetch');
697+
698+
// First child span
699+
const firstChildSpan = trace[1];
700+
expect(firstChildSpan.traceId).toBe(traceId);
701+
expect(firstChildSpan.parentId).toBe(rootSpan.id);
702+
expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH);
703+
expect(firstChildSpan.duration).not.toBe(0);
704+
705+
await worker.stop();
706+
});
707+
708+
test('Context is passed in tracedFetch', async () => {
709+
devWorker = await startWorker(`${SCRIPT_PATH}/propagation/test-id.ts`);
710+
711+
const worker = await startWorker(`${SCRIPT_PATH}/propagation/traced-fetch.ts`);
712+
713+
const res = await worker.fetch(`http://worker/?address=${devWorker.address}&port=${devWorker.port}`);
714+
715+
expect(res.status).toBe(200);
716+
717+
const traceId = res.headers.get('trace-id');
718+
if (traceId === null) {
719+
expect(traceId).not.toBeNull();
720+
return;
721+
}
722+
const spanId = res.headers.get('span-id');
723+
if (spanId === null) {
724+
expect(spanId).not.toBeNull();
725+
return;
726+
}
727+
const fetchedTraceId = res.headers.get('fetched-trace-id');
728+
if (fetchedTraceId === null) {
729+
expect(fetchedTraceId).not.toBeNull();
730+
return;
731+
}
732+
const fetchedParentId = res.headers.get('fetched-parent-id');
733+
if (fetchedParentId === null) {
734+
expect(fetchedParentId).not.toBeNull();
735+
return;
736+
}
737+
738+
const trace = await getTrace<ZipkinJson>(collectorWorker, traceId);
739+
740+
// Validate the context was passed down
741+
expect(fetchedTraceId).toBe(traceId);
742+
expect(fetchedParentId).toBe(spanId);
743+
744+
// Root + 1 child
745+
expect(trace.length).toBe(2);
746+
747+
// Root span
748+
const rootSpan = trace[0];
749+
expect(rootSpan.traceId).toBe(traceId);
750+
expect(rootSpan.name).toBe('Request');
751+
expect(rootSpan.localEndpoint.serviceName).toBe('traced-fetch');
752+
753+
// First child span
754+
const firstChildSpan = trace[1];
755+
expect(firstChildSpan.traceId).toBe(traceId);
756+
expect(firstChildSpan.parentId).toBe(rootSpan.id);
757+
expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH);
758+
expect(firstChildSpan.duration).not.toBe(0);
759+
760+
await worker.stop();
761+
});
762+
763+
test.todo('Can pass context in service binding');
764+
});
649765
});

0 commit comments

Comments
 (0)