From 6811480fa4794d4a602947c78b86367ce4beeb43 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Feb 2019 12:52:04 -0500 Subject: [PATCH 1/4] Add deterministic sampling --- README.md | 69 ++++++++++----- examples/simple.ts | 55 ++++++++++++ package.json | 2 +- src/deterministic-sampler.ts | 30 +++++-- src/index.ts | 4 +- src/shared.ts | 11 ++- src/span.ts | 35 ++++++-- src/tracer.ts | 27 ++++-- test/deterministic-sampler.test.ts | 6 ++ test/span.test.ts | 138 +++++++++++++++++++++++++++-- test/tracer.test.ts | 24 +++-- 11 files changed, 337 insertions(+), 64 deletions(-) create mode 100644 examples/simple.ts diff --git a/README.md b/README.md index d2c6e8f..c77f1ae 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,62 @@ A partial implementation of the [OpenTracing JavaScript API](https://opentracing ## Usage ```ts -import { Tracer } from '@zeit/tracing-js'; +import { + Tracer, + Tags, + DeterministicSampler, + SpanContext, +} 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: 'service-name', + sampler: new DeterministicSampler(process.env.HONEYCOMB_SAMPLERATE), + }, + { + 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, parentSpan: any) { + const span = tracer.startSpan(sleep.name, { childOf: parentSpan }); + 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'); - span.finish(); -}; +async function handler(req: any, res: any) { + const tags = {}; + if (req.headers && req.headers['x-now-trace-priority']) { + const priority = Number.parseInt(req.headers['x-now-trace-priority']); + tags[Tags.SAMPLING_PRIORITY] = priority; + } + + let childOf: SpanContext | undefined; + if (req.headers && req.headers['x-now-id']) { + const traceId = req.headers['x-now-id']; + const parentId = req.headers['x-now-parent-id']; + childOf = new SpanContext(traceId, parentId, tags); + } + + const span = tracer.startSpan(handler.name, { tags, childOf }); + + await sleep(300, span); + await sleep(300, span); + + span.finish(); +} handler('req', 'res') - .then(() => console.log('done')) - .catch(e => console.error(e)); + .then(() => console.log('done')) + .catch(e => console.error(e)); + ``` ## Connecting traces across multiple services diff --git a/examples/simple.ts b/examples/simple.ts new file mode 100644 index 0000000..92dd0da --- /dev/null +++ b/examples/simple.ts @@ -0,0 +1,55 @@ +import { + Tracer, + Tags, + DeterministicSampler, + SpanContext, +} from '../dist/src/index'; + +const tracer = new Tracer( + { + serviceName: 'service-name', + sampler: new DeterministicSampler(process.env.HONEYCOMB_SAMPLERATE), + }, + { + writeKey: process.env.HONEYCOMB_KEY, + dataset: process.env.HONEYCOMB_DATASET, + }, +); + +// example child function we wish to trace +async function sleep(ms: number, parentSpan: any) { + const span = tracer.startSpan(sleep.name, { childOf: parentSpan }); + return new Promise(resolve => + setTimeout(() => { + span.finish(); + resolve(); + }, ms), + ); +} + +// example parent function we wish to trace +async function handler(req: any, res: any) { + const tags = {}; + if (req.headers && req.headers['x-now-trace-priority']) { + const priority = Number.parseInt(req.headers['x-now-trace-priority']); + tags[Tags.SAMPLING_PRIORITY] = priority; + } + + let childOf: SpanContext | undefined; + if (req.headers && req.headers['x-now-id']) { + const traceId = req.headers['x-now-id']; + const parentId = req.headers['x-now-parent-id']; + childOf = new SpanContext(traceId, parentId, tags); + } + + const span = tracer.startSpan(handler.name, { tags, childOf }); + + await sleep(300, span); + await sleep(300, span); + + span.finish(); +} + +handler('req', 'res') + .then(() => console.log('done')) + .catch(e => console.error(e)); diff --git a/package.json b/package.json index f06276b..423ee2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/deterministic-sampler.ts b/src/deterministic-sampler.ts index d81d8ad..07d5b49 100644 --- a/src/deterministic-sampler.ts +++ b/src/deterministic-sampler.ts @@ -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; } diff --git a/src/index.ts b/src/index.ts index dd945d3..bf0a71a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import { Tracer } from './tracer'; import { SpanContext } from './span-context'; import * as Tags from './tags'; +import { DeterministicSampler } from './deterministic-sampler'; +import { SamplerBase } from './shared'; -export { Tracer, SpanContext, Tags }; +export { Tracer, SpanContext, Tags, DeterministicSampler, SamplerBase }; diff --git a/src/shared.ts b/src/shared.ts index 96b6a20..271a02c 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -3,10 +3,19 @@ import { Span } from './span'; import { SpanContext } from './span-context'; import { HoneyOptions } from 'libhoney'; -export type TracerOptions = HoneyOptions | Libhoney; +export interface TracerOptions { + serviceName: 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; +} diff --git a/src/span.ts b/src/span.ts index e9a8dc4..337bf98 100644 --- a/src/span.ts +++ b/src/span.ts @@ -1,34 +1,37 @@ import { HoneyEvent } from 'libhoney'; import { SpanContext } from './span-context'; import { generateId } from './generate-id'; -import { SpanTags } from './shared'; +import { SpanTags, SamplerBase } from './shared'; +import { SAMPLING_PRIORITY } from './tags'; export class Span { + private spanId: string; private event: HoneyEvent; private serviceName: string; private name: string; private traceId: string; private parentId: string | undefined; private tags: SpanTags; - private spanId: string; + private sampler: SamplerBase; private start: Date; constructor( event: HoneyEvent, serviceName: string, name: string, - traceId = generateId(), - parentId?: string, - tags?: SpanTags, + traceId: string | undefined, + parentId: string | undefined, + tags: SpanTags, + sampler: SamplerBase, ) { - const spanId = generateId(); + this.spanId = generateId(); this.event = event; this.name = name; this.serviceName = serviceName; - this.traceId = traceId; - this.spanId = spanId; + this.traceId = traceId || generateId(); this.parentId = parentId; - this.tags = tags || {}; + this.tags = tags; + this.sampler = sampler; this.start = new Date(); } @@ -52,7 +55,21 @@ export class Span { this.name = name; } + private isSendable() { + const priority = this.tags[SAMPLING_PRIORITY]; + + if (typeof priority === 'undefined') { + return this.sampler.sample(this.traceId); + } + + return priority > 0; + } + finish() { + if (!this.isSendable()) { + return; + } + const duration = Date.now() - this.start.getTime(); this.event.addField('duration_ms', duration); this.event.addField('name', this.name); diff --git a/src/tracer.ts b/src/tracer.ts index e0e712a..1043fe4 100644 --- a/src/tracer.ts +++ b/src/tracer.ts @@ -1,23 +1,31 @@ -import { TracerOptions, SpanOptions } from './shared'; +import { + TracerOptions, + TracerHoneyOptions, + SpanOptions, + SamplerBase, +} from './shared'; import { Span } from './span'; import { SpanContext } from './span-context'; import { SAMPLING_PRIORITY } from './tags'; import Libhoney from 'libhoney'; +import { DeterministicSampler } from './deterministic-sampler'; export class Tracer { - private hny: Libhoney; + private honey: Libhoney; private serviceName: string; + private sampler: SamplerBase; - constructor(serviceName: string, tracerOptions: TracerOptions) { - this.serviceName = serviceName; - if (tracerOptions instanceof Libhoney) { - this.hny = tracerOptions; + constructor(tracerOptions: TracerOptions, honeyOptions: TracerHoneyOptions) { + this.serviceName = tracerOptions.serviceName; + this.sampler = tracerOptions.sampler || new DeterministicSampler(1); + if (honeyOptions instanceof Libhoney) { + this.honey = honeyOptions; } else { - this.hny = new Libhoney(tracerOptions); + this.honey = new Libhoney(honeyOptions); } } startSpan(name: string, spanOptions: SpanOptions = {}) { - const { childOf, tags={} } = spanOptions; + const { childOf, tags = {} } = spanOptions; let traceId: string | undefined; let parentId: string | undefined; let samplingPriority: number | undefined; @@ -41,12 +49,13 @@ export class Tracer { } return new Span( - this.hny.newEvent(), + this.honey.newEvent(), this.serviceName, name, traceId, parentId, tags, + this.sampler, ); } } diff --git a/test/deterministic-sampler.test.ts b/test/deterministic-sampler.test.ts index 7f8e613..a3b3faa 100644 --- a/test/deterministic-sampler.test.ts +++ b/test/deterministic-sampler.test.ts @@ -23,6 +23,12 @@ test('deterministic sampler same result each time', t => { t.equal(first, second); }); +test('deterministic sampler allow 0%', t => { + t.plan(1); + const passed = testSampleRate(0, 1); + t.false(passed); +}); + test('deterministic sampler allow 100%', t => { t.plan(1); const passed = testSampleRate(1, 1); diff --git a/test/span.test.ts b/test/span.test.ts index 31358cb..3636ba2 100644 --- a/test/span.test.ts +++ b/test/span.test.ts @@ -1,6 +1,8 @@ import test from 'tape'; import { Span } from '../src/span'; import { HoneyEvent } from 'libhoney'; +import { DeterministicSampler } from '../src/deterministic-sampler'; +import { SAMPLING_PRIORITY } from '../src/tags'; const noop = () => {}; @@ -9,11 +11,22 @@ test('test span context', t => { const serviceName = 'service name'; const name = 'function name'; const traceId = 'trace123'; + const parentId = undefined; + const tags = {}; + const sampler = new DeterministicSampler(1); const event: HoneyEvent = { addField: noop, send: noop, }; - const span = new Span(event, serviceName, name, traceId); + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); const ctx = span.context(); t.equal(ctx.toTraceId(), traceId); t.notEqual(ctx.toSpanId(), ''); @@ -24,6 +37,9 @@ test('test span setTag', t => { const serviceName = 'service name'; const name = 'function name'; const traceId = 'trace123'; + const parentId = undefined; + const tags = {}; + const sampler = new DeterministicSampler(1); const event: HoneyEvent = { addField: (key: string, value: any) => { if (key === 'tag.key1') { @@ -34,7 +50,15 @@ test('test span setTag', t => { }, send: noop, }; - const span = new Span(event, serviceName, name, traceId); + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); span .setTag('key1', 'value1') .setTag('key2', 'value2') @@ -46,6 +70,9 @@ test('test span addTags', t => { const serviceName = 'service name'; const name = 'function name'; const traceId = 'trace123'; + const parentId = undefined; + const tags = {}; + const sampler = new DeterministicSampler(1); const event: HoneyEvent = { addField: (key: string, value: any) => { if (key === 'tag.key1') { @@ -56,7 +83,15 @@ test('test span addTags', t => { }, send: noop, }; - const span = new Span(event, serviceName, name, traceId); + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); span.addTags({ key1: 'value1', key2: 'value2' }).finish(); }); @@ -66,7 +101,12 @@ test('test span addField', t => { const name = 'function name'; const traceId = 'trace123'; const parentId = 'parent123'; + const tags = {}; + const sampler = new DeterministicSampler(1); const event: HoneyEvent = { + send: () => { + t.true(event.timestamp && event.timestamp > new Date(0)); + }, addField: (key: string, value: any) => { switch (key) { case 'duration_ms': @@ -89,10 +129,96 @@ test('test span addField', t => { break; } }, + }; + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); + setTimeout(() => span.finish(), 50); +}); + +test('test span sample rate 0 should not send', t => { + t.plan(1); + const serviceName = 'service name'; + const name = 'function name'; + const traceId = 'trace123'; + const parentId = 'parent123'; + const event: HoneyEvent = { + addField: noop, send: () => { - t.true(event.timestamp && event.timestamp > new Date(0)); + t.true(false, 'should not send'); }, }; - const span = new Span(event, serviceName, name, traceId, parentId); - setTimeout(() => span.finish(), 50); + const tags = {}; + const sampler = new DeterministicSampler(0); + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); + span.finish(); + t.true(true, 'finish'); +}); + +test('test span sample rate 0, tag priority 1 should send', t => { + t.plan(2); + const serviceName = 'service name'; + const name = 'function name'; + const traceId = 'trace123'; + const parentId = 'parent123'; + const event: HoneyEvent = { + addField: noop, + send: () => { + t.true(true, 'should send'); + }, + }; + const tags = { [SAMPLING_PRIORITY]: 1 }; + const sampler = new DeterministicSampler(0); + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); + span.finish(); + t.true(true, 'finish'); +}); + +test('test span sample rate 1, tag priority 0 should not send', t => { + t.plan(1); + const serviceName = 'service name'; + const name = 'function name'; + const traceId = 'trace123'; + const parentId = 'parent123'; + const event: HoneyEvent = { + addField: noop, + send: () => { + t.true(false, 'should not send'); + }, + }; + const tags = { [SAMPLING_PRIORITY]: 0 }; + const sampler = new DeterministicSampler(1); + const span = new Span( + event, + serviceName, + name, + traceId, + parentId, + tags, + sampler, + ); + span.finish(); + t.true(true, 'finish'); }); diff --git a/test/tracer.test.ts b/test/tracer.test.ts index d958fd7..328cbff 100644 --- a/test/tracer.test.ts +++ b/test/tracer.test.ts @@ -1,11 +1,14 @@ import test from 'tape'; import { Tracer } from '../src/tracer'; import Libhoney, { HoneyEvent, HoneyOptions } from 'libhoney'; -import {SAMPLING_PRIORITY} from '../src/tags'; +import { SAMPLING_PRIORITY } from '../src/tags'; -function newDummyHoney(addField?: (key: string, value: any) => void) { - const noop = () => {}; +const noop = () => {}; +function newDummyHoney( + addField?: (key: string, value: any) => void, + send?: () => void, +) { class DummyHoney extends Libhoney { constructor(options: HoneyOptions) { super(options); @@ -13,7 +16,7 @@ function newDummyHoney(addField?: (key: string, value: any) => void) { newEvent(): HoneyEvent { return { addField: addField || noop, - send: noop, + send: send || noop, }; } } @@ -27,7 +30,7 @@ function newDummyHoney(addField?: (key: string, value: any) => void) { test('test tracer with no options', t => { t.plan(3); - const tracer = new Tracer('service name', newDummyHoney()); + const tracer = new Tracer({ serviceName: 'service name' }, newDummyHoney()); const span = tracer.startSpan('hello'); const ctx = span.context(); const traceId = ctx.toTraceId(); @@ -39,7 +42,7 @@ test('test tracer with no options', t => { test('test tracer traceId is same for parent & child', t => { t.plan(1); - const tracer = new Tracer('service name', newDummyHoney()); + const tracer = new Tracer({ serviceName: 'service name' }, newDummyHoney()); const parentSpan = tracer.startSpan('parent'); const childSpan = tracer.startSpan('child', { childOf: parentSpan }); t.equal(parentSpan.context().toTraceId(), childSpan.context().toTraceId()); @@ -47,9 +50,9 @@ test('test tracer traceId is same for parent & child', t => { test('test tracer sample priority is same for parent & child', t => { t.plan(2); - const tracer = new Tracer('service name', newDummyHoney()); + const tracer = new Tracer({ serviceName: 'service name' }, newDummyHoney()); const tags = { [SAMPLING_PRIORITY]: 75 }; - const parentSpan = tracer.startSpan('parent', {tags}); + const parentSpan = tracer.startSpan('parent', { tags }); const childSpan = tracer.startSpan('child', { childOf: parentSpan }); const parentPriority = parentSpan.context().getTag(SAMPLING_PRIORITY); const childPriority = childSpan.context().getTag(SAMPLING_PRIORITY); @@ -66,7 +69,10 @@ test('test tracer tags', t => { t.equal(value, 'value2'); } }; - const tracer = new Tracer('service name', newDummyHoney(addField)); + const tracer = new Tracer( + { serviceName: 'service name' }, + newDummyHoney(addField), + ); const tags = { key1: 'value1', key2: 'value2' }; const span = tracer.startSpan('hello', { tags }); span.finish(); From 91774c3f5285110e1d2e506df85066c6111dc019 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Feb 2019 13:00:10 -0500 Subject: [PATCH 2/4] Update example in readme --- README.md | 13 +++---------- package.json | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index c77f1ae..0f8a703 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,8 @@ A partial implementation of the [OpenTracing JavaScript API](https://opentracing ## Usage ```ts -import { - Tracer, - Tags, - DeterministicSampler, - SpanContext, -} from '@zeit/tracing-js'; +import micro from 'micro'; +import { Tracer, Tags, DeterministicSampler, SpanContext } from '@zeit/tracing-js'; const tracer = new Tracer( { @@ -60,10 +56,7 @@ async function handler(req: any, res: any) { span.finish(); } -handler('req', 'res') - .then(() => console.log('done')) - .catch(e => console.error(e)); - +micro(handler).listen(3000); ``` ## Connecting traces across multiple services diff --git a/package.json b/package.json index 423ee2a..f06276b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "build": "tsc", "watch": "tsc --watch", - "fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types,examples}/**/*.ts'", + "fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types}/**/*.ts'", "test": "tape dist/test/**.js" }, "author": "styfle", From 423c2f8998b56f436518e7d20feee9f9887150af Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Feb 2019 16:50:02 -0500 Subject: [PATCH 3/4] Improve example, add more env vars --- examples/simple.ts | 101 +++++++++++++++++++++++++++++++------------- package.json | 2 +- src/index.ts | 5 ++- src/shared.ts | 4 ++ src/span-context.ts | 5 ++- src/tags.ts | 2 +- tsconfig.json | 2 +- 7 files changed, 86 insertions(+), 35 deletions(-) diff --git a/examples/simple.ts b/examples/simple.ts index 92dd0da..77cc3dc 100644 --- a/examples/simple.ts +++ b/examples/simple.ts @@ -1,24 +1,24 @@ -import { - Tracer, - Tags, - DeterministicSampler, - SpanContext, -} from '../dist/src/index'; +import { IncomingMessage, ServerResponse, createServer } from 'http'; +import { Tracer, SpanContext, Tags, DeterministicSampler } from '../src/index'; const tracer = new Tracer( { - serviceName: 'service-name', - sampler: new DeterministicSampler(process.env.HONEYCOMB_SAMPLERATE), + 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, + writeKey: process.env.HONEYCOMB_KEY!, + dataset: process.env.HONEYCOMB_DATASET!, }, ); // example child function we wish to trace -async function sleep(ms: number, parentSpan: any) { - const span = tracer.startSpan(sleep.name, { childOf: parentSpan }); +async function sleep(ms: number, childOf: SpanContext) { + const span = tracer.startSpan(sleep.name, { childOf }); return new Promise(resolve => setTimeout(() => { span.finish(); @@ -27,29 +27,72 @@ async function sleep(ms: number, parentSpan: any) { ); } -// example parent function we wish to trace -async function handler(req: any, res: any) { - const tags = {}; - if (req.headers && req.headers['x-now-trace-priority']) { - const priority = Number.parseInt(req.headers['x-now-trace-priority']); - tags[Tags.SAMPLING_PRIORITY] = priority; - } +// 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(); - let childOf: SpanContext | undefined; - if (req.headers && req.headers['x-now-id']) { - const traceId = req.headers['x-now-id']; - const parentId = req.headers['x-now-parent-id']; - childOf = new SpanContext(traceId, parentId, tags); + 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; - await sleep(300, span); - await sleep(300, span); + 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(); } -handler('req', 'res') - .then(() => console.log('done')) - .catch(e => console.error(e)); +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); diff --git a/package.json b/package.json index f06276b..423ee2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index bf0a71a..af585ad 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +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, DeterministicSampler, SamplerBase }; +const Span = typeof PrivateSpan; + +export { Tracer, Span, SpanContext, Tags, DeterministicSampler, SamplerBase }; diff --git a/src/shared.ts b/src/shared.ts index 271a02c..6e4a12e 100644 --- a/src/shared.ts +++ b/src/shared.ts @@ -5,6 +5,10 @@ import { HoneyOptions } from 'libhoney'; export interface TracerOptions { serviceName: string; + environment?: string; + dc?: string; + podName?: string; + hostName?: string; sampler?: SamplerBase; } diff --git a/src/span-context.ts b/src/span-context.ts index d5f7faf..8f0a002 100644 --- a/src/span-context.ts +++ b/src/span-context.ts @@ -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() { diff --git a/src/tags.ts b/src/tags.ts index e7b6dd6..640bf55 100644 --- a/src/tags.ts +++ b/src/tags.ts @@ -20,7 +20,7 @@ export const SPAN_KIND_MESSAGING_CONSUMER = 'consumer'; export const ERROR = 'error'; /** - * COMPONENT (string) ia s low-cardinality identifier of the module, library, + * COMPONENT (string) A low-cardinality identifier of the module, library, * or package that is generating a span. */ export const COMPONENT = 'component'; diff --git a/tsconfig.json b/tsconfig.json index 402cd19..94dea03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,6 @@ "./node_modules/@types" ] }, - "include": ["./src", "./test", "./types"] + "include": ["./src", "./test", "./types", "./examples"] } \ No newline at end of file From 6dcf392af0d4cf6bb1d2df18acf155cea2864702 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 25 Feb 2019 16:53:11 -0500 Subject: [PATCH 4/4] Update examples --- README.md | 102 +++++++++++++++------ examples/{simple.ts => routing-example.ts} | 0 2 files changed, 75 insertions(+), 27 deletions(-) rename examples/{simple.ts => routing-example.ts} (100%) diff --git a/README.md b/README.md index 0f8a703..8cd8095 100644 --- a/README.md +++ b/README.md @@ -2,29 +2,32 @@ 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 micro from 'micro'; -import { Tracer, Tags, DeterministicSampler, SpanContext } from '@zeit/tracing-js'; +import { IncomingMessage, ServerResponse, createServer } from 'http'; +import { Tracer, SpanContext, Tags, DeterministicSampler } from '@zeit/tracing-js'; const tracer = new Tracer( { - serviceName: 'service-name', - sampler: new DeterministicSampler(process.env.HONEYCOMB_SAMPLERATE), + 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, + writeKey: process.env.HONEYCOMB_KEY!, + dataset: process.env.HONEYCOMB_DATASET!, }, ); // example child function we wish to trace -async function sleep(ms: number, parentSpan: any) { - const span = tracer.startSpan(sleep.name, { childOf: parentSpan }); +async function sleep(ms: number, childOf: SpanContext) { + const span = tracer.startSpan(sleep.name, { childOf }); return new Promise(resolve => setTimeout(() => { span.finish(); @@ -33,30 +36,75 @@ async function sleep(ms: number, parentSpan: any) { ); } +// 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: any, res: any) { - const tags = {}; - if (req.headers && req.headers['x-now-trace-priority']) { - const priority = Number.parseInt(req.headers['x-now-trace-priority']); - tags[Tags.SAMPLING_PRIORITY] = priority; +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; - if (req.headers && req.headers['x-now-id']) { - const traceId = req.headers['x-now-id']; - const parentId = req.headers['x-now-parent-id']; + const traceId = getFirstHeader(req, 'x-now-id'); + const parentId = getFirstHeader(req, 'x-now-parent-id'); + if (traceId) { childOf = new SpanContext(traceId, parentId, tags); } - const span = tracer.startSpan(handler.name, { tags, childOf }); - - await sleep(300, span); - await sleep(300, span); - - span.finish(); + return { tags, childOf }; } -micro(handler).listen(3000); +createServer(handler).listen(3000); ``` ## Connecting traces across multiple services @@ -65,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(); diff --git a/examples/simple.ts b/examples/routing-example.ts similarity index 100% rename from examples/simple.ts rename to examples/routing-example.ts