├── .github ├── labeler-config.yml └── workflows │ ├── labeler.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.github/labeler-config.yml: -------------------------------------------------------------------------------- 1 | # add 'agent-nodejs' label to all new issues 2 | agent-nodejs: 3 | - '.*' 4 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Issue Labeler" 2 | on: 3 | issues: 4 | types: [opened] 5 | pull_request_target: 6 | types: [opened] 7 | 8 | jobs: 9 | triage: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: AlexanderWert/issue-labeler@v2.3 13 | with: 14 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 15 | configuration-path: .github/labeler-config.yml 16 | enable-versioned-regex: 0 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | # https://github.community/t/how-to-trigger-an-action-on-push-or-pull-request-but-not-both/16662/2 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | test-vers: 12 | strategy: 13 | matrix: 14 | node: ['6', '8', '10', '12', '14', '15'] 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-node@v2 19 | with: 20 | node-version: ${{ matrix.node }} 21 | - run: npm install 22 | - run: npm test 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | coverage* 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .codecov.yml 2 | .nyc_output 3 | test.js 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Elasticsearch BV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # traceparent 2 | 3 | [![npm](https://img.shields.io/npm/v/traceparent.svg)](https://www.npmjs.com/package/traceparent) 4 | [![Test status](https://github.com/elastic/node-traceparent/workflows/Test/badge.svg)](https://github.com/elastic/node-traceparent/actions) 5 | 6 | This is a basic implementation of the traceparent header part of the W3C trace context spec. 7 | 8 | ## Installation 9 | 10 | ``` 11 | npm install traceparent 12 | ``` 13 | 14 | ## Example Usage 15 | 16 | ```js 17 | const crypto = require('crypto') 18 | const TraceParent = require('traceparent') 19 | 20 | const version = Buffer.alloc(1).toString('hex') 21 | const traceId = crypto.randomBytes(16).toString('hex') 22 | const id = crypto.randomBytes(8).toString('hex') 23 | const flags = '01' 24 | 25 | const header = `${version}-${traceId}-${id}-${flags}` 26 | 27 | const parent = TraceParent.fromString(header) 28 | ``` 29 | 30 | ## API 31 | 32 | ### `new TraceParent(buffer)` 33 | 34 | Construct a new `TraceParent` instance from an existing buffer. The contents are binary data that corresponds to the structure of the [W3C traceparent header][traceparent] format, with separators removed. 35 | 36 | ### `TraceParent.fromString(header)` 37 | 38 | Reconstruct a `TraceParent` instance from a formatted [W3C traceparent header][traceparent] string. 39 | 40 | ### `TraceParent.startOrResume(parent, settings)` 41 | 42 | Resume from a parent context, if given, or start a new context. Accepts another `TraceParent` instance, a [W3C traceparent header][traceparent] string, or a `Span` or `Transaction` instance from [elastic-apm-node](http://npmjs.org/package/elastic-apm-node). 43 | 44 | Requires a `settings` object with a `transactionSampleRate` value from 0.0 to 1.0 to generate a sampling decision for the context. This will only be applied when starting a _new_ context. When continuing an existing context, the sampling decision will be propagated into all child contexts. 45 | 46 | ### `traceParent.recorded` 47 | 48 | Returns `true` if this `TraceParent` is sampled. 49 | 50 | ### `traceParent.traceId` 51 | 52 | The `traceId` property will propagate through all children in the tree to link them all together. 53 | 54 | ### `traceParent.id` 55 | 56 | The `id` property is used to uniquely identify a given `TraceParent` instance within the tree. 57 | 58 | ### `traceParent.parentId` 59 | 60 | The `parentId` property links this context to its direct parent in the tree. 61 | 62 | ### `traceParent.flags` 63 | 64 | The `flags` property is used to store metadata such as the sampling decision. 65 | 66 | ### `traceParent.version` 67 | 68 | The `version` property corresponds to the version segment of the [W3C traceparent header][traceparent]. 69 | 70 | ### `traceParent.child()` 71 | 72 | Create a new `TraceParent` instance that is a child of this one. 73 | 74 | ### `traceParent.toString()` 75 | 76 | Formats the `TraceParent` instance as a [W3C traceparent header][traceparent]. 77 | 78 | ### `traceParent.ensureParentId()` 79 | 80 | Return the parent ID, if there is none, generate one. This is useful in browser instrumentation to produce a starting span around a browser request which was not instrumented prior to page load. 81 | 82 | ## License 83 | 84 | [MIT](LICENSE) 85 | 86 | [traceparent]: https://github.com/w3c/trace-context/blob/main/spec/20-http_request_header_format.md 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { randomFillSync } = require('random-poly-fill') // TODO: Remove when Node.js 6 is no longer supported 4 | 5 | const SIZES = { 6 | version: 1, 7 | traceId: 16, 8 | id: 8, 9 | flags: 1, 10 | parentId: 8, 11 | 12 | // Aggregate sizes 13 | ids: 24, // traceId + id 14 | all: 34 15 | } 16 | 17 | const OFFSETS = { 18 | version: 0, 19 | traceId: SIZES.version, 20 | id: SIZES.version + SIZES.traceId, 21 | flags: SIZES.version + SIZES.ids, 22 | 23 | // Additional parentId is stored after the header content 24 | parentId: SIZES.version + SIZES.ids + SIZES.flags 25 | } 26 | 27 | const FLAGS = { 28 | recorded: 0b00000001 29 | } 30 | 31 | function defineLazyProp (obj, prop, fn) { 32 | Object.defineProperty(obj, prop, { 33 | configurable: true, 34 | enumerable: true, 35 | get () { 36 | const value = fn() 37 | if (value !== undefined) { 38 | Object.defineProperty(obj, prop, { 39 | configurable: true, 40 | enumerable: true, 41 | value 42 | }) 43 | } 44 | return value 45 | } 46 | }) 47 | } 48 | 49 | function hexSliceFn (buffer, offset, length) { 50 | return () => buffer.slice(offset, length).toString('hex') 51 | } 52 | 53 | function maybeHexSliceFn (buffer, offset, length) { 54 | const fn = hexSliceFn(buffer, offset, length) 55 | return () => { 56 | const value = fn() 57 | // Check for any non-zero characters to identify a valid ID 58 | if (/[1-9a-f]/.test(value)) { 59 | return value 60 | } 61 | } 62 | } 63 | 64 | function makeChild (buffer) { 65 | // Move current id into parentId region 66 | buffer.copy(buffer, OFFSETS.parentId, OFFSETS.id, OFFSETS.flags) 67 | 68 | // Generate new id 69 | randomFillSync(buffer, OFFSETS.id, SIZES.id) 70 | 71 | return new TraceParent(buffer) 72 | } 73 | 74 | function isValidHeader (header) { 75 | return /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/.test(header) 76 | } 77 | 78 | // NOTE: The version byte is not fully supported yet, but is not important until 79 | // we use the official header name rather than elastic-apm-traceparent. 80 | // https://w3c.github.io/distributed-tracing/report-trace-context.html#versioning-of-traceparent 81 | function headerToBuffer (header) { 82 | const buffer = Buffer.alloc(SIZES.all) 83 | buffer.write(header.replace(/-/g, ''), 'hex') 84 | return buffer 85 | } 86 | 87 | function resume (header) { 88 | return makeChild(headerToBuffer(header)) 89 | } 90 | 91 | function start (sampled = false) { 92 | const buffer = Buffer.alloc(SIZES.all) 93 | 94 | // Generate new ids 95 | randomFillSync(buffer, OFFSETS.traceId, SIZES.ids) 96 | 97 | if (sampled) { 98 | buffer[OFFSETS.flags] |= FLAGS.recorded 99 | } 100 | 101 | return new TraceParent(buffer) 102 | } 103 | 104 | const bufferSymbol = Symbol('trace-context-buffer') 105 | 106 | class TraceParent { 107 | constructor (buffer) { 108 | this[bufferSymbol] = buffer 109 | Object.defineProperty(this, 'recorded', { 110 | value: !!(buffer[OFFSETS.flags] & FLAGS.recorded), 111 | enumerable: true 112 | }) 113 | 114 | defineLazyProp(this, 'version', hexSliceFn(buffer, OFFSETS.version, OFFSETS.traceId)) 115 | defineLazyProp(this, 'traceId', hexSliceFn(buffer, OFFSETS.traceId, OFFSETS.id)) 116 | defineLazyProp(this, 'id', hexSliceFn(buffer, OFFSETS.id, OFFSETS.flags)) 117 | defineLazyProp(this, 'flags', hexSliceFn(buffer, OFFSETS.flags, OFFSETS.parentId)) 118 | defineLazyProp(this, 'parentId', maybeHexSliceFn(buffer, OFFSETS.parentId)) 119 | } 120 | 121 | static startOrResume (childOf, conf) { 122 | if (childOf instanceof TraceParent) return childOf.child() 123 | if (childOf && childOf._context instanceof TraceParent) return childOf._context.child() 124 | 125 | return isValidHeader(childOf) 126 | ? resume(childOf) 127 | : start(Math.random() <= conf.transactionSampleRate) 128 | } 129 | 130 | static fromString (header) { 131 | return new TraceParent(headerToBuffer(header)) 132 | } 133 | 134 | ensureParentId () { 135 | let id = this.parentId 136 | if (!id) { 137 | randomFillSync(this[bufferSymbol], OFFSETS.parentId, SIZES.id) 138 | id = this.parentId 139 | } 140 | return id 141 | } 142 | 143 | child () { 144 | return makeChild(Buffer.from(this[bufferSymbol])) 145 | } 146 | 147 | toString () { 148 | return `${this.version}-${this.traceId}-${this.id}-${this.flags}` 149 | } 150 | } 151 | 152 | TraceParent.FLAGS = FLAGS 153 | 154 | module.exports = TraceParent 155 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "traceparent", 3 | "version": "1.0.0", 4 | "description": "Context management helper for the w3c traceparent header format", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test.js" 8 | }, 9 | "keywords": [ 10 | "elastic", 11 | "apm", 12 | "trace", 13 | "context", 14 | "w3c", 15 | "traceparent" 16 | ], 17 | "author": "Stephen Belanger (https://github.com/qard)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "random-poly-fill": "^1.0.1" 21 | }, 22 | "devDependencies": { 23 | "tape": "^4.9.2" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/elastic/node-traceparent.git" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/elastic/node-traceparent/issues" 31 | }, 32 | "homepage": "https://github.com/elastic/node-traceparent#readme", 33 | "engines": { 34 | "node": ">=6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const test = require('tape') 5 | 6 | const TraceParent = require('./') 7 | 8 | const version = Buffer.alloc(1).toString('hex') 9 | const traceId = crypto.randomBytes(16).toString('hex') 10 | const id = crypto.randomBytes(8).toString('hex') 11 | const flags = '01' 12 | 13 | const header = `${version}-${traceId}-${id}-${flags}` 14 | 15 | function jsonify (object) { 16 | return JSON.parse(JSON.stringify(object)) 17 | } 18 | 19 | function isValid (t, traceParent) { 20 | t.ok(traceParent instanceof TraceParent, 'has a trace parent object') 21 | t.ok(/^[\da-f]{2}$/.test(traceParent.version), 'has valid version') 22 | t.ok(/^[\da-f]{32}$/.test(traceParent.traceId), 'has valid traceId') 23 | t.ok(/^[\da-f]{16}$/.test(traceParent.id), 'has valid id') 24 | t.ok(/^[\da-f]{2}$/.test(traceParent.flags), 'has valid flags') 25 | } 26 | 27 | test('fromString', t => { 28 | const traceParent = TraceParent.fromString(header) 29 | 30 | isValid(t, traceParent) 31 | t.equal(traceParent.version, version, 'version matches') 32 | t.equal(traceParent.traceId, traceId, 'traceId matches') 33 | t.equal(traceParent.id, id, 'id matches') 34 | t.equal(traceParent.flags, flags, 'flags matches') 35 | 36 | t.end() 37 | }) 38 | 39 | test('toString', t => { 40 | const traceParent = TraceParent.fromString(header) 41 | 42 | isValid(t, traceParent) 43 | t.equal(traceParent.toString(), header, 'trace parent stringifies to valid header') 44 | 45 | t.end() 46 | }) 47 | 48 | test('toJSON', t => { 49 | const traceParent = TraceParent.fromString(header) 50 | 51 | isValid(t, traceParent) 52 | t.deepEqual(jsonify(traceParent), { 53 | version, 54 | traceId, 55 | id, 56 | flags, 57 | recorded: true 58 | }, 'trace parent serializes fields to hex strings, in JSON form') 59 | 60 | t.end() 61 | }) 62 | 63 | test('startOrResume', t => { 64 | t.test('resume from header', t => { 65 | const traceParent = TraceParent.startOrResume(header) 66 | 67 | isValid(t, traceParent) 68 | t.equal(traceParent.version, version, 'version matches') 69 | t.equal(traceParent.traceId, traceId, 'traceId matches') 70 | t.notEqual(traceParent.id, id, 'has new id') 71 | t.equal(traceParent.flags, flags, 'flags matches') 72 | 73 | t.end() 74 | }) 75 | 76 | t.test('resume from TraceParent', t => { 77 | const traceParent = TraceParent.startOrResume( 78 | TraceParent.fromString(header) 79 | ) 80 | 81 | isValid(t, traceParent) 82 | t.equal(traceParent.version, version, 'version matches') 83 | t.equal(traceParent.traceId, traceId, 'traceId matches') 84 | t.notEqual(traceParent.id, id, 'has new id') 85 | t.equal(traceParent.flags, flags, 'flags matches') 86 | 87 | t.end() 88 | }) 89 | 90 | t.test('resume from Span-like', t => { 91 | const trans = { _context: TraceParent.fromString(header) } 92 | const traceParent = TraceParent.startOrResume(trans) 93 | 94 | isValid(t, traceParent) 95 | t.equal(traceParent.version, version, 'version matches') 96 | t.equal(traceParent.traceId, traceId, 'traceId matches') 97 | t.notEqual(traceParent.id, id, 'has new id') 98 | t.equal(traceParent.flags, flags, 'flags matches') 99 | 100 | t.end() 101 | }) 102 | 103 | t.test('start sampled', t => { 104 | const traceParent = TraceParent.startOrResume(null, { 105 | transactionSampleRate: 1.0 106 | }) 107 | 108 | isValid(t, traceParent) 109 | t.equal(traceParent.version, version, 'version matches') 110 | t.notEqual(traceParent.traceId, traceId, 'has new traceId') 111 | t.notEqual(traceParent.id, id, 'has new id') 112 | t.equal(traceParent.recorded, true, 'is sampled') 113 | 114 | t.end() 115 | }) 116 | 117 | t.test('start unsampled', t => { 118 | const traceParent = TraceParent.startOrResume(null, { 119 | transactionSampleRate: 0.0 120 | }) 121 | 122 | isValid(t, traceParent) 123 | t.equal(traceParent.version, version, 'version matches') 124 | t.notEqual(traceParent.traceId, traceId, 'has new traceId') 125 | t.notEqual(traceParent.id, id, 'has new id') 126 | t.equal(traceParent.recorded, false, 'is sampled') 127 | 128 | t.end() 129 | }) 130 | }) 131 | 132 | test('child', t => { 133 | t.test('recorded', t => { 134 | const header = `${version}-${traceId}-${id}-01` 135 | const traceParent = TraceParent.fromString(header).child() 136 | 137 | isValid(t, traceParent) 138 | t.equal(traceParent.version, version, 'version matches') 139 | t.equal(traceParent.traceId, traceId, 'traceId matches') 140 | t.notEqual(traceParent.id, id, 'has new id') 141 | t.equal(traceParent.flags, '01', 'recorded remains recorded') 142 | 143 | t.end() 144 | }) 145 | 146 | t.test('not recorded', t => { 147 | const header = `${version}-${traceId}-${id}-00` 148 | const traceParent = TraceParent.fromString(header).child() 149 | 150 | isValid(t, traceParent) 151 | t.equal(traceParent.version, version, 'version matches') 152 | t.equal(traceParent.traceId, traceId, 'traceId matches') 153 | t.notEqual(traceParent.id, id, 'has new id') 154 | t.equal(traceParent.flags, '00', 'not recorded remains not recorded') 155 | 156 | t.end() 157 | }) 158 | }) 159 | 160 | test('ensureParentId', t => { 161 | const traceParent = TraceParent.fromString(header) 162 | 163 | isValid(t, traceParent) 164 | t.equal(traceParent.version, version, 'version matches') 165 | t.equal(traceParent.traceId, traceId, 'traceId matches') 166 | t.equal(traceParent.id, id, 'id matches') 167 | t.equal(traceParent.flags, flags, 'flags matches') 168 | t.notOk(traceParent.parentId, 'no parent id before') 169 | 170 | const first = traceParent.ensureParentId() 171 | t.ok(first, 'returns parent id') 172 | t.equal(traceParent.parentId, first, 'parent id of trace parent matches returned parent id') 173 | 174 | const second = traceParent.ensureParentId() 175 | t.equal(first, second, 'future calls return the first parent id') 176 | 177 | t.end() 178 | }) 179 | --------------------------------------------------------------------------------