Skip to content

Commit ab08ae6

Browse files
authored
Merge pull request #3065 from ClickHouse/polyglot/client-v2-opentelemetry-span-recorder
feat(client-v2-otel): add OpenTelemetry span recorder module
2 parents 91e7d68 + 6d5dc4c commit ab08ae6

17 files changed

Lines changed: 1275 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,30 @@
22

33
[Release Migration Guide](docs/releases/0_11_0.md)
44

5+
### Breaking Changes
6+
7+
- **[client-v2]** `com.clickhouse.client.api.metrics.OperationMetrics` now has a single constructor,
8+
`OperationMetrics(ClientStatisticsHolder, OperationType)`; the constructor without an operation type was removed.
9+
Metrics are created by the client, which always knows the kind of the operation it runs, and the constructor takes
10+
an internal type (`com.clickhouse.client.api.internal.ClientStatisticsHolder`), so application code is not expected
11+
to call it. (https://github.com/ClickHouse/clickhouse-java/issues/2974)
12+
513
### New Features
614

15+
- **[client-v2]** Added an OpenTelemetry implementation of the observability SPI.
16+
`Client.Builder.setSpanRecorder(new OpenTelemetrySpanRecorder(openTelemetry))`
17+
reports every client operation and every transport request as an OpenTelemetry `CLIENT` span: an operation span is
18+
started as a child of the current OpenTelemetry context, so it joins the application's own trace, and each request
19+
span - including one per retry - is a child of its operation span. Span names and attribute keys are the standard
20+
ones of the SPI (the recorder derives them through `SpanSupport`), every value is recorded with the OpenTelemetry
21+
attribute type that matches it, and a failure sets the span status to `ERROR` and is recorded as an OpenTelemetry
22+
exception event next to the `error.type` and `db.response.status_code` attributes. The recorder reports to a
23+
supplied `OpenTelemetry` instance, to a `Tracer` given to `new OpenTelemetrySpanRecorder(Tracer)`, or to
24+
`GlobalOpenTelemetry` - read when a span is started - when constructed without arguments. Previously an application that wanted
25+
OpenTelemetry spans had to write that mapping itself. The OpenTelemetry API is a compile-only dependency of
26+
`client-v2`: the recorder is used only by an application that already provides `opentelemetry-api` at runtime, so
27+
nothing is added to the classpath of a client that does not use it.
28+
(https://github.com/ClickHouse/clickhouse-java/issues/2974)
729
- **[client-v2]** Added an observability SPI that lets an application observe client operations as spans.
830
`Client.Builder.setSpanRecorder(SpanRecorder)` registers a backend-agnostic recorder from the new
931
`com.clickhouse.client.api.observability` package: each operation (a query, a command or an insert - including
@@ -20,12 +42,19 @@
2042
`SpanSupport`, so all recorders that use it report the same information (statement text, target database and table, query id,
2143
statement parameters, batch size, the first configured endpoint on the operation span and the per-attempt
2244
server address and port on the request spans, HTTP status, returned rows, and the error type and ClickHouse
23-
error code on failure). An operation span is started on the calling thread, so it joins
45+
error code on failure). The outcome of a completed operation is reported per operation kind - `recordQuerySuccess`
46+
for a read and `recordInsertSuccess` for an insert - because the metrics that describe a read are not the ones that
47+
describe a write: a query reports `db.response.returned_rows`, `clickhouse.response.read_rows` and
48+
`clickhouse.response.read_bytes`, an insert reports `clickhouse.response.written_rows` and
49+
`clickhouse.response.written_bytes`. The same distinction is available on the metrics themselves through the new
50+
`OperationMetrics#getOperationType()`, which returns the new `com.clickhouse.client.api.metrics.OperationType` -
51+
the kind of the call the application made, so a command that writes is reported as a query.
52+
An operation span is started on the calling thread, so it joins
2453
the caller's ambient trace even when the operation runs on the client's executor, and it is ended exactly once
2554
for every operation that starts. Previously the client exposed no hook for tracing, so an
2655
application could not attribute a query or a retried request to its own trace. When no recorder is registered
2756
nothing is recorded and no span-related work is done, so the default path is unchanged. An OpenTelemetry
28-
implementation of the SPI follows in a separate module.
57+
implementation of the SPI is available as `OpenTelemetrySpanRecorder`.
2958
(https://github.com/ClickHouse/clickhouse-java/issues/2974)
3059
- **[client-v2, jdbc-v2]** Added support for the `BFloat16` data type (ClickHouse `24.11+`). `BFloat16` columns are read as
3160
Java `float` values (widening is lossless) and written from `float`/`Float` values, including through generic records, POJO

‎client-v2/pom.xml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,28 @@
8484
<version>${guava.version}</version>
8585
</dependency>
8686

87+
<!-- Compile-only: OpenTelemetrySpanRecorder is used only when the
88+
application already provides the OpenTelemetry API at runtime. -->
89+
<dependency>
90+
<groupId>io.opentelemetry</groupId>
91+
<artifactId>opentelemetry-api</artifactId>
92+
<version>${opentelemetry.version}</version>
93+
<scope>provided</scope>
94+
</dependency>
95+
8796
<!-- Test Dependencies -->
97+
<dependency>
98+
<groupId>io.opentelemetry</groupId>
99+
<artifactId>opentelemetry-sdk</artifactId>
100+
<version>${opentelemetry.version}</version>
101+
<scope>test</scope>
102+
</dependency>
103+
<dependency>
104+
<groupId>io.opentelemetry</groupId>
105+
<artifactId>opentelemetry-sdk-testing</artifactId>
106+
<version>${opentelemetry.version}</version>
107+
<scope>test</scope>
108+
</dependency>
88109
<dependency>
89110
<groupId>com.fasterxml.jackson.core</groupId>
90111
<artifactId>jackson-databind</artifactId>

‎client-v2/src/main/java/com/clickhouse/client/api/Client.java‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.clickhouse.client.api.metadata.TableSchema;
3131
import com.clickhouse.client.api.metrics.ClientMetrics;
3232
import com.clickhouse.client.api.metrics.OperationMetrics;
33+
import com.clickhouse.client.api.metrics.OperationType;
3334
import com.clickhouse.client.api.observability.DefaultSpanRecorder;
3435
import com.clickhouse.client.api.observability.Span;
3536
import com.clickhouse.client.api.observability.SpanRecorder;
@@ -1532,9 +1533,10 @@ public CompletableFuture<InsertResponse> insert(String tableName, List<?> data,
15321533

15331534
try (TransportResponse transportResponse = httpClientHelper.executeRequest(transportRequest, operationSpan)) {
15341535
ClientStatisticsHolder clientStats = globalClientStats.remove(operationId);
1535-
OperationMetrics metrics = completeOperation(transportResponse, clientStats, requestSettings.getQueryId());
1536+
OperationMetrics metrics = completeOperation(transportResponse, clientStats,
1537+
requestSettings.getQueryId(), OperationType.INSERT);
15361538

1537-
spanRecorder.recordSuccess(operationSpan, metrics);
1539+
spanRecorder.recordInsertSuccess(operationSpan, metrics);
15381540
return new InsertResponse(transportResponse, metrics);
15391541
} catch (Exception e) {
15401542
String msg = requestExMsg("Insert", (i + 1), durationSince(startTime).toMillis(), requestSettings.getQueryId());
@@ -1749,8 +1751,9 @@ public CompletableFuture<InsertResponse> insert(String tableName,
17491751
registerTransportReq(queryId, transportRequest);
17501752

17511753
try (TransportResponse transportResponse = httpClientHelper.executeRequest(transportRequest, operationSpan)) {
1752-
OperationMetrics metrics = completeOperation(transportResponse, finalClientStats, requestSettings.getQueryId());
1753-
spanRecorder.recordSuccess(operationSpan, metrics);
1754+
OperationMetrics metrics = completeOperation(transportResponse, finalClientStats,
1755+
requestSettings.getQueryId(), OperationType.INSERT);
1756+
spanRecorder.recordInsertSuccess(operationSpan, metrics);
17541757
return new InsertResponse(transportResponse, metrics);
17551758
} catch (Exception e) {
17561759
String msg = requestExMsg("Insert", (i + 1), durationSince(startTime).toMillis(), requestSettings.getQueryId());
@@ -1895,13 +1898,14 @@ public CompletableFuture<QueryResponse> query(String sqlQuery, Map<String, Objec
18951898
TransportResponse transportResp = null;
18961899
try {
18971900
transportResp = httpClientHelper.executeRequest(request, operationSpan);
1898-
OperationMetrics metrics = completeOperation(transportResp, clientStats, requestSettings.getQueryId());
1901+
OperationMetrics metrics = completeOperation(transportResp, clientStats,
1902+
requestSettings.getQueryId(), OperationType.QUERY);
18991903
ClickHouseFormat responseFormat = transportResp.getDataFormat();
19001904
if (responseFormat == null) {
19011905
responseFormat = requestSettings.getFormat();
19021906
}
19031907

1904-
spanRecorder.recordSuccess(operationSpan, metrics);
1908+
spanRecorder.recordQuerySuccess(operationSpan, metrics);
19051909
return new QueryResponse(transportResp, responseFormat, requestSettings, metrics);
19061910

19071911
} catch (Exception e) {
@@ -1999,8 +2003,9 @@ public CompletableFuture<QueryResponse> query(String sqlQuery, Map<String, Objec
19992003
return query(sqlQuery, queryParams, null);
20002004
}
20012005

2002-
private OperationMetrics completeOperation(TransportResponse transportResponse, ClientStatisticsHolder clientStats, String originalQueryId) {
2003-
OperationMetrics metrics = new OperationMetrics(clientStats);
2006+
private OperationMetrics completeOperation(TransportResponse transportResponse, ClientStatisticsHolder clientStats,
2007+
String originalQueryId, OperationType operationType) {
2008+
OperationMetrics metrics = new OperationMetrics(clientStats, operationType);
20042009
String summary = transportResponse.getSummaryJson();
20052010
ProcessParser.parseSummary(summary, metrics);
20062011
String queryId = transportResponse.getQueryId();

‎client-v2/src/main/java/com/clickhouse/client/api/metrics/OperationMetrics.java‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import java.util.HashMap;
88
import java.util.Map;
9+
import java.util.Objects;
910

1011
/**
1112
* OperationStatistics objects hold various stats for complete operations.
@@ -19,8 +20,29 @@ public class OperationMetrics {
1920

2021
private final ClientStatisticsHolder clientStatistics;
2122

22-
public OperationMetrics(ClientStatisticsHolder clientStatisticsHolder) {
23+
private final OperationType operationType;
24+
25+
/**
26+
* Creates metrics of an operation of the given kind. Called by the client, which always knows
27+
* the kind of the operation it runs.
28+
*
29+
* @param clientStatisticsHolder - holder of the client-side statistics of the operation
30+
* @param operationType - kind of the operation
31+
*/
32+
public OperationMetrics(ClientStatisticsHolder clientStatisticsHolder, OperationType operationType) {
2333
this.clientStatistics = clientStatisticsHolder;
34+
this.operationType = Objects.requireNonNull(operationType, "operationType must not be null");
35+
}
36+
37+
/**
38+
* Returns the kind of the operation these metrics were collected for. It tells which of the
39+
* metrics are meaningful - a read operation reports what the server read and returned, an insert
40+
* reports what it wrote.
41+
*
42+
* @return kind of the operation; never {@code null}
43+
*/
44+
public OperationType getOperationType() {
45+
return operationType;
2446
}
2547

2648
public Metric getMetric(ServerMetrics metric) {
@@ -59,6 +81,7 @@ public void setQueryId(String queryId) {
5981
public String toString() {
6082
return "OperationStatistics{" +
6183
"\"queryId\"=\"" + queryId + "\", " +
84+
"\"operationType\"=\"" + operationType + "\", " +
6285
"\"metrics\"=" + metrics +
6386
'}';
6487
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.clickhouse.client.api.metrics;
2+
3+
/**
4+
* Kind of client operation a set of {@link OperationMetrics} was collected for.
5+
* <p>
6+
* The kind decides which metrics of the operation are meaningful: a read operation reports how much
7+
* the server read and returned, an insert reports how much the server wrote.
8+
*/
9+
public enum OperationType {
10+
11+
/**
12+
* Operation the client ran as a statement - a query, a command, a ping or a table-schema lookup.
13+
* It is the kind of the call the application made, not of the work the server did: a command that
14+
* writes, such as {@code INSERT INTO ... SELECT}, is run as a statement and is reported here.
15+
*/
16+
QUERY,
17+
18+
/**
19+
* Operation the client ran as an insert, through one of the {@code insert} methods.
20+
*/
21+
INSERT
22+
}

‎client-v2/src/main/java/com/clickhouse/client/api/observability/DefaultSpanRecorder.java‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ public void recordHttpStatus(Span requestSpan, int statusCode) {
7171
}
7272

7373
@Override
74-
public void recordSuccess(Span operationSpan, OperationMetrics metrics) {
74+
public void recordQuerySuccess(Span operationSpan, OperationMetrics metrics) {
75+
// records nothing
76+
}
77+
78+
@Override
79+
public void recordInsertSuccess(Span operationSpan, OperationMetrics metrics) {
7580
// records nothing
7681
}
7782

‎client-v2/src/main/java/com/clickhouse/client/api/observability/SpanAttribute.java‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public enum SpanAttribute {
5252
DB_RESPONSE_STATUS_CODE("db.response.status_code"),
5353

5454
/**
55-
* Number of rows returned by the server. Recorded when an operation succeeds and the server
55+
* Number of rows returned by the server. Recorded when a read operation succeeds and the server
5656
* reported a progress summary.
5757
*/
5858
DB_RESPONSE_RETURNED_ROWS("db.response.returned_rows"),
@@ -62,6 +62,30 @@ public enum SpanAttribute {
6262
*/
6363
CLICKHOUSE_QUERY_ID("clickhouse.query_id"),
6464

65+
/**
66+
* Number of rows the server read from the storage. Recorded when a read operation succeeds and
67+
* the server reported a progress summary.
68+
*/
69+
CLICKHOUSE_RESPONSE_READ_ROWS("clickhouse.response.read_rows"),
70+
71+
/**
72+
* Number of bytes the server read from the storage. Recorded when a read operation succeeds and
73+
* the server reported a progress summary.
74+
*/
75+
CLICKHOUSE_RESPONSE_READ_BYTES("clickhouse.response.read_bytes"),
76+
77+
/**
78+
* Number of rows the server wrote to the storage. Recorded when an insert succeeds and the server
79+
* reported a progress summary.
80+
*/
81+
CLICKHOUSE_RESPONSE_WRITTEN_ROWS("clickhouse.response.written_rows"),
82+
83+
/**
84+
* Number of bytes the server wrote to the storage. Recorded when an insert succeeds and the
85+
* server reported a progress summary.
86+
*/
87+
CLICKHOUSE_RESPONSE_WRITTEN_BYTES("clickhouse.response.written_bytes"),
88+
6589
/**
6690
* Hostname of the server the request is sent to.
6791
*/

‎client-v2/src/main/java/com/clickhouse/client/api/observability/SpanRecorder.java‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.clickhouse.client.api.insert.InsertSettings;
44
import com.clickhouse.client.api.metrics.OperationMetrics;
5+
import com.clickhouse.client.api.metrics.OperationType;
56
import com.clickhouse.client.api.query.QuerySettings;
67
import com.clickhouse.client.api.transport.Endpoint;
78

@@ -93,13 +94,27 @@ public interface SpanRecorder {
9394
void recordHttpStatus(Span requestSpan, int statusCode);
9495

9596
/**
96-
* Reports that an operation completed successfully.
97+
* Reports that a read operation completed successfully. It is the counterpart of
98+
* {@link #startQuerySpan(QuerySettings, String, Endpoint)}.
9799
*
98100
* @param operationSpan - span of the operation
99-
* @param metrics - metrics of the completed operation; source of the query id and of the number
100-
* of returned rows. May be {@code null}
101+
* @param metrics - metrics of the completed operation, whose
102+
* {@link OperationMetrics#getOperationType()} is {@link OperationType#QUERY};
103+
* source of the query id and of what the server read and returned. May be
104+
* {@code null}
101105
*/
102-
void recordSuccess(Span operationSpan, OperationMetrics metrics);
106+
void recordQuerySuccess(Span operationSpan, OperationMetrics metrics);
107+
108+
/**
109+
* Reports that an insert operation completed successfully. It is the counterpart of
110+
* {@link #startInsertSpan(InsertSettings, String, int, Endpoint)}.
111+
*
112+
* @param operationSpan - span of the operation
113+
* @param metrics - metrics of the completed operation, whose
114+
* {@link OperationMetrics#getOperationType()} is {@link OperationType#INSERT};
115+
* source of the query id and of what the server wrote. May be {@code null}
116+
*/
117+
void recordInsertSuccess(Span operationSpan, OperationMetrics metrics);
103118

104119
/**
105120
* Reports that an operation failed.

‎client-v2/src/main/java/com/clickhouse/client/api/observability/SpanSupport.java‎

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,23 +156,67 @@ public void recordEndpoint(Span span, String host, int port) {
156156
}
157157

158158
/**
159-
* Records the outcome of a successfully completed operation.
159+
* Records the outcome of a successfully completed read operation - what the server read and what
160+
* it returned.
160161
*
161162
* @param span - span of the operation
162163
* @param metrics - metrics of the completed operation, may be {@code null}
163164
*/
164-
public void recordSuccess(Span span, OperationMetrics metrics) {
165+
public void recordQuerySuccess(Span span, OperationMetrics metrics) {
165166
if (metrics == null) {
166167
return;
167168
}
168169

170+
recordQueryId(span, metrics);
171+
recordServerMetric(span, metrics, ServerMetrics.RESULT_ROWS, SpanAttribute.DB_RESPONSE_RETURNED_ROWS);
172+
recordServerMetric(span, metrics, ServerMetrics.NUM_ROWS_READ, SpanAttribute.CLICKHOUSE_RESPONSE_READ_ROWS);
173+
recordServerMetric(span, metrics, ServerMetrics.NUM_BYTES_READ, SpanAttribute.CLICKHOUSE_RESPONSE_READ_BYTES);
174+
}
175+
176+
/**
177+
* Records the outcome of a successfully completed insert operation - what the server wrote.
178+
*
179+
* @param span - span of the operation
180+
* @param metrics - metrics of the completed operation, may be {@code null}
181+
*/
182+
public void recordInsertSuccess(Span span, OperationMetrics metrics) {
183+
if (metrics == null) {
184+
return;
185+
}
186+
187+
recordQueryId(span, metrics);
188+
recordServerMetric(span, metrics, ServerMetrics.NUM_ROWS_WRITTEN,
189+
SpanAttribute.CLICKHOUSE_RESPONSE_WRITTEN_ROWS);
190+
recordServerMetric(span, metrics, ServerMetrics.NUM_BYTES_WRITTEN,
191+
SpanAttribute.CLICKHOUSE_RESPONSE_WRITTEN_BYTES);
192+
}
193+
194+
/**
195+
* Records the query id of a completed operation, which the server may have assigned itself.
196+
*
197+
* @param span - span of the operation
198+
* @param metrics - metrics of the completed operation
199+
*/
200+
protected void recordQueryId(Span span, OperationMetrics metrics) {
169201
if (metrics.getQueryId() != null) {
170202
span.setAttribute(SpanAttribute.CLICKHOUSE_QUERY_ID.getKey(), metrics.getQueryId());
171203
}
172-
// the row count comes from the server's progress summary, which is not always available
173-
Metric returnedRows = metrics.getMetric(ServerMetrics.RESULT_ROWS);
174-
if (returnedRows != null && returnedRows.getLong() >= 0) {
175-
span.setAttribute(SpanAttribute.DB_RESPONSE_RETURNED_ROWS.getKey(), returnedRows.getLong());
204+
}
205+
206+
/**
207+
* Records one server metric of a completed operation. The value comes from the server's progress
208+
* summary, which is not always available, so a metric the server did not report is left out.
209+
*
210+
* @param span - span of the operation
211+
* @param metrics - metrics of the completed operation
212+
* @param metric - server metric to read
213+
* @param attribute - attribute to record it under
214+
*/
215+
protected void recordServerMetric(Span span, OperationMetrics metrics, ServerMetrics metric,
216+
SpanAttribute attribute) {
217+
Metric value = metrics.getMetric(metric);
218+
if (value != null && value.getLong() >= 0) {
219+
span.setAttribute(attribute.getKey(), value.getLong());
176220
}
177221
}
178222

0 commit comments

Comments
 (0)