├── .circleci └── config.yml ├── .github └── CODEOWNERS ├── .gitignore ├── README.md ├── examples ├── db-example.ts └── routing-example.ts ├── license.md ├── package.json ├── src ├── deterministic-sampler.ts ├── generate-id.ts ├── headers.ts ├── index.ts ├── setup-fetch.ts ├── setup-http.ts ├── shared.ts ├── span-context.ts ├── span.ts ├── tags.ts └── tracer.ts ├── test ├── deterministic-sampler.test.ts ├── dummy-honey.ts ├── generate-id.test.ts ├── setup-fetch.test.ts ├── setup-http.test.ts ├── span-context.test.ts ├── span.test.ts └── tracer.test.ts ├── tsconfig.json └── types └── libhoney.d.ts /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test-linux: 4 | docker: 5 | - image: circleci/node:14 6 | working_directory: ~/repo 7 | steps: 8 | - checkout 9 | - run: 10 | name: Installing dependencies 11 | command: yarn install 12 | - run: 13 | name: Build 14 | command: yarn build 15 | - run: 16 | name: Test Coverage 17 | command: yarn test-coverage 18 | workflows: 19 | version: 2 20 | test: 21 | jobs: 22 | - test-linux 23 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @styfle @javivelasco 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | yarn.lock 5 | .nyc* 6 | coverage* 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @vercel/tracing-js 2 | 3 | [![npm](https://badgen.net/npm/v/@vercel/tracing-js)](https://www.npmjs.com/package/@vercel/tracing-js) [![install size](https://badgen.net/packagephobia/install/@vercel/tracing-js)](https://packagephobia.now.sh/result?p=@vercel/tracing-js) [![circleci](https://badgen.net/circleci/github/vercel/tracing-js)](https://circleci.com/gh/vercel/workflows/tracing-js) 4 | 5 | A partial implementation of the [OpenTracing JavaScript API](https://opentracing-javascript.surge.sh) for [honeycomb.io](https://www.honeycomb.io) backend. 6 | 7 | [![homecomb-ui](https://user-images.githubusercontent.com/229881/53371218-a1a09000-391d-11e9-9956-8ee2b5d62a0f.png)](https://ui.honeycomb.io) 8 | 9 | ## Getting Started 10 | 11 | The minimum code you need to get started is the following: 12 | 13 | ```ts 14 | import { Tracer } from '@vercel/tracing-js'; 15 | const tracer = new Tracer({ serviceName }, { writeKey, dataset }); 16 | const span = tracer.startSpan(spanName); 17 | functionToTrace(); 18 | span.finish(); 19 | ``` 20 | 21 | ## Connecting Traces Across Multiple Services 22 | 23 | You can set a parent trace, even if you don't have a reference to the `Span` object. 24 | 25 | Instead, you can create a new `SpanContext`. 26 | 27 | You'll need the `traceId` and `parentSpanId` (typically found in `req.headers`). 28 | 29 | ```ts 30 | const spanContext = new SpanContext(traceId, parentSpanId); 31 | const childSpan = tracer.startSpan('child', { childOf: spanContext }); 32 | // ...do work here, call function, etc 33 | childSpan.finish(); 34 | ``` 35 | 36 | But a better solution is to use the `setupHttpTracing` helper function like the following: 37 | 38 | ```ts 39 | async function handler(req: IncomingMessage, res: ServerResponse) { 40 | const spanContext = setupHttpTracing({ tracer, req, res }); 41 | const fetch = setupFetchTracing({ spanContext }); 42 | await sleep(100, spanContext); 43 | const output = await fetch(upstreamUrl); 44 | res.write(output); 45 | } 46 | ``` 47 | 48 | ## Advanced Usage 49 | 50 | This is the canonical usage in most API services with a couple of example child functions we can trace called `sleep` and `route`. 51 | 52 | We take advantage of the `SpanContext` of the parent span when creating a child span. 53 | 54 | We also use a `DeterministicSampler` so that all services will use the same sample rate because one trace might have multiple services and we don't want to lose part of a trace due to sampling. 55 | 56 | ```ts 57 | import micro from 'micro'; 58 | import { Tracer, SpanContext, DeterministicSampler } from '@vercel/tracing-js'; 59 | 60 | const tracer = new Tracer( 61 | { 62 | serviceName: 'my-first-service', 63 | environment: process.env.ENVIRONMENT, 64 | dc: process.env.DC, 65 | podName: process.env.POD_NAME, 66 | nodeName: process.env.NODE_NAME, 67 | sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE), 68 | }, 69 | { 70 | writeKey: process.env.HONEYCOMB_KEY!, 71 | dataset: process.env.HONEYCOMB_DATASET!, 72 | }, 73 | ); 74 | 75 | // example child function we wish to trace 76 | async function sleep(ms: number, childOf: SpanContext) { 77 | const span = tracer.startSpan(sleep.name, { childOf }); 78 | return new Promise(resolve => 79 | setTimeout(() => { 80 | span.finish(); 81 | resolve(); 82 | }, ms), 83 | ); 84 | } 85 | 86 | // example child function we wish to trace 87 | async function route(path: string, childOf: SpanContext) { 88 | const span = tracer.startSpan(route.name, { childOf }); 89 | await sleep(200, span.context()); 90 | 91 | if (!path || path === '/') { 92 | span.finish(); 93 | return 'Home page'; 94 | } else if (path === '/next') { 95 | span.finish(); 96 | return 'Next page'; 97 | } else { 98 | span.finish(); 99 | throw new Error('Page not found'); 100 | } 101 | } 102 | 103 | // example parent function we wish to trace 104 | async function handler(req: IncomingMessage, res: ServerResponse) { 105 | const span = tracer.startSpan(handler.name); 106 | const spanContext = span.context(); 107 | await sleep(100, spanContext); 108 | const output = await route(req.url, spanContext); 109 | res.end(output); 110 | span.finish(); 111 | } 112 | 113 | micro(handler).listen(3000); 114 | ``` 115 | 116 | See a complete example of multi-service tracing in the [examples](https://github.com/vercel/tracing-js/tree/master/examples) directory. 117 | -------------------------------------------------------------------------------- /examples/db-example.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse, createServer } from 'http'; 2 | import { 3 | Tracer, 4 | SpanContext, 5 | DeterministicSampler, 6 | setupHttpTracing, 7 | } from '../src/index'; 8 | 9 | const tracer = new Tracer( 10 | { 11 | serviceName: 'db-example', 12 | environment: process.env.ENVIRONMENT, 13 | dc: process.env.DC, 14 | podName: process.env.POD_NAME, 15 | nodeName: process.env.NODE_NAME, 16 | sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE), 17 | }, 18 | { 19 | writeKey: process.env.HONEYCOMB_KEY!, 20 | dataset: process.env.HONEYCOMB_DATASET!, 21 | }, 22 | ); 23 | 24 | // example child function we wish to trace 25 | async function getDocumentById(ms: number, childOf: SpanContext) { 26 | const span = tracer.startSpan(getDocumentById.name, { childOf }); 27 | return new Promise(resolve => 28 | setTimeout(() => { 29 | span.finish(); 30 | resolve({ name: 'child', date: new Date() }); 31 | }, ms), 32 | ); 33 | } 34 | 35 | // example parent function we wish to trace 36 | async function handler(req: IncomingMessage, res: ServerResponse) { 37 | const spanContext = setupHttpTracing({ tracer, req, res }); 38 | console.log(spanContext.toTraceId(), spanContext.toSpanId()); 39 | let statusCode: number; 40 | let data: any; 41 | 42 | try { 43 | statusCode = 200; 44 | data = await getDocumentById(3100, spanContext); 45 | } catch (e) { 46 | statusCode = 500; 47 | data = { error: e.message }; 48 | } 49 | 50 | res.statusCode = statusCode; 51 | res.setHeader('Content-Type', 'application/json'); 52 | res.write(JSON.stringify(data)); 53 | res.end(); 54 | } 55 | 56 | createServer(handler).listen(8080); 57 | -------------------------------------------------------------------------------- /examples/routing-example.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse, createServer } from 'http'; 2 | import { 3 | Tracer, 4 | SpanContext, 5 | DeterministicSampler, 6 | setupHttpTracing, 7 | setupFetchTracing, 8 | } from '../src/index'; 9 | 10 | import nodeFetch from 'node-fetch'; 11 | 12 | const tracer = new Tracer( 13 | { 14 | serviceName: 'routing-example', 15 | environment: process.env.ENVIRONMENT, 16 | dc: process.env.DC, 17 | podName: process.env.POD_NAME, 18 | nodeName: process.env.NODE_NAME, 19 | sampler: new DeterministicSampler(process.env.TRACE_SAMPLE_RATE), 20 | }, 21 | { 22 | writeKey: process.env.HONEYCOMB_KEY!, 23 | dataset: process.env.HONEYCOMB_DATASET!, 24 | }, 25 | ); 26 | 27 | // example child function we wish to trace 28 | async function sleep(ms: number, childOf: SpanContext) { 29 | const span = tracer.startSpan(sleep.name, { childOf }); 30 | return new Promise(resolve => 31 | setTimeout(() => { 32 | span.finish(); 33 | resolve(); 34 | }, ms), 35 | ); 36 | } 37 | 38 | // example child function we wish to trace 39 | async function route(path: string, childOf: SpanContext) { 40 | const span = tracer.startSpan(route.name, { childOf }); 41 | const spanContext = span.context(); 42 | 43 | await sleep(200, spanContext); 44 | 45 | if (!path || path === '/') { 46 | span.finish(); 47 | return 'Home page'; 48 | } else if (path === '/next') { 49 | span.finish(); 50 | return 'Next Page'; 51 | } else if (path === '/another') { 52 | span.finish(); 53 | return 'Another Page'; 54 | } 55 | 56 | span.finish(); 57 | throw new Error('Page not found'); 58 | } 59 | 60 | // example parent function we wish to trace 61 | async function handler(req: IncomingMessage, res: ServerResponse) { 62 | const spanContext = setupHttpTracing({ tracer, req, res }); 63 | const fetch = setupFetchTracing({ spanContext, fetch: nodeFetch }); 64 | console.log(spanContext.toTraceId(), spanContext.toSpanId()); 65 | let statusCode = 200; 66 | 67 | try { 68 | const { url = '/' } = req; 69 | await sleep(100, spanContext); 70 | const title = await route(url, spanContext); 71 | const response = await fetch('http://localhost:8080'); 72 | const data = await response.json(); 73 | data.title = title; 74 | res.setHeader('Content-Type', 'application/json'); 75 | res.write(JSON.stringify(data)); 76 | } catch (error) { 77 | statusCode = 500; 78 | res.write(error.message); 79 | } 80 | 81 | res.statusCode = statusCode; 82 | res.end(); 83 | } 84 | 85 | createServer(handler).listen(3000); 86 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | https://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2017 Vercel, Inc. 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | https://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vercel/tracing-js", 3 | "version": "0.5.1", 4 | "description": "A partial implementation of the Opentracing JavaScript API for honeycomb.io backend", 5 | "main": "dist/src/index.js", 6 | "files": [ 7 | "dist/src/**", 8 | "dist/types/**" 9 | ], 10 | "scripts": { 11 | "build": "tsc && yarn copy-types", 12 | "watch": "tsc --watch", 13 | "copy-types": "copyfiles -u 1 './types/**/*.d.ts' dist/types", 14 | "fmt": "prettier --single-quote --bracket-spacing --trailing-comma all --write './{src,test,types,examples}/**/*.ts'", 15 | "test": "tape dist/test/**.js", 16 | "test-coverage": "npx c8 --reporter=lcov tape -r ts-node/register test/**.ts && npx codecov" 17 | }, 18 | "repository": "vercel/tracing-js", 19 | "author": "styfle", 20 | "license": "Apache-2.0", 21 | "engines": { 22 | "node": ">=8" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^11.9.4", 26 | "@types/node-fetch": "^2.1.6", 27 | "@types/tape": "^4.2.33", 28 | "copyfiles": "^2.1.0", 29 | "node-fetch": "^2.3.0", 30 | "prettier": "^1.16.4", 31 | "tape": "^4.10.1", 32 | "ts-node": "^8.3.0", 33 | "typescript": "^3.3.3" 34 | }, 35 | "dependencies": { 36 | "libhoney": "^1.2.1" 37 | }, 38 | "peerDependencies": { 39 | "node-fetch": "*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/deterministic-sampler.ts: -------------------------------------------------------------------------------- 1 | // TODO: add browser support with Web Crypto 2 | // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest 3 | import { createHash } from 'crypto'; 4 | import { SamplerBase } from './shared'; 5 | 6 | const MAX_UINT32 = Math.pow(2, 32) - 1; 7 | 8 | export class DeterministicSampler implements SamplerBase { 9 | private rate: number; 10 | private upperBound: number; 11 | 12 | /** 13 | * Determinisically sample a trace based on the trace id. 14 | * Each service will share the same trace id so this works 15 | * across multiple services/spans that are part of the same trace. 16 | * @param sampleRate Defaults to 1 (100%). Set to 2 for 50%, 4 for 25%, etc. 17 | */ 18 | constructor(sampleRate: string | number | undefined) { 19 | let rate: number; 20 | if (typeof sampleRate === 'number') { 21 | rate = sampleRate; 22 | } else if (typeof sampleRate === 'string') { 23 | rate = Number.parseInt(sampleRate); 24 | } else { 25 | rate = 1; 26 | } 27 | this.upperBound = (MAX_UINT32 / rate) >>> 0; 28 | this.rate = rate; 29 | } 30 | 31 | sample(traceId: string) { 32 | const sum = createHash('SHA1') 33 | .update(traceId) 34 | .digest(); 35 | return sum.readUInt32BE(0) <= this.upperBound; 36 | } 37 | 38 | getRate() { 39 | return this.rate; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/generate-id.ts: -------------------------------------------------------------------------------- 1 | export const generateId = (bytes = 32) => 2 | range(bytes) 3 | .map(getRandomByte) 4 | .map(toHex) 5 | .join(''); 6 | 7 | const range = (length: number) => Array.from({ length }); 8 | 9 | const getRandomBit = () => Math.round(Math.random()); 10 | 11 | const concatenateBits = (accumulator: number, bit: number, i: number) => 12 | accumulator + (bit << i); 13 | 14 | const getRandomByte = () => 15 | range(8) 16 | .map(getRandomBit) 17 | .reduce(concatenateBits); 18 | 19 | const toHex = (n: number) => 20 | n 21 | .toString(16) 22 | .toUpperCase() 23 | .padStart(2, '0'); 24 | -------------------------------------------------------------------------------- /src/headers.ts: -------------------------------------------------------------------------------- 1 | export const TRACE_ID = 'x-now-id'; 2 | export const PARENT_ID = 'x-now-parent-id'; 3 | export const PRIORITY = 'x-now-trace-priority'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Tracer } from './tracer'; 2 | import { SpanContext } from './span-context'; 3 | import * as Tags from './tags'; 4 | import { DeterministicSampler } from './deterministic-sampler'; 5 | import { SamplerBase } from './shared'; 6 | import { setupHttpTracing } from './setup-http'; 7 | import { setupFetchTracing } from './setup-fetch'; 8 | 9 | export { 10 | Tracer, 11 | SpanContext, 12 | Tags, 13 | DeterministicSampler, 14 | SamplerBase, 15 | setupHttpTracing, 16 | setupFetchTracing, 17 | }; 18 | -------------------------------------------------------------------------------- /src/setup-fetch.ts: -------------------------------------------------------------------------------- 1 | import { SpanContext } from './span-context'; 2 | import * as Tags from './tags'; 3 | import * as Hdrs from './headers'; 4 | import { Request, RequestInit, Headers } from 'node-fetch'; 5 | type Fetch = (url: string | Request, opts?: RequestInit) => void; 6 | 7 | interface SetupFetchTracingOptions { 8 | spanContext: SpanContext; 9 | fetch?: T; 10 | } 11 | 12 | export function setupFetchTracing(options: SetupFetchTracingOptions): T { 13 | const { fetch, spanContext } = options; 14 | let fetchOriginal: Fetch; 15 | if (fetch) { 16 | fetchOriginal = (fetch as unknown) as Fetch; 17 | } else { 18 | fetchOriginal = require('node-fetch'); 19 | } 20 | 21 | const fetchTracing = (url: string | Request, opts?: RequestInit) => { 22 | if (!opts) { 23 | opts = { headers: new Headers() }; 24 | } 25 | const headers = new Headers(opts.headers as any); 26 | opts.headers = headers; 27 | 28 | const traceId = spanContext.toTraceId(); 29 | const parentId = spanContext.toSpanId(); 30 | const priority = spanContext.getTag(Tags.SAMPLING_PRIORITY); 31 | 32 | headers.set(Hdrs.TRACE_ID, traceId); 33 | if (typeof parentId !== 'undefined') { 34 | headers.set(Hdrs.PARENT_ID, parentId); 35 | } 36 | if (typeof priority !== 'undefined') { 37 | headers.set(Hdrs.PRIORITY, priority); 38 | } 39 | 40 | return fetchOriginal(url, opts); 41 | }; 42 | 43 | // TS doesn't know about decorated runtime data 44 | // so we copy from the original just to be safe. 45 | for (const key of Object.keys(fetchOriginal)) { 46 | const tracing = fetchTracing as any; 47 | const original = fetchOriginal as any; 48 | tracing[key] = original[key]; 49 | } 50 | 51 | fetchTracing.default = fetchTracing; 52 | 53 | return (fetchTracing as unknown) as T; 54 | } 55 | -------------------------------------------------------------------------------- /src/setup-http.ts: -------------------------------------------------------------------------------- 1 | import { SpanContext } from './span-context'; 2 | import * as Tags from './tags'; 3 | import * as Hdrs from './headers'; 4 | import { Tracer } from './tracer'; 5 | import { SpanOptions, SpanTags, HttpRequest, HttpResponse } from './shared'; 6 | 7 | interface SetupHttpTracingOptions { 8 | name?: string; 9 | tracer: Tracer; 10 | req: HttpRequest; 11 | res: HttpResponse; 12 | } 13 | 14 | export function setupHttpTracing(options: SetupHttpTracingOptions) { 15 | const { name = 'setupHttpTracing', tracer, req, res } = options; 16 | const spanOptions = getSpanOptions(req); 17 | const span = tracer.startSpan(name, spanOptions); 18 | const spanContext = span.context(); 19 | 20 | res.on('finish', () => { 21 | const { statusCode = 200 } = res; 22 | span.setTag(Tags.HTTP_STATUS_CODE, statusCode); 23 | if (statusCode >= 400) { 24 | span.setTag(Tags.ERROR, true); 25 | } 26 | span.finish(); 27 | }); 28 | 29 | return spanContext; 30 | } 31 | 32 | function getFirstHeader(req: HttpRequest, key: string): string | undefined { 33 | const value = req.headers[key]; 34 | return Array.isArray(value) ? value[0] : value; 35 | } 36 | 37 | function getSpanOptions(req: HttpRequest): SpanOptions { 38 | const tags: SpanTags = {}; 39 | tags[Tags.HTTP_METHOD] = req.method; 40 | tags[Tags.HTTP_URL] = req.url; 41 | 42 | const priority = getFirstHeader(req, Hdrs.PRIORITY); 43 | if (typeof priority !== 'undefined') { 44 | tags[Tags.SAMPLING_PRIORITY] = Number.parseInt(priority); 45 | } 46 | 47 | let childOf: SpanContext | undefined; 48 | const traceId = getFirstHeader(req, Hdrs.TRACE_ID); 49 | const parentId = getFirstHeader(req, Hdrs.PARENT_ID); 50 | if (traceId) { 51 | childOf = new SpanContext(traceId, parentId, tags); 52 | } 53 | 54 | return { tags, childOf }; 55 | } 56 | -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import Libhoney from 'libhoney'; 4 | import { Span } from './span'; 5 | import { SpanContext } from './span-context'; 6 | import { HoneyOptions } from 'libhoney'; 7 | 8 | export interface TracerOptions { 9 | serviceName: string; 10 | environment?: string; 11 | dc?: string; 12 | podName?: string; 13 | nodeName?: string; 14 | sampler?: SamplerBase; 15 | } 16 | 17 | export type TracerHoneyOptions = HoneyOptions | Libhoney; 18 | export type SpanTags = { [key: string]: any }; 19 | 20 | export interface SpanOptions { 21 | childOf?: SpanContext | Span; 22 | tags?: SpanTags; 23 | } 24 | 25 | export interface SamplerBase { 26 | sample(data: string): boolean; 27 | getRate(): number; 28 | } 29 | 30 | export interface HttpRequest { 31 | headers: { [header: string]: string | string[] | undefined }; 32 | method?: string; 33 | url?: string; 34 | } 35 | 36 | export interface HttpResponse { 37 | statusCode: number; 38 | on(event: string, listener: () => void): this; 39 | } 40 | -------------------------------------------------------------------------------- /src/span-context.ts: -------------------------------------------------------------------------------- 1 | import { SpanTags } from './shared'; 2 | 3 | export class SpanContext { 4 | constructor( 5 | private traceId: string, 6 | private spanId: string | undefined, 7 | private tags: SpanTags, 8 | ) { 9 | this.traceId = traceId; 10 | this.spanId = spanId; 11 | this.tags = tags; 12 | } 13 | toSpanId() { 14 | return this.spanId; 15 | } 16 | toTraceId() { 17 | return this.traceId; 18 | } 19 | getTag(key: string) { 20 | return this.tags[key]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/span.ts: -------------------------------------------------------------------------------- 1 | import { HoneyEvent } from 'libhoney'; 2 | import { SpanContext } from './span-context'; 3 | import { generateId } from './generate-id'; 4 | import { SpanTags, TracerOptions } from './shared'; 5 | import { SAMPLING_PRIORITY } from './tags'; 6 | import { SamplerBase } from '.'; 7 | 8 | export class Span { 9 | private spanId: string; 10 | private event: HoneyEvent; 11 | private tracerOptions: TracerOptions; 12 | private name: string; 13 | private traceId: string; 14 | private parentId: string | undefined; 15 | private tags: SpanTags; 16 | private start: Date; 17 | private duration: number | undefined; 18 | 19 | constructor( 20 | event: HoneyEvent, 21 | tracerOptions: TracerOptions, 22 | name: string, 23 | traceId: string | undefined, 24 | parentId: string | undefined, 25 | tags: SpanTags, 26 | ) { 27 | this.spanId = generateId(); 28 | this.event = event; 29 | this.tracerOptions = tracerOptions; 30 | this.name = name; 31 | this.traceId = traceId || generateId(); 32 | this.parentId = parentId; 33 | this.tags = tags; 34 | this.start = new Date(); 35 | } 36 | 37 | context() { 38 | return new SpanContext(this.traceId, this.spanId, this.tags); 39 | } 40 | 41 | addTags(tags: SpanTags) { 42 | Object.keys(tags).forEach(key => { 43 | this.tags[key] = tags[key]; 44 | }); 45 | return this; 46 | } 47 | 48 | setTag(key: string, value: any) { 49 | this.tags[key] = value; 50 | return this; 51 | } 52 | 53 | setOperationName(name: string) { 54 | this.name = name; 55 | } 56 | 57 | private isSendable(sampler: SamplerBase) { 58 | const priority = this.tags[SAMPLING_PRIORITY]; 59 | 60 | if (typeof priority === 'undefined') { 61 | return sampler.sample(this.traceId); 62 | } 63 | 64 | return priority > 0; 65 | } 66 | 67 | private getRate(sampler: SamplerBase) { 68 | const priority = this.tags[SAMPLING_PRIORITY]; 69 | return priority > 0 ? 1 : sampler.getRate(); 70 | } 71 | 72 | finish() { 73 | if (typeof this.duration !== 'undefined') { 74 | return; 75 | } 76 | this.duration = Date.now() - this.start.getTime(); 77 | const { 78 | serviceName, 79 | environment, 80 | dc, 81 | podName, 82 | nodeName, 83 | sampler, 84 | } = this.tracerOptions; 85 | 86 | if (!sampler || !this.isSendable(sampler)) { 87 | return; 88 | } 89 | 90 | this.event.addField('duration_ms', this.duration); 91 | this.event.addField('name', this.name); 92 | this.event.addField('service_name', serviceName); 93 | this.event.addField('environment', environment); 94 | this.event.addField('dc', dc); 95 | this.event.addField('pod_name', podName); 96 | this.event.addField('node_name', nodeName); 97 | this.event.addField('trace.trace_id', this.traceId); 98 | this.event.addField('trace.span_id', this.spanId); 99 | this.event.addField('trace.parent_id', this.parentId); 100 | this.event.addField('samplerate', this.getRate(sampler)); 101 | for (const [key, value] of Object.entries(this.tags)) { 102 | this.event.addField('tag.' + key, value); 103 | } 104 | this.event.timestamp = this.start; 105 | this.event.send(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/tags.ts: -------------------------------------------------------------------------------- 1 | /** SPAN_KIND hints at relationship between spans, e.g. client/server */ 2 | export const SPAN_KIND = 'span.kind'; 3 | 4 | /** Marks a span representing the client-side of an RPC or other remote call */ 5 | export const SPAN_KIND_RPC_CLIENT = 'client'; 6 | 7 | /** Marks a span representing the server-side of an RPC or other remote call */ 8 | export const SPAN_KIND_RPC_SERVER = 'server'; 9 | 10 | /** Marks a span representing the producing-side within a messaging system or other remote call */ 11 | export const SPAN_KIND_MESSAGING_PRODUCER = 'producer'; 12 | 13 | /** Marks a span representing the consuming-side within a messaging system or other remote call */ 14 | export const SPAN_KIND_MESSAGING_CONSUMER = 'consumer'; 15 | 16 | /** 17 | * ERROR (boolean) true if and only if the application considers the operation 18 | * represented by the Span to have failed 19 | */ 20 | export const ERROR = 'error'; 21 | 22 | /** 23 | * COMPONENT (string) A low-cardinality identifier of the module, library, 24 | * or package that is generating a span. 25 | */ 26 | export const COMPONENT = 'component'; 27 | 28 | /** 29 | * SAMPLING_PRIORITY (number) determines the priority of sampling this Span. 30 | * If greater than 0, a hint to the Tracer to do its best to capture the trace. 31 | * If 0, a hint to the trace to not-capture the trace. If absent, the Tracer 32 | * should use its default sampling mechanism. 33 | */ 34 | export const SAMPLING_PRIORITY = 'sampling.priority'; 35 | 36 | // --------------------------------------------------------------------------- 37 | // PEER_* tags can be emitted by either client-side of server-side to describe 38 | // the other side/service in a peer-to-peer communications, like an RPC call. 39 | // --------------------------------------------------------------------------- 40 | 41 | /** 42 | * PEER_SERVICE (string) Remote service name (for some unspecified 43 | * definition of "service"). E.g., "elasticsearch", "a_custom_microservice", "memcache" 44 | */ 45 | export const PEER_SERVICE = 'peer.service'; 46 | 47 | /** PEER_HOSTNAME (string) Remote hostname. E.g., "opentracing.io", "internal.dns.name" */ 48 | export const PEER_HOSTNAME = 'peer.hostname'; 49 | 50 | /** 51 | * PEER_ADDRESS (string) Remote "address", suitable for use in a 52 | * networking client library. This may be a "ip:port", a bare 53 | * "hostname", a FQDN, or even a JDBC substring like "mysql://prod-db:3306" 54 | */ 55 | export const PEER_ADDRESS = 'peer.address'; 56 | 57 | /** 58 | * PEER_HOST_IPV4 (number) Remote IPv4 address as a .-separated tuple. 59 | * E.g., "127.0.0.1" 60 | */ 61 | export const PEER_HOST_IPV4 = 'peer.ipv4'; 62 | 63 | // PEER_HOST_IPV6 (string) Remote IPv6 address as a string of 64 | // colon-separated 4-char hex tuples. E.g., "2001:0db8:85a3:0000:0000:8a2e:0370:7334" 65 | export const PEER_HOST_IPV6 = 'peer.ipv6'; 66 | 67 | // PEER_PORT (number) Remote port. E.g., 80 68 | export const PEER_PORT = 'peer.port'; 69 | 70 | // --------------------------------------------------------------------------- 71 | // HTTP tags 72 | // --------------------------------------------------------------------------- 73 | 74 | /** 75 | * HTTP_URL (string) URL of the request being handled in this segment of the 76 | * trace, in standard URI format. E.g., "https://domain.net/path/to?resource=here" 77 | */ 78 | export const HTTP_URL = 'http.url'; 79 | 80 | /** 81 | * HTTP_METHOD (string) HTTP method of the request for the associated Span. E.g., 82 | * "GET", "POST" 83 | */ 84 | export const HTTP_METHOD = 'http.method'; 85 | 86 | /** 87 | * HTTP_STATUS_CODE (number) HTTP response status code for the associated Span. 88 | * E.g., 200, 503, 404 89 | */ 90 | export const HTTP_STATUS_CODE = 'http.status_code'; 91 | 92 | // ------------------------------------------------------------------------- 93 | // Messaging tags 94 | // ------------------------------------------------------------------------- 95 | 96 | /** 97 | * MESSAGE_BUS_DESTINATION (string) An address at which messages can be exchanged. 98 | * E.g. A Kafka record has an associated "topic name" that can be extracted 99 | * by the instrumented producer or consumer and stored using this tag. 100 | */ 101 | export const MESSAGE_BUS_DESTINATION = 'message_bus.destination'; 102 | 103 | // -------------------------------------------------------------------------- 104 | // Database tags 105 | // -------------------------------------------------------------------------- 106 | 107 | /** 108 | * DB_INSTANCE (string) Database instance name. E.g., In java, if the 109 | * jdbc.url="jdbc:mysql://127.0.0.1:3306/customers", the instance name is "customers". 110 | */ 111 | export const DB_INSTANCE = 'db.instance'; 112 | 113 | /** 114 | * DB_STATEMENT (string) A database statement for the given database type. 115 | * E.g., for db.type="SQL", "SELECT * FROM wuser_table"; 116 | * for db.type="redis", "SET mykey 'WuValue'". 117 | */ 118 | export const DB_STATEMENT = 'db.statement'; 119 | 120 | /** 121 | * DB_TYPE (string) Database type. For any SQL database, "sql". For others, 122 | * the lower-case database category, e.g. "cassandra", "hbase", or "redis". 123 | */ 124 | export const DB_TYPE = 'db.type'; 125 | 126 | /** 127 | * DB_USER (string) Username for accessing database. E.g., "readonly_user" 128 | * or "reporting_user" 129 | */ 130 | export const DB_USER = 'db.user'; 131 | -------------------------------------------------------------------------------- /src/tracer.ts: -------------------------------------------------------------------------------- 1 | import { TracerOptions, TracerHoneyOptions, SpanOptions } from './shared'; 2 | import { Span } from './span'; 3 | import { SpanContext } from './span-context'; 4 | import { SAMPLING_PRIORITY } from './tags'; 5 | import Libhoney from 'libhoney'; 6 | import { DeterministicSampler } from './deterministic-sampler'; 7 | 8 | export class Tracer { 9 | private honey: Libhoney; 10 | private tracerOptions: TracerOptions; 11 | 12 | constructor(tracerOptions: TracerOptions, honeyOptions: TracerHoneyOptions) { 13 | this.tracerOptions = tracerOptions; 14 | this.tracerOptions.sampler = 15 | tracerOptions.sampler || new DeterministicSampler(1); 16 | if (honeyOptions instanceof Libhoney) { 17 | this.honey = honeyOptions; 18 | } else { 19 | this.honey = new Libhoney(honeyOptions); 20 | } 21 | } 22 | startSpan(name: string, spanOptions: SpanOptions = {}) { 23 | const { childOf, tags = {} } = spanOptions; 24 | let traceId: string | undefined; 25 | let parentId: string | undefined; 26 | let samplingPriority: number | undefined; 27 | 28 | if (childOf instanceof Span) { 29 | const ctx = childOf.context(); 30 | traceId = ctx.toTraceId(); 31 | parentId = ctx.toSpanId(); 32 | samplingPriority = ctx.getTag(SAMPLING_PRIORITY); 33 | } else if (childOf instanceof SpanContext) { 34 | traceId = childOf.toTraceId(); 35 | parentId = childOf.toSpanId(); 36 | samplingPriority = childOf.getTag(SAMPLING_PRIORITY); 37 | } else if (childOf) { 38 | throw new Error('Expected `childOf` to be Span or SpanContext'); 39 | } 40 | 41 | // If the parent has a sampling priority, copy value to the child 42 | if (typeof samplingPriority !== 'undefined') { 43 | tags[SAMPLING_PRIORITY] = samplingPriority; 44 | } 45 | 46 | return new Span( 47 | this.honey.newEvent(), 48 | this.tracerOptions, 49 | name, 50 | traceId, 51 | parentId, 52 | tags, 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/deterministic-sampler.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { DeterministicSampler } from '../src/deterministic-sampler'; 3 | import { generateId } from '../src/generate-id'; 4 | 5 | function testSampleRate(sampleRate: number, variancePercent: number) { 6 | const total = 100; 7 | const expected = total / sampleRate; 8 | const s = new DeterministicSampler(sampleRate); 9 | const actual = Array.from({ length: total }) 10 | .map(() => generateId()) 11 | .filter(id => s.sample(id)).length; 12 | const variance = total * variancePercent; 13 | const lower = expected - variance; 14 | const upper = expected + variance; 15 | return lower < actual && actual < upper; 16 | } 17 | 18 | test('deterministic sampler same result each time', t => { 19 | t.plan(1); 20 | const s = new DeterministicSampler(17); 21 | const first = s.sample('hello'); 22 | const second = s.sample('hello'); 23 | t.equal(first, second); 24 | }); 25 | 26 | test('deterministic sampler allow 0%', t => { 27 | t.plan(1); 28 | const passed = testSampleRate(0, 1); 29 | t.false(passed); 30 | }); 31 | 32 | test('deterministic sampler allow 100%', t => { 33 | t.plan(1); 34 | const passed = testSampleRate(1, 1); 35 | t.true(passed); 36 | }); 37 | 38 | test('deterministic sampler allow 50%', t => { 39 | t.plan(1); 40 | const passed = testSampleRate(2, 0.2); 41 | t.true(passed, 'this test may not always pass due to random inputs'); 42 | }); 43 | 44 | test('deterministic sampler allow 25%', t => { 45 | t.plan(1); 46 | const passed = testSampleRate(4, 0.1); 47 | t.true(passed, 'this test may not always pass due to random inputs'); 48 | }); 49 | 50 | test('deterministic sampler allow 20%', t => { 51 | t.plan(1); 52 | const passed = testSampleRate(5, 0.1); 53 | t.true(passed, 'this test may not always pass due to random inputs'); 54 | }); 55 | -------------------------------------------------------------------------------- /test/dummy-honey.ts: -------------------------------------------------------------------------------- 1 | import Libhoney, { HoneyEvent, HoneyOptions } from 'libhoney'; 2 | 3 | const noop = () => {}; 4 | 5 | interface DummyOptions { 6 | addField?: (key: string, value: any) => void; 7 | send?: () => void; 8 | } 9 | 10 | class DummyHoney extends Libhoney { 11 | private dummyOptions: DummyOptions; 12 | 13 | constructor(options: HoneyOptions, dummyOptions?: DummyOptions) { 14 | super(options); 15 | this.dummyOptions = dummyOptions || {}; 16 | } 17 | 18 | newEvent(): HoneyEvent { 19 | const { addField, send } = this.dummyOptions; 20 | return { 21 | addField: addField || noop, 22 | send: send || noop, 23 | }; 24 | } 25 | } 26 | 27 | export function newDummyHoney(options?: DummyOptions) { 28 | return new DummyHoney( 29 | { 30 | writeKey: 'test', 31 | dataset: 'test', 32 | disabled: true, 33 | }, 34 | options, 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /test/generate-id.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { generateId } from '../src/generate-id'; 3 | 4 | test('generate-id test length', t => { 5 | t.plan(1); 6 | const bytes = 1024; 7 | const id = generateId(bytes); 8 | t.equal(id.length, bytes * 2, 'each byte is represented as 2 chars'); 9 | }); 10 | -------------------------------------------------------------------------------- /test/setup-fetch.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { setupFetchTracing } from '../src/setup-fetch'; 3 | import { SpanContext } from '../src/span-context'; 4 | import * as Tags from '../src/tags'; 5 | import * as Hdrs from '../src/headers'; 6 | import { RequestInit, Headers } from 'node-fetch'; 7 | 8 | test('setup-fetch test empty fetch', t => { 9 | t.plan(1); 10 | const spanContext = new SpanContext('trace-id', 'span-id', {}); 11 | const fetch = setupFetchTracing({ spanContext }); 12 | t.equal(typeof fetch, 'function'); 13 | }); 14 | 15 | test('setup-fetch test fetch properties', t => { 16 | t.plan(1); 17 | const spanContext = new SpanContext('trace-id', 'span-id', {}); 18 | const oldFetch = (_: string) => {}; 19 | oldFetch.dummy = true; 20 | const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch }); 21 | t.equal(newFetch.dummy, oldFetch.dummy); 22 | }); 23 | 24 | test('setup-fetch test fetch response', async t => { 25 | t.plan(1); 26 | const spanContext = new SpanContext('trace-id', 'span-id', {}); 27 | const oldFetch = (_: string) => { 28 | return 'hello'; 29 | }; 30 | const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch }); 31 | const oldResponse = await oldFetch('fake-url'); 32 | const newResponse = await newFetch('fake-url'); 33 | t.equal(newResponse, oldResponse); 34 | }); 35 | 36 | test('setup-fetch test fetch headers when empty', async t => { 37 | t.plan(3); 38 | const spanContext = new SpanContext('trace-id', 'span-id', { 39 | [Tags.SAMPLING_PRIORITY]: 99, 40 | }); 41 | const oldFetch = (_: string, opts?: RequestInit) => { 42 | if (opts && opts.headers) { 43 | const headers = 44 | opts.headers instanceof Headers 45 | ? opts.headers 46 | : new Headers(opts.headers as any); 47 | t.equal(headers.get(Hdrs.TRACE_ID), 'trace-id'); 48 | t.equal(headers.get(Hdrs.PARENT_ID), 'span-id'); 49 | t.equal(headers.get(Hdrs.PRIORITY), '99'); 50 | } 51 | return 'hello'; 52 | }; 53 | const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch }); 54 | await newFetch('fake-url'); 55 | }); 56 | 57 | test('setup-fetch test fetch headers when non-empty', async t => { 58 | t.plan(4); 59 | const spanContext = new SpanContext('trace-id', 'span-id', { 60 | [Tags.SAMPLING_PRIORITY]: 99, 61 | }); 62 | const oldFetch = (_: string, opts?: RequestInit) => { 63 | if (opts && opts.headers) { 64 | const headers = new Headers(opts.headers as any); 65 | t.equal(headers.get(Hdrs.TRACE_ID), 'trace-id'); 66 | t.equal(headers.get(Hdrs.PARENT_ID), 'span-id'); 67 | t.equal(headers.get(Hdrs.PRIORITY), '99'); 68 | t.equal(headers.get('x-test-header'), 'the-value'); 69 | } 70 | return 'hello'; 71 | }; 72 | const newFetch = setupFetchTracing({ spanContext, fetch: oldFetch }); 73 | await newFetch('fake-url', { headers: { 'x-test-header': 'the-value' } }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/setup-http.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { setupHttpTracing } from '../src/setup-http'; 3 | import { Tracer } from '../src/tracer'; 4 | import { HttpResponse, HttpRequest } from '../src/shared'; 5 | import * as Tags from '../src/tags'; 6 | import { newDummyHoney } from './dummy-honey'; 7 | 8 | test('setup-http test trace id exists', t => { 9 | t.plan(1); 10 | const req: HttpRequest = { 11 | headers: {}, 12 | }; 13 | const res: HttpResponse = { 14 | statusCode: 200, 15 | on: () => res, 16 | }; 17 | const tracer = new Tracer({ serviceName: 'service' }, newDummyHoney()); 18 | const spanContext = setupHttpTracing({ tracer, req, res }); 19 | const traceId = spanContext.toTraceId(); 20 | t.equal(typeof traceId, 'string'); 21 | }); 22 | 23 | test('setup-http test tags with success', t => { 24 | t.plan(6); 25 | const req: HttpRequest = { 26 | headers: {}, 27 | method: 'POST', 28 | url: '/foo', 29 | }; 30 | const res: HttpResponse = { 31 | statusCode: 200, 32 | on: (event: string, listener: () => void) => { 33 | t.equal(event, 'finish', 'expected to listen to finish event'); 34 | t.true(!!listener, 'expected a listener'); 35 | listener(); 36 | return res; 37 | }, 38 | }; 39 | const tracer = new Tracer({ serviceName: 'service' }, newDummyHoney()); 40 | const spanContext = setupHttpTracing({ tracer, req, res }); 41 | t.equal(spanContext.getTag(Tags.HTTP_STATUS_CODE), res.statusCode); 42 | t.equal(spanContext.getTag(Tags.HTTP_METHOD), req.method); 43 | t.equal(spanContext.getTag(Tags.HTTP_URL), req.url); 44 | t.equal(spanContext.getTag(Tags.ERROR), undefined); 45 | }); 46 | 47 | test('setup-http test tags with error', t => { 48 | t.plan(6); 49 | const req: HttpRequest = { 50 | headers: {}, 51 | method: 'GET', 52 | url: '/notfound', 53 | }; 54 | const res: HttpResponse = { 55 | statusCode: 404, 56 | on: (event: string, listener: () => void) => { 57 | t.equal(event, 'finish', 'expected to listen to finish event'); 58 | t.true(!!listener, 'expected a listener'); 59 | listener(); 60 | return res; 61 | }, 62 | }; 63 | const tracer = new Tracer({ serviceName: 'service' }, newDummyHoney()); 64 | const spanContext = setupHttpTracing({ tracer, req, res }); 65 | t.equal(spanContext.getTag(Tags.HTTP_STATUS_CODE), res.statusCode); 66 | t.equal(spanContext.getTag(Tags.HTTP_METHOD), req.method); 67 | t.equal(spanContext.getTag(Tags.HTTP_URL), req.url); 68 | t.equal(spanContext.getTag(Tags.ERROR), true); 69 | }); 70 | -------------------------------------------------------------------------------- /test/span-context.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { SpanContext } from '../src/span-context'; 3 | import { SpanTags } from '../src/shared'; 4 | 5 | test('span-context test public methods', t => { 6 | t.plan(3); 7 | const traceId = 'trace-id'; 8 | const spanId = 'span-id'; 9 | const tags: SpanTags = { foo: 'bar' }; 10 | const ctx = new SpanContext(traceId, spanId, tags); 11 | t.equal(ctx.toTraceId(), traceId); 12 | t.equal(ctx.toSpanId(), spanId); 13 | t.equal(ctx.getTag('foo'), 'bar'); 14 | }); 15 | 16 | test('span-context test undefined parent spanId', t => { 17 | t.plan(3); 18 | const traceId = 'trace-id'; 19 | const parentId = undefined; 20 | const tags: SpanTags = {}; 21 | const ctx = new SpanContext(traceId, parentId, tags); 22 | t.equal(ctx.toTraceId(), traceId); 23 | t.equal(ctx.toSpanId(), parentId); 24 | t.equal(ctx.getTag('not-found'), tags['not-found']); 25 | }); 26 | -------------------------------------------------------------------------------- /test/span.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { Span } from '../src/span'; 3 | import { HoneyEvent } from 'libhoney'; 4 | import { DeterministicSampler } from '../src/deterministic-sampler'; 5 | import { SAMPLING_PRIORITY } from '../src/tags'; 6 | import { TracerOptions } from '../src/shared'; 7 | 8 | const noop = () => {}; 9 | 10 | function getTracerOptions(sampler: DeterministicSampler): TracerOptions { 11 | return { 12 | serviceName: 'service name', 13 | environment: 'local', 14 | sampler, 15 | }; 16 | } 17 | 18 | test('test span context', t => { 19 | t.plan(2); 20 | const options = getTracerOptions(new DeterministicSampler(1)); 21 | const name = 'function name'; 22 | const traceId = 'trace123'; 23 | const parentId = undefined; 24 | const tags = {}; 25 | const event: HoneyEvent = { 26 | addField: noop, 27 | send: noop, 28 | }; 29 | const span = new Span(event, options, name, traceId, parentId, tags); 30 | const ctx = span.context(); 31 | t.equal(ctx.toTraceId(), traceId); 32 | t.notEqual(ctx.toSpanId(), ''); 33 | }); 34 | 35 | test('test span setTag', t => { 36 | t.plan(2); 37 | const options = getTracerOptions(new DeterministicSampler(1)); 38 | const name = 'function name'; 39 | const traceId = 'trace123'; 40 | const parentId = undefined; 41 | const tags = {}; 42 | const event: HoneyEvent = { 43 | addField: (key: string, value: any) => { 44 | if (key === 'tag.key1') { 45 | t.equal(value, 'value1'); 46 | } else if (key === 'tag.key2') { 47 | t.equal(value, 'value2'); 48 | } 49 | }, 50 | send: noop, 51 | }; 52 | const span = new Span(event, options, name, traceId, parentId, tags); 53 | span 54 | .setTag('key1', 'value1') 55 | .setTag('key2', 'value2') 56 | .finish(); 57 | }); 58 | 59 | test('test span addTags', t => { 60 | t.plan(2); 61 | const options = getTracerOptions(new DeterministicSampler(1)); 62 | const name = 'function name'; 63 | const traceId = 'trace123'; 64 | const parentId = undefined; 65 | const tags = {}; 66 | const event: HoneyEvent = { 67 | addField: (key: string, value: any) => { 68 | if (key === 'tag.key1') { 69 | t.equal(value, 'value1'); 70 | } else if (key === 'tag.key2') { 71 | t.equal(value, 'value2'); 72 | } 73 | }, 74 | send: noop, 75 | }; 76 | const span = new Span(event, options, name, traceId, parentId, tags); 77 | span.addTags({ key1: 'value1', key2: 'value2' }).finish(); 78 | }); 79 | 80 | test('test span addField', t => { 81 | t.plan(9); 82 | const rate = 1; 83 | const options = getTracerOptions(new DeterministicSampler(rate)); 84 | const name = 'function name'; 85 | const traceId = 'trace123'; 86 | const parentId = 'parent123'; 87 | const tags = {}; 88 | const event: HoneyEvent = { 89 | send: () => { 90 | t.true(event.timestamp && event.timestamp > new Date(0)); 91 | }, 92 | addField: (key: string, value: any) => { 93 | switch (key) { 94 | case 'duration_ms': 95 | t.true(0 < value && value < 100); 96 | break; 97 | case 'name': 98 | t.equal(value, name); 99 | break; 100 | case 'service_name': 101 | t.equal(value, options.serviceName); 102 | break; 103 | case 'environment': 104 | t.equal(value, options.environment); 105 | break; 106 | case 'trace.trace_id': 107 | t.equal(value, traceId); 108 | break; 109 | case 'trace.span_id': 110 | t.notEqual(value, ''); 111 | break; 112 | case 'trace.parent_id': 113 | t.equal(value, parentId); 114 | break; 115 | case 'samplerate': 116 | t.equal(value, rate); 117 | break; 118 | } 119 | }, 120 | }; 121 | const span = new Span(event, options, name, traceId, parentId, tags); 122 | setTimeout(() => span.finish(), 50); 123 | }); 124 | 125 | test('test span addField should favor priority over sampler', t => { 126 | t.plan(1); 127 | const options = getTracerOptions(new DeterministicSampler(20)); 128 | const name = 'function name'; 129 | const traceId = 'trace123'; 130 | const parentId = 'parent123'; 131 | const tags = { [SAMPLING_PRIORITY]: 1 }; 132 | const event: HoneyEvent = { 133 | send: noop, 134 | addField: (key: string, value: any) => { 135 | switch (key) { 136 | case 'samplerate': 137 | t.equal(value, 1); 138 | break; 139 | } 140 | }, 141 | }; 142 | const span = new Span(event, options, name, traceId, parentId, tags); 143 | setTimeout(() => span.finish(), 50); 144 | }); 145 | 146 | 147 | test('test span finish should call send at most once', t => { 148 | t.plan(1); 149 | const options = getTracerOptions(new DeterministicSampler(20)); 150 | const name = 'function name'; 151 | const traceId = 'trace123'; 152 | const parentId = 'parent123'; 153 | const tags = { [SAMPLING_PRIORITY]: 1 }; 154 | const event: HoneyEvent = { 155 | send: () => { 156 | t.true(true, 'should be called exactly once'); 157 | }, 158 | addField: noop 159 | }; 160 | const span = new Span(event, options, name, traceId, parentId, tags); 161 | span.finish(); 162 | span.finish(); 163 | }); 164 | 165 | test('test span sample rate 0 should not send', t => { 166 | t.plan(1); 167 | const options = getTracerOptions(new DeterministicSampler(0)); 168 | const name = 'function name'; 169 | const traceId = 'trace123'; 170 | const parentId = 'parent123'; 171 | const event: HoneyEvent = { 172 | addField: noop, 173 | send: () => { 174 | t.true(false, 'should not send'); 175 | }, 176 | }; 177 | const tags = {}; 178 | const span = new Span(event, options, name, traceId, parentId, tags); 179 | span.finish(); 180 | t.true(true, 'finish'); 181 | }); 182 | 183 | test('test span sample rate 0, tag priority 1 should send', t => { 184 | t.plan(2); 185 | const options = getTracerOptions(new DeterministicSampler(0)); 186 | const name = 'function name'; 187 | const traceId = 'trace123'; 188 | const parentId = 'parent123'; 189 | const event: HoneyEvent = { 190 | addField: noop, 191 | send: () => { 192 | t.true(true, 'should send'); 193 | }, 194 | }; 195 | const tags = { [SAMPLING_PRIORITY]: 1 }; 196 | const span = new Span(event, options, name, traceId, parentId, tags); 197 | span.finish(); 198 | t.true(true, 'finish'); 199 | }); 200 | 201 | test('test span sample rate 1, tag priority 0 should not send', t => { 202 | t.plan(1); 203 | const options = getTracerOptions(new DeterministicSampler(1)); 204 | const name = 'function name'; 205 | const traceId = 'trace123'; 206 | const parentId = 'parent123'; 207 | const event: HoneyEvent = { 208 | addField: noop, 209 | send: () => { 210 | t.true(false, 'should not send'); 211 | }, 212 | }; 213 | const tags = { [SAMPLING_PRIORITY]: 0 }; 214 | const span = new Span(event, options, name, traceId, parentId, tags); 215 | span.finish(); 216 | t.true(true, 'finish'); 217 | }); 218 | -------------------------------------------------------------------------------- /test/tracer.test.ts: -------------------------------------------------------------------------------- 1 | import test from 'tape'; 2 | import { Tracer } from '../src/tracer'; 3 | import { SAMPLING_PRIORITY } from '../src/tags'; 4 | import { newDummyHoney } from './dummy-honey'; 5 | 6 | test('test tracer with no options', t => { 7 | t.plan(3); 8 | const tracer = new Tracer({ serviceName: 'service name' }, newDummyHoney()); 9 | const span = tracer.startSpan('hello'); 10 | const ctx = span.context(); 11 | const traceId = ctx.toTraceId(); 12 | const spanId = ctx.toSpanId(); 13 | t.notEqual(traceId, ''); 14 | t.notEqual(spanId, ''); 15 | t.notEqual(traceId, spanId); 16 | }); 17 | 18 | test('test tracer traceId is same for parent & child', t => { 19 | t.plan(1); 20 | const tracer = new Tracer({ serviceName: 'service name' }, newDummyHoney()); 21 | const parentSpan = tracer.startSpan('parent'); 22 | const childSpan = tracer.startSpan('child', { childOf: parentSpan }); 23 | t.equal(parentSpan.context().toTraceId(), childSpan.context().toTraceId()); 24 | }); 25 | 26 | test('test tracer sample priority is same for parent & child', t => { 27 | t.plan(2); 28 | const tracer = new Tracer({ serviceName: 'service name' }, newDummyHoney()); 29 | const tags = { [SAMPLING_PRIORITY]: 75 }; 30 | const parentSpan = tracer.startSpan('parent', { tags }); 31 | const childSpan = tracer.startSpan('child', { childOf: parentSpan }); 32 | const parentPriority = parentSpan.context().getTag(SAMPLING_PRIORITY); 33 | const childPriority = childSpan.context().getTag(SAMPLING_PRIORITY); 34 | t.equal(parentPriority, 75); 35 | t.equal(childPriority, 75); 36 | }); 37 | 38 | test('test tracer tags', t => { 39 | t.plan(2); 40 | const addField = (key: string, value: any) => { 41 | if (key === 'tag.key1') { 42 | t.equal(value, 'value1'); 43 | } else if (key === 'tag.key2') { 44 | t.equal(value, 'value2'); 45 | } 46 | }; 47 | const tracer = new Tracer( 48 | { serviceName: 'service name' }, 49 | newDummyHoney({ addField }), 50 | ); 51 | const tags = { key1: 'value1', key2: 'value2' }; 52 | const span = tracer.startSpan('hello', { tags }); 53 | span.finish(); 54 | }); 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["esnext"], 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noFallthroughCasesInSwitch": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "outDir": "./dist", 14 | "types": ["node"], 15 | "strict": true, 16 | "target": "esnext" 17 | }, 18 | "include": ["./src", "./test", "./types", "./examples"] 19 | } 20 | -------------------------------------------------------------------------------- /types/libhoney.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'libhoney' { 2 | export default class Libhoney { 3 | constructor(options: HoneyOptions); 4 | newEvent(): HoneyEvent; 5 | } 6 | 7 | export interface HoneyOptions { 8 | /** 9 | * Write key for your Honeycomb team 10 | */ 11 | writeKey: string; 12 | /** 13 | * Name of the dataset that should contain this event. The dataset will be created for your team if it doesn't already exist. 14 | */ 15 | dataset: string; 16 | /** 17 | * (Default 1) Sample rate of data. If set, causes us to send 1/sampleRate of events and drop the rest. 18 | */ 19 | sampleRate?: number; 20 | /** 21 | * (Default 50) We send a batch to the API when this many outstanding events exist in our event queue. 22 | */ 23 | batchSizeTrigger?: number; 24 | /** 25 | * (Default 100) We send a batch to the API after this many milliseconds have passed. 26 | */ 27 | batchTimeTrigger?: number; 28 | /** 29 | * (Default 10) We process batches concurrently to increase parallelism while sending. 30 | */ 31 | maxConcurrentBatches?: number; 32 | /** 33 | * (Default 10000) The maximum number of pending events we allow to accumulate in our sending queue before dropping them. 34 | */ 35 | pendingWorkCapacity?: number; 36 | /** 37 | * (Default 1000) The maximum number of responses we enqueue before dropping them. 38 | */ 39 | maxResponseQueueSize?: number; 40 | /** 41 | * Disable transmission of events to the specified `apiHost`, particularly useful for testing or development. 42 | */ 43 | disabled?: boolean; 44 | /** 45 | * (Default "https://api.honeycomb.io/") Server host to receive Honeycomb events (Defaults to https://api.honeycomb.io). 46 | */ 47 | apiHost?: string; 48 | } 49 | 50 | export interface HoneyEvent { 51 | timestamp?: Date; 52 | addField: (key: string, value: any) => void; 53 | send: () => void; 54 | } 55 | } 56 | --------------------------------------------------------------------------------