From 2bcc74b45e2edb5fad920ae8b3e309d3998dcd1d Mon Sep 17 00:00:00 2001 From: igoramf Date: Fri, 26 Jun 2026 14:53:05 -0300 Subject: [PATCH] feat(sdk): add DECO_OTEL_ACCESS_LOG_RATE sampling for INFO access logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 5xx requests always emit an ERROR-level access log (never sampled). INFO access logs (2xx/3xx/4xx) are now gated by DECO_OTEL_ACCESS_LOG_RATE (float 0–1, default 1.0 = 100% = current behavior). When the rate is < 1, sampling uses FNV-1a 32-bit hash of requestId/ traceId for consistent per-request decisions — same request always gets the same answer, regardless of isolate. Falls back to Math.random() only when neither ID is available (should not happen in practice). Set DECO_OTEL_ACCESS_LOG_RATE=0.1 to reduce INFO access log volume by 90%, eliminating the main source of high-volume INFO traffic to otel-ingest without affecting error visibility. Co-Authored-By: Claude Sonnet 4.6 --- src/sdk/workerEntry.ts | 56 +++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/src/sdk/workerEntry.ts b/src/sdk/workerEntry.ts index 7a667f5b..026a66c7 100644 --- a/src/sdk/workerEntry.ts +++ b/src/sdk/workerEntry.ts @@ -1170,16 +1170,50 @@ export function createDecoWorkerEntry( ? await appMw(request, () => handleRequest(request, env, ctx)) : await handleRequest(request, env, ctx); - // logRequest must run inside the span so getSpan() returns the - // active root span and the trace-sampling gate in otelHttpLog.ts - // (traceFlags & 0x01) correctly reflects whether this trace was - // sampled. Outside the span getSpan() is null → gate always drops - // INFO logs even at 100% sampling rate. + // Access log — always emit for 5xx (ERROR level); sample INFO + // access logs via DECO_OTEL_ACCESS_LOG_RATE (float 0–1, default + // 1.0 = 100%). Set to 0 to silence INFO access logs entirely. try { - logRequest(request, innerResponse.status, performance.now() - startedAt, { - ...(identity.requestId ? { "request.id": identity.requestId } : {}), - ...(identity.traceId ? { "trace.id": identity.traceId } : {}), - }); + const status = innerResponse.status; + let shouldLog = status >= 500; // errors always logged + if (!shouldLog) { + const rateRaw = (env as Record) + .DECO_OTEL_ACCESS_LOG_RATE; + const rate = + typeof rateRaw === "string" ? parseFloat(rateRaw) : 1.0; + if (rate >= 1.0) { + shouldLog = true; + } else if (rate > 0) { + // FNV-1a 32-bit on requestId/traceId for consistent + // per-request sampling (same request always same decision). + const key = identity.requestId || identity.traceId; + if (key) { + let h = 2166136261; + for (let i = 0; i < key.length; i++) { + h ^= key.charCodeAt(i); + h = Math.imul(h, 16777619); + } + shouldLog = (h >>> 0) / 4294967295 < rate; + } else { + shouldLog = Math.random() < rate; + } + } + } + if (shouldLog) { + logRequest( + request, + innerResponse.status, + performance.now() - startedAt, + { + ...(identity.requestId + ? { "request.id": identity.requestId } + : {}), + ...(identity.traceId + ? { "trace.id": identity.traceId } + : {}), + }, + ); + } } catch { /* swallow — observability must never fail the request */ } @@ -1255,8 +1289,8 @@ export function createDecoWorkerEntry( // NOTE: `request.id` and `trace.id` are intentionally NOT stamped // on the metric. They are per-request identifiers and would // collapse aggregation (every request → its own histogram data - // point). They are stamped on the span (see line 1129) and on - // the access log (see logRequest call below); use those for + // point). They are stamped on the span and the access log + // (logRequest inside withTracing above); use those for // request-level correlation. recordRequestMetric(method, reqUrl.pathname, finalResponse.status, durationMs, { ...(cacheDecision ? { cache_decision: cacheDecision } : {}),