Repository navigation
feat: trace ClickHouse queries and escape gateway metric labels - #3574
Conversation
atharva-bhange
left a comment
There was a problem hiding this comment.
Requesting changes because the new client wrapping breaks an existing test and backend CI is failing. The HTTP query result handling also omits the row count promised by this PR. Details and minimal fixes are in the inline comments.
Validation: both gateway metric test packages passed locally. Isolated checks against the pinned ClickHouse dependencies confirmed HTTP stream cleanup/error status and reproduced the missing HTTP row count. Reviewed the failed backend CI job; did not run the entire backend suite locally.
There was a problem hiding this comment.
[P1] Update the existing test for the newly wrapped query method
Returning the instrumented client replaces client.query with a function. test_user_detail_reverse_lookup_keeps_transport_safety_not_statement_caps still accesses client.query.call_args at tracer/tests/test_session_list_bounded_view.py:841, so it now fails with AttributeError: function object has no attribute call_args. This is the failure in pytest shard 3 and causes Backend Tests Pass to fail: https://github.com/future-agi/future-agi/actions/runs/39586937859/job/119178225927
Retain the original query mock before the factory instruments the client, and inspect that mock for the existing transport-safety assertions. Please rerun the affected test and get backend CI green.
There was a problem hiding this comment.
Fixed in 713651a, test-only as you suggested: the test now keeps a reference to the client.query mock before the reader builds the client and asserts on that mock, since the factory wraps the method with a plain function. Kept production code mock-unaware and avoided __wrapped__ because it does not exist when tracing is disabled. tracer/tests/test_session_list_bounded_view.py and tfc/tests/test_telemetry_clickhouse.py pass locally (71 passed); CI shard 3 should be green on this push.
There was a problem hiding this comment.
[P2] Record row_count from HTTP QueryResult objects
The HTTP client returns clickhouse_connect.driver.query.QueryResult. In the pinned clickhouse-connect 1.1.1 it exposes row_count, not len, so this branch never records db.rows_returned for normal HTTP queries. I reproduced this with a real QueryResult containing two rows: the query returns both rows but its span has no row-count attribute. The new HTTP query test does not assert that attribute.
Handle QueryResult.row_count alongside the native list/tuple handling, and add a test using the real result type that asserts db.rows_returned. Otherwise HTTP and native queries expose inconsistent telemetry.
There was a problem hiding this comment.
Fixed in 713651a. _record_clickhouse_result gains an elif that records db.rows_returned from row_count when it is an int, after the existing int and sized-result branches, so native list/tuple handling and the command() string case are unchanged. The HTTP fixture now returns a real clickhouse_connect QueryResult built from a column block and the span test asserts db.rows_returned == 2 (verified against both the 1.1.1 and 1.8.0 pins). One behavioural note: row_count materialises the rows, so the response body is now read inside the span and a mid-body read error surfaces from client.query() itself, which lands inside the readers' existing reset-and-reraise handling.
atharva-bhange
left a comment
There was a problem hiding this comment.
Re-reviewed 713651a. Both requested changes are addressed: the existing test retains the original query mock without weakening its safety assertions, and HTTP QueryResult row counts are recorded with regression coverage. Independently verified populated and empty HTTP results, native counts, command exclusion, and HTTP body-read error status. All ten backend CI shards, gateway checks, and E2E checks pass. Approved. The full backend suite was verified through CI rather than rerun locally.

Summary
Backend ClickHouse queries emitted no OpenTelemetry spans, so Tempo and the span-metrics RED panels only ever showed Postgres and Redis. This wires the existing
instrument_clickhouse_clienthelper into the three places ClickHouse clients are created, so every query gets adb.system=clickhouseclient span with a truncated statement, operation, row count and error status. The AgentCC gateway's Prometheus exposition did not escape label values, and themodellabel comes from the request body, so a quoted or multi-line model name could corrupt/-/metrics; label values are now escaped per the text exposition format.Linked issues
Contributes to TH-8757
Linear: https://linear.app/future-agi/issue/TH-8757
AI use
AI use:
Type of change
E2E coverage
E2E: exempt (backend-internal and gateway-internal telemetry; no user-visible flow)
1) What changes were done
A. ClickHouse query spans (
tfc/telemetry/instrumentation.py)instrument_clickhouse_clientnow wraps every query entry point present on the client (execute,execute_iter,query,command,query_row_block_stream), is a no-op whenOTEL_ENABLEDis false, and is idempotent.trace_clickhouse_streamkeeps the span open until a streaming result is consumed or closed, soexecute_iterandquery_row_block_streamreport real durations and errors.SpanKind.CLIENT, attributes and error recording for the sync, async and stream wrappers;db.rows_returnedunwraps(rows, column_types)tuples and is omitted forcommand()strings.B. Wiring at the client factories
tfc/utils/clickhouse.pysingleton,tracer/services/clickhouse/client.pypooled native driver, andtracer/services/clickhouse/application_read_transport.pyHTTP read client now return instrumented clients.C. Gateway metrics label escaping (
agentcc-gateway/internal/metrics/registry.go)formatLabelsescapes backslash, double quote and newline in label values.2) Why the changes were done
db.systemdimension on Tempo span metrics is the only DB dimension those panels use.3) Tests written + scenarios each covers
tfc/tests/test_telemetry_clickhouse.py— span emission through the three real client factories with an in-memory exportertest_singleton_client_is_instrumented— the analytics singleton returns a wrapped clienttest_native_execute_emits_client_span—executespan hasdb.system, statement, operation,CLIENTkind and row counttest_with_column_types_counts_rows_not_tuple_parts—with_column_types=Truetuple results count rows, not tuple partstest_execute_iter_span_ends_when_rows_are_consumed— stream span stays open until the generator is exhaustedtest_execute_iter_error_while_consuming_marks_span_error— errors during iteration set ERROR statustest_http_application_client_query_emits_span— clickhouse-connectqueryis tracedtest_http_application_client_command_records_no_row_count— string results record no row counttest_http_application_client_block_stream_span_ends_on_exit— context-manager stream span ends on exittest_otel_disabled_returns_client_untouched— no wrapping when OTel is offtest_instrumentation_is_idempotent— wrapping twice does not double-traceagentcc-gateway/internal/metrics/registry_test.goTestFormatLabels_EscapesValues— quote, backslash and newline render escaped4) How to run / test
5) Rollout notes
release-images); no migration, no new env vars, no values change. ClickHouse spans start the moment the new backend image is live becauseOTEL_ENABLEDand the Tempo endpoint are already set in prod.agentcc-gatewayimage is deployed, flipprometheus.enabledtotruein the deployment repo (templates/agentcc-gateway-cm.yaml, step 5 of deployment PR fix(annotations): consolidate TH-4812 annotation fixes #401) and redeploy the umbrella chart; the Alloy scrape for/-/metricsis already live and will go from HTTP 404 toup == 1.6) Edge cases & considerations
OTEL_ENABLED=false: factories return the raw client; no OTel import happens on the hot path.command()returns a string: nodb.rows_returned, so dashboards do not see string lengths as row counts.SELECT 1on pooled connections is traced like any query; volume is low and it is filterable by statement.7) Pre-existing issues (NOT introduced by this PR)
black --checkflags a hunk intracer/services/clickhouse/client.pyand ruff reports fiveUP035/UP045findings intfc/telemetry/instrumentation.py; both reproduce ondevon lines this PR does not touch.8) Architectural / important decisions
CLIENTkind: matches the psycopg and redis instrumentors so DB panels can filter by kind uniformly.strconv.Quoteescapes more than the text format allows.Checklist