Skip to content

Commit 63176a6

Browse files
authored
fix(webapp): stop api inheriting inbound sampled traceparents so trace sampling applies (#4532)
## What The internal tracing `ParentBasedSampler` in `tracer.server.ts` left `remoteParentSampled` at its default of `AlwaysOn`. Any request arriving with a `traceparent` whose sampled flag was set got recorded in full, bypassing `INTERNAL_OTEL_TRACE_SAMPLING_RATE` entirely. Because the SDK propagates its (always-sampled) trace context on calls back to the platform from inside running tasks, the large majority of API server spans inherited a sampled parent and ignored the divisor. The sampling knob was effectively inert on the busiest service. This registers a custom propagator (`NonInheritingTraceContextPropagator`) that stops adopting the inbound trace as the parent: - `inject` still delegates to the standard W3C trace + baggage propagators, so outbound propagation is unchanged. - `extract` drops the parent span (`trace.deleteSpan`) while preserving baggage, so every incoming request roots its own trace and the ratio sampler applies uniformly. `remoteParentSampled` is also set to the ratio sampler as a belt-and-suspenders fallback, in case an inbound sampled parent ever reaches the sampler another way. Two effects: the divisor becomes effective on the API server, and the API no longer stitches onto (and inflates) the propagated task-run traces, which is where the very large, un-thinnable trace chains came from. Rooting each request removes those chains rather than only diluting them. Only the internal APM trace pipeline (`INTERNAL_OTEL_TRACE_EXPORTER_URL`) is affected. The user-facing run-trace pipeline (`otel.v1.traces` -> ClickHouse) is a separate path and is untouched. The only consumer of the global propagator's `extract` is the OTel HTTP/Express auto-instrumentation, so the blast radius is inbound-request trace shape. ## Evidence (local full-stack red/green, divisor 10) A local OTLP/JSON sink counting spans; a driver fires N requests at a real endpoint, each carrying a distinct sampled `traceparent`, then counts how many spans/traces carry that run's marker. | run | code | sent | kept traces | kept fraction | | --- | --- | --- | --- | --- | | before | unmodified | 500 | 500 | 1.00 | | after | this PR | 500 | 67 | 0.134 | | after | this PR | 2000 | 213 | 0.1065 | Before: 100% of inherited-sampled requests kept, divisor ignored. After: ~10% kept (the divisor), converging on it at larger N. In every after-run each kept request is a single self-rooted trace (kept spans == kept distinct traces), confirming the inherited chains are gone, not just thinned. `typecheck` passes. ## Rollout / rollback No flag. Behavior stays governed by the existing `INTERNAL_OTEL_TRACE_SAMPLING_RATE`. Rollback is a straight revert with no data migration. ## Notes Internal dashboards that count raw span or request volume from this pipeline will read lower once this ships. That is expected: those counts were inflated by the bypass, not a real drop in traffic. Latency/percentile monitors retain plenty of samples at the current divisor. refs TRI-13031
1 parent 98cdf89 commit 63176a6

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Reduced internal overhead on the API under high load.

apps/webapp/app/v3/tracer.server.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ import {
1414
trace,
1515
metrics,
1616
type Meter,
17+
type TextMapPropagator,
18+
type TextMapGetter,
19+
type TextMapSetter,
1720
} from "@opentelemetry/api";
21+
import {
22+
CompositePropagator,
23+
W3CBaggagePropagator,
24+
W3CTraceContextPropagator,
25+
} from "@opentelemetry/core";
1826
import sentryRemix from "@sentry/remix";
1927
import { logs, SeverityNumber } from "@opentelemetry/api-logs";
2028
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
@@ -125,6 +133,24 @@ class CustomWebappSampler implements Sampler {
125133
}
126134
}
127135

136+
class NonInheritingTraceContextPropagator implements TextMapPropagator {
137+
private readonly _delegate = new CompositePropagator({
138+
propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()],
139+
});
140+
141+
inject(context: Context, carrier: unknown, setter: TextMapSetter): void {
142+
this._delegate.inject(context, carrier, setter);
143+
}
144+
145+
extract(context: Context, carrier: unknown, getter: TextMapGetter): Context {
146+
return trace.deleteSpan(this._delegate.extract(context, carrier, getter));
147+
}
148+
149+
fields(): string[] {
150+
return this._delegate.fields();
151+
}
152+
}
153+
128154
export const {
129155
tracer,
130156
logger: otelLogger,
@@ -281,11 +307,14 @@ function setupTelemetry() {
281307
}
282308
}
283309

310+
const ratioSampler = new TraceIdRatioBasedSampler(samplingRate);
311+
284312
const provider = new NodeTracerProvider({
285313
forceFlushTimeoutMillis: 15_000,
286314
resource: getResource(),
287315
sampler: new ParentBasedSampler({
288-
root: new CustomWebappSampler(new TraceIdRatioBasedSampler(samplingRate)),
316+
root: new CustomWebappSampler(ratioSampler),
317+
remoteParentSampled: ratioSampler,
289318
}),
290319
spanLimits: {
291320
attributeCountLimit: 1024,
@@ -324,7 +353,10 @@ function setupTelemetry() {
324353
);
325354
}
326355

327-
provider.register({ contextManager: createContextManager() });
356+
provider.register({
357+
contextManager: createContextManager(),
358+
propagator: new NonInheritingTraceContextPropagator(),
359+
});
328360

329361
let instrumentations: Instrumentation[] = [
330362
new AwsSdkInstrumentation({

0 commit comments

Comments
 (0)