dd-trace
Version:
Datadog APM tracing client for JavaScript
188 lines (166 loc) • 6.62 kB
JavaScript
// 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,
}