Skip to content

Commit b8d9f7d

Browse files
committed
add db client metrics
1 parent c6a6fa6 commit b8d9f7d

File tree

23 files changed

+305
-10
lines changed

23 files changed

+305
-10
lines changed

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientAttributesExtractor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ public final class DbClientAttributesExtractor<REQUEST, RESPONSE>
2828
// copied from DbIncubatingAttributes
2929
private static final AttributeKey<String> DB_STATEMENT = AttributeKey.stringKey("db.statement");
3030
private static final AttributeKey<String> DB_QUERY_TEXT = AttributeKey.stringKey("db.query.text");
31-
32-
private static final AttributeKey<String> DB_OPERATION = AttributeKey.stringKey("db.operation");
33-
private static final AttributeKey<String> DB_OPERATION_NAME =
34-
AttributeKey.stringKey("db.operation.name");
31+
static final AttributeKey<String> DB_OPERATION = AttributeKey.stringKey("db.operation");
32+
static final AttributeKey<String> DB_OPERATION_NAME = AttributeKey.stringKey("db.operation.name");
33+
static final AttributeKey<Long> DB_RESPONSE_STATUS_CODE =
34+
AttributeKey.longKey("db.response.status_code");
3535

3636
/** Creates the database client attributes extractor with default configuration. */
3737
public static <REQUEST, RESPONSE> AttributesExtractor<REQUEST, RESPONSE> create(

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/DbClientCommonAttributesExtractor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ abstract class DbClientCommonAttributesExtractor<
2222

2323
// copied from DbIncubatingAttributes
2424
private static final AttributeKey<String> DB_NAME = AttributeKey.stringKey("db.name");
25-
private static final AttributeKey<String> DB_NAMESPACE = AttributeKey.stringKey("db.namespace");
26-
private static final AttributeKey<String> DB_SYSTEM = AttributeKey.stringKey("db.system");
25+
static final AttributeKey<String> DB_NAMESPACE = AttributeKey.stringKey("db.namespace");
26+
static final AttributeKey<String> DB_SYSTEM = AttributeKey.stringKey("db.system");
2727
private static final AttributeKey<String> DB_USER = AttributeKey.stringKey("db.user");
2828
private static final AttributeKey<String> DB_CONNECTION_STRING =
2929
AttributeKey.stringKey("db.connection_string");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.db;
7+
8+
import static java.util.logging.Level.FINE;
9+
10+
import com.google.auto.value.AutoValue;
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.metrics.DoubleHistogram;
13+
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
14+
import io.opentelemetry.api.metrics.Meter;
15+
import io.opentelemetry.context.Context;
16+
import io.opentelemetry.context.ContextKey;
17+
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
18+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
19+
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
20+
import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil;
21+
import java.util.concurrent.TimeUnit;
22+
import java.util.logging.Logger;
23+
24+
/**
25+
* {@link OperationListener} which keeps track of <a
26+
* href="https://opentelemetry.io/docs/specs/semconv/database/database-metrics/#metric-dbclientoperationduration">Database
27+
* client metrics</a>.
28+
*
29+
* @since 2.11.0
30+
*/
31+
public final class DbClientMetrics implements OperationListener {
32+
33+
private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1);
34+
35+
private static final ContextKey<State> DB_CLIENT_REQUEST_METRICS_STATE =
36+
ContextKey.named("db-client-metrics-state");
37+
38+
private static final Logger logger = Logger.getLogger(DbClientMetrics.class.getName());
39+
40+
/**
41+
* Returns an {@link OperationMetrics} instance which can be used to enable recording of {@link
42+
* DbClientMetrics}.
43+
*
44+
* @see InstrumenterBuilder#addOperationMetrics(OperationMetrics)
45+
*/
46+
public static OperationMetrics get() {
47+
return OperationMetricsUtil.create("database client", DbClientMetrics::new);
48+
}
49+
50+
private final DoubleHistogram duration;
51+
52+
private DbClientMetrics(Meter meter) {
53+
DoubleHistogramBuilder stableDurationBuilder =
54+
meter
55+
.histogramBuilder("db.client.operation.duration")
56+
.setUnit("s")
57+
.setDescription("Duration of database client operations.")
58+
.setExplicitBucketBoundariesAdvice(DbClientMetricsAdvice.DURATION_SECONDS_BUCKETS);
59+
DbClientMetricsAdvice.applyClientDurationAdvice(stableDurationBuilder);
60+
duration = stableDurationBuilder.build();
61+
}
62+
63+
@Override
64+
public Context onStart(Context context, Attributes startAttributes, long startNanos) {
65+
return context.with(
66+
DB_CLIENT_REQUEST_METRICS_STATE,
67+
new AutoValue_DbClientMetrics_State(startAttributes, startNanos));
68+
}
69+
70+
@Override
71+
public void onEnd(Context context, Attributes endAttributes, long endNanos) {
72+
State state = context.get(DB_CLIENT_REQUEST_METRICS_STATE);
73+
if (state == null) {
74+
logger.log(
75+
FINE,
76+
"No state present when ending context {0}. Cannot record database request metrics.",
77+
context);
78+
return;
79+
}
80+
81+
Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build();
82+
83+
duration.record((endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context);
84+
}
85+
86+
@AutoValue
87+
abstract static class State {
88+
89+
abstract Attributes startAttributes();
90+
91+
abstract long startTimeNanos();
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.db;
7+
8+
import static java.util.Arrays.asList;
9+
import static java.util.Collections.unmodifiableList;
10+
11+
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder;
12+
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
13+
import io.opentelemetry.semconv.ErrorAttributes;
14+
import io.opentelemetry.semconv.NetworkAttributes;
15+
import io.opentelemetry.semconv.ServerAttributes;
16+
import java.util.List;
17+
18+
final class DbClientMetricsAdvice {
19+
20+
static final List<Double> DURATION_SECONDS_BUCKETS =
21+
unmodifiableList(
22+
asList(0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0));
23+
24+
static void applyClientDurationAdvice(DoubleHistogramBuilder builder) {
25+
if (!(builder instanceof ExtendedDoubleHistogramBuilder)) {
26+
return;
27+
}
28+
((ExtendedDoubleHistogramBuilder) builder)
29+
.setAttributesAdvice(
30+
asList(
31+
DbClientCommonAttributesExtractor.DB_SYSTEM,
32+
SqlClientAttributesExtractor.DB_COLLECTION_NAME,
33+
DbClientCommonAttributesExtractor.DB_NAMESPACE,
34+
DbClientAttributesExtractor.DB_OPERATION_NAME,
35+
// will be implemented in
36+
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/12804
37+
DbClientAttributesExtractor.DB_RESPONSE_STATUS_CODE,
38+
// will be implemented in
39+
// https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/12804
40+
ErrorAttributes.ERROR_TYPE,
41+
NetworkAttributes.NETWORK_PEER_ADDRESS,
42+
NetworkAttributes.NETWORK_PEER_PORT,
43+
ServerAttributes.SERVER_ADDRESS,
44+
ServerAttributes.SERVER_PORT));
45+
}
46+
47+
private DbClientMetricsAdvice() {}
48+
}

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/db/SqlClientAttributesExtractor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class SqlClientAttributesExtractor<REQUEST, RESPONSE>
3333
AttributeKey.stringKey("db.operation.name");
3434
private static final AttributeKey<String> DB_STATEMENT = AttributeKey.stringKey("db.statement");
3535
private static final AttributeKey<String> DB_QUERY_TEXT = AttributeKey.stringKey("db.query.text");
36-
private static final AttributeKey<String> DB_COLLECTION_NAME =
36+
static final AttributeKey<String> DB_COLLECTION_NAME =
3737
AttributeKey.stringKey("db.collection.name");
3838

3939
/** Creates the SQL client attributes extractor with default configuration. */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.db;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo;
10+
11+
import io.opentelemetry.api.common.Attributes;
12+
import io.opentelemetry.api.trace.Span;
13+
import io.opentelemetry.api.trace.SpanContext;
14+
import io.opentelemetry.api.trace.TraceFlags;
15+
import io.opentelemetry.api.trace.TraceState;
16+
import io.opentelemetry.context.Context;
17+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
18+
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
19+
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
20+
import io.opentelemetry.semconv.ErrorAttributes;
21+
import io.opentelemetry.semconv.NetworkAttributes;
22+
import io.opentelemetry.semconv.ServerAttributes;
23+
import java.util.concurrent.TimeUnit;
24+
import org.junit.jupiter.api.Test;
25+
26+
class DbClientMetricsTest {
27+
28+
static final double[] DURATION_BUCKETS =
29+
DbClientMetricsAdvice.DURATION_SECONDS_BUCKETS.stream().mapToDouble(d -> d).toArray();
30+
31+
@Test
32+
void collectsMetrics() {
33+
InMemoryMetricReader metricReader = InMemoryMetricReader.create();
34+
SdkMeterProvider meterProvider =
35+
SdkMeterProvider.builder().registerMetricReader(metricReader).build();
36+
37+
OperationListener listener = DbClientMetrics.get().create(meterProvider.get("test"));
38+
39+
Attributes requestAttributes =
40+
Attributes.builder()
41+
.put(DbClientCommonAttributesExtractor.DB_SYSTEM, "myDb")
42+
.put(SqlClientAttributesExtractor.DB_COLLECTION_NAME, "table")
43+
.put(DbClientCommonAttributesExtractor.DB_NAMESPACE, "potatoes")
44+
.put(DbClientAttributesExtractor.DB_OPERATION_NAME, "SELECT")
45+
.put(ServerAttributes.SERVER_ADDRESS, "localhost")
46+
.put(ServerAttributes.SERVER_PORT, 1234)
47+
.build();
48+
49+
Attributes responseAttributes =
50+
Attributes.builder()
51+
.put(DbClientAttributesExtractor.DB_RESPONSE_STATUS_CODE, 200)
52+
.put(ErrorAttributes.ERROR_TYPE, "400")
53+
.put(NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4")
54+
.put(NetworkAttributes.NETWORK_PEER_PORT, 8080)
55+
.build();
56+
57+
Context parent =
58+
Context.root()
59+
.with(
60+
Span.wrap(
61+
SpanContext.create(
62+
"ff01020304050600ff0a0b0c0d0e0f00",
63+
"090a0b0c0d0e0f00",
64+
TraceFlags.getSampled(),
65+
TraceState.getDefault())));
66+
67+
Context context1 = listener.onStart(parent, requestAttributes, nanos(100));
68+
69+
assertThat(metricReader.collectAllMetrics()).isEmpty();
70+
71+
listener.onEnd(context1, responseAttributes, nanos(250));
72+
73+
assertThat(metricReader.collectAllMetrics())
74+
.satisfiesExactlyInAnyOrder(
75+
metric ->
76+
assertThat(metric)
77+
.hasName("db.client.operation.duration")
78+
.hasUnit("s")
79+
.hasDescription("Duration of database client operations.")
80+
.hasHistogramSatisfying(
81+
histogram ->
82+
histogram.hasPointsSatisfying(
83+
point ->
84+
point
85+
.hasSum(0.15 /* seconds */)
86+
.hasAttributesSatisfying(
87+
equalTo(
88+
DbClientCommonAttributesExtractor.DB_SYSTEM,
89+
"myDb"),
90+
equalTo(
91+
DbClientCommonAttributesExtractor.DB_NAMESPACE,
92+
"potatoes"),
93+
equalTo(
94+
DbClientAttributesExtractor.DB_OPERATION_NAME,
95+
"SELECT"),
96+
equalTo(
97+
SqlClientAttributesExtractor.DB_COLLECTION_NAME,
98+
"table"),
99+
equalTo(ServerAttributes.SERVER_ADDRESS, "localhost"),
100+
equalTo(ServerAttributes.SERVER_PORT, 1234),
101+
equalTo(
102+
DbClientAttributesExtractor.DB_RESPONSE_STATUS_CODE,
103+
200),
104+
equalTo(ErrorAttributes.ERROR_TYPE, "400"),
105+
equalTo(
106+
NetworkAttributes.NETWORK_PEER_ADDRESS, "1.2.3.4"),
107+
equalTo(NetworkAttributes.NETWORK_PEER_PORT, 8080))
108+
.hasExemplarsSatisfying(
109+
exemplar ->
110+
exemplar
111+
.hasTraceId("ff01020304050600ff0a0b0c0d0e0f00")
112+
.hasSpanId("090a0b0c0d0e0f00"))
113+
.hasBucketBoundaries(DURATION_BUCKETS))));
114+
115+
}
116+
117+
private static long nanos(int millis) {
118+
return TimeUnit.MILLISECONDS.toNanos(millis);
119+
}
120+
}

instrumentation/clickhouse-client-0.5/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/clickhouse/ClickHouseSingletons.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import io.opentelemetry.api.GlobalOpenTelemetry;
99
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor;
10+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
1011
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
1112
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1213
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
@@ -27,6 +28,7 @@ public final class ClickHouseSingletons {
2728
.addAttributesExtractor(DbClientAttributesExtractor.create(dbAttributesGetter))
2829
.addAttributesExtractor(
2930
ServerAttributesExtractor.create(new ClickHouseNetworkAttributesGetter()))
31+
.addOperationMetrics(DbClientMetrics.get())
3032
.buildInstrumenter(SpanKindExtractor.alwaysClient());
3133
}
3234

instrumentation/couchbase/couchbase-2.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/couchbase/v2_0/CouchbaseSingletons.java

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

88
import io.opentelemetry.api.GlobalOpenTelemetry;
99
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor;
10+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
1011
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
1112
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1213
import io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder;
@@ -34,7 +35,8 @@ public final class CouchbaseSingletons {
3435
.addAttributesExtractor(NetworkAttributesExtractor.create(netAttributesGetter))
3536
.addContextCustomizer(
3637
(context, couchbaseRequest, startAttributes) ->
37-
CouchbaseRequestInfo.init(context, couchbaseRequest));
38+
CouchbaseRequestInfo.init(context, couchbaseRequest))
39+
.addOperationMetrics(DbClientMetrics.get());
3840

3941
if (AgentInstrumentationConfig.get()
4042
.getBoolean("otel.instrumentation.couchbase.experimental-span-attributes", false)) {

instrumentation/elasticsearch/elasticsearch-rest-common/library/src/main/java/io/opentelemetry/instrumentation/elasticsearch/rest/internal/ElasticsearchRestInstrumenterFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import io.opentelemetry.api.OpenTelemetry;
99
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor;
10+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
1011
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
1112
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
1213
import io.opentelemetry.instrumentation.api.instrumenter.SpanKindExtractor;
@@ -47,6 +48,7 @@ public static Instrumenter<ElasticsearchRestRequest, Response> create(
4748
.addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter))
4849
.addAttributesExtractor(esClientAtrributesExtractor)
4950
.addAttributesExtractors(attributesExtractors)
51+
.addOperationMetrics(DbClientMetrics.get())
5052
.buildInstrumenter(SpanKindExtractor.alwaysClient());
5153
}
5254
}

instrumentation/elasticsearch/elasticsearch-transport-common/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/elasticsearch/transport/ElasticsearchTransportInstrumenterFactory.java

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

88
import io.opentelemetry.api.GlobalOpenTelemetry;
99
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientAttributesExtractor;
10+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
1011
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientSpanNameExtractor;
1112
import io.opentelemetry.instrumentation.api.instrumenter.AttributesExtractor;
1213
import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter;
@@ -34,7 +35,8 @@ public static Instrumenter<ElasticTransportRequest, ActionResponse> create(
3435
instrumentationName,
3536
DbClientSpanNameExtractor.create(dbClientAttributesGetter))
3637
.addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter))
37-
.addAttributesExtractor(netAttributesExtractor);
38+
.addAttributesExtractor(netAttributesExtractor)
39+
.addOperationMetrics(DbClientMetrics.get());
3840

3941
if (CAPTURE_EXPERIMENTAL_SPAN_ATTRIBUTES) {
4042
instrumenterBuilder.addAttributesExtractor(experimentalAttributesExtractor);

0 commit comments

Comments
 (0)