Skip to content
This repository was archived by the owner on Oct 30, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 94 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,109 @@

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


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

## Usage

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

const tracer = new Tracer('service-name', {
writeKey: process.env.HONEYCOMB_KEY,
dataset: process.env.HONEYCOMB_DATASET
});
const tracer = new Tracer(
{
serviceName: 'routing-example',
environment: process.env.ENVIRONMENT,
dc: process.env.DC,
podName: process.env.PODNAME,
hostName: process.env.HOSTNAME,
sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE),
},
{
writeKey: process.env.HONEYCOMB_KEY!,
dataset: process.env.HONEYCOMB_DATASET!,
},
);

// example child function we wish to trace
async function sleep(ms, parentSpan) {
const span = tracer.startSpan(sleep.name, { childOf: parentSpan });
return new Promise(resolve =>
setTimeout(() => {
span.finish();
resolve();
}, ms)
);
async function sleep(ms: number, childOf: SpanContext) {
const span = tracer.startSpan(sleep.name, { childOf });
return new Promise(resolve =>
setTimeout(() => {
span.finish();
resolve();
}, ms),
);
}

// example parent function we wish to trace
async function handler(req, res) {
const span = tracer.startSpan(handler.name);
await sleep(300, span);
console.log('end trace');
// example child function we wish to trace
async function route(path: string, childOf: SpanContext) {
const span = tracer.startSpan(route.name, { childOf });
const spanContext = span.context();

await sleep(200, spanContext);

if (!path || path === '/') {
span.finish();
};
return 'Home page';
} else if (path === '/next') {
span.finish();
return 'Next page';
}

span.finish();
throw new Error('Page not found');
}

// example parent function we wish to trace
async function handler(req: IncomingMessage, res: ServerResponse) {
const { tags, childOf } = parseRequest(req);
const span = tracer.startSpan(handler.name, { tags, childOf });
const spanContext = span.context();
let statusCode = 200;

try {
const { url = '/' } = req;
await sleep(100, spanContext);
const output = await route(url, spanContext);
res.write(output);
} catch (error) {
statusCode = 500;
tags[Tags.ERROR] = true;
res.write(error.message);
}

tags[Tags.HTTP_STATUS_CODE] = statusCode;
res.statusCode = statusCode;
res.end();
span.finish();
}

function getFirstHeader(req: IncomingMessage, key: string) {
const value = req.headers[key];
return Array.isArray(value) ? value[0] : value;
}

function parseRequest(req: IncomingMessage) {
const tags: { [key: string]: any } = {};
tags[Tags.HTTP_METHOD] = req.method;
tags[Tags.HTTP_URL] = req.url;

const priority = getFirstHeader(req, 'x-now-trace-priority');
if (typeof priority !== 'undefined') {
tags[Tags.SAMPLING_PRIORITY] = Number.parseInt(priority);
}

let childOf: SpanContext | undefined;
const traceId = getFirstHeader(req, 'x-now-id');
const parentId = getFirstHeader(req, 'x-now-parent-id');
if (traceId) {
childOf = new SpanContext(traceId, parentId, tags);
}

return { tags, childOf };
}

handler('req', 'res')
.then(() => console.log('done'))
.catch(e => console.error(e));
createServer(handler).listen(3000);
```

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

Instead, you can create a new `SpanContext`.

You'll need the `parentTraceId` and `parentSpanId` (typically found in `req.headers`).
You'll need the `traceId` and `parentSpanId` (typically found in `req.headers`).

```ts
const context = new SpanContext(parentTraceId, parentSpanId);
const context = new SpanContext(traceId, parentSpanId);
const childSpan = tracer.startSpan('child', { childOf: context });
// ...do stuff like normal
childSpan.finish();
Expand Down
98 changes: 98 additions & 0 deletions examples/routing-example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { IncomingMessage, ServerResponse, createServer } from 'http';
import { Tracer, SpanContext, Tags, DeterministicSampler } from '../src/index';

const tracer = new Tracer(
{
serviceName: 'routing-example',
environment: process.env.ENVIRONMENT,
dc: process.env.DC,
podName: process.env.PODNAME,
hostName: process.env.HOSTNAME,
sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE),
},
{
writeKey: process.env.HONEYCOMB_KEY!,
dataset: process.env.HONEYCOMB_DATASET!,
},
);

// example child function we wish to trace
async function sleep(ms: number, childOf: SpanContext) {
const span = tracer.startSpan(sleep.name, { childOf });
return new Promise(resolve =>
setTimeout(() => {
span.finish();
resolve();
}, ms),
);
}

// example child function we wish to trace
async function route(path: string, childOf: SpanContext) {
const span = tracer.startSpan(route.name, { childOf });
const spanContext = span.context();

await sleep(200, spanContext);

if (!path || path === '/') {
span.finish();
return 'Home page';
} else if (path === '/next') {
span.finish();
return 'Next page';
}

span.finish();
throw new Error('Page not found');
}

// example parent function we wish to trace
async function handler(req: IncomingMessage, res: ServerResponse) {
const { tags, childOf } = parseRequest(req);
const span = tracer.startSpan(handler.name, { tags, childOf });
const spanContext = span.context();
let statusCode = 200;

try {
const { url = '/' } = req;
await sleep(100, spanContext);
const output = await route(url, spanContext);
res.write(output);
} catch (error) {
statusCode = 500;
tags[Tags.ERROR] = true;
res.write(error.message);
}

tags[Tags.HTTP_STATUS_CODE] = statusCode;
res.statusCode = statusCode;
res.end();
span.finish();
}

function getFirstHeader(req: IncomingMessage, key: string) {
const value = req.headers[key];
return Array.isArray(value) ? value[0] : value;
}

function parseRequest(req: IncomingMessage) {
const tags: { [key: string]: any } = {};
tags[Tags.HTTP_METHOD] = req.method;
tags[Tags.HTTP_URL] = req.url;

const priority = getFirstHeader(req, 'x-now-trace-priority');
if (typeof priority !== 'undefined') {
tags[Tags.SAMPLING_PRIORITY] = Number.parseInt(priority);
}

let childOf: SpanContext | undefined;
const traceId = getFirstHeader(req, 'x-now-id');
const parentId = getFirstHeader(req, 'x-now-parent-id');
if (traceId) {
childOf = new SpanContext(traceId, parentId, tags);
}

return { tags, childOf };
}

createServer(handler).listen(3000);
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types}/**/*.ts'",
"fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types,examples}/**/*.ts'",
"test": "tape dist/test/**.js"
},
"author": "styfle",
Expand Down
30 changes: 23 additions & 7 deletions src/deterministic-sampler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import { createHash } from 'crypto';
// TODO: add browser support with Web Crypto
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
import { createHash } from 'crypto';
import { SamplerBase } from './shared';

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

export class DeterministicSampler {
export class DeterministicSampler implements SamplerBase {
private upperBound: number;
constructor(sampleRate: number) {
this.upperBound = (MAX_UINT32 / sampleRate) >>> 0;

/**
* Determinisically sample a trace based on the trace id.
* Each service will share the same trace id so this works
* across multiple services/spans that are part of the same trace.
* @param sampleRate Defaults to 1 (100%). Set to 2 for 50%, 4 for 25%, etc.
*/
constructor(sampleRate: string | number | undefined) {
let rate: number;
if (typeof sampleRate === 'number') {
rate = sampleRate;
} else if (typeof sampleRate === 'string') {
rate = Number.parseInt(sampleRate);
} else {
rate = 1;
}
this.upperBound = (MAX_UINT32 / rate) >>> 0;
}

sample(data: string) {
let sum = createHash('SHA1')
.update(data)
sample(traceId: string) {
const sum = createHash('SHA1')
.update(traceId)
.digest();
return sum.readUInt32BE(0) <= this.upperBound;
}
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Tracer } from './tracer';
import { Span as PrivateSpan } from './span';
import { SpanContext } from './span-context';
import * as Tags from './tags';
import { DeterministicSampler } from './deterministic-sampler';
import { SamplerBase } from './shared';

export { Tracer, SpanContext, Tags };
const Span = typeof PrivateSpan;

export { Tracer, Span, SpanContext, Tags, DeterministicSampler, SamplerBase };
15 changes: 14 additions & 1 deletion src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,23 @@ import { Span } from './span';
import { SpanContext } from './span-context';
import { HoneyOptions } from 'libhoney';

export type TracerOptions = HoneyOptions | Libhoney;
export interface TracerOptions {
serviceName: string;
environment?: string;
dc?: string;
podName?: string;
hostName?: string;
sampler?: SamplerBase;
}

export type TracerHoneyOptions = HoneyOptions | Libhoney;
export type SpanTags = { [key: string]: any };

export interface SpanOptions {
childOf?: SpanContext | Span;
tags?: SpanTags;
}

export interface SamplerBase {
sample(data: string): boolean;
}
5 changes: 3 additions & 2 deletions src/span-context.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { SpanTags } from './shared';
import { generateId } from './generate-id';

export class SpanContext {
constructor(
private traceId: string,
private spanId: string,
private spanId: string | undefined,
private tags: SpanTags,
) {
this.traceId = traceId;
this.spanId = spanId;
this.spanId = spanId || generateId();
this.tags = tags;
}
toSpanId() {
Expand Down
Loading