Skip to content

Commit 54f2f0c

Browse files
authored
Add gen_ai metrics to AWS Bedrock instrumentation (#13408)
1 parent fdcf0d2 commit 54f2f0c

File tree

5 files changed

+346
-12
lines changed

5 files changed

+346
-12
lines changed

instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesExtractor.java

+6-10
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,14 @@ public final class GenAiAttributesExtractor<REQUEST, RESPONSE>
2929
implements AttributesExtractor<REQUEST, RESPONSE> {
3030

3131
// copied from GenAiIncubatingAttributes
32-
private static final AttributeKey<String> GEN_AI_OPERATION_NAME =
33-
stringKey("gen_ai.operation.name");
32+
static final AttributeKey<String> GEN_AI_OPERATION_NAME = stringKey("gen_ai.operation.name");
3433
private static final AttributeKey<List<String>> GEN_AI_REQUEST_ENCODING_FORMATS =
3534
stringArrayKey("gen_ai.request.encoding_formats");
3635
private static final AttributeKey<Double> GEN_AI_REQUEST_FREQUENCY_PENALTY =
3736
doubleKey("gen_ai.request.frequency_penalty");
3837
private static final AttributeKey<Long> GEN_AI_REQUEST_MAX_TOKENS =
3938
longKey("gen_ai.request.max_tokens");
40-
private static final AttributeKey<String> GEN_AI_REQUEST_MODEL =
41-
stringKey("gen_ai.request.model");
39+
static final AttributeKey<String> GEN_AI_REQUEST_MODEL = stringKey("gen_ai.request.model");
4240
private static final AttributeKey<Double> GEN_AI_REQUEST_PRESENCE_PENALTY =
4341
doubleKey("gen_ai.request.presence_penalty");
4442
private static final AttributeKey<Long> GEN_AI_REQUEST_SEED = longKey("gen_ai.request.seed");
@@ -53,12 +51,10 @@ public final class GenAiAttributesExtractor<REQUEST, RESPONSE>
5351
private static final AttributeKey<List<String>> GEN_AI_RESPONSE_FINISH_REASONS =
5452
stringArrayKey("gen_ai.response.finish_reasons");
5553
private static final AttributeKey<String> GEN_AI_RESPONSE_ID = stringKey("gen_ai.response.id");
56-
private static final AttributeKey<String> GEN_AI_RESPONSE_MODEL =
57-
stringKey("gen_ai.response.model");
58-
private static final AttributeKey<String> GEN_AI_SYSTEM = stringKey("gen_ai.system");
59-
private static final AttributeKey<Long> GEN_AI_USAGE_INPUT_TOKENS =
60-
longKey("gen_ai.usage.input_tokens");
61-
private static final AttributeKey<Long> GEN_AI_USAGE_OUTPUT_TOKENS =
54+
static final AttributeKey<String> GEN_AI_RESPONSE_MODEL = stringKey("gen_ai.response.model");
55+
static final AttributeKey<String> GEN_AI_SYSTEM = stringKey("gen_ai.system");
56+
static final AttributeKey<Long> GEN_AI_USAGE_INPUT_TOKENS = longKey("gen_ai.usage.input_tokens");
57+
static final AttributeKey<Long> GEN_AI_USAGE_OUTPUT_TOKENS =
6258
longKey("gen_ai.usage.output_tokens");
6359

6460
/** Creates the GenAI attributes extractor. */
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.genai;
7+
8+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
9+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor.GEN_AI_USAGE_INPUT_TOKENS;
10+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor.GEN_AI_USAGE_OUTPUT_TOKENS;
11+
import static java.util.logging.Level.FINE;
12+
13+
import com.google.auto.value.AutoValue;
14+
import io.opentelemetry.api.common.AttributeKey;
15+
import io.opentelemetry.api.common.Attributes;
16+
import io.opentelemetry.api.common.AttributesBuilder;
17+
import io.opentelemetry.api.metrics.DoubleHistogram;
18+
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
19+
import io.opentelemetry.api.metrics.LongHistogram;
20+
import io.opentelemetry.api.metrics.LongHistogramBuilder;
21+
import io.opentelemetry.api.metrics.Meter;
22+
import io.opentelemetry.context.Context;
23+
import io.opentelemetry.context.ContextKey;
24+
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
25+
import io.opentelemetry.instrumentation.api.instrumenter.OperationListener;
26+
import io.opentelemetry.instrumentation.api.instrumenter.OperationMetrics;
27+
import io.opentelemetry.instrumentation.api.internal.OperationMetricsUtil;
28+
import java.util.concurrent.TimeUnit;
29+
import java.util.logging.Logger;
30+
31+
/**
32+
* {@link OperationListener} which keeps track of <a
33+
* href="https://opentelemetry.io/docs/specs/semconv/gen-ai/gen-ai-metrics/#generative-ai-client-metrics">Generative
34+
* AI Client Metrics</a>.
35+
*/
36+
public final class GenAiClientMetrics implements OperationListener {
37+
38+
private static final double NANOS_PER_S = TimeUnit.SECONDS.toNanos(1);
39+
40+
private static final ContextKey<State> GEN_AI_CLIENT_METRICS_STATE =
41+
ContextKey.named("gen-ai-client-metrics-state");
42+
43+
private static final Logger logger = Logger.getLogger(DbClientMetrics.class.getName());
44+
45+
static final AttributeKey<String> GEN_AI_TOKEN_TYPE = stringKey("gen_ai.token.type");
46+
47+
/**
48+
* Returns an {@link OperationMetrics} instance which can be used to enable recording of {@link
49+
* GenAiClientMetrics}.
50+
*
51+
* @see
52+
* io.opentelemetry.instrumentation.api.instrumenter.InstrumenterBuilder#addOperationMetrics(OperationMetrics)
53+
*/
54+
public static OperationMetrics get() {
55+
return OperationMetricsUtil.create("gen_ai client", GenAiClientMetrics::new);
56+
}
57+
58+
private final LongHistogram tokenUsage;
59+
private final DoubleHistogram operationDuration;
60+
61+
private GenAiClientMetrics(Meter meter) {
62+
LongHistogramBuilder tokenUsageBuilder =
63+
meter
64+
.histogramBuilder("gen_ai.client.token.usage")
65+
.ofLongs()
66+
.setUnit("{token}")
67+
.setDescription("Measures number of input and output tokens used")
68+
.setExplicitBucketBoundariesAdvice(GenAiMetricsAdvice.CLIENT_TOKEN_USAGE_BUCKETS);
69+
GenAiMetricsAdvice.applyClientTokenUsageAdvice(tokenUsageBuilder);
70+
this.tokenUsage = tokenUsageBuilder.build();
71+
DoubleHistogramBuilder operationDurationBuilder =
72+
meter
73+
.histogramBuilder("gen_ai.client.operation.duration")
74+
.setUnit("s")
75+
.setDescription("GenAI operation duration")
76+
.setExplicitBucketBoundariesAdvice(
77+
GenAiMetricsAdvice.CLIENT_OPERATION_DURATION_BUCKETS);
78+
GenAiMetricsAdvice.applyClientOperationDurationAdvice(operationDurationBuilder);
79+
this.operationDuration = operationDurationBuilder.build();
80+
}
81+
82+
@Override
83+
public Context onStart(Context context, Attributes startAttributes, long startNanos) {
84+
return context.with(
85+
GEN_AI_CLIENT_METRICS_STATE,
86+
new AutoValue_GenAiClientMetrics_State(startAttributes, startNanos));
87+
}
88+
89+
@Override
90+
public void onEnd(Context context, Attributes endAttributes, long endNanos) {
91+
State state = context.get(GEN_AI_CLIENT_METRICS_STATE);
92+
if (state == null) {
93+
logger.log(
94+
FINE,
95+
"No state present when ending context {0}. Cannot record gen_ai operation metrics.",
96+
context);
97+
return;
98+
}
99+
100+
AttributesBuilder attributesBuilder = state.startAttributes().toBuilder().putAll(endAttributes);
101+
102+
operationDuration.record(
103+
(endNanos - state.startTimeNanos()) / NANOS_PER_S, attributesBuilder.build(), context);
104+
105+
Long inputTokens = endAttributes.get(GEN_AI_USAGE_INPUT_TOKENS);
106+
if (inputTokens != null) {
107+
tokenUsage.record(
108+
inputTokens, attributesBuilder.put(GEN_AI_TOKEN_TYPE, "input").build(), context);
109+
}
110+
Long outputTokens = endAttributes.get(GEN_AI_USAGE_OUTPUT_TOKENS);
111+
if (outputTokens != null) {
112+
tokenUsage.record(
113+
outputTokens, attributesBuilder.put(GEN_AI_TOKEN_TYPE, "output").build(), context);
114+
}
115+
}
116+
117+
@AutoValue
118+
abstract static class State {
119+
abstract Attributes startAttributes();
120+
121+
abstract long startTimeNanos();
122+
}
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.api.incubator.semconv.genai;
7+
8+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor.GEN_AI_OPERATION_NAME;
9+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor.GEN_AI_REQUEST_MODEL;
10+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor.GEN_AI_RESPONSE_MODEL;
11+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor.GEN_AI_SYSTEM;
12+
import static io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiClientMetrics.GEN_AI_TOKEN_TYPE;
13+
import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS;
14+
import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT;
15+
import static java.util.Arrays.asList;
16+
import static java.util.Collections.unmodifiableList;
17+
18+
import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder;
19+
import io.opentelemetry.api.incubator.metrics.ExtendedLongHistogramBuilder;
20+
import io.opentelemetry.api.metrics.DoubleHistogramBuilder;
21+
import io.opentelemetry.api.metrics.LongHistogramBuilder;
22+
import io.opentelemetry.semconv.ErrorAttributes;
23+
import java.util.List;
24+
25+
final class GenAiMetricsAdvice {
26+
27+
static final List<Double> CLIENT_OPERATION_DURATION_BUCKETS =
28+
unmodifiableList(
29+
asList(
30+
0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96,
31+
81.92));
32+
33+
static final List<Long> CLIENT_TOKEN_USAGE_BUCKETS =
34+
unmodifiableList(
35+
asList(
36+
1L, 4L, 16L, 64L, 256L, 1024L, 4096L, 16384L, 65536L, 262144L, 1048576L, 4194304L,
37+
16777216L, 67108864L));
38+
39+
static void applyClientTokenUsageAdvice(LongHistogramBuilder builder) {
40+
if (!(builder instanceof ExtendedLongHistogramBuilder)) {
41+
return;
42+
}
43+
((ExtendedLongHistogramBuilder) builder)
44+
.setAttributesAdvice(
45+
asList(
46+
GEN_AI_OPERATION_NAME,
47+
GEN_AI_SYSTEM,
48+
GEN_AI_TOKEN_TYPE,
49+
GEN_AI_REQUEST_MODEL,
50+
SERVER_PORT,
51+
GEN_AI_RESPONSE_MODEL,
52+
SERVER_ADDRESS));
53+
}
54+
55+
static void applyClientOperationDurationAdvice(DoubleHistogramBuilder builder) {
56+
if (!(builder instanceof ExtendedDoubleHistogramBuilder)) {
57+
return;
58+
}
59+
((ExtendedDoubleHistogramBuilder) builder)
60+
.setAttributesAdvice(
61+
asList(
62+
GEN_AI_OPERATION_NAME,
63+
GEN_AI_SYSTEM,
64+
ErrorAttributes.ERROR_TYPE,
65+
GEN_AI_REQUEST_MODEL,
66+
SERVER_PORT,
67+
GEN_AI_RESPONSE_MODEL,
68+
SERVER_ADDRESS));
69+
}
70+
71+
private GenAiMetricsAdvice() {}
72+
}

instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkInstrumenterFactory.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.opentelemetry.context.propagation.TextMapPropagator;
1616
import io.opentelemetry.instrumentation.api.incubator.semconv.db.DbClientMetrics;
1717
import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesExtractor;
18+
import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiClientMetrics;
1819
import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiSpanNameExtractor;
1920
import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessageOperation;
2021
import io.opentelemetry.instrumentation.api.incubator.semconv.messaging.MessagingAttributesExtractor;
@@ -228,8 +229,10 @@ public Instrumenter<ExecutionAttributes, Response> bedrockRuntimeInstrumenter()
228229
SpanKindExtractor.alwaysClient(),
229230
attributesExtractors(),
230231
builder ->
231-
builder.addAttributesExtractor(
232-
GenAiAttributesExtractor.create(BedrockRuntimeAttributesGetter.INSTANCE)),
232+
builder
233+
.addAttributesExtractor(
234+
GenAiAttributesExtractor.create(BedrockRuntimeAttributesGetter.INSTANCE))
235+
.addOperationMetrics(GenAiClientMetrics.get()),
233236
true);
234237
}
235238

0 commit comments

Comments
 (0)