Skip to content

Commit c025d2e

Browse files
erik-the-implementeralcoclaude
authored
feat(telemetry): export per-status serve_shape request counter (#4500)
## What Adds a new unsampled, per-request metric from the shape-serving path: ``` electric.plug.serve_shape.requests.count tags: [status, known_error, live] ``` It hangs off the existing `[:electric, :plug, :serve_shape]` telemetry event (no new emission point) and threads a `known_error` flag into that event's metadata, read straight off the `electric-internal-known-error` response header. ## Why We sample shape-request **spans** aggressively (head sampling + tail overrides) to stay within our tracing event budget. That makes the span dataset great for drill-down but unreliable as a health signal: - It under-represents true volume (sampled), and - It **drops some request classes entirely** — admission-control rejections are marked `known_error` and are intentionally excluded from span export, so a request plot built from spans can look perfectly healthy while the system is actually shedding load under overload. Admission rejection counts already exist as a metric (`electric.admission_control.reject.count`), but there was no general, status-dimensioned request counter. The existing `serve_shape` metrics also (a) drop live/long-poll requests and (b) aren't tagged by response status, so they can't express request mix or error rate. This counter fills that gap: - **Unsampled** — every request is counted, so there's no detection floor. Errors and rejections are visible the moment they rise, not once they cross a sampling threshold. - **Counts live requests too** — unlike the other `serve_shape.*` metrics, which `keep: live != true`. Cheap to do as an aggregated metric (the reason we couldn't do it for spans doesn't apply). - **Admission rejections appear inline** as `status=503, known_error=true`. `check_admission` halts the conn, but a halt isn't a raise, so the halted conn still flows through `emit_shape_telemetry/1` and gets counted. - **`known_error` matches the wire signal** — it's derived from the `electric-internal-known-error` response header, the same byte downstream consumers (e.g. the edge worker's tracing) key on, so the classification is consistent end to end. Intended use: this becomes the authoritative "requests by status / error rate" dashboard panel and alert source, leaving the sampled span dataset for exemplar drill-down. ## Changes - `electric-telemetry`: define `counter("electric.plug.serve_shape.requests.count", tags: [:status, :known_error, :live])` against the existing event (explicit `event_name`/`measurement` to avoid colliding with the existing `serve_shape.count`). - `sync-service`: add `known_error` to the `serve_shape` event metadata, derived from the `electric-internal-known-error` response header. The header check lives next to the code that sets it so the expected values stay single-sourced. ## Cardinality Bounded: `status` (~6 codes) × `known_error` (2) × `live` (2), per stack. ## Notes - One pre-existing gap is unchanged: the mid-stream re-raise path (`Plug.Conn.chunk/2` raising after the response is committed) doesn't emit the `serve_shape` event, so those requests aren't counted here — same limitation that already affects every `serve_shape.*` metric. - No rollout/rollback concerns: additive metric only, no behavior change on the request path. --------- Co-authored-by: Oleksii Sholik <oleksii@sholik.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c015163 commit c025d2e

5 files changed

Lines changed: 117 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+
'@core/electric-telemetry': patch
3+
'@core/sync-service': patch
4+
---
5+
6+
Export a per-request `electric.plug.serve_shape.requests.count` metric tagged by `status`, `known_error` and `live`.

packages/electric-telemetry/lib/electric/telemetry/stack_telemetry.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ defmodule ElectricTelemetry.StackTelemetry do
9393
unit: :byte,
9494
keep: fn metadata -> metadata[:live] != true end
9595
),
96+
counter("electric.plug.serve_shape.requests.count",
97+
event_name: [:electric, :plug, :serve_shape],
98+
measurement: :count,
99+
tags: [:status, :known_error, :live]
100+
),
96101
distribution("electric.shape.response_size.bytes",
97102
unit: :byte,
98103
tags: [:root_table, :is_live, :stack_id]

packages/sync-service/lib/electric/plug/serve_shape_plug.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,8 @@ defmodule Electric.Plug.ServeShapePlug do
451451
shape_handle: get_handle(assigns) || conn.query_params["handle"],
452452
client_ip: conn.remote_ip,
453453
status: conn.status,
454-
stack_id: stack_id
454+
stack_id: stack_id,
455+
known_error: Api.Response.conn_has_known_error?(conn)
455456
}
456457
)
457458

packages/sync-service/lib/electric/shapes/api/response.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,13 @@ defmodule Electric.Shapes.Api.Response do
390390
Plug.Conn.put_resp_header(conn, @electric_known_error_header, "#{known_error}")
391391
end
392392

393+
# keeping this function close to `put_known_error_header/2` above so that we know exactly
394+
# which value to expect for a set known_error header: i.e. "true" or "false" (and absent when
395+
# known_error is nil), as opposed to e.g. "1" etc.
396+
def conn_has_known_error?(conn) do
397+
Plug.Conn.get_resp_header(conn, @electric_known_error_header) == ["true"]
398+
end
399+
393400
defp put_retry_after_header(conn, %__MODULE__{retry_after: nil}) do
394401
conn
395402
end

packages/sync-service/test/electric/plug/serve_shape_plug_test.exs

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ defmodule Electric.Plug.ServeShapePlugTest do
8686
sse_timeout: sse_timeout(ctx),
8787
max_age: max_age(ctx),
8888
stale_age: stale_age(ctx),
89-
max_concurrent_requests: %{initial: 300, existing: 10_000}
89+
max_concurrent_requests:
90+
Access.get(ctx, :max_concurrent_requests, %{initial: 300, existing: 10_000})
9091
)
9192
end
9293

@@ -1044,6 +1045,101 @@ defmodule Electric.Plug.ServeShapePlugTest do
10441045
:telemetry.detach(handler_id)
10451046
end
10461047
end
1048+
1049+
test "[:electric, :plug, :serve_shape] tags a successful response with known_error: false",
1050+
ctx do
1051+
expect_shape_cache(
1052+
get_or_create_shape_handle: fn @test_shape, _stack_id, _opts ->
1053+
{@test_shape_handle, @test_offset}
1054+
end,
1055+
await_snapshot_start: fn @test_shape_handle, _ -> :started end
1056+
)
1057+
1058+
patch_shape_cache(has_shape?: fn @test_shape_handle, _opts -> true end)
1059+
1060+
next_offset = LogOffset.increment(@first_offset)
1061+
1062+
expect_storage(
1063+
get_chunk_end_log_offset: fn @before_all_offset, _ ->
1064+
@first_offset
1065+
end,
1066+
get_log_stream: fn @before_all_offset, @first_offset, @test_opts ->
1067+
[Jason.encode!(%{key: "log", value: "foo", headers: %{}, offset: next_offset})]
1068+
end
1069+
)
1070+
1071+
test_pid = self()
1072+
ref = make_ref()
1073+
handler_id = "test-serve-shape-known-error-false-#{inspect(ref)}"
1074+
1075+
:telemetry.attach(
1076+
handler_id,
1077+
[:electric, :plug, :serve_shape],
1078+
fn event, measurements, metadata, _config ->
1079+
send(test_pid, {:telemetry_serve_shape, event, measurements, metadata})
1080+
end,
1081+
nil
1082+
)
1083+
1084+
stack_id = ctx.stack_id
1085+
1086+
try do
1087+
conn =
1088+
ctx
1089+
|> conn(:get, %{"table" => "public.users"}, "?offset=-1")
1090+
|> call_serve_shape_plug(ctx)
1091+
1092+
assert conn.status == 200
1093+
1094+
assert_receive {:telemetry_serve_shape, [:electric, :plug, :serve_shape], _measurements,
1095+
%{stack_id: ^stack_id} = metadata}
1096+
1097+
assert metadata.status == 200
1098+
assert metadata.known_error == false
1099+
after
1100+
:telemetry.detach(handler_id)
1101+
end
1102+
end
1103+
1104+
test "[:electric, :plug, :serve_shape] tags an admission-rejected 503 with known_error: true",
1105+
ctx do
1106+
# Force the load-shedding path: with a zero concurrency limit, check_admission
1107+
# rejects immediately with a 503 carrying the `electric-internal-known-error`
1108+
# header.
1109+
ctx = Map.put(ctx, :max_concurrent_requests, %{initial: 0, existing: 0})
1110+
1111+
test_pid = self()
1112+
ref = make_ref()
1113+
handler_id = "test-serve-shape-known-error-true-#{inspect(ref)}"
1114+
1115+
:telemetry.attach(
1116+
handler_id,
1117+
[:electric, :plug, :serve_shape],
1118+
fn event, measurements, metadata, _config ->
1119+
send(test_pid, {:telemetry_serve_shape, event, measurements, metadata})
1120+
end,
1121+
nil
1122+
)
1123+
1124+
stack_id = ctx.stack_id
1125+
1126+
try do
1127+
conn =
1128+
ctx
1129+
|> conn(:get, %{"table" => "public.users"}, "?offset=-1")
1130+
|> call_serve_shape_plug(ctx)
1131+
1132+
assert conn.status == 503
1133+
1134+
assert_receive {:telemetry_serve_shape, [:electric, :plug, :serve_shape], _measurements,
1135+
%{stack_id: ^stack_id} = metadata}
1136+
1137+
assert metadata.status == 503
1138+
assert metadata.known_error == true
1139+
after
1140+
:telemetry.detach(handler_id)
1141+
end
1142+
end
10471143
end
10481144

10491145
describe "serving shapes with sse mode" do

0 commit comments

Comments
 (0)