test: producer-to-collector routing on pg_stat_ch.block_format - #117
Conversation
Closes the producer-side contract gap: the unified Arrow exporter emits pg_stat_ch.block_format=arrow_events_raw on its OTLP envelope, while the legacy ArrowBatchBuilder path emits arrow_ipc. The central OTel collector's routingconnector is supposed to fan batches between the legacy query_logs_arrow target and the new events_raw target based on that marker. This test pins the producer half of the contract. New TAP test t/037_arrow_routing.pl exercises both arms: - arm 1 (unified=off, arrow_passthrough=on): legacy.jsonl grows, events_raw.jsonl and default.jsonl don't. - arm 2 (unified=on): events_raw.jsonl grows with arrow_events_raw in the routed payload, legacy.jsonl and default.jsonl don't. Stock otelcol-contrib image (matches existing t/024 setup, version 0.120.0). Routingconnector keyed on the resource attribute (where pg_stat_ch.block_format lives via PopulateResource), two file exporters writing JSONL to a bind-mounted dir the test reads from. No CH receiver — the events_raw column-set contract isn't validated here; that's covered by t/036 (producer Arrow IPC -> direct CH ingest) and by a separate receiver-faithful test in clickgres-platform. New helpers in t/psch.pm (psch_routing_collector_available, psch_start_routing_collector, psch_stop_routing_collector) live on a separate compose profile + container/ports so the new test can coexist with t/024's collector if both fire in the same suite. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ae80bf3 to
f87af5e
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new TAP test that validates the producer-side pg_stat_ch.block_format marker and an otelcol-contrib routingconnector “shape” by routing OTLP logs into different file exporters, ensuring legacy (arrow_ipc) vs unified (arrow_events_raw) batches fan out as expected during the Arrow exporter migration.
Changes:
- Introduces a new TAP test (
t/037_arrow_routing.pl) that runs legacy vs unified exporter arms and asserts which routed JSONL file grew. - Adds Docker Compose + collector config for a standalone routing collector using
routingconnectorandfileexporters. - Extends TAP helper module (
t/psch.pm) with start/stop/availability helpers for the routing collector.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| t/psch.pm | Adds helper functions to manage a dedicated routing-collector docker compose deployment for the new TAP test. |
| t/037_arrow_routing.pl | New TAP test that drives two producer configurations and asserts routing outcomes via file growth + marker grep. |
| docker/otel-routing/output/.gitignore | Keeps routed JSONL outputs out of git while preserving the directory. |
| docker/otel-routing/collector-config.yaml | Minimal collector pipeline using routingconnector and per-route file exporters for assertions. |
| docker/docker-compose.arrow-route.yml | Standalone compose stack for the routing collector with distinct ports/container name from existing otelcol test stack. |
Comments suppressed due to low confidence (1)
t/037_arrow_routing.pl:178
- With cleanup handled via an END block (see earlier comment), the unconditional stop call here should be removed so the test doesn’t tear down a collector it didn’t start.
psch_stop_routing_collector();
done_testing();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
t/013_clickhouse_tls.pl's reconnect subtest polled for the resumed export within 30s of restarting the ClickHouse container. The exporter's reconnect backoff (stats_exporter.cc PschGetRetryDelayMs) is exponential — base 1s, doubling per consecutive failure, capped at 60s. Failed flush attempts start accumulating as soon as CH goes down, well before the container finishes restarting, and the cumulative wait before the Nth retry is the sum of all prior delays: 1, 3, 7, 15, 31, 63, 123s. A restart slow enough to rack up 5-6 failures — plausible under a loaded CI runner — pushes the bgworker's next attempt past the 30s mark even though ClickHouse itself came back much sooner. Observed exactly this on CI twice in a row while rebasing PR #117 onto post-#116 main: identical assertion, identical 'got 0' failure, while the same commit had passed cleanly on a less-loaded run days earlier. Not a regression — the backoff constants and this test's timeout have coexisted; it just needed a slow enough runner to surface. Bump to 90s, comfortably past the 6-failure (63s) cumulative-backoff case plus round-trip margin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
t/037_arrow_routing.pl:26
- The header comment describes three test arms (unified off, then off+passthrough, then unified on), but the script actually runs only two arms and both set otel_arrow_passthrough=on. This mismatch can confuse future maintainers when updating the test.
# 2. Start a node configured to ship to it; first with the unified GUC OFF,
# then OFF + with otel_arrow_passthrough so the legacy ArrowBatchBuilder
# path fires; finally with the unified GUC ON.
t/037_arrow_routing.pl:46
- The test always tears down the routing collector at the end, even if it was already running before the test started. This can disrupt local dev workflows or parallel runs. It’s also safer to register teardown in an END block so cleanup still happens if the test exits early.
eval { psch_start_routing_collector() };
if ($@) {
plan skip_all => "Docker / routing collector not available: $@";
}
}
t/037_arrow_routing.pl:177
- With teardown handled conditionally in an END block, this unconditional shutdown is redundant and will still bring down a pre-existing collector when the test didn’t start it.
psch_stop_routing_collector();
Addresses review feedback (1 Copilot + 4 Cursor comments) on t/037_arrow_routing.pl, verified live via prove against a real build (not just code review): - Track whether this run started the routing collector ($started_routing_collector, set before the eval'd start attempt so a partial startup still counts). Only tear it down if so — a developer's manually-started collector, or one left behind by a crashed prior run, is left alone. - Move teardown into an END block (with 'local $?' to stop system() inside END from clobbering the script's exit code) so cleanup runs on every exit path: normal completion, plan skip_all, or a die() anywhere in the test body. Previously a die (e.g. the open-or-die below) skipped cleanup entirely, leaking the container and its fixed host ports (14317, 23133) -- ports t/026_arrow_dump.pl and t/036_unified_arrow_e2e.pl rely on being unbound. - Replace the bare 'open ... or die' on arm 2's spot-check with a Test::More SKIP block gated on whether the file actually grew, so a growth timeout produces a clean failed assertion instead of aborting the script mid-run. - Add per-arm producer-side bookkeeping (psch_wait_for_export + psch_get_stats asserting exported/send_failures), matching the existing t/024_otel_export.pl idiom, before the JSONL-growth checks. On failure this distinguishes 'producer never exported' from 'producer exported but routing/collector dropped it'. - Document the fixed-host-port single-instance-per-machine constraint in the header rather than parameterizing container/output naming: CI runs the TAP suite sequentially (prove without -j, single TAP job), and parameterizing names without also parameterizing the ports (14317/23133) wouldn't actually fix concurrent local runs while risking t/026 and t/036's assumption that those ports are unbound. Live-run verification surfaced a genuine pre-existing race unrelated to the above: the otelcol fileexporter writes a JSON line and its trailing newline as separate writes, and arm 1's node can flush trailing batches right up through stop(). If arm 2 snapshots its baselines mid-write, those late bytes get mis-attributed. Added wait_for_quiet() (polls combined JSONL size across all three files until stable for 1.5s, 15s cap) before arm 2's baselines to close the window. Confirmed via 3 live prove runs against a real build: one initial 11/12 run caught the race, then 12/12 with the collector auto-started (and torn down after), and 12/12 with the collector pre-started manually (and correctly left running after). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
t/037_arrow_routing.pl has skipped in every GitHub Actions CI run so far with only 'Routing collector container failed to become healthy' and no further detail. Locally, the exact same compose config starts and reports healthy in well under a second, and 'docker compose ... components' confirms the routing connector exists in this image version — ruling out a config bug or missing component. The failure appears specific to the GH-hosted runner environment, but there's been no way to tell why from the CI log alone. Dump 'docker compose ps' and 'docker compose logs' into the die message on healthcheck timeout, so the next occurrence (this one, on this push) actually carries diagnostic evidence instead of a bare 'failed to become healthy'. Also fixes a latent (currently harmless) bug found along the way: the service's Docker-level healthcheck used 'wget', which doesn't exist in the distroless otel/opentelemetry-collector-contrib image — it would always report unhealthy regardless of actual readiness. Doesn't affect today's behavior (psch_start_routing_collector polls host-side HTTP directly, independent of Docker's own healthcheck), but is misleading in 'docker compose ps' and would hang forever if anyone later adds '--wait' to the start command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (6)
t/037_arrow_routing.pl:200
wait_for_quiet()can time out and return 0, but the return value is ignored here. If the collector is still writing when the baselines are snapped, the second arm can become flaky (late bytes mis-attributed). Please assert that the collector output actually went quiet before proceeding.
# Let arm 1's trailing collector writes settle before snapshotting this
# arm's baselines (see wait_for_quiet).
wait_for_quiet(1.5, 15,
map { "$output_dir/$_.jsonl" } qw(legacy events_raw default));
t/037_arrow_routing.pl:229
- Same as arm 1: after observing growth in
events_raw.jsonl, it’s worth waiting for the collector output to go quiet before asserting that the other route files didn’t grow and before tail-inspectingevents_raw.jsonl. Without this, late/partial writes can cause flaky or false-pass behavior.
my $events_raw_after =
wait_for_growth("$output_dir/events_raw.jsonl", $events_raw_before, 15);
ok(defined $events_raw_after,
'arm 2 (arrow_events_raw): events_raw.jsonl received bytes after producer flush');
my $legacy_after = current_size("$output_dir/legacy.jsonl");
is($legacy_after, $legacy_before,
'arm 2 (arrow_events_raw): legacy.jsonl did NOT grow (routing kept it isolated)');
my $default_after = current_size("$output_dir/default.jsonl");
is($default_after, $default_before,
'arm 2 (arrow_events_raw): default.jsonl did NOT grow (no unmatched batches)');
t/psch.pm:276
- Same as
psch_routing_collector_available(): add a--max-timeto the health polling curl so each iteration can’t block longer than expected.
for my $i (1..30) {
my $result = `curl -sf 'http://localhost:23133/' 2>/dev/null`;
return 1 if $result =~ /Server available/;
sleep(1);
t/037_arrow_routing.pl:186
- The routing isolation assertions are made immediately after the first observed growth in
legacy.jsonl. Because the collector writes asynchronously (and may split a JSON line + newline into separate writes), bytes routed to the wrong file could arrive after these checks and be missed. Waiting for the output files to go quiet before asserting non-growth makes this test deterministic and prevents false passes.
This issue also appears on line 218 of the same file.
my $legacy_after = wait_for_growth("$output_dir/legacy.jsonl", $legacy_before, 15);
ok(defined $legacy_after,
'arm 1 (arrow_ipc): legacy.jsonl received bytes after producer flush');
my $events_raw_after = current_size("$output_dir/events_raw.jsonl");
is($events_raw_after, $events_raw_before,
'arm 1 (arrow_ipc): events_raw.jsonl did NOT grow (routing kept it isolated)');
my $default_after = current_size("$output_dir/default.jsonl");
is($default_after, $default_before,
'arm 1 (arrow_ipc): default.jsonl did NOT grow (no unmatched batches)');
t/013_clickhouse_tls.pl:102
- The comment explains that cumulative backoff can reach 123s (7 failures), but the test only waits 90s. Given
PschGetRetryDelayMs()caps per-attempt delay at 60s but allows many consecutive failures, a slow CI restart can still exceed 90s even if ClickHouse is already back. Either adjust the explanation or bump the timeout so it actually covers the documented 123s case.
# 90s, not 30s: the exporter's reconnect backoff (stats_exporter.cc,
# PschGetRetryDelayMs) is exponential — base 1s, doubling per
# consecutive failure, capped at 60s. Failed flush attempts start
# accumulating the moment CH goes down (well before docker restart's
# container comes back), and the cumulative wait before the Nth retry
# is the sum of all prior delays: 1, 3, 7, 15, 31, 63, 123s... A CH
# restart slow enough to rack up 5-6 failures (very plausible under a
# loaded CI runner) pushes the bgworker's next attempt past the 30s
# mark even though CH itself came back much sooner. 90s comfortably
# covers the 63s (6-failure) cumulative-backoff case plus round-trip.
my $after = psch_wait_for_clickhouse_query(
'SELECT count() FROM pg_stat_ch.events_raw', sub { $_[0] >= 1 }, 90);
t/psch.pm:257
- These health-check
curlcalls have no max-time, so a stalled TCP connect/read can hang the TAP test beyond its intended timeout loop. Adding a small--max-timekeeps the polling loop bounded and makes failures fail fast.
This issue also appears on line 273 of the same file.
sub psch_routing_collector_available {
return 0 unless system("docker ps >/dev/null 2>&1") == 0;
my $result = `curl -sf 'http://localhost:23133/' 2>/dev/null`;
return $result =~ /Server available/;
The prior commit folded 'docker compose ps'/'docker compose logs' output into the die message on healthcheck timeout, but that die message becomes the reason string for t/037_arrow_routing.pl's 'plan skip_all' — and TAP's protocol doesn't tolerate embedded newlines there. Test::More silently dropped everything past the first line, so the CI run that should have shown the diagnostics showed nothing new at all (confirmed: this exact commit's SHA ran in CI, zero trace of the dump anywhere in the log). Emit the dump via warn (STDERR) immediately before the die instead, decoupled from the skip reason string entirely. prove -v interleaves STDERR with STDOUT per test file, so this should show up inline in the CI log this time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
t/037_arrow_routing.pl:200
wait_for_quiet(...)is called but its return value is ignored. If the collector output never settles within the timeout, the test will still snapshot baselines and proceed, which can mis-attribute late bytes from arm 1 to arm 2 and make the routing assertions flaky/incorrect. Please assert that the output actually became quiet (and bail out if it didn’t) before continuing.
wait_for_quiet(1.5, 15,
map { "$output_dir/$_.jsonl" } qw(legacy events_raw default));
docker/docker-compose.arrow-route.yml:26
- The comment claims
otel/opentelemetry-collector-contrib:0.120.0is a distroless image with no wget/curl/shell and that a CMD healthcheck can “never succeed”, butdocker/docker-compose.otel.ymluses a CMD-basedwgethealthcheck with the same image tag. This inconsistency makes it unclear whether the healthcheck is truly impossible or just intentionally omitted here; please soften/rewrite the comment to avoid stating a false constraint.
# No Docker-level healthcheck: otel/opentelemetry-collector-contrib is a
# distroless image with no wget/curl/shell, so a CMD-based healthcheck
# can never succeed and would always report "unhealthy" regardless of
# actual readiness — misleading in `docker compose ps`, and a landmine
# for anyone who later adds `--wait` to the start command (would hang
# forever). Readiness is checked from the host via a real HTTP request
# in psch_start_routing_collector (t/psch.pm), independent of Docker's
# own healthcheck machinery.
warn (STDERR) didn't work either: confirmed empirically across two CI runs that plain STDERR output from a test ending in SKIP (rather than FAIL) never appears in prove -v's displayed output, even though the warn() call itself executes. diag() writes through Test::Builder's own output handle instead of raw STDERR, and prove -v documents showing diag() output regardless of the subtest's pass/fail/skip outcome. Verified locally with a throwaway script: a plan-skip_all test emitting a multi-line diag() shows every line, '#'-prefixed, under prove -v — exactly the behavior needed here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
t/037_arrow_routing.pl:200
- wait_for_quiet() returns 0 on timeout, but the return value is currently ignored. If the collector never becomes quiet (or is slow), the arm 2 baselines can be snapshotted mid-write, causing late bytes from arm 1 to be mis-attributed to arm 2 and making the test flaky. Please assert that wait_for_quiet() succeeded so failures are explicit and actionable.
# Let arm 1's trailing collector writes settle before snapshotting this
# arm's baselines (see wait_for_quiet).
wait_for_quiet(1.5, 15,
map { "$output_dir/$_.jsonl" } qw(legacy events_raw default));
Root cause found via the new Test::More::diag diagnostic: the otelcol-contrib container (runs as UID 10001 by default) crashed immediately on startup in every GitHub Actions CI run with error: open /output/legacy.jsonl: permission denied Error: cannot start pipelines: open /output/legacy.jsonl: permission denied On a fresh CI checkout, docker/otel-routing/output/ is owned by the runner account with default 0755 permissions. UID 10001 falls into the 'other' bucket, which gets r-x but not w on the bind-mounted directory, so the file exporter can't even create legacy.jsonl / events_raw.jsonl / default.jsonl. This is why t/037_arrow_routing.pl has skipped in literally every CI run since it was added: the collector never gets far enough to open its health-check port, so the 30x1s host-side curl poll always times out. Never reproduced locally: confirmed macOS's Docker Desktop/colima VM bind-mount layer doesn't enforce the same UID-based permission checks a real Linux host does (verified by force-running the container with --user 10001:10001 against a 0755-owned directory locally — started fine, unlike the real failure). The GH Actions runner is a real Linux host where these semantics apply for real. Fix: chmod the output dir to 0777 in psch_start_routing_collector right before 'docker compose up', rather than committing directory permission bits (which don't reliably survive git checkout across platforms/configs anyway). Self-healing every run; no risk since the directory holds only gitignored scratch test output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (5)
t/037_arrow_routing.pl:72
- The END cleanup will run even when the routing collector start failed due to Docker being unavailable (e.g., docker not installed). In that skip path, calling docker compose in psch_stop_routing_collector can add noisy "docker: not found" output after plan skip_all. Gate teardown on Docker being usable to keep skips clean while still cleaning up real partial startups.
# system() inside END sets $?, which would clobber the script's exit
# code; preserve it.
local $?;
psch_stop_routing_collector() if $started_routing_collector;
}
t/037_arrow_routing.pl:200
- wait_for_quiet() returns 0 on timeout, but the result is ignored. If the collector output never becomes quiet (or takes longer than the timeout), the arm-2 baselines may be captured mid-write and misattribute late bytes to the wrong arm, causing flakes. Assert the quiet period before snapshotting baselines.
wait_for_quiet(1.5, 15,
map { "$output_dir/$_.jsonl" } qw(legacy events_raw default));
t/psch.pm:290
- The routing collector startup loop also runs curl without timeouts; a single hung curl would stall the entire start routine. Add connect/overall timeouts to keep failures bounded and make CI behavior deterministic.
for my $i (1..30) {
my $result = `curl -sf 'http://localhost:23133/' 2>/dev/null`;
return 1 if $result =~ /Server available/;
sleep(1);
t/psch.pm:282
- psch_start_routing_collector assumes the bind-mounted output directory already exists; if PROJECT_DIR points at a checkout missing docker/otel-routing/output (or the directory was deleted), chmod will die before docker compose even runs. Creating the directory makes the helper more robust.
chmod 0777, $output_dir
or die "Failed to chmod routing collector output dir ($output_dir): $!";
t/psch.pm:258
- psch_routing_collector_available runs curl without any connect/overall timeout. If localhost:23133 is blackholed or a proxy intercepts, the TAP suite can hang indefinitely. Add curl timeouts so availability checks fail fast.
This issue also appears on line 287 of the same file.
sub psch_routing_collector_available {
return 0 unless system("docker ps >/dev/null 2>&1") == 0;
my $result = `curl -sf 'http://localhost:23133/' 2>/dev/null`;
return $result =~ /Server available/;
Three real, still-open findings from the review pass: 1. (Copilot) Header comment called the collector startup a compose 'profile' — it's a dedicated compose file, no --profile flag involved anywhere. Corrected the wording. 2. (Copilot + Cursor, duplicate findings) wait_for_quiet's return value was ignored at its one call site (settling between arm 1 and arm 2). If settling timed out, the test proceeded with unstable baselines instead of failing loudly. Wrapped in ok() so a timeout is now an explicit, attributable failure. 3. (Cursor) Isolation checks raced async writes: each arm calls wait_for_growth on its OWN expected file, then immediately snapshots the OTHER two files for the 'did NOT grow' assertions — with no settle time in between. A genuine misrouting bug whose erroneous write to another sink landed a moment late could race that snapshot and produce a false pass, defeating the entire point of the isolation check. Added a short wait_for_quiet on just the other two files (1s stable / 5s cap — narrower than the between-arms settle, since this is guarding against a fast erroneous write, not accommodating a slow legitimate one) before taking those snapshots, in both arms. Also fixed, Low severity (Cursor): arm 2's marker spot-check used 'open ... or die', so a transient reopen failure (the file's existence was already confirmed by the growth check, so this really would mean something changed underneath us) aborted the whole script instead of recording a failed assertion. Restructured as nested SKIP blocks — ok(open) now records pass/fail explicitly, and only the content checks that depend on a successful open get skipped if it fails. One further Copilot finding (t/psch.pm:315, healthcheck 30s vs compose's 50s) is now moot: the compose-level healthcheck it refers to was removed entirely in an earlier commit (wget doesn't exist in the distroless otelcol-contrib image, so it could never succeed regardless of timing) — replied and resolved on GitHub rather than included here since there's no code left for it to apply to. Syntax-checked (perl -c); could not get a live prove run in this local environment (pg_regress binary missing from a stale local install_tap tree, unrelated to this diff) — relying on CI, which has been the working verification loop all session for this file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
docker/docker-compose.arrow-route.yml:26
- The comment claims the otelcol-contrib image has no wget/curl/shell so an in-container CMD healthcheck can never succeed, but this repo’s existing
docker-compose.otel.ymldefines awget-based healthcheck for the same image. To avoid misleading future readers, reword this as “we intentionally avoid a Docker healthcheck and poll the host health endpoint instead”, without asserting what tools exist in the image.
# No Docker-level healthcheck: otel/opentelemetry-collector-contrib is a
# distroless image with no wget/curl/shell, so a CMD-based healthcheck
# can never succeed and would always report "unhealthy" regardless of
# actual readiness — misleading in `docker compose ps`, and a landmine
# for anyone who later adds `--wait` to the start command (would hang
t/037_arrow_routing.pl:62
- The eval-caught error string ($@) typically includes a trailing newline (and often "at ... line ..."), and embedding that directly in
plan skip_allcan produce TAP output with embedded newlines; earlier in this PR you note that TAP drops text past the first line. Sanitize/chomp the error before using it as the skip reason so the harness gets a single-line message.
eval { psch_start_routing_collector() };
if ($@) {
plan skip_all => "Docker / routing collector not available: $@";
}
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit db1f9be. Configure here.
| is($default_after, $default_before, | ||
| 'arm 1 (arrow_ipc): default.jsonl did NOT grow (no unmatched batches)'); | ||
|
|
||
| $node->stop(); |
There was a problem hiding this comment.
Shutdown exports bypass routing isolation
Medium Severity
Each arm asserts sink isolation, then stops the Postgres node. Shutdown and in-flight OTLP can still append to the JSONL sinks after those checks. A misroute that lands only during teardown is never compared to the arm baselines, so arm 1 can falsely pass while writing to events_raw.jsonl or default.jsonl.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit db1f9be. Configure here.


Summary
Closes the producer-side half of the Arrow exporter migration contract: pin
pg_stat_ch.block_formatemission and the routing-connector match-shape via a generic TAP test.The unified Arrow exporter (PR #116's commit 4) emits
pg_stat_ch.block_format=arrow_events_rawon its OTLP envelope; the legacyArrowBatchBuilderpath emitsarrow_ipc. In prod, the centraldatagres-otelcoldeployment'sroutingconnectoris supposed to fan batches between the legacyquery_logs_arrowreceiver target and a newevents_rawreceiver target based on that marker. This test pins the producer side of that contract: it doesn't run a real receiver (no CH, nodatagres-arrow-exporter) — it runs stockotelcol-contribwith twofileexporters and asserts which one received which arm's batches.What it catches (and what it doesn't)
Catches:
pg_stat_ch.block_formatemission (e.g. unified path silently falling back toarrow_ipc).Does NOT catch:
events_rawcolumn-set contract or the receiver's wire-format expectations — those live behinddatagres-arrow-exporterinclickgres-platformand are covered by a separate receiver-faithful test there (in flight).t/036_unified_arrow_e2e.plcontinues to handle the producer Arrow IPC -> direct CH ingest path.Mechanics
docker/docker-compose.arrow-route.yml(separate from the existingdocker-compose.otel.ymlsot/024andt/037can coexist; different ports, different container name).docker/otel-routing/collector-config.yaml— stockotel/opentelemetry-collector-contrib:0.120.0,routingconnectorkeyed on the resource attribute, threefileexporters (legacy / events_raw / default fall-through) writing JSONL to a bind-mounted dir.t/psch.pmhelpers:psch_routing_collector_available/psch_start_routing_collector/psch_stop_routing_collector.t/037_arrow_routing.plruns two arms (legacy + unified) and asserts that the corresponding file grew while the other files didn't. The unified arm also greps the routed JSONL tail forarrow_events_rawto pin the marker value content.Stacking
Based on
unified_arrow_exporter(PR #116). Hard-depends on commits 2 (the GUC) and 4 (the marker swap). Will retarget tomainafter #116 merges.Test plan
unified_arrow_exporter.psch_*_otelcol).🤖 Generated with Claude Code