Flatten 1:1 shape GET child spans into Plug_shape_get root-span attributes#4561
Conversation
…butes
shape_get.api.load_shape_info and shape_get.plug.serve_shape_log are
emitted once per shape GET request, strictly 1:1 with the Plug_shape_get
root span (~147M Honeycomb events/week, ~33% of the event budget) while
carrying no information that can't live on the root span.
Replace them with OpenTelemetry.with_flattened_span/2, which records
<name>.duration_ms and <name>.memory.{start,end}.* attributes on the
current span instead of emitting a child span. Attributes are recorded
even when the wrapped function raises; exceptions still propagate to the
plug's error handler, which records them on the root span as before.
shape_get.plug.stream_chunk and shape_get.plug.serve_subset_response are
left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4561 +/- ##
=======================================
Coverage 58.06% 58.07%
=======================================
Files 369 369
Lines 40459 40459
Branches 11468 11469 +1
=======================================
+ Hits 23494 23498 +4
+ Misses 16890 16887 -3
+ Partials 75 74 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Claude Code ReviewSummaryThis PR replaces two OTel child spans ( What's Working Well
Issues FoundCritical (Must Fix)None. Important (Should Fix)None. Suggestions (Nice to Have)
Issue ConformanceNo linked issue (per the prompt context). The PR description and changeset together are unusually thorough — they document the preserved-data mapping, error semantics, the exclude_spans implication, and embedded-mode behavior. Per project convention a PR should still reference an issue; consider linking the Honeycomb-budget tracking issue if one exists. Previous Review StatusThis is an incremental review (iteration 2). Since iteration 1, commit
The remaining suggestion (3, float vs integer units) is a documented stylistic choice and needs no change. No new issues introduced; the implementation is unchanged from iteration 1 and remains correct. Review iteration: 2 | 2026-06-11 |
Motivation
We are over our Honeycomb event budget, and two spans are an outsized contributor:
shape_get.api.load_shape_infoandshape_get.plug.serve_shape_logtogether produce ~147M events/week — roughly 33% of the org's event budget.Both spans are strictly 1:1 with the
Plug_shape_getroot span (oneload_shape_infoper request; oneserve_shape_logper request on the log-serving path), and neither carries information that can't live on the root span.load_shape_infois a leaf span;serve_shape_log's only children are theshape_get.plug.stream_chunkspans, which are untouched by this PR and now parent directly to the root.What changed
Electric.Telemetry.OpenTelemetry.with_flattened_span/2: runs a function and records its duration and process-memory footprint as attributes on the current span instead of emitting a child span.Electric.Shapes.Api.do_load_shape_info/1andserve_shape_log/1now use it instead ofwith_span/4. No other spans were changed (shape_get.plug.serve_subset_responseandshape_get.plug.stream_chunkare left as-is).@core/sync-service.What's preserved, and where
All numeric data the child spans used to carry now lands on the
Plug_shape_getroot span:shape_get.api.load_shape_infodurationload_shape_info.duration_ms(float ms, sub-ms precision)memory.start.{process,binary}_bytes/memory.end.{process,binary}_bytesload_shape_info.memory.start.*/load_shape_info.memory.end.*shape_get.plug.serve_shape_logdurationserve_shape_log.duration_msmemory.start.*/memory.end.*serve_shape_log.memory.start.*/serve_shape_log.memory.end.*The memory attributes are prefixed with the flattened span's short name so they don't collide with the root span's own
memory.start.*/memory.end.*attributes.Error semantics are unchanged: attributes (including duration) are recorded in an
afterblock even when the wrapped code raises, and the exception propagates up toServeShapePlug's error handler, which records the exception event and sets error status on the root span exactly as before (the child spans never carried error status themselves —:otel_tracer.with_span/4only ends the span on raise).Trade-offs
[:electric, :shape_get, :api, :load_shape_info, :start|:stop|:exception]and[:electric, :shape_get, :plug, :serve_shape_log, ...]:telemetry.spanevents are no longer emitted. A repo-wide search found no handlers attached to them.Api.serve_shape_log/1previously created a root span of its own; with no enclosing span the helper is now a no-op (it just runs the function), so embedded calls produce no span — consistent with the unsampled-request path.Verification
mix test test/electric/telemetry/open_telemetry_test.exs test/electric/shapes/api_test.exs test/electric/plug/serve_shape_plug_test.exs test/electric/plug/serve_shape_plug_logging_test.exs— 121 tests, 0 failures.with_flattened_span/2cover attribute recording, the raise path, and the no-span-context no-op.🤖 Generated with Claude Code