From 37ddbcd16a24e20902eaba2bd7aff488e3574373 Mon Sep 17 00:00:00 2001 From: Yeonggi Kim Date: Fri, 4 Sep 2026 10:04:42 +0900 Subject: [PATCH] MEDIUM: otel: fast path for non-recording (sampled-out) streams When the sampler decides not to record a trace, the root span is a non-recording span and everything attached to it is discarded by the SDK. The filter nevertheless kept doing all of its per-scope work for the rest of the stream: evaluating attribute/event/baggage/link/status samples, creating and finishing every child span, recording exceptions. Only the export was saved, so a sampled-out stream cost almost as much as a fully recorded one: with a 9-span scope set (HAProxy 3.4.2, h2load -c 64, nbthread 4, loopback origin) 'trace_id_ratio_based' ratio 0.0 -- nothing exported at all -- still cut the maximum throughput from 108k to 25k req/s. 'rate-limit' bounds the filter's cost on the first hop, but it does not cover the sampler path: a stream it leaves out emits no 'traceparent' at all, and every downstream HAProxy that extracts a parent context with sampled=0 goes through the non-recording path whatever its own rate-limit is. The C wrapper already exposes 'is_recording' for this purpose, so use it: - the runtime context remembers whether the root span is recording ('flag_norec') and keeps a pointer to the root span; - flt_otel_scope_run_span() sets the flag right after a root span has been created and turns out not to be recording; - flt_otel_scope_run() then skips the spans that do not inject a context and evaluates no attribute, event, baggage, link or status sample for the spans it still creates; exceptions are skipped as well; - flt_otel_scope_span_init() resolves a skipped parent to the root span, so that the injecting span is still created (non-recording, sampled flag cleared) and 'traceparent' keeps reaching the downstream services, which therefore follow the sampling decision. With the same benchmark ratio 0.0 goes from 25.1k to 52.3k req/s (2.08x) and ratio 0.1 from 22.4k to 38.2k (1.7x); ratio 1.0 is unchanged as the fast path is never taken. The remaining cost is the single non-recording root span itself (about 7 us per request, mostly in the C wrapper), which cannot be avoided as long as the sampling decision is made inside StartSpan. 'finish' of a skipped span only triggers the existing debug warning. 'rate-limit', 'otel-stop' and 'require-context' are unaffected. --- include/scope.h | 2 ++ src/event.c | 49 ++++++++++++++++++++++++++++++++++++++++++------- src/scope.c | 18 +++++++++++++++--- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/include/scope.h b/include/scope.h index 5307e903..4b701ac2 100644 --- a/include/scope.h +++ b/include/scope.h @@ -122,6 +122,8 @@ struct flt_otel_runtime_context { bool flag_harderr; /* [0 1] */ bool flag_disabled; /* [0 1] */ bool flag_ctx_valid; /* [0 1] */ + bool flag_norec; /* Root span is not recording (sampled out): fast path. */ + struct otelc_span *root_span; /* The stream's root span (parent fallback in fast path). */ uint8_t logging; /* [0 1 3] */ uint analyzers; /* Executed channel analyzers. */ uint idle_timeout; /* Idle timeout interval in milliseconds (0 = off). */ diff --git a/src/event.c b/src/event.c index 45b39559..b5b4486b 100644 --- a/src/event.c +++ b/src/event.c @@ -630,6 +630,26 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch span->span = OTELC_OPS(conf->instr->tracer, start_span_with_options, span->id, span->ref_span, span->ref_ctx, ts_steady, ts_system, conf_span->kind, NULL, 0); if (span->span == NULL) OTELC_RETURN_INT(FLT_OTEL_RET_ERROR); + + /* + * Non-recording fast path. The sampler decides at the root: a + * root that is not recording (sampled out) means every span of + * this stream will be a non-recording span whose attributes, + * events and status the SDK silently discards. Remember that + * on the runtime context so the remaining scopes can skip the + * sample evaluation and the creation of spans that do not take + * part in context propagation (see flt_otel_scope_run()). + */ + if (conf_span->flag_root) { + struct flt_otel_runtime_context *rt_ctx = FLT_OTEL_RT_CTX(f->ctx); + + rt_ctx->root_span = span->span; + if (OTELC_OPS(span->span, is_recording) == 0) { + OTELC_DBG(INFO, "root span '%s' is not recording: fast path enabled", span->id); + + rt_ctx->flag_norec = 1; + } + } } /* Add all resolved span links to the current span. */ @@ -669,7 +689,7 @@ static int flt_otel_scope_run_span(struct stream *s, struct filter *f, struct ch retval = FLT_OTEL_RET_ERROR; /* Record exceptions on the span via the wrapper's record_exception(). */ - if (!LIST_ISEMPTY(&(conf_span->exceptions))) { + if (!FLT_OTEL_RT_CTX(f->ctx)->flag_norec && !LIST_ISEMPTY(&(conf_span->exceptions))) { struct flt_otel_conf_exception *conf_exc; list_for_each_entry(conf_exc, &(conf_span->exceptions), list) { @@ -933,7 +953,7 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, struct flt_otel_conf_unset_var *unset_var; struct timespec ts_now_steady, ts_now_system; int retval = FLT_OTEL_RET_OK; - bool flag_stop = 0; + bool flag_stop = 0, norec = 0; OTELC_FUNC("%p, %p, %p, %p, %p, %p, %u, %p:%p", s, f, chn, conf_scope, ts_steady, ts_system, dir, OTELC_DPTR_ARGS(err)); @@ -1078,6 +1098,21 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, OTELC_DBG(INFO, "run span '%s' -> '%s'", conf_scope->id, conf_span->id); FLT_OTEL_DBG_CONF_SPAN("run span ", conf_span); + /* + * Non-recording fast path: the root span was sampled out, so + * nothing produced here will ever be exported. Spans that do + * not inject a context are not created at all, and for the ones + * that are (propagation must stay intact so that downstream + * follows the sampling decision) no attribute, event, baggage, + * link or status sample is evaluated. + */ + norec = FLT_OTEL_RT_CTX(f->ctx)->flag_norec; + if (norec && !conf_span->flag_root && (conf_span->ctx_id == NULL)) { + OTELC_DBG(DEBUG, "fast path: span '%s' skipped", conf_span->id); + + continue; + } + flt_otel_scope_data_init(&data); span = flt_otel_scope_span_init(f->ctx, conf_span->id, conf_span->id_len, conf_span->ref_id, conf_span->ref_id_len, dir, err); @@ -1092,7 +1127,7 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, * Each link name is looked up first in the active spans, then * in the extracted contexts. */ - if (!LIST_ISEMPTY(&(conf_span->links))) { + if (!norec && !LIST_ISEMPTY(&(conf_span->links))) { struct flt_otel_runtime_context *rt_ctx = FLT_OTEL_RT_CTX(f->ctx); struct flt_otel_conf_link *conf_link; @@ -1148,7 +1183,7 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, } } - list_for_each_entry(sample, &(conf_span->attributes), list) { + if (!norec) list_for_each_entry(sample, &(conf_span->attributes), list) { if (flt_otel_cond_pass(sample->cond, s, dir) == 0) continue; @@ -1158,7 +1193,7 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, retval = FLT_OTEL_RET_ERROR; } - list_for_each_entry(sample, &(conf_span->events), list) { + if (!norec) list_for_each_entry(sample, &(conf_span->events), list) { if (flt_otel_cond_pass(sample->cond, s, dir) == 0) continue; @@ -1168,7 +1203,7 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, retval = FLT_OTEL_RET_ERROR; } - list_for_each_entry(sample, &(conf_span->baggages), list) { + if (!norec) list_for_each_entry(sample, &(conf_span->baggages), list) { if (flt_otel_cond_pass(sample->cond, s, dir) == 0) continue; @@ -1183,7 +1218,7 @@ int flt_otel_scope_run(struct stream *s, struct filter *f, struct channel *chn, * defined, each gated by a condition; the first whose condition * holds is applied and the remaining lines are skipped. */ - list_for_each_entry(sample, &(conf_span->statuses), list) { + if (!norec) list_for_each_entry(sample, &(conf_span->statuses), list) { if (flt_otel_cond_pass(sample->cond, s, dir) == 0) continue; diff --git a/src/scope.c b/src/scope.c index 73c74b86..aac8bde3 100644 --- a/src/scope.c +++ b/src/scope.c @@ -204,9 +204,21 @@ struct flt_otel_scope_span *flt_otel_scope_span_init(struct flt_otel_runtime_con * regardless would silently turn it into a trace * root, which is meaningless. */ - FLT_OTEL_ERR("cannot find referenced span/context '%s'", ref_id); - - OTELC_RETURN_PTR(retptr); + if (rt_ctx->flag_norec && (rt_ctx->root_span != NULL)) { + /* + * Non-recording fast path: the parent was + * skipped on purpose; hang this span off + * the root so that the trace/flags it + * propagates stay those of the stream. + */ + OTELC_DBG(DEBUG, "fast path: parent '%s' skipped, using root span", ref_id); + + ref_span = rt_ctx->root_span; + } else { + FLT_OTEL_ERR("cannot find referenced span/context '%s'", ref_id); + + OTELC_RETURN_PTR(retptr); + } } } }