Skip to content

Commit

Permalink
feat: make TraceParent a local class module (#2669)
Browse files Browse the repository at this point in the history
* feat: Make traceparent module a local class file instead of an NPM module, remove pre-Node 8 random-poly-fill module
  • Loading branch information
astorm authored May 3, 2022
1 parent 2f503a4 commit f6c4ee8
Show file tree
Hide file tree
Showing 9 changed files with 343 additions and 6 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Notes:
[float]
===== Features
- Pulled the `traceparent` NPM module into a local module and replaced the
`random-poly-fill` module with the built in `require('crypto').randomFillSync`
function call ({pull}2669[#2669])
- Add a `parent` option to `agent.captureError(err[, options][, cb])` to allow
passing in a Transaction or Span to use as the parent for the error. Before
this change the *current* span or transaction, if any, was always used.
Expand Down
2 changes: 1 addition & 1 deletion lib/tracecontext/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'use strict'
const TraceParent = require('traceparent')
const { TraceParent } = require('./traceparent')
const TraceState = require('./tracestate')

class TraceContext {
Expand Down
156 changes: 156 additions & 0 deletions lib/tracecontext/traceparent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
'use strict'

const { randomFillSync } = require('crypto')

const SIZES = {
version: 1,
traceId: 16,
id: 8,
flags: 1,
parentId: 8,

// Aggregate sizes
ids: 24, // traceId + id
all: 34
}

const OFFSETS = {
version: 0,
traceId: SIZES.version,
id: SIZES.version + SIZES.traceId,
flags: SIZES.version + SIZES.ids,

// Additional parentId is stored after the header content
parentId: SIZES.version + SIZES.ids + SIZES.flags
}

const FLAGS = {
recorded: 0b00000001
}

function defineLazyProp (obj, prop, fn) {
Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
get () {
const value = fn()
if (value !== undefined) {
Object.defineProperty(obj, prop, {
configurable: true,
enumerable: true,
value
})
}
return value
}
})
}

function hexSliceFn (buffer, offset, length) {
return () => buffer.slice(offset, length).toString('hex')
}

function maybeHexSliceFn (buffer, offset, length) {
const fn = hexSliceFn(buffer, offset, length)
return () => {
const value = fn()
// Check for any non-zero characters to identify a valid ID
if (/[1-9a-f]/.test(value)) {
return value
}
}
}

function makeChild (buffer) {
// Move current id into parentId region
buffer.copy(buffer, OFFSETS.parentId, OFFSETS.id, OFFSETS.flags)

// Generate new id
randomFillSync(buffer, OFFSETS.id, SIZES.id)

return new TraceParent(buffer)
}

function isValidHeader (header) {
return /^[\da-f]{2}-[\da-f]{32}-[\da-f]{16}-[\da-f]{2}$/.test(header)
}

// NOTE: The version byte is not fully supported yet, but is not important until
// we use the official header name rather than elastic-apm-traceparent.
// https://w3c.github.io/distributed-tracing/report-trace-context.html#versioning-of-traceparent
function headerToBuffer (header) {
const buffer = Buffer.alloc(SIZES.all)
buffer.write(header.replace(/-/g, ''), 'hex')
return buffer
}

function resume (header) {
return makeChild(headerToBuffer(header))
}

function start (sampled = false) {
const buffer = Buffer.alloc(SIZES.all)

// Generate new ids
randomFillSync(buffer, OFFSETS.traceId, SIZES.ids)

if (sampled) {
buffer[OFFSETS.flags] |= FLAGS.recorded
}

return new TraceParent(buffer)
}

const bufferSymbol = Symbol('trace-context-buffer')

class TraceParent {
constructor (buffer) {
this[bufferSymbol] = buffer
Object.defineProperty(this, 'recorded', {
value: !!(buffer[OFFSETS.flags] & FLAGS.recorded),
enumerable: true
})

defineLazyProp(this, 'version', hexSliceFn(buffer, OFFSETS.version, OFFSETS.traceId))
defineLazyProp(this, 'traceId', hexSliceFn(buffer, OFFSETS.traceId, OFFSETS.id))
defineLazyProp(this, 'id', hexSliceFn(buffer, OFFSETS.id, OFFSETS.flags))
defineLazyProp(this, 'flags', hexSliceFn(buffer, OFFSETS.flags, OFFSETS.parentId))
defineLazyProp(this, 'parentId', maybeHexSliceFn(buffer, OFFSETS.parentId))
}

static startOrResume (childOf, conf) {
if (childOf instanceof TraceParent) return childOf.child()
if (childOf && childOf._context instanceof TraceParent) return childOf._context.child()

return isValidHeader(childOf)
? resume(childOf)
: start(Math.random() <= conf.transactionSampleRate)
}

static fromString (header) {
return new TraceParent(headerToBuffer(header))
}

ensureParentId () {
let id = this.parentId
if (!id) {
randomFillSync(this[bufferSymbol], OFFSETS.parentId, SIZES.id)
id = this.parentId
}
return id
}

child () {
return makeChild(Buffer.from(this[bufferSymbol]))
}

toString () {
return `${this.version}-${this.traceId}-${this.id}-${this.flags}`
}
}

TraceParent.FLAGS = FLAGS

module.exports = {
TraceParent
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@
"shallow-clone-shim": "^2.0.0",
"source-map": "^0.8.0-beta.0",
"sql-summary": "^1.0.1",
"traceparent": "^1.0.0",
"traverse": "^0.6.6",
"unicode-byte-truncate": "^1.0.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const es = require(esClientPkgName)

const { Readable } = require('stream')
const test = require('tape')
const TraceParent = require('traceparent')
const { TraceParent } = require('../../../../lib/tracecontext/traceparent')

const findObjInArray = require('../../../_utils').findObjInArray
const mockClient = require('../../../_mock_http_client')
Expand Down
2 changes: 1 addition & 1 deletion test/instrumentation/modules/http/basic.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var test = require('tape')

var assert = require('./_assert')
var mockClient = require('../../../_mock_http_client')
var TraceParent = require('traceparent')
var { TraceParent } = require('../../../../lib/tracecontext/traceparent')

test('http.createServer', function (t) {
t.test('direct callback', function (t) {
Expand Down
2 changes: 1 addition & 1 deletion test/instrumentation/modules/http/outgoing.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var test = require('tape')

var echoServer = require('./_echo_server_util').echoServer
var mockClient = require('../../../_mock_http_client')
var TraceParent = require('traceparent')
var { TraceParent } = require('../../../../lib/tracecontext/traceparent')

var methods = ['request', 'get']

Expand Down
2 changes: 1 addition & 1 deletion test/tracecontext/tracecontext.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const agent = require('../..').start({
const tape = require('tape')
const TraceContext = require('../../lib/tracecontext')
const TraceState = require('../../lib/tracecontext/tracestate')
const TraceParent = require('traceparent')
const { TraceParent } = require('../../lib/tracecontext/traceparent')

tape.test('propagateTraceContextHeaders tests', function (suite) {
suite.test('Span test', function (t) {
Expand Down
178 changes: 178 additions & 0 deletions test/tracecontext/traceparent.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
'use strict'

const crypto = require('crypto')
const test = require('tape')

const { TraceParent } = require('../../lib/tracecontext/traceparent')

const version = Buffer.alloc(1).toString('hex')
const traceId = crypto.randomBytes(16).toString('hex')
const id = crypto.randomBytes(8).toString('hex')
const flags = '01'

const header = `${version}-${traceId}-${id}-${flags}`

function jsonify (object) {
return JSON.parse(JSON.stringify(object))
}

function isValid (t, traceParent) {
t.ok(traceParent instanceof TraceParent, 'has a trace parent object')
t.ok(/^[\da-f]{2}$/.test(traceParent.version), 'has valid version')
t.ok(/^[\da-f]{32}$/.test(traceParent.traceId), 'has valid traceId')
t.ok(/^[\da-f]{16}$/.test(traceParent.id), 'has valid id')
t.ok(/^[\da-f]{2}$/.test(traceParent.flags), 'has valid flags')
}

test('fromString', t => {
const traceParent = TraceParent.fromString(header)

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.equal(traceParent.id, id, 'id matches')
t.equal(traceParent.flags, flags, 'flags matches')

t.end()
})

test('toString', t => {
const traceParent = TraceParent.fromString(header)

isValid(t, traceParent)
t.equal(traceParent.toString(), header, 'trace parent stringifies to valid header')

t.end()
})

test('toJSON', t => {
const traceParent = TraceParent.fromString(header)

isValid(t, traceParent)
t.deepEqual(jsonify(traceParent), {
version,
traceId,
id,
flags,
recorded: true
}, 'trace parent serializes fields to hex strings, in JSON form')

t.end()
})

test('startOrResume', t => {
t.test('resume from header', t => {
const traceParent = TraceParent.startOrResume(header)

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.flags, flags, 'flags matches')

t.end()
})

t.test('resume from TraceParent', t => {
const traceParent = TraceParent.startOrResume(
TraceParent.fromString(header)
)

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.flags, flags, 'flags matches')

t.end()
})

t.test('resume from Span-like', t => {
const trans = { _context: TraceParent.fromString(header) }
const traceParent = TraceParent.startOrResume(trans)

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.flags, flags, 'flags matches')

t.end()
})

t.test('start sampled', t => {
const traceParent = TraceParent.startOrResume(null, {
transactionSampleRate: 1.0
})

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.notEqual(traceParent.traceId, traceId, 'has new traceId')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.recorded, true, 'is sampled')

t.end()
})

t.test('start unsampled', t => {
const traceParent = TraceParent.startOrResume(null, {
transactionSampleRate: 0.0
})

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.notEqual(traceParent.traceId, traceId, 'has new traceId')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.recorded, false, 'is sampled')

t.end()
})
})

test('child', t => {
t.test('recorded', t => {
const header = `${version}-${traceId}-${id}-01`
const traceParent = TraceParent.fromString(header).child()

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.flags, '01', 'recorded remains recorded')

t.end()
})

t.test('not recorded', t => {
const header = `${version}-${traceId}-${id}-00`
const traceParent = TraceParent.fromString(header).child()

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.notEqual(traceParent.id, id, 'has new id')
t.equal(traceParent.flags, '00', 'not recorded remains not recorded')

t.end()
})
})

test('ensureParentId', t => {
const traceParent = TraceParent.fromString(header)

isValid(t, traceParent)
t.equal(traceParent.version, version, 'version matches')
t.equal(traceParent.traceId, traceId, 'traceId matches')
t.equal(traceParent.id, id, 'id matches')
t.equal(traceParent.flags, flags, 'flags matches')
t.notOk(traceParent.parentId, 'no parent id before')

const first = traceParent.ensureParentId()
t.ok(first, 'returns parent id')
t.equal(traceParent.parentId, first, 'parent id of trace parent matches returned parent id')

const second = traceParent.ensureParentId()
t.equal(first, second, 'future calls return the first parent id')

t.end()
})

0 comments on commit f6c4ee8

Please sign in to comment.