-
Notifications
You must be signed in to change notification settings - Fork 407
feat(otel): otel sampling support in distributed tracing #10117
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
49eb625
5785a9b
9a7dfdd
af0fa51
1186c76
30ab81c
42005a6
660cb44
46548fb
e8d4fd6
9c18613
f9ff0ad
8fa5940
9dc3f5d
cf02963
6c0513a
3e895ea
59c8dd7
c0614ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| 'use strict' | ||
|
|
||
| const UINT64_MODULO = 2n ** 64n | ||
|
|
||
| // Knuth's factor for the sampling algorithm shared across Datadog tracers. | ||
| const SAMPLING_KNUTH_FACTOR = 1_111_111_111_111_111_111n | ||
|
|
||
| /** | ||
| * Hashes the lower 64 bits of a trace ID for deterministic trace sampling. | ||
| * | ||
| * @param {bigint} traceId | ||
| * @returns {bigint} | ||
| */ | ||
| function knuthHash (traceId) { | ||
| return (traceId * SAMPLING_KNUTH_FACTOR) % UINT64_MODULO | ||
| } | ||
|
|
||
| module.exports = knuthHash |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -204,8 +204,8 @@ class DatadogSpan { | |
| setTag (key, value) { | ||
| this._spanContext.setTag(key, value) | ||
|
|
||
| if (isSamplingPriorityTag(key) && this._spanContext._sampling.priority === undefined) { | ||
| this._prioritySampler.sample(this, false) | ||
| if (isSamplingPriorityTag(key)) { | ||
| this._prioritySampler.setPriorityFromTag(this, key, value) | ||
|
Comment on lines
+216
to
+217
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
After sampling has already been materialized—for example by injection or reading OTel AGENTS.md reference: AGENTS.md:L109-L109 Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| if (tagsUpdateCh.hasSubscribers) { | ||
|
|
@@ -223,9 +223,11 @@ class DatadogSpan { | |
| // `options.tags` callers that pass `'key:val,key:val'` strings. | ||
| const tags = this._spanContext.getTags() | ||
| let mayChangeSamplingPriority | ||
| let samplingTags | ||
|
|
||
| if (keyValueMap !== null && typeof keyValueMap === 'object' && !Array.isArray(keyValueMap)) { | ||
| Object.assign(tags, keyValueMap) | ||
| samplingTags = keyValueMap | ||
| mayChangeSamplingPriority = | ||
| MANUAL_KEEP in keyValueMap || | ||
| MANUAL_DROP in keyValueMap || | ||
|
|
@@ -234,14 +236,15 @@ class DatadogSpan { | |
| /* istanbul ignore if: v5 fallback, master ships 6.0.0-pre */ | ||
| if (DD_MAJOR < 6 && (typeof keyValueMap === 'string' || Array.isArray(keyValueMap))) { | ||
| tagger.add(tags, keyValueMap) | ||
| samplingTags = tags | ||
| mayChangeSamplingPriority = true | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With AGENTS.md reference: AGENTS.md:L119-L122 Useful? React with 👍 / 👎. |
||
| } else { | ||
| return this | ||
| } | ||
| } | ||
|
|
||
| if (mayChangeSamplingPriority && this._spanContext._sampling.priority === undefined) { | ||
| this._prioritySampler.sample(this, false) | ||
| if (mayChangeSamplingPriority) { | ||
| this._prioritySampler.setPriorityFromTags(this, samplingTags) | ||
| } | ||
|
|
||
| if (tagsUpdateCh.hasSubscribers) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| 'use strict' | ||
|
|
||
| const { AUTO_KEEP } = require('../../../ext/priority') | ||
| const knuthHash = require('./knuth_hash') | ||
|
|
||
| const MAX_OTEL_VALUE_BYTES = 256 | ||
| const MAX_THRESHOLD = 2n ** 56n | ||
| const MAX_ENCODABLE_THRESHOLD = MAX_THRESHOLD - 1n | ||
| const UINT64_MASK = 2n ** 64n - 1n | ||
| const validRandomValue = /^[0-9a-f]{14}$/ | ||
| const validThreshold = /^[0-9a-f]{1,14}$/ | ||
|
|
||
| /** | ||
| * Derives the OTel 56-bit random value from Datadog's sampling hash. | ||
| * | ||
| * @param {bigint} traceId | ||
| * @returns {bigint} | ||
| */ | ||
| function randomValueFor (traceId) { | ||
| return ((~knuthHash(traceId)) & UINT64_MASK) >> 8n | ||
| } | ||
|
|
||
| /** | ||
| * Converts a sample rate to an OTel 56-bit rejection threshold. | ||
| * | ||
| * @param {number} sampleRate | ||
| * @returns {bigint} | ||
| */ | ||
| function thresholdFor (sampleRate) { | ||
| if (sampleRate === 1) return 0n | ||
| if (sampleRate === 0) return MAX_ENCODABLE_THRESHOLD | ||
|
|
||
| const threshold = BigInt(Math.round((1 - sampleRate) * Number(MAX_THRESHOLD))) | ||
| if (threshold < 0n) return 0n | ||
| if (threshold > MAX_ENCODABLE_THRESHOLD) return MAX_ENCODABLE_THRESHOLD | ||
| return threshold | ||
| } | ||
|
|
||
| /** | ||
| * Formats an OTel threshold with trailing zero nibbles removed. | ||
| * | ||
| * @param {bigint} threshold | ||
| * @returns {string} | ||
| */ | ||
| function formatThreshold (threshold) { | ||
| return threshold.toString(16).padStart(14, '0').replace(/0+$/, '') || '0' | ||
| } | ||
|
|
||
| /** | ||
| * Generates OTel sampling fields for a local probability decision. | ||
| * | ||
| * @param {import('./opentracing/span_context')} context | ||
| * @returns {{ randomValue: string, threshold: string } | undefined} | ||
| */ | ||
| function generateFields (context) { | ||
| const { priority, probabilityRate } = context._sampling | ||
| if (priority === undefined || probabilityRate === undefined) return | ||
|
|
||
| const thresholdValue = thresholdFor(probabilityRate) | ||
| let randomValue = randomValueFor(context._traceId.toBigInt()) | ||
| const kept = priority >= AUTO_KEEP | ||
|
|
||
| if (kept && randomValue < thresholdValue) { | ||
| randomValue = thresholdValue | ||
| } else if (!kept && randomValue >= thresholdValue) { | ||
| randomValue = thresholdValue > 0n ? thresholdValue - 1n : 0n | ||
| } | ||
|
|
||
| return { | ||
| randomValue: randomValue.toString(16).padStart(14, '0'), | ||
| threshold: formatThreshold(thresholdValue), | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Adds a complete sub-field while the OTel member remains within its byte cap. | ||
| * | ||
| * @param {string[]} fields | ||
| * @param {string} field | ||
| * @param {number} byteLength | ||
| * @returns {number} | ||
| */ | ||
| function addField (fields, field, byteLength) { | ||
| const fieldLength = Buffer.byteLength(field) + (fields.length === 0 ? 0 : 1) | ||
| if (byteLength + fieldLength <= MAX_OTEL_VALUE_BYTES) { | ||
| fields.push(field) | ||
| return byteLength + fieldLength | ||
| } | ||
| return byteLength | ||
| } | ||
|
|
||
| /** | ||
| * Parses and rebuilds the OTel tracestate member, preserving unknown sub-fields. | ||
| * | ||
| * @param {import('./opentracing/span_context')} context | ||
| * @param {string | undefined} member | ||
| * @returns {string | undefined} | ||
| */ | ||
| function buildOtelMember (context, member) { | ||
| if (member === undefined) { | ||
| if (context._sampling.isProbabilityDecision === false) return | ||
| const generated = generateFields(context) | ||
| if (!generated) return | ||
| return `rv:${generated.randomValue};th:${generated.threshold}` | ||
| } | ||
|
|
||
| let randomValue | ||
| let threshold | ||
| const unknownFields = [] | ||
| let start = 0 | ||
|
|
||
| while (start <= member.length) { | ||
| let end = member.indexOf(';', start) | ||
| if (end === -1) end = member.length | ||
| const field = member.slice(start, end) | ||
| if (field) { | ||
| const separator = field.indexOf(':') | ||
| const key = separator === -1 ? field : field.slice(0, separator) | ||
| const value = separator === -1 ? undefined : field.slice(separator + 1) | ||
| if (key === 'rv') { | ||
| randomValue = value | ||
| } else if (key === 'th') { | ||
| threshold = value | ||
| } else { | ||
| unknownFields.push(field) | ||
| } | ||
| } | ||
| if (end === member.length) break | ||
| start = end + 1 | ||
| } | ||
|
|
||
| if (!validRandomValue.test(randomValue)) randomValue = undefined | ||
| if (!validThreshold.test(threshold)) threshold = undefined | ||
|
|
||
| if (context._sampling.isProbabilityDecision === false) { | ||
| threshold = undefined | ||
| } else if (randomValue === undefined && threshold === undefined) { | ||
| const generated = generateFields(context) | ||
| if (generated) { | ||
| randomValue = generated.randomValue | ||
| threshold = generated.threshold | ||
| } | ||
| } | ||
|
|
||
| const fields = [] | ||
| let byteLength = 0 | ||
| if (randomValue !== undefined) byteLength = addField(fields, `rv:${randomValue}`, byteLength) | ||
| if (threshold !== undefined) byteLength = addField(fields, `th:${threshold}`, byteLength) | ||
| for (const field of unknownFields) { | ||
| byteLength = addField(fields, field, byteLength) | ||
| } | ||
|
|
||
| return fields.length === 0 ? undefined : fields.join(';') | ||
| } | ||
|
|
||
| module.exports = { | ||
| buildOtelMember, | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.