Skip to content

Commit ca16c48

Browse files
authored
add db client metrics (#12806)
1 parent a8d7eeb commit ca16c48

File tree

24 files changed

+313
-11
lines changed

24 files changed

+313
-11
lines changed

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

+4-4
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

+2-2
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,97 @@
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 io.opentelemetry.instrumentation.api.internal.SemconvStability;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.logging.Logger;
24+
25+
/**
26+
* {@link OperationListener} which keeps track of <a
27+
* href="https://opentelemetry.io/docs/specs/semconv/database/database-metrics/#metric-dbclientoperationduration">Database
28+
* client metrics</a>.
29+
*
30+
* @since 2.11.0
31+
*/
32+
public final class DbClientMetrics implements OperationListener {
33+
34+
private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1);
35+
36+
private static final ContextKey<State> DB_CLIENT_OPERATION_METRICS_STATE =
37+
ContextKey.named("db-client-metrics-state");
38+
39+
private static final Logger logger = Logger.getLogger(DbClientMetrics.class.getName());
40+
41+
/**
42+
* Returns an {@link OperationMetrics} instance which can be used to enable recording of {@link
43+
* DbClientMetrics}.
44+
*
45+
* @see InstrumenterBuilder#addOperationMetrics(OperationMetrics)
46+
*/
47+
public static OperationMetrics get() {
48+
if (SemconvStability.emitStableDatabaseSemconv()) {
49+
return OperationMetricsUtil.create("database client", DbClientMetrics::new);
50+
}
51+
return meter -> OperationMetricsUtil.NOOP_OPERATION_LISTENER;
52+
}
53+
54+
private final DoubleHistogram duration;
55+
56+
private DbClientMetrics(Meter meter) {
57+
DoubleHistogramBuilder stableDurationBuilder =
58+
meter
59+
.histogramBuilder("db.client.operation.duration")
60+
.setUnit("s")
61+
.setDescription("Duration of database client operations.")
62+
.setExplicitBucketBoundariesAdvice(DbClientMetricsAdvice.DURATION_SECONDS_BUCKETS);
63+
DbClientMetricsAdvice.applyClientDurationAdvice(stableDurationBuilder);
64+
duration = stableDurationBuilder.build();
65+
}
66+
67+
@Override
68+
public Context onStart(Context context, Attributes startAttributes, long startNanos) {
69+
return context.with(
70+
DB_CLIENT_OPERATION_METRICS_STATE,
71+
new AutoValue_DbClientMetrics_State(startAttributes, startNanos));
72+
}
73+
74+
@Override
75+
public void onEnd(Context context, Attributes endAttributes, long endNanos) {
76+
State state = context.get(DB_CLIENT_OPERATION_METRICS_STATE);
77+
if (state == null) {
78+
logger.log(
79+
FINE,
80+
"No state present when ending context {0}. Cannot record database operation metrics.",
81+
context);
82+
return;
83+
}
84+
85+
Attributes attributes = state.startAttributes().toBuilder().putAll(endAttributes).build();
86+
87+
duration.record((endNanos - state.startTimeNanos()) / NANOS_PER_S, attributes, context);
88+
}
89+
90+
@AutoValue
91+
abstract static class State {
92+
93+
abstract Attributes startAttributes();
94+
95+
abstract long startTimeNanos();
96+
}
97+
}
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

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

instrumentation-api/src/main/java/io/opentelemetry/instrumentation/api/internal/OperationMetricsUtil.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*/
2424
public class OperationMetricsUtil {
2525
private static final Logger logger = Logger.getLogger(OperationMetricsUtil.class.getName());
26-
private static final OperationListener NOOP_OPERATION_LISTENER =
26+
public static final OperationListener NOOP_OPERATION_LISTENER =
2727
new OperationListener() {
2828

2929
@Override

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

+2
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

+3-1
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

+2
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

+3-1
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);

instrumentation/geode-1.4/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/geode/GeodeSingletons.java

+2
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;
@@ -25,6 +26,7 @@ public final class GeodeSingletons {
2526
INSTRUMENTATION_NAME,
2627
DbClientSpanNameExtractor.create(dbClientAttributesGetter))
2728
.addAttributesExtractor(DbClientAttributesExtractor.create(dbClientAttributesGetter))
29+
.addOperationMetrics(DbClientMetrics.get())
2830
.buildInstrumenter(SpanKindExtractor.alwaysClient());
2931
}
3032

0 commit comments

Comments
 (0)