UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

203 lines (174 loc) 7.1 kB
'use strict' const { normalizeSpan } = require('./tags-processors') const { AgentEncoder: BaseEncoder, stringifySpanEvents } = require('./0.4') const ARRAY_OF_TWO = 0x92 const ARRAY_OF_TWELVE = 0x9C // Per-span fused head: `[0x9C, service-idx, name-idx, resource-idx, // trace-id, span-id, parent-id]` — three uint32 indexes (5 bytes each) + // three uint64 IDs (9 bytes each) + the array marker. Replaces seven // separate reserves (`writeByte` + 3 × `writeInteger` + 3 × `_encodeId`) // with one block-sized reserve per span. const HEAD_BLOCK_SIZE = 1 + 5 * 3 + 9 * 3 function formatSpan (span) { span = normalizeSpan(span) // v0.5 has no native span_events slot; always serialize as a meta tag. if (span.span_events) { // TODO: this is a costly operation. Consolidate this with the formatter span.meta.events = stringifySpanEvents(span.span_events) // `= undefined` over `delete` to keep the span's hidden class. span.span_events = undefined } return span } class AgentEncoder extends BaseEncoder { makePayload () { const prefixSize = 1 const stringSize = this._stringBytes.length + 5 const traceSize = this._traceBytes.length + 5 const buffer = Buffer.allocUnsafe(prefixSize + stringSize + traceSize) buffer[0] = ARRAY_OF_TWO const offset = this._writeStrings(buffer, 1) this._writeTraces(buffer, offset) this._reset() return buffer } _encode (bytes, trace) { bytes.writeArrayPrefix(trace) const stringMap = this._stringMap for (let span of trace) { span = formatSpan(span) // Resolve the three head string indices up front. `_cacheString` // writes into `_stringBytes`, an independent chunk, so the side // effect is safe to interleave with the `_traceBytes` reserve // below. const serviceIndex = stringMap[span.service] ?? this._cacheString(span.service) const nameIndex = stringMap[span.name] ?? this._cacheString(span.name) const resourceIndex = stringMap[span.resource] ?? this._cacheString(span.resource) const blockOffset = bytes.length bytes.reserve(HEAD_BLOCK_SIZE) const target = bytes.buffer target[blockOffset] = ARRAY_OF_TWELVE let cursor = this.#writeIndexAt(target, blockOffset + 1, serviceIndex) cursor = this.#writeIndexAt(target, cursor, nameIndex) cursor = this.#writeIndexAt(target, cursor, resourceIndex) cursor = this.#writeIdAt(target, cursor, span.trace_id) cursor = this.#writeIdAt(target, cursor, span.span_id) this.#writeIdAt(target, cursor, span.parent_id) bytes.writeIntOrFloat(span.start || 0) bytes.writeIntOrFloat(span.duration || 0) bytes.writeIntOrFloat(span.error) this._encodeMap(bytes, span.meta || {}) this._encodeMap(bytes, span.metrics || {}) this._encodeString(bytes, span.type) } } // Override the inherited 0.4 `_encodeMap` so the v0.5 wire emits each numeric // value via `_encodeIntOrFloat` (compact unsigned/signed int when integer, // float64 otherwise) instead of always float64. The 0.4 base method stays on // float64 because the CI-visibility encoders inherit it and target a // different intake. _encodeMap (bytes, value) { const offset = bytes.length bytes.reserve(5) bytes.buffer[offset] = 0xDF const stringMap = this._stringMap let count = 0 for (const key of Object.keys(value)) { const entryValue = value[key] if (typeof entryValue !== 'string' && typeof entryValue !== 'number') continue const keyIndex = stringMap[key] ?? this._cacheString(key) const writeOffset = bytes.length if (typeof entryValue === 'string') { // Both halves are uint32 indices on the v0.5 wire — known // size, so the key and value pair fuses into one reserve. const valueIndex = stringMap[entryValue] ?? this._cacheString(entryValue) bytes.reserve(10) const target = bytes.buffer this.#writeIndexAt(target, writeOffset, keyIndex) this.#writeIndexAt(target, writeOffset + 5, valueIndex) } else { // Speculate that the value is a positive fixint (0..127). The // metrics map is mostly small unsigned integers (sample priority, // `_dd.measured`, attribute counts), so one reserve covers the // key (5 bytes) and the value (1 byte). Misses rewind the // speculative value byte and route the value through the full // encoder so the wire still picks the shortest valid encoding. bytes.reserve(6) const target = bytes.buffer this.#writeIndexAt(target, writeOffset, keyIndex) if (entryValue === (entryValue & 0x7F)) { target[writeOffset + 5] = entryValue } else { bytes.length = writeOffset + 5 bytes.writeIntOrFloat(entryValue) } } count++ } const target = bytes.buffer target[offset + 1] = count >>> 24 target[offset + 2] = count >>> 16 target[offset + 3] = count >>> 8 target[offset + 4] = count } _encodeString (bytes, value = '') { const index = this._stringMap[value] ?? this._cacheString(value) bytes.writeInteger(index) } _cacheString (value) { let index = this._stringMap[value] if (index === undefined) { index = this._stringCount++ this._stringMap[value] = index this._stringBytes.write(value) } return index } _writeStrings (buffer, offset) { offset = this._writeArrayPrefix(buffer, offset, this._stringCount) offset += this._stringBytes.buffer.copy(buffer, offset, 0, this._stringBytes.length) return offset } /** * Write `[0xCE, uint32(index)]` into `target` at `offset` and return the * new cursor. Caller is responsible for having reserved enough room. * * @param {Uint8Array} target * @param {number} offset * @param {number} index * @returns {number} */ #writeIndexAt (target, offset, index) { target[offset] = 0xCE target[offset + 1] = index >> 24 target[offset + 2] = index >> 16 target[offset + 3] = index >> 8 target[offset + 4] = index return offset + 5 } /** * Write `[0xCF, uint64(id)]` into `target` at `offset` and return the * new cursor. The id is truncated to the low 8 bytes, matching the * inherited `_encodeId` behavior. * * @param {Uint8Array} target * @param {number} offset * @param {{ toBuffer: () => Uint8Array | number[] }} identifier * @returns {number} */ #writeIdAt (target, offset, identifier) { target[offset] = 0xCF const idBuffer = identifier.toBuffer() const start = idBuffer.length - 8 target[offset + 1] = idBuffer[start] target[offset + 2] = idBuffer[start + 1] target[offset + 3] = idBuffer[start + 2] target[offset + 4] = idBuffer[start + 3] target[offset + 5] = idBuffer[start + 4] target[offset + 6] = idBuffer[start + 5] target[offset + 7] = idBuffer[start + 6] target[offset + 8] = idBuffer[start + 7] return offset + 9 } } module.exports = { AgentEncoder }