UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

188 lines (166 loc) 6.62 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 { encodeVarint, 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 logKeys = [CONTEXT_PROPAGATION_KEY, CONTEXT_PROPAGATION_KEY_BASE64] function shaHash (checkpointString) { const hash = crypto.createHash('sha256').update(checkpointString).digest('hex').slice(0, 16) return Buffer.from(hash, 'hex') } /** * @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.filter(item => item !== 'manual_checkpoint:true') // Cache key includes parentHash to handle fan-in/fan-out scenarios where the same // service+env+tags+propagationHash can have different parents. This ensures we cache // the complete pathway context, not just the current node's identity. const propagationPart = propagationHashBigInt ? `:${propagationHashBigInt.toString(16)}` : '' const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}${propagationPart}` let value = cache.get(key) if (value) { return value } // Key vs hashInput distinction: // - 'key' (above) is used for caching and includes parentHash to differentiate pathways // with the same node but different parents (e.g., multiple queues feeding one consumer) // - 'hashInput' (below) excludes parentHash to compute only the current node's identity hash, // which is then XORed with parentHash (line 54) to build the complete pathway hash // This two-step approach (hash current node independently, then combine with parent) is // required for proper pathway construction in the DSM protocol. const baseString = `${service}${env}` + hashableEdgeTags.join('') const hashInput = propagationHashBigInt ? `${baseString}:${propagationHashBigInt.toString(16)}` : 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) { return Buffer.concat([ dataStreamsContext.hash, Buffer.from(encodeVarint(Math.round(dataStreamsContext.pathwayStartNs / 1e6))), Buffer.from(encodeVarint(Math.round(dataStreamsContext.edgeStartNs / 1e6))), ], 20) } /** * @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 = { computePathwayHash: computeHash, encodePathwayContext, decodePathwayContext, encodePathwayContextBase64, decodePathwayContextBase64, DsmPathwayCodec, }