Skip to content
This repository was archived by the owner on Oct 30, 2025. It is now read-only.

Commit ecaf2cf

Browse files
authored
Merge pull request #4 from zeit/determinisitic-sampling
Add deterministic sampling
2 parents 29c967d + 6dcf392 commit ecaf2cf

File tree

14 files changed

+438
-73
lines changed

14 files changed

+438
-73
lines changed

README.md

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,109 @@
22

33
A partial implementation of the [OpenTracing JavaScript API](https://opentracing-javascript.surge.sh) for [honeycomb.io](https://www.honeycomb.io) backend.
44

5-
6-
[![homecomb-ui](https://user-images.githubusercontent.com/229881/53273403-ed56fd80-36c1-11e9-95b5-d5277bb621ff.png)](https://ui.honeycomb.io)
5+
[![homecomb-ui](https://user-images.githubusercontent.com/229881/53371218-a1a09000-391d-11e9-9956-8ee2b5d62a0f.png)](https://ui.honeycomb.io)
76

87
## Usage
98

109
```ts
11-
import { Tracer } from '@zeit/tracing-js';
10+
import { IncomingMessage, ServerResponse, createServer } from 'http';
11+
import { Tracer, SpanContext, Tags, DeterministicSampler } from '@zeit/tracing-js';
1212

13-
const tracer = new Tracer('service-name', {
14-
writeKey: process.env.HONEYCOMB_KEY,
15-
dataset: process.env.HONEYCOMB_DATASET
16-
});
13+
const tracer = new Tracer(
14+
{
15+
serviceName: 'routing-example',
16+
environment: process.env.ENVIRONMENT,
17+
dc: process.env.DC,
18+
podName: process.env.PODNAME,
19+
hostName: process.env.HOSTNAME,
20+
sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE),
21+
},
22+
{
23+
writeKey: process.env.HONEYCOMB_KEY!,
24+
dataset: process.env.HONEYCOMB_DATASET!,
25+
},
26+
);
1727

1828
// example child function we wish to trace
19-
async function sleep(ms, parentSpan) {
20-
const span = tracer.startSpan(sleep.name, { childOf: parentSpan });
21-
return new Promise(resolve =>
22-
setTimeout(() => {
23-
span.finish();
24-
resolve();
25-
}, ms)
26-
);
29+
async function sleep(ms: number, childOf: SpanContext) {
30+
const span = tracer.startSpan(sleep.name, { childOf });
31+
return new Promise(resolve =>
32+
setTimeout(() => {
33+
span.finish();
34+
resolve();
35+
}, ms),
36+
);
2737
}
2838

29-
// example parent function we wish to trace
30-
async function handler(req, res) {
31-
const span = tracer.startSpan(handler.name);
32-
await sleep(300, span);
33-
console.log('end trace');
39+
// example child function we wish to trace
40+
async function route(path: string, childOf: SpanContext) {
41+
const span = tracer.startSpan(route.name, { childOf });
42+
const spanContext = span.context();
43+
44+
await sleep(200, spanContext);
45+
46+
if (!path || path === '/') {
3447
span.finish();
35-
};
48+
return 'Home page';
49+
} else if (path === '/next') {
50+
span.finish();
51+
return 'Next page';
52+
}
53+
54+
span.finish();
55+
throw new Error('Page not found');
56+
}
57+
58+
// example parent function we wish to trace
59+
async function handler(req: IncomingMessage, res: ServerResponse) {
60+
const { tags, childOf } = parseRequest(req);
61+
const span = tracer.startSpan(handler.name, { tags, childOf });
62+
const spanContext = span.context();
63+
let statusCode = 200;
64+
65+
try {
66+
const { url = '/' } = req;
67+
await sleep(100, spanContext);
68+
const output = await route(url, spanContext);
69+
res.write(output);
70+
} catch (error) {
71+
statusCode = 500;
72+
tags[Tags.ERROR] = true;
73+
res.write(error.message);
74+
}
75+
76+
tags[Tags.HTTP_STATUS_CODE] = statusCode;
77+
res.statusCode = statusCode;
78+
res.end();
79+
span.finish();
80+
}
81+
82+
function getFirstHeader(req: IncomingMessage, key: string) {
83+
const value = req.headers[key];
84+
return Array.isArray(value) ? value[0] : value;
85+
}
86+
87+
function parseRequest(req: IncomingMessage) {
88+
const tags: { [key: string]: any } = {};
89+
tags[Tags.HTTP_METHOD] = req.method;
90+
tags[Tags.HTTP_URL] = req.url;
91+
92+
const priority = getFirstHeader(req, 'x-now-trace-priority');
93+
if (typeof priority !== 'undefined') {
94+
tags[Tags.SAMPLING_PRIORITY] = Number.parseInt(priority);
95+
}
96+
97+
let childOf: SpanContext | undefined;
98+
const traceId = getFirstHeader(req, 'x-now-id');
99+
const parentId = getFirstHeader(req, 'x-now-parent-id');
100+
if (traceId) {
101+
childOf = new SpanContext(traceId, parentId, tags);
102+
}
103+
104+
return { tags, childOf };
105+
}
36106

37-
handler('req', 'res')
38-
.then(() => console.log('done'))
39-
.catch(e => console.error(e));
107+
createServer(handler).listen(3000);
40108
```
41109

42110
## Connecting traces across multiple services
@@ -45,10 +113,10 @@ You can set a parent trace, even if you don't have a reference to the `Span` obj
45113

46114
Instead, you can create a new `SpanContext`.
47115

48-
You'll need the `parentTraceId` and `parentSpanId` (typically found in `req.headers`).
116+
You'll need the `traceId` and `parentSpanId` (typically found in `req.headers`).
49117

50118
```ts
51-
const context = new SpanContext(parentTraceId, parentSpanId);
119+
const context = new SpanContext(traceId, parentSpanId);
52120
const childSpan = tracer.startSpan('child', { childOf: context });
53121
// ...do stuff like normal
54122
childSpan.finish();

examples/routing-example.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { IncomingMessage, ServerResponse, createServer } from 'http';
2+
import { Tracer, SpanContext, Tags, DeterministicSampler } from '../src/index';
3+
4+
const tracer = new Tracer(
5+
{
6+
serviceName: 'routing-example',
7+
environment: process.env.ENVIRONMENT,
8+
dc: process.env.DC,
9+
podName: process.env.PODNAME,
10+
hostName: process.env.HOSTNAME,
11+
sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE),
12+
},
13+
{
14+
writeKey: process.env.HONEYCOMB_KEY!,
15+
dataset: process.env.HONEYCOMB_DATASET!,
16+
},
17+
);
18+
19+
// example child function we wish to trace
20+
async function sleep(ms: number, childOf: SpanContext) {
21+
const span = tracer.startSpan(sleep.name, { childOf });
22+
return new Promise(resolve =>
23+
setTimeout(() => {
24+
span.finish();
25+
resolve();
26+
}, ms),
27+
);
28+
}
29+
30+
// example child function we wish to trace
31+
async function route(path: string, childOf: SpanContext) {
32+
const span = tracer.startSpan(route.name, { childOf });
33+
const spanContext = span.context();
34+
35+
await sleep(200, spanContext);
36+
37+
if (!path || path === '/') {
38+
span.finish();
39+
return 'Home page';
40+
} else if (path === '/next') {
41+
span.finish();
42+
return 'Next page';
43+
}
44+
45+
span.finish();
46+
throw new Error('Page not found');
47+
}
48+
49+
// example parent function we wish to trace
50+
async function handler(req: IncomingMessage, res: ServerResponse) {
51+
const { tags, childOf } = parseRequest(req);
52+
const span = tracer.startSpan(handler.name, { tags, childOf });
53+
const spanContext = span.context();
54+
let statusCode = 200;
55+
56+
try {
57+
const { url = '/' } = req;
58+
await sleep(100, spanContext);
59+
const output = await route(url, spanContext);
60+
res.write(output);
61+
} catch (error) {
62+
statusCode = 500;
63+
tags[Tags.ERROR] = true;
64+
res.write(error.message);
65+
}
66+
67+
tags[Tags.HTTP_STATUS_CODE] = statusCode;
68+
res.statusCode = statusCode;
69+
res.end();
70+
span.finish();
71+
}
72+
73+
function getFirstHeader(req: IncomingMessage, key: string) {
74+
const value = req.headers[key];
75+
return Array.isArray(value) ? value[0] : value;
76+
}
77+
78+
function parseRequest(req: IncomingMessage) {
79+
const tags: { [key: string]: any } = {};
80+
tags[Tags.HTTP_METHOD] = req.method;
81+
tags[Tags.HTTP_URL] = req.url;
82+
83+
const priority = getFirstHeader(req, 'x-now-trace-priority');
84+
if (typeof priority !== 'undefined') {
85+
tags[Tags.SAMPLING_PRIORITY] = Number.parseInt(priority);
86+
}
87+
88+
let childOf: SpanContext | undefined;
89+
const traceId = getFirstHeader(req, 'x-now-id');
90+
const parentId = getFirstHeader(req, 'x-now-parent-id');
91+
if (traceId) {
92+
childOf = new SpanContext(traceId, parentId, tags);
93+
}
94+
95+
return { tags, childOf };
96+
}
97+
98+
createServer(handler).listen(3000);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"scripts": {
88
"build": "tsc",
99
"watch": "tsc --watch",
10-
"fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types}/**/*.ts'",
10+
"fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types,examples}/**/*.ts'",
1111
"test": "tape dist/test/**.js"
1212
},
1313
"author": "styfle",

src/deterministic-sampler.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
1-
import { createHash } from 'crypto';
21
// TODO: add browser support with Web Crypto
32
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
3+
import { createHash } from 'crypto';
4+
import { SamplerBase } from './shared';
45

56
const MAX_UINT32 = Math.pow(2, 32) - 1;
67

7-
export class DeterministicSampler {
8+
export class DeterministicSampler implements SamplerBase {
89
private upperBound: number;
9-
constructor(sampleRate: number) {
10-
this.upperBound = (MAX_UINT32 / sampleRate) >>> 0;
10+
11+
/**
12+
* Determinisically sample a trace based on the trace id.
13+
* Each service will share the same trace id so this works
14+
* across multiple services/spans that are part of the same trace.
15+
* @param sampleRate Defaults to 1 (100%). Set to 2 for 50%, 4 for 25%, etc.
16+
*/
17+
constructor(sampleRate: string | number | undefined) {
18+
let rate: number;
19+
if (typeof sampleRate === 'number') {
20+
rate = sampleRate;
21+
} else if (typeof sampleRate === 'string') {
22+
rate = Number.parseInt(sampleRate);
23+
} else {
24+
rate = 1;
25+
}
26+
this.upperBound = (MAX_UINT32 / rate) >>> 0;
1127
}
1228

13-
sample(data: string) {
14-
let sum = createHash('SHA1')
15-
.update(data)
29+
sample(traceId: string) {
30+
const sum = createHash('SHA1')
31+
.update(traceId)
1632
.digest();
1733
return sum.readUInt32BE(0) <= this.upperBound;
1834
}

src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Tracer } from './tracer';
2+
import { Span as PrivateSpan } from './span';
23
import { SpanContext } from './span-context';
34
import * as Tags from './tags';
5+
import { DeterministicSampler } from './deterministic-sampler';
6+
import { SamplerBase } from './shared';
47

5-
export { Tracer, SpanContext, Tags };
8+
const Span = typeof PrivateSpan;
9+
10+
export { Tracer, Span, SpanContext, Tags, DeterministicSampler, SamplerBase };

src/shared.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,23 @@ import { Span } from './span';
33
import { SpanContext } from './span-context';
44
import { HoneyOptions } from 'libhoney';
55

6-
export type TracerOptions = HoneyOptions | Libhoney;
6+
export interface TracerOptions {
7+
serviceName: string;
8+
environment?: string;
9+
dc?: string;
10+
podName?: string;
11+
hostName?: string;
12+
sampler?: SamplerBase;
13+
}
14+
15+
export type TracerHoneyOptions = HoneyOptions | Libhoney;
716
export type SpanTags = { [key: string]: any };
817

918
export interface SpanOptions {
1019
childOf?: SpanContext | Span;
1120
tags?: SpanTags;
1221
}
22+
23+
export interface SamplerBase {
24+
sample(data: string): boolean;
25+
}

src/span-context.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { SpanTags } from './shared';
2+
import { generateId } from './generate-id';
23

34
export class SpanContext {
45
constructor(
56
private traceId: string,
6-
private spanId: string,
7+
private spanId: string | undefined,
78
private tags: SpanTags,
89
) {
910
this.traceId = traceId;
10-
this.spanId = spanId;
11+
this.spanId = spanId || generateId();
1112
this.tags = tags;
1213
}
1314
toSpanId() {

0 commit comments

Comments
 (0)