feat(client-v2,jdbc-v2): add metrics recorder SPI - #3082
Conversation
Adds a backend-agnostic metrics SPI so an application can export the metrics the client already collects to any metrics backend. Client.Builder.setMetricsRecorder(MetricsRecorder) registers a recorder in client-v2, and the jdbc-v2 property jdbc_metrics_recorder names the recorder class a connection registers with its own client. The SPI follows the span recorder SPI: MetricsRecorder is a plain interface, DefaultMetricsRecorder is a no-op base class, and MetricsSupport derives the standard names, units and attributes so every recorder reports the same values. Names and attributes follow the OpenTelemetry semantic conventions for database clients where a convention exists. Implements: #2975
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 4782586. Configure here.
Client V2 CoverageCoverage Report
Class Coverage
|
JDBC V2 CoverageCoverage Report
Class Coverage
|
JDBC V1 CoverageCoverage Report
Class Coverage
|
Client V1 CoverageCoverage Report
Class Coverage
|
…successful one Review finding on the metrics SPI: the duration of a failed operation started after the client had already started OP_DURATION (query-parameter formatting, serializer lookup and the operation span were excluded) and was taken only after the span recorder had run, while the duration of a successful operation stops in completeOperation before any recorder runs. Success and failure therefore did not share an origin and could not be joined into one latency series. The origin is now taken where the client starts OP_DURATION, and the duration is taken before any recorder runs, on all three operation paths (query, POJO insert, stream insert). Two tests pin both ends with a span recorder that spends a known and different amount of time at each of them.
TriageCategory: Summary What this impacts
Concerns
Required reviewer action
|
|




Description
Implements #2975, with the design decided in this comment: the SPI only, shaped like the span recorder SPI; the Micrometer and OpenTelemetry adapters follow in separate PRs.
The client collects operation metrics today but only returns them to the caller, so exporting them is left to the application. This adds a backend-agnostic metrics SPI:
Client.Builder.setMetricsRecorder(MetricsRecorder)(packagecom.clickhouse.client.api.observability).jdbc_metrics_recorderproperty names the recorder class; a JDBC URL carries strings only, so the value is the fully-qualified class name of a recorder with a public no-argument constructor, and each connection creates its own instance.Everything is additive, Java 8 compatible, and changes no existing behaviour. When no recorder is registered nothing is recorded and no metrics-related work is done.
Design
Same pattern as
SpanRecorder(#2988), so there is one observability pattern to learn:MetricsRecorder- plain interface, called with everything the client knows about the operation.DefaultMetricsRecorder- no-op base class; an implementation overrides only what it cares about and keeps working when the client starts reporting an event it does not know about.MetricsSupport- derives the standard values from the same structures; opt-in throughgetMetricsSupport()and overridable, exactly likeSpanSupport.MetricName/MetricAttribute- the names, units, descriptions and attribute keys, defined on the SPI side so every recorder reports the same thing.Events, one per completed operation plus one per retried attempt:
recordQuerySuccess/recordInsertSuccessOperationMetrics)recordQueryFailure/recordInsertFailureOperationMetrics)recordQueryRetry/recordInsertRetryExactly one success or one failure is reported per operation the client started, so the operation count by outcome is the count of those events grouped by
error.type, and the number of retries is the count of the retry events.Metrics (
MetricName), following the OpenTelemetry database-client conventions where they exist andclickhouse.where they do not:db.client.operation.duration(s)clickhouse.client.operation.serialization.duration(s)clickhouse.client.operation.count({operation})clickhouse.client.operation.retries({retry})Durations are reported in seconds while
ClientMetricsvalues are milliseconds;MetricsSupportconverts them from the stopwatch nanoseconds, and returnsMetricsSupport.DURATION_UNKNOWNfor a duration the client did not measure, so a recorder never reports a made-up value.Attributes (
MetricAttribute):db.system.name,db.namespace,db.operation.name,db.collection.name(insert), anderror.typeplusdb.response.status_codeon a failure. This is deliberately a smaller set thanSpanAttribute, because an attribute of a metric becomes a time series: the statement text, the query id and the statement parameters stay on spans. A success has noerror.type, so the successful series is the one without it.Two consistency rules the implementation keeps, so the series can be joined:
error.typeidentifies a server failure by theServerExceptionit carries, the same waySpanSupportdoes, so a span and a metric of the same failure report the same error type.Entry points covered: query, command, ping and table-schema lookup (all reported as
query, as with spans), POJO insert and stream insert, each with its own retry loop; sync and async operations; client-v2 and jdbc-v2.Further attributes or events can be added later as
defaultmethods on the interface, without breaking existing implementations.Changes
client-v2observability: newMetricsRecorder,DefaultMetricsRecorder,MetricsSupport,MetricName,MetricAttribute.client-v2Client:Builder.setMetricsRecorder(...), and the recorder calls on the query, POJO-insert and stream-insert paths (success, failure, retry).jdbc-v2DriverProperties.METRICS_RECORDER+ConnectionImpl: the recorder is instantiated and registered before the client is built. The reflective loading of a class named by a property is now one helper (instantiateUserClass), shared withjdbc_json_parser_factory.CHANGELOG.md,docs/features.md(client-v2 and jdbc-v2 sections).Test
client-v2MetricsRecorderUnitTest(16 cases, WireMock + a dead endpoint): duration and attributes of a successful query and of a POJO and stream insert; serialization duration reported for an insert and not reported for a query (contrast case); a failed query and a failed insert counted witherror.typeand the target table; one retry event per retried attempt (@DataProviderover 0/1/3 retries) with the retry and the failure reporting the same error type; a retried operation still counted once; metric attributes are low-cardinality (no query id or statement); an unmeasured duration reported asDURATION_UNKNOWN; milliseconds converted to seconds;nullrecorder rejected.client-v2MetricsRecorderTest(integration, live server): successful query, successful POJO insert reporting the target table, a server error reportingerror.typeanddb.response.status_code, and a table-schema lookup reported as a query.jdbc-v2MetricsRecorderTest(integration): a connection reports through the recorder named byjdbc_metrics_recorder; a connection without the property reports nothing; an unknown class and a class that is not a recorder are rejected with a message naming the property.client-v2unit (639),jdbc-v2unit, the observability integration tests, thejdbc_json_parser_factoryintegration tests (the shared loading helper), andpackages/clickhouse-jdbc-allbuilds.Known limitation, not addressed here
ClientMetrics.OP_SERIALIZATIONis started with the POJO insert but never stopped when serialization finishes -OperationMetrics.operationComplete()stops every stopwatch at the end of the operation - so the value the client collects, and therefore the value this SPI exports asclickhouse.client.operation.serialization.duration, is currently the operation duration. That is a pre-existing defect of the metric itself and changing it would change the valueOperationMetricsalready returns to applications, so it is left out of this PR. Happy to fix it in a follow-up if you want it in the same release.Docs / surface
docs/features.mdupdated in both theclient-v2and thejdbc-v2section.0.11.0-rc1cycle.docs/changes_checklist.md: new public API is documented indocs/features.mdand the CHANGELOG; the newDriverPropertiesconstant follows the existing key naming (jdbc_prefix, likejdbc_json_parser_factory) with javadoc; no logging, enum-constant, signature or config-default changes to existing surface.Pre-PR validation gate