autotel
Version:
Write Once, Observe Anywhere
358 lines (355 loc) • 10.7 kB
JavaScript
'use strict';
var api = require('@opentelemetry/api');
// src/sampling.ts
var AUTOTEL_SAMPLING_TAIL_KEEP = "autotel.sampling.tail.keep";
var AUTOTEL_SAMPLING_TAIL_EVALUATED = "autotel.sampling.tail.evaluated";
var RandomSampler = class {
constructor(sampleRate) {
this.sampleRate = sampleRate;
if (sampleRate < 0 || sampleRate > 1) {
throw new Error("Sample rate must be between 0 and 1");
}
}
sampleRate;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
shouldSample(_context) {
return Math.random() < this.sampleRate;
}
};
var AlwaysSampler = class {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
shouldSample(_context) {
return true;
}
};
var NeverSampler = class {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
shouldSample(_context) {
return false;
}
};
var AdaptiveSampler = class {
baselineSampleRate;
slowThresholdMs;
alwaysSampleErrors;
alwaysSampleSlow;
linksBased;
linksRate;
logger;
// Track whether we should sample this request
samplingDecisions = /* @__PURE__ */ new WeakMap();
// Track operation results to enable post-execution decision
operationResults = /* @__PURE__ */ new WeakMap();
constructor(options = {}) {
this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
this.slowThresholdMs = options.slowThresholdMs ?? 1e3;
this.alwaysSampleErrors = options.alwaysSampleErrors ?? true;
this.alwaysSampleSlow = options.alwaysSampleSlow ?? true;
this.linksBased = options.linksBased ?? false;
this.linksRate = options.linksRate ?? 1;
this.logger = options.logger;
if (this.baselineSampleRate < 0 || this.baselineSampleRate > 1) {
throw new Error("Baseline sample rate must be between 0 and 1");
}
if (this.linksRate < 0 || this.linksRate > 1) {
throw new Error("Links rate must be between 0 and 1");
}
}
needsTailSampling() {
return true;
}
shouldSample(context) {
const baselineDecision = Math.random() < this.baselineSampleRate;
this.samplingDecisions.set(context.args, baselineDecision);
return true;
}
/**
* Check if any links point to sampled spans.
*
* A span is considered linked to a sampled span if any of its links
* have trace_flags with the sampled bit set (0x01).
*
* @param links - Array of span links to check
* @returns true if any linked span is sampled, false otherwise
*/
hasSampledLink(links) {
if (!links || links.length === 0) {
return false;
}
return links.some(
(link) => link.context && (link.context.traceFlags & api.TraceFlags.SAMPLED) !== 0
);
}
/**
* Re-evaluate sampling decision after operation completes
*
* This allows us to always capture errors and slow requests,
* even if they weren't initially sampled.
*
* @param context - Sampling context
* @param result - Operation result
* @returns true if this operation should be kept (not discarded)
*/
shouldKeepTrace(context, result) {
const baselineDecision = this.samplingDecisions.get(context.args) ?? false;
if (this.alwaysSampleErrors && !result.success) {
if (!baselineDecision) {
this.logger?.debug(
{
operation: context.operationName,
error: result.error?.message
},
"Adaptive sampling: Keeping error trace"
);
}
return true;
}
if (this.alwaysSampleSlow && result.duration >= this.slowThresholdMs) {
if (!baselineDecision) {
this.logger?.debug(
{
operation: context.operationName,
duration: result.duration
},
"Adaptive sampling: Keeping slow trace"
);
}
return true;
}
if (this.linksBased && context.links && this.hasSampledLink(context.links)) {
const keepLinked = Math.random() < this.linksRate;
if (keepLinked && !baselineDecision) {
this.logger?.debug(
{
operation: context.operationName,
linkCount: context.links.length
},
"Adaptive sampling: Keeping trace due to sampled link"
);
}
return keepLinked;
}
return baselineDecision;
}
};
var UserIdSampler = class {
baselineSampleRate;
alwaysSampleUsers;
extractUserId;
logger;
constructor(options) {
this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
this.alwaysSampleUsers = new Set(options.alwaysSampleUsers || []);
this.extractUserId = options.extractUserId;
this.logger = options.logger;
}
shouldSample(context) {
const userId = this.extractUserId(context.args);
if (userId && this.alwaysSampleUsers.has(userId)) {
this.logger?.debug(
{
operation: context.operationName,
userId
},
"Sampling user request"
);
return true;
}
if (userId) {
const hash = this.hashString(userId);
return hash < this.baselineSampleRate;
}
return Math.random() < this.baselineSampleRate;
}
/**
* Add user IDs to always-sample list
*/
addAlwaysSampleUsers(...userIds) {
for (const userId of userIds) {
this.alwaysSampleUsers.add(userId);
}
}
/**
* Remove user IDs from always-sample list
*/
removeAlwaysSampleUsers(...userIds) {
for (const userId of userIds) {
this.alwaysSampleUsers.delete(userId);
}
}
/**
* Simple hash function for consistent user sampling
*/
hashString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.codePointAt(i) ?? 0;
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash) / 2147483647;
}
};
var CompositeSampler = class {
constructor(samplers) {
this.samplers = samplers;
if (samplers.length === 0) {
throw new Error("CompositeSampler requires at least one child sampler");
}
}
samplers;
shouldSample(context) {
return this.samplers.some((sampler) => sampler.shouldSample(context));
}
};
var FeatureFlagSampler = class {
baselineSampleRate;
alwaysSampleFlags;
extractFlags;
logger;
constructor(options) {
this.baselineSampleRate = options.baselineSampleRate ?? 0.1;
this.alwaysSampleFlags = new Set(options.alwaysSampleFlags || []);
this.extractFlags = options.extractFlags;
this.logger = options.logger;
}
shouldSample(context) {
const flags = this.extractFlags(context.args, context.metadata);
if (flags && flags.some((flag) => this.alwaysSampleFlags.has(flag))) {
this.logger?.debug(
{
operation: context.operationName,
flags
},
"Sampling feature flag request"
);
return true;
}
return Math.random() < this.baselineSampleRate;
}
/**
* Add feature flags to always-sample list
*/
addAlwaysSampleFlags(...flags) {
for (const flag of flags) {
this.alwaysSampleFlags.add(flag);
}
}
/**
* Remove feature flags from always-sample list
*/
removeAlwaysSampleFlags(...flags) {
for (const flag of flags) {
this.alwaysSampleFlags.delete(flag);
}
}
};
var samplingPresets = {
/** Capture everything — best for local development and debugging */
development: () => new AlwaysSampler(),
/** Only bad outcomes — zero baseline, errors always kept */
errorsOnly: () => new AdaptiveSampler({
baselineSampleRate: 0,
alwaysSampleErrors: true
}),
/**
* Balanced production defaults — 10% baseline + errors + slow traces.
* Pass overrides to tune (uses the same option names as AdaptiveSampler).
*/
production: (overrides) => new AdaptiveSampler({
baselineSampleRate: 0.1,
alwaysSampleErrors: true,
alwaysSampleSlow: true,
slowThresholdMs: 1e3,
...overrides
}),
/** Disable sampling entirely */
off: () => new NeverSampler()
};
function resolveSamplingPreset(preset) {
switch (preset) {
case "development":
return samplingPresets.development();
case "errors-only":
return samplingPresets.errorsOnly();
case "production":
return samplingPresets.production();
case "off":
return samplingPresets.off();
default:
throw new Error(
`Unknown sampling preset: "${preset}". Valid presets: development, errors-only, production, off`
);
}
}
function createLinkFromHeaders(headers, attributes) {
const traceparent = headers.traceparent || headers["traceparent"];
if (!traceparent) {
return null;
}
const spanContext = parseTraceparent(traceparent);
if (!spanContext || !isValidSpanContext(spanContext)) {
return null;
}
return {
context: spanContext,
attributes: attributes ?? {}
};
}
function extractLinksFromBatch(messages, headersKey = "headers") {
const links = [];
for (const msg of messages) {
const msgHeaders = msg[headersKey];
if (msgHeaders && typeof msgHeaders === "object" && msgHeaders !== null) {
const link = createLinkFromHeaders(msgHeaders, {
"messaging.batch.message_index": links.length
});
if (link) {
links.push(link);
}
}
}
return links;
}
function parseTraceparent(traceparent) {
const TRACEPARENT_REGEX = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
const match = traceparent.match(TRACEPARENT_REGEX);
if (!match || match.length < 5) {
return null;
}
const version = match[1];
const traceId = match[2];
const spanId = match[3];
const flags = match[4];
if (!version || !traceId || !spanId || !flags) {
return null;
}
if (version === "ff") {
return null;
}
return {
traceId,
spanId,
traceFlags: Number.parseInt(flags, 16),
isRemote: true
};
}
function isValidSpanContext(spanContext) {
if (!spanContext) return false;
return spanContext.traceId !== "00000000000000000000000000000000" && spanContext.spanId !== "0000000000000000";
}
exports.AUTOTEL_SAMPLING_TAIL_EVALUATED = AUTOTEL_SAMPLING_TAIL_EVALUATED;
exports.AUTOTEL_SAMPLING_TAIL_KEEP = AUTOTEL_SAMPLING_TAIL_KEEP;
exports.AdaptiveSampler = AdaptiveSampler;
exports.AlwaysSampler = AlwaysSampler;
exports.CompositeSampler = CompositeSampler;
exports.FeatureFlagSampler = FeatureFlagSampler;
exports.NeverSampler = NeverSampler;
exports.RandomSampler = RandomSampler;
exports.UserIdSampler = UserIdSampler;
exports.createLinkFromHeaders = createLinkFromHeaders;
exports.extractLinksFromBatch = extractLinksFromBatch;
exports.resolveSamplingPreset = resolveSamplingPreset;
exports.samplingPresets = samplingPresets;
//# sourceMappingURL=chunk-VH77IPJN.cjs.map
//# sourceMappingURL=chunk-VH77IPJN.cjs.map