UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

191 lines (167 loc) 6.79 kB
'use strict' // encoding used here is sha256 // other languages use FNV1 // this inconsistency is ok because hashes do not need to be consistent across services const crypto = require('crypto') const { LRUCache } = require('../../../../vendor/dist/lru-cache') const log = require('../log') const pick = require('../../../datadog-core/src/utils/src/pick') const { encodeVarintInto, decodeVarint } = require('./encoding') const cache = new LRUCache({ max: 500 }) const CONTEXT_PROPAGATION_KEY = 'dd-pathway-ctx' const CONTEXT_PROPAGATION_KEY_BASE64 = 'dd-pathway-ctx-base64' const PATHWAY_CONTEXT_BYTES = 20 // Reused across `encodePathwayContext` calls; the buffer is fully rewritten before each // `Buffer.from(...)` copy-out so callers never observe mutation between checkpoints. const pathwayScratch = Buffer.allocUnsafe(PATHWAY_CONTEXT_BYTES) const logKeys = [CONTEXT_PROPAGATION_KEY, CONTEXT_PROPAGATION_KEY_BASE64] function shaHash (checkpointString) { // Copy out of the 32-byte digest so the LRU cache doesn't retain it. return Buffer.from(crypto.createHash('sha256').update(checkpointString).digest().subarray(0, 8)) } /** * @param {string} service * @param {string} env * @param {string[]} edgeTags * @param {Buffer} parentHash * @param {bigint | null} propagationHashBigInt - Optional propagation hash for process/container tags */ function computeHash (service, env, edgeTags, parentHash, propagationHashBigInt = null) { edgeTags.sort() const hashableEdgeTags = edgeTags.includes('manual_checkpoint:true') ? edgeTags.filter(item => item !== 'manual_checkpoint:true') : edgeTags // The cache key includes parentHash so a fan-in node with different parents // gets distinct cache entries; the hash input below excludes parentHash and // gets combined with it via a second sha pass to produce the final hash. const joinedEdgeTags = hashableEdgeTags.join('') const propagationHex = propagationHashBigInt ? propagationHashBigInt.toString(16) : '' const propagationPart = propagationHex ? `:${propagationHex}` : '' const key = `${service}${env}${joinedEdgeTags}${parentHash}${propagationPart}` let value = cache.get(key) if (value) { return value } const baseString = `${service}${env}${joinedEdgeTags}` const hashInput = propagationHex ? `${baseString}:${propagationHex}` : baseString const currentHash = shaHash(hashInput) const buf = Buffer.concat([currentHash, parentHash], 16) value = shaHash(buf.toString()) cache.set(key, value) return value } /** * @param {object} dataStreamsContext * @param {Buffer} dataStreamsContext.hash * @param {number} dataStreamsContext.pathwayStartNs * @param {number} dataStreamsContext.edgeStartNs * @returns {Buffer} */ function encodePathwayContext (dataStreamsContext) { let offset = dataStreamsContext.hash.copy(pathwayScratch, 0) offset = encodeVarintInto(pathwayScratch, offset, Math.round(dataStreamsContext.pathwayStartNs / 1e6)) offset = encodeVarintInto(pathwayScratch, offset, Math.round(dataStreamsContext.edgeStartNs / 1e6)) // No-op when offset >= PATHWAY_CONTEXT_BYTES; otherwise pads stale bytes from a previous call. pathwayScratch.fill(0, offset, PATHWAY_CONTEXT_BYTES) return Buffer.from(pathwayScratch.subarray(0, PATHWAY_CONTEXT_BYTES)) } /** * @param {object} dataStreamsContext * @param {Buffer} dataStreamsContext.hash * @param {number} dataStreamsContext.pathwayStartNs * @param {number} dataStreamsContext.edgeStartNs * @returns {string} */ function encodePathwayContextBase64 (dataStreamsContext) { const encodedPathway = encodePathwayContext(dataStreamsContext) return encodedPathway.toString('base64') } /** * @param {Buffer} pathwayContext * @returns {object} */ function decodePathwayContext (pathwayContext) { if (pathwayContext == null || pathwayContext.length < 8) { return null } // hash and parent hash are in LE const pathwayHash = pathwayContext.subarray(0, 8) const encodedTimestamps = pathwayContext.subarray(8) const [pathwayStartMs, encodedTimeSincePrev] = decodeVarint(encodedTimestamps) if (pathwayStartMs === undefined) { return null } const [edgeStartMs] = decodeVarint(encodedTimeSincePrev) if (edgeStartMs === undefined) { return null } return { hash: pathwayHash, pathwayStartNs: pathwayStartMs * 1e6, edgeStartNs: edgeStartMs * 1e6 } } /** * @param {string} pathwayContext * @returns {ReturnType<typeof decodePathwayContext>|undefined} */ function decodePathwayContextBase64 (pathwayContext) { if (pathwayContext == null || pathwayContext.length < 8) { return } if (Buffer.isBuffer(pathwayContext)) { pathwayContext = pathwayContext.toString() } const encodedPathway = Buffer.from(pathwayContext, 'base64') return decodePathwayContext(encodedPathway) } const DsmPathwayCodec = { // we use a class for encoding / decoding in case we update our encoding/decoding. A class will make updates easier // instead of using individual functions. /** * @param {object} dataStreamsContext * @param {Buffer} dataStreamsContext.hash * @param {number} dataStreamsContext.pathwayStartNs * @param {number} dataStreamsContext.edgeStartNs * @param {object} carrier */ encode (dataStreamsContext, carrier) { if (!dataStreamsContext || !dataStreamsContext.hash) { return } carrier[CONTEXT_PROPAGATION_KEY_BASE64] = encodePathwayContextBase64(dataStreamsContext) // eslint-disable-next-line eslint-rules/eslint-log-printf-style log.debug(() => `Injected into DSM carrier: ${JSON.stringify(pick(carrier, logKeys))}.`) }, /** * @param {object} carrier * @returns {ReturnType<typeof decodePathwayContext>|undefined} */ decode (carrier) { // eslint-disable-next-line eslint-rules/eslint-log-printf-style log.debug(() => `Attempting extract from DSM carrier: ${JSON.stringify(pick(carrier, logKeys))}.`) if (carrier == null) return let ctx if (CONTEXT_PROPAGATION_KEY_BASE64 in carrier) { // decode v2 encoding of base64 ctx = decodePathwayContextBase64(carrier[CONTEXT_PROPAGATION_KEY_BASE64]) } else if (CONTEXT_PROPAGATION_KEY in carrier) { try { // decode v1 encoding ctx = decodePathwayContext(carrier[CONTEXT_PROPAGATION_KEY]) } catch { // pass } // cover case where base64 context was received under wrong key if (!ctx && CONTEXT_PROPAGATION_KEY in carrier) { ctx = decodePathwayContextBase64(carrier[CONTEXT_PROPAGATION_KEY]) } } return ctx }, } module.exports = { CONTEXT_PROPAGATION_KEY_BASE64, computePathwayHash: computeHash, encodePathwayContext, decodePathwayContext, encodePathwayContextBase64, decodePathwayContextBase64, DsmPathwayCodec, }