UNPKG

autotel

Version:
358 lines (355 loc) 10.7 kB
'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