diff --git a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java index e133518fb40e..91b5a9f5bb02 100644 --- a/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java +++ b/instrumentation-api-incubator/src/main/java/io/opentelemetry/instrumentation/api/incubator/semconv/genai/GenAiAttributesGetter.java @@ -50,7 +50,6 @@ public interface GenAiAttributesGetter { @Nullable Double getRequestTopP(REQUEST request); - @Nullable List getResponseFinishReasons(REQUEST request, RESPONSE response); @Nullable diff --git a/instrumentation/aws-sdk/README.md b/instrumentation/aws-sdk/README.md index dd332b7cfe36..6a45865b9ef7 100644 --- a/instrumentation/aws-sdk/README.md +++ b/instrumentation/aws-sdk/README.md @@ -6,7 +6,8 @@ For more information, see the respective public setters in the `AwsSdkTelemetryB - [SDK v2](./aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java) | System property | Type | Default | Description | -|--------------------------------------------------------------------------| ------- | ------- |---------------------------------------------------------------------------------------------------------------------------------------| +|--------------------------------------------------------------------------|---------|---------|---------------------------------------------------------------------------------------------------------------------------------------| | `otel.instrumentation.aws-sdk.experimental-span-attributes` | Boolean | `false` | Enable the capture of experimental span attributes. | | `otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging` | Boolean | `false` | v2 only, inject into SNS/SQS attributes with configured propagator: See [v2 README](aws-sdk-2.2/library/README.md#trace-propagation). | | `otel.instrumentation.aws-sdk.experimental-record-individual-http-error` | Boolean | `false` | v2 only, record errors returned by each individual HTTP request as events for the SDK span. | +| `otel.instrumentation.genai.capture-message-content` | Boolean | `false` | v2 only, record content of user and LLM messages when using Bedrock. | diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts index 446f729fb580..3067ba91ae19 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/build.gradle.kts @@ -147,6 +147,15 @@ testing { implementation("software.amazon.awssdk:bedrockruntime:2.25.63") } } + + targets { + all { + testTask.configure { + // TODO run tests both with and without genai message capture + systemProperty("otel.instrumentation.genai.capture-message-content", "true") + } + } + } } } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java index dddb124eb938..96a8c9778695 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/autoconfigure/AwsSdkSingletons.java @@ -24,6 +24,7 @@ public final class AwsSdkSingletons { .setMessagingReceiveInstrumentationEnabled(messagingReceiveInstrumentationEnabled()) .setUseConfiguredPropagatorForMessaging(useMessagingPropagator()) .setRecordIndividualHttpError(recordIndividualHttpError()) + .setGenaiCaptureMessageContent(genaiCaptureMessageContent()) .build(); private static boolean hasAgentConfiguration() { @@ -67,6 +68,10 @@ private static boolean recordIndividualHttpError() { "otel.instrumentation.aws-sdk.experimental-record-individual-http-error", false); } + private static boolean genaiCaptureMessageContent() { + return getBoolean("otel.instrumentation.genai.capture-message-content", false); + } + private static boolean getBoolean(String name, boolean defaultValue) { if (HAS_INSTRUMENTATION_CONFIG) { return AgentInstrumentationConfig.get().getBoolean(name, defaultValue); diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java index af08297953fd..ef12069c928f 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetry.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.logs.Logger; import io.opentelemetry.context.propagation.TextMapPropagator; import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; import io.opentelemetry.instrumentation.awssdk.v2_2.internal.AwsSdkInstrumenterFactory; @@ -56,10 +57,12 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { private final Instrumenter producerInstrumenter; private final Instrumenter dynamoDbInstrumenter; private final Instrumenter bedrockRuntimeInstrumenter; + private final Logger eventLogger; private final boolean captureExperimentalSpanAttributes; @Nullable private final TextMapPropagator messagingPropagator; private final boolean useXrayPropagator; private final boolean recordIndividualHttpError; + private final boolean genAiCaptureMessageContent; AwsSdkTelemetry( OpenTelemetry openTelemetry, @@ -68,7 +71,8 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { boolean useMessagingPropagator, boolean useXrayPropagator, boolean recordIndividualHttpError, - boolean messagingReceiveInstrumentationEnabled) { + boolean messagingReceiveInstrumentationEnabled, + boolean genAiCaptureMessageContent) { this.useXrayPropagator = useXrayPropagator; this.messagingPropagator = useMessagingPropagator ? openTelemetry.getPropagators().getTextMapPropagator() : null; @@ -88,8 +92,10 @@ public static AwsSdkTelemetryBuilder builder(OpenTelemetry openTelemetry) { this.producerInstrumenter = instrumenterFactory.producerInstrumenter(); this.dynamoDbInstrumenter = instrumenterFactory.dynamoDbInstrumenter(); this.bedrockRuntimeInstrumenter = instrumenterFactory.bedrockRuntimeInstrumenter(); + this.eventLogger = instrumenterFactory.eventLogger(); this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; this.recordIndividualHttpError = recordIndividualHttpError; + this.genAiCaptureMessageContent = genAiCaptureMessageContent; } /** @@ -104,10 +110,12 @@ public ExecutionInterceptor newExecutionInterceptor() { producerInstrumenter, dynamoDbInstrumenter, bedrockRuntimeInstrumenter, + eventLogger, captureExperimentalSpanAttributes, messagingPropagator, useXrayPropagator, - recordIndividualHttpError); + recordIndividualHttpError, + genAiCaptureMessageContent); } /** diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java index 5cf5c757b4f9..176d0a894a39 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkTelemetryBuilder.java @@ -24,6 +24,7 @@ public final class AwsSdkTelemetryBuilder { private boolean recordIndividualHttpError; private boolean useXrayPropagator = true; private boolean messagingReceiveInstrumentationEnabled; + private boolean genaiCaptureMessageContent; AwsSdkTelemetryBuilder(OpenTelemetry openTelemetry) { this.openTelemetry = openTelemetry; @@ -115,6 +116,18 @@ public AwsSdkTelemetryBuilder setMessagingReceiveInstrumentationEnabled( return this; } + /** + * Set whether Generative AI events include full content of user and assistant messages. + * + *

Note that full content can have data privacy and size concerns and care should be taken when + * enabling this. + */ + @CanIgnoreReturnValue + public AwsSdkTelemetryBuilder setGenaiCaptureMessageContent(boolean genaiCaptureMessageContent) { + this.genaiCaptureMessageContent = genaiCaptureMessageContent; + return this; + } + /** * Returns a new {@link AwsSdkTelemetry} with the settings of this {@link AwsSdkTelemetryBuilder}. */ @@ -126,6 +139,7 @@ public AwsSdkTelemetry build() { useMessagingPropagator, useXrayPropagator, recordIndividualHttpError, - messagingReceiveInstrumentationEnabled); + messagingReceiveInstrumentationEnabled, + genaiCaptureMessageContent); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkInstrumenterFactory.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkInstrumenterFactory.java index b35a174b244a..b6e8cb44c867 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkInstrumenterFactory.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/AwsSdkInstrumenterFactory.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.opentelemetry.context.propagation.TextMapPropagator; @@ -236,6 +237,10 @@ public Instrumenter bedrockRuntimeInstrumenter() true); } + public Logger eventLogger() { + return openTelemetry.getLogsBridge().get(INSTRUMENTATION_NAME); + } + private static Instrumenter createInstrumenter( OpenTelemetry openTelemetry, SpanNameExtractor spanNameExtractor, diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAccess.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAccess.java index 15ceecffe005..6bf10ae48620 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAccess.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAccess.java @@ -5,6 +5,8 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.context.Context; import io.opentelemetry.javaagent.tooling.muzzle.NoMuzzle; import java.util.List; import javax.annotation.Nullable; @@ -37,6 +39,11 @@ static boolean isBedrockRuntimeRequest(SdkRequest request) { return enabled && BedrockRuntimeImpl.isBedrockRuntimeRequest(request); } + @NoMuzzle + static boolean isBedrockRuntimeResponse(SdkResponse response) { + return enabled && BedrockRuntimeImpl.isBedrockRuntimeResponse(response); + } + @Nullable @NoMuzzle static String getModelId(SdkRequest request) { @@ -84,4 +91,25 @@ static Long getUsageInputTokens(SdkResponse response) { static Long getUsageOutputTokens(SdkResponse response) { return enabled ? BedrockRuntimeImpl.getUsageOutputTokens(response) : null; } + + @NoMuzzle + static void recordRequestEvents( + Context otelContext, Logger eventLogger, SdkRequest request, boolean captureMessageContent) { + if (enabled) { + BedrockRuntimeImpl.recordRequestEvents( + otelContext, eventLogger, request, captureMessageContent); + } + } + + @NoMuzzle + static void recordResponseEvents( + Context otelContext, + Logger eventLogger, + SdkResponse response, + boolean captureMessageContent) { + if (enabled) { + BedrockRuntimeImpl.recordResponseEvents( + otelContext, eventLogger, response, captureMessageContent); + } + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAttributesGetter.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAttributesGetter.java index 94ec6e71f59f..d91b5a47b009 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAttributesGetter.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeAttributesGetter.java @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; import static io.opentelemetry.instrumentation.awssdk.v2_2.internal.TracingExecutionInterceptor.SDK_REQUEST_ATTRIBUTE; +import static java.util.Collections.emptyList; import io.opentelemetry.instrumentation.api.incubator.semconv.genai.GenAiAttributesGetter; import java.util.Arrays; @@ -25,7 +26,7 @@ private static final class GenAiOperationNameIncubatingValues { private GenAiOperationNameIncubatingValues() {} } - private static final class GenAiSystemIncubatingValues { + static final class GenAiSystemIncubatingValues { static final String AWS_BEDROCK = "aws.bedrock"; private GenAiSystemIncubatingValues() {} @@ -113,13 +114,15 @@ public Double getRequestTopP(ExecutionAttributes executionAttributes) { return BedrockRuntimeAccess.getTopP(executionAttributes.getAttribute(SDK_REQUEST_ATTRIBUTE)); } - @Nullable @Override public List getResponseFinishReasons( - ExecutionAttributes executionAttributes, Response response) { + ExecutionAttributes executionAttributes, @Nullable Response response) { + if (response == null) { + return emptyList(); + } String stopReason = BedrockRuntimeAccess.getStopReason(response.getSdkResponse()); if (stopReason == null) { - return null; + return emptyList(); } return Arrays.asList(stopReason); } @@ -138,13 +141,21 @@ public String getResponseModel(ExecutionAttributes executionAttributes, Response @Nullable @Override - public Long getUsageInputTokens(ExecutionAttributes executionAttributes, Response response) { + public Long getUsageInputTokens( + ExecutionAttributes executionAttributes, @Nullable Response response) { + if (response == null) { + return null; + } return BedrockRuntimeAccess.getUsageInputTokens(response.getSdkResponse()); } @Nullable @Override - public Long getUsageOutputTokens(ExecutionAttributes executionAttributes, Response response) { + public Long getUsageOutputTokens( + ExecutionAttributes executionAttributes, @Nullable Response response) { + if (response == null) { + return null; + } return BedrockRuntimeAccess.getUsageOutputTokens(response.getSdkResponse()); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeImpl.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeImpl.java index 855063dddf4f..e48825ecbc66 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeImpl.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/BedrockRuntimeImpl.java @@ -5,19 +5,42 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; +import static io.opentelemetry.api.common.AttributeKey.stringKey; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.logs.LogRecordBuilder; +import io.opentelemetry.api.logs.Logger; +import io.opentelemetry.context.Context; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; import software.amazon.awssdk.core.SdkRequest; import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.protocols.json.SdkJsonGenerator; +import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock; import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest; import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; import software.amazon.awssdk.services.bedrockruntime.model.InferenceConfiguration; +import software.amazon.awssdk.services.bedrockruntime.model.Message; import software.amazon.awssdk.services.bedrockruntime.model.StopReason; import software.amazon.awssdk.services.bedrockruntime.model.TokenUsage; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolUseBlock; +import software.amazon.awssdk.thirdparty.jackson.core.JsonFactory; final class BedrockRuntimeImpl { private BedrockRuntimeImpl() {} + private static final AttributeKey EVENT_NAME = stringKey("event.name"); + private static final AttributeKey GEN_AI_SYSTEM = stringKey("gen_ai.system"); + + private static final JsonFactory JSON_FACTORY = new JsonFactory(); + static boolean isBedrockRuntimeRequest(SdkRequest request) { if (request instanceof ConverseRequest) { return true; @@ -25,6 +48,13 @@ static boolean isBedrockRuntimeRequest(SdkRequest request) { return false; } + static boolean isBedrockRuntimeResponse(SdkResponse request) { + if (request instanceof ConverseResponse) { + return true; + } + return false; + } + @Nullable static String getModelId(SdkRequest request) { if (request instanceof ConverseRequest) { @@ -110,6 +140,61 @@ static Long getUsageOutputTokens(SdkResponse response) { return null; } + static void recordRequestEvents( + Context otelContext, Logger eventLogger, SdkRequest request, boolean captureMessageContent) { + if (request instanceof ConverseRequest) { + for (Message message : ((ConverseRequest) request).messages()) { + long numToolResults = + message.content().stream().filter(block -> block.toolResult() != null).count(); + if (numToolResults > 0) { + // Tool results are different from others, emitting multiple events for a single message, + // so treat them separately. + emitToolResultEvents(otelContext, eventLogger, message, captureMessageContent); + if (numToolResults == message.content().size()) { + continue; + } + // There are content blocks besides tool results in the same message. While models + // generally don't expect such usage, the SDK allows it so go ahead and generate a normal + // message too. + } + LogRecordBuilder event = newEvent(otelContext, eventLogger); + switch (message.role()) { + case ASSISTANT: + event.setAttribute(EVENT_NAME, "gen_ai.assistant.message"); + break; + case USER: + event.setAttribute(EVENT_NAME, "gen_ai.user.message"); + break; + default: + // unknown role, shouldn't happen in practice + continue; + } + // Requests don't have index or stop reason. + event.setBody(convertMessage(message, -1, null, captureMessageContent)).emit(); + } + } + } + + static void recordResponseEvents( + Context otelContext, + Logger eventLogger, + SdkResponse response, + boolean captureMessageContent) { + if (response instanceof ConverseResponse) { + ConverseResponse converseResponse = (ConverseResponse) response; + newEvent(otelContext, eventLogger) + .setAttribute(EVENT_NAME, "gen_ai.choice") + // Bedrock Runtime does not support multiple choices so index is always 0. + .setBody( + convertMessage( + converseResponse.output().message(), + 0, + converseResponse.stopReason(), + captureMessageContent)) + .emit(); + } + } + @Nullable private static Long integerToLong(Integer value) { if (value == null) { @@ -125,4 +210,91 @@ private static Double floatToDouble(Float value) { } return Double.valueOf(value); } + + private static LogRecordBuilder newEvent(Context otelContext, Logger eventLogger) { + return eventLogger + .logRecordBuilder() + .setContext(otelContext) + .setAttribute( + GEN_AI_SYSTEM, BedrockRuntimeAttributesGetter.GenAiSystemIncubatingValues.AWS_BEDROCK); + } + + private static void emitToolResultEvents( + Context otelContext, Logger eventLogger, Message message, boolean captureMessageContent) { + for (ContentBlock content : message.content()) { + if (content.toolResult() == null) { + continue; + } + Map> body = new HashMap<>(); + body.put("id", Value.of(content.toolResult().toolUseId())); + if (captureMessageContent) { + StringBuilder text = new StringBuilder(); + for (ToolResultContentBlock toolContent : content.toolResult().content()) { + if (toolContent.text() != null) { + text.append(toolContent.text()); + } + if (toolContent.json() != null) { + text.append(serializeDocument(toolContent.json())); + } + } + body.put("content", Value.of(text.toString())); + } + newEvent(otelContext, eventLogger) + .setAttribute(EVENT_NAME, "gen_ai.tool.message") + .setBody(Value.of(body)) + .emit(); + } + } + + private static Value convertMessage( + Message message, int index, @Nullable StopReason stopReason, boolean captureMessageContent) { + StringBuilder text = null; + List> toolCalls = null; + for (ContentBlock content : message.content()) { + if (captureMessageContent && content.text() != null) { + if (text == null) { + text = new StringBuilder(); + } + text.append(content.text()); + } + if (content.toolUse() != null) { + if (toolCalls == null) { + toolCalls = new ArrayList<>(); + } + toolCalls.add(convertToolCall(content.toolUse(), captureMessageContent)); + } + } + Map> body = new HashMap<>(); + if (text != null) { + body.put("content", Value.of(text.toString())); + } + if (toolCalls != null) { + body.put("toolCalls", Value.of(toolCalls)); + } + if (stopReason != null) { + body.put("finish_reason", Value.of(stopReason.toString())); + } + if (index >= 0) { + body.put("index", Value.of(index)); + } + return Value.of(body); + } + + private static Value convertToolCall(ToolUseBlock toolCall, boolean captureMessageContent) { + Map> body = new HashMap<>(); + body.put("id", Value.of(toolCall.toolUseId())); + body.put("name", Value.of(toolCall.name())); + body.put("type", Value.of("function")); + if (captureMessageContent) { + body.put("arguments", Value.of(serializeDocument(toolCall.input()))); + } + return Value.of(body); + } + + private static String serializeDocument(Document document) { + SdkJsonGenerator generator = new SdkJsonGenerator(JSON_FACTORY, "application/json"); + DocumentTypeJsonMarshaller marshaller = new DocumentTypeJsonMarshaller(generator); + document.accept(marshaller); + return new String(generator.getBytes(), StandardCharsets.UTF_8); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DocumentTypeJsonMarshaller.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DocumentTypeJsonMarshaller.java new file mode 100644 index 000000000000..4c8731778128 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/DocumentTypeJsonMarshaller.java @@ -0,0 +1,80 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2.internal; + +import java.util.List; +import java.util.Map; +import software.amazon.awssdk.core.SdkNumber; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.core.document.VoidDocumentVisitor; +import software.amazon.awssdk.protocols.json.StructuredJsonGenerator; + +// Copied as-is from +// https://github.com/aws/aws-sdk-java-v2/blob/7b946c1d03cbfab1252cca0b5845b18af0b2dc43/core/protocols/aws-json-protocol/src/main/java/software/amazon/awssdk/protocols/json/internal/marshall/DocumentTypeJsonMarshaller.java +final class DocumentTypeJsonMarshaller implements VoidDocumentVisitor { + + private final StructuredJsonGenerator jsonGenerator; + + public DocumentTypeJsonMarshaller(StructuredJsonGenerator jsonGenerator) { + this.jsonGenerator = jsonGenerator; + } + + @Override + public void visitNull() { + jsonGenerator.writeNull(); + } + + @Override + public void visitBoolean(Boolean document) { + jsonGenerator.writeValue(document); + } + + @Override + public void visitString(String document) { + jsonGenerator.writeValue(document); + } + + @Override + public void visitNumber(SdkNumber document) { + jsonGenerator.writeNumber(document.stringValue()); + } + + @Override + public void visitMap(Map documentMap) { + jsonGenerator.writeStartObject(); + documentMap + .entrySet() + .forEach( + entry -> { + jsonGenerator.writeFieldName(entry.getKey()); + entry.getValue().accept(this); + }); + jsonGenerator.writeEndObject(); + } + + @Override + public void visitList(List documentList) { + jsonGenerator.writeStartArray(); + documentList.stream().forEach(document -> document.accept(this)); + jsonGenerator.writeEndArray(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java index c857c6ba8bd5..eabf2bf22874 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/TracingExecutionInterceptor.java @@ -10,6 +10,7 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.logs.Logger; import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.TextMapPropagator; @@ -77,6 +78,7 @@ public final class TracingExecutionInterceptor implements ExecutionInterceptor { private final Instrumenter producerInstrumenter; private final Instrumenter dynamoDbInstrumenter; private final Instrumenter bedrockRuntimeInstrumenter; + private final Logger eventLogger; private final boolean captureExperimentalSpanAttributes; static final AttributeKey HTTP_ERROR_MSG = @@ -103,6 +105,7 @@ boolean shouldUseXrayPropagator() { @Nullable private final TextMapPropagator messagingPropagator; private final boolean useXrayPropagator; private final boolean recordIndividualHttpError; + private final boolean genAiCaptureMessageContent; private final FieldMapper fieldMapper; @SuppressWarnings("TooManyParameters") // internal method @@ -113,20 +116,24 @@ public TracingExecutionInterceptor( Instrumenter producerInstrumenter, Instrumenter dynamoDbInstrumenter, Instrumenter bedrockRuntimeInstrumenter, + Logger eventLogger, boolean captureExperimentalSpanAttributes, TextMapPropagator messagingPropagator, boolean useXrayPropagator, - boolean recordIndividualHttpError) { + boolean recordIndividualHttpError, + boolean genAiCaptureMessageContent) { this.requestInstrumenter = requestInstrumenter; this.consumerReceiveInstrumenter = consumerReceiveInstrumenter; this.consumerProcessInstrumenter = consumerProcessInstrumenter; this.producerInstrumenter = producerInstrumenter; this.dynamoDbInstrumenter = dynamoDbInstrumenter; this.bedrockRuntimeInstrumenter = bedrockRuntimeInstrumenter; + this.eventLogger = eventLogger; this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes; this.messagingPropagator = messagingPropagator; this.useXrayPropagator = useXrayPropagator; this.recordIndividualHttpError = recordIndividualHttpError; + this.genAiCaptureMessageContent = genAiCaptureMessageContent; this.fieldMapper = new FieldMapper(); } @@ -228,6 +235,11 @@ public SdkRequest modifyRequest( return modifiedRequest; } + if (BedrockRuntimeAccess.isBedrockRuntimeRequest(request)) { + BedrockRuntimeAccess.recordRequestEvents( + otelContext, eventLogger, request, genAiCaptureMessageContent); + } + // Insert other special handling here, following the same pattern as SQS and SNS. return request; @@ -357,8 +369,7 @@ public void afterExecution( // http request has been changed executionAttributes.putAttribute(SDK_HTTP_REQUEST_ATTRIBUTE, context.httpRequest()); - Span span = Span.fromContext(otelContext); - onSdkResponse(span, context.response(), executionAttributes); + onSdkResponse(otelContext, context.response(), executionAttributes); SdkHttpResponse httpResponse = context.httpResponse(); @@ -372,10 +383,17 @@ public void afterExecution( } private void onSdkResponse( - Span span, SdkResponse response, ExecutionAttributes executionAttributes) { + io.opentelemetry.context.Context otelContext, + SdkResponse response, + ExecutionAttributes executionAttributes) { + Span span = Span.fromContext(otelContext); if (response instanceof AwsResponse) { span.setAttribute(AWS_REQUEST_ID, ((AwsResponse) response).responseMetadata().requestId()); } + if (BedrockRuntimeAccess.isBedrockRuntimeResponse(response)) { + BedrockRuntimeAccess.recordResponseEvents( + otelContext, eventLogger, response, genAiCaptureMessageContent); + } if (captureExperimentalSpanAttributes) { AwsSdkRequest sdkRequest = executionAttributes.getAttribute(AWS_SDK_REQUEST_ATTRIBUTE); if (sdkRequest != null) { diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testBedrockRuntime/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Aws2BedrockRuntimeTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testBedrockRuntime/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Aws2BedrockRuntimeTest.java index 2f7d6789f476..65af3a7c1a85 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testBedrockRuntime/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Aws2BedrockRuntimeTest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testBedrockRuntime/java/io/opentelemetry/instrumentation/awssdk/v2_2/internal/Aws2BedrockRuntimeTest.java @@ -5,13 +5,50 @@ package io.opentelemetry.instrumentation.awssdk.v2_2.internal; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_OPERATION_NAME; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_REQUEST_MODEL; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_RESPONSE_FINISH_REASONS; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_SYSTEM; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_TOKEN_TYPE; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_INPUT_TOKENS; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiSystemIncubatingValues.AWS_BEDROCK; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2BedrockRuntimeTest; import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkTelemetry; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.document.Document; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; +import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClientBuilder; +import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ConversationRole; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseRequest; +import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; +import software.amazon.awssdk.services.bedrockruntime.model.Message; +import software.amazon.awssdk.services.bedrockruntime.model.Tool; +import software.amazon.awssdk.services.bedrockruntime.model.ToolConfiguration; +import software.amazon.awssdk.services.bedrockruntime.model.ToolInputSchema; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolSpecification; class Aws2BedrockRuntimeTest extends AbstractAws2BedrockRuntimeTest { @@ -28,7 +65,10 @@ protected InstrumentationExtension getTesting() { @BeforeAll static void setup() { - telemetry = AwsSdkTelemetry.create(testing.getOpenTelemetry()); + telemetry = + AwsSdkTelemetry.builder(testing.getOpenTelemetry()) + .setGenaiCaptureMessageContent(true) + .build(); } @Override @@ -36,4 +76,368 @@ protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder return ClientOverrideConfiguration.builder() .addExecutionInterceptor(telemetry.newExecutionInterceptor()); } + + @Test + void testConverseToolCallNoMessageContent() { + BedrockRuntimeClientBuilder builder = BedrockRuntimeClient.builder(); + builder.overrideConfiguration( + ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTelemetry.builder(testing.getOpenTelemetry()) + .build() + .newExecutionInterceptor()) + .build()); + configureClient(builder); + BedrockRuntimeClient client = builder.build(); + + String modelId = "amazon.nova-micro-v1:0"; + List messages = new ArrayList<>(); + messages.add( + Message.builder() + .role(ConversationRole.USER) + .content( + ContentBlock.fromText("What is the weather in Seattle and San Francisco today?")) + .build()); + ConverseResponse response0 = + client.converse( + ConverseRequest.builder() + .modelId(modelId) + .messages(messages) + .toolConfig(currentWeatherToolConfig()) + .build()); + + String seattleToolUseId0 = ""; + String sanFranciscoToolUseId0 = ""; + for (ContentBlock content : response0.output().message().content()) { + if (content.toolUse() == null) { + continue; + } + String toolUseId = content.toolUse().toolUseId(); + switch (content.toolUse().input().asMap().get("location").asString()) { + case "Seattle": + seattleToolUseId0 = toolUseId; + break; + case "San Francisco": + sanFranciscoToolUseId0 = toolUseId; + break; + default: + throw new IllegalArgumentException("Invalid tool use: " + content.toolUse()); + } + } + String seattleToolUseId = seattleToolUseId0; + String sanFranciscoToolUseId = sanFranciscoToolUseId0; + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("chat amazon.nova-micro-v1:0") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes.GenAiOperationNameIncubatingValues + .CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId), + equalTo(GEN_AI_USAGE_INPUT_TOKENS, 415), + equalTo(GEN_AI_USAGE_OUTPUT_TOKENS, 162), + equalTo(GEN_AI_RESPONSE_FINISH_REASONS, asList("tool_use"))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.token.usage") + .hasUnit("{token}") + .hasDescription("Measures number of input and output tokens used") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(415) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_TOKEN_TYPE, + GenAiIncubatingAttributes + .GenAiTokenTypeIncubatingValues.INPUT), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)), + point -> + point + .hasSum(162) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_TOKEN_TYPE, + GenAiIncubatingAttributes + .GenAiTokenTypeIncubatingValues.COMPLETION), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)))), + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasUnit("s") + .hasDescription("GenAI operation duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId))))); + + SpanContext spanCtx0 = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx0) + .hasBody(Value.of(Collections.emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx0) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("tool_use")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "toolCalls", + Value.of( + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of("id", Value.of(seattleToolUseId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of("id", Value.of(sanFranciscoToolUseId)), + KeyValue.of("type", Value.of("function")))))))); + + getTesting().clearData(); + + messages.add(response0.output().message()); + messages.add( + Message.builder() + .role(ConversationRole.USER) + .content( + ContentBlock.fromToolResult( + ToolResultBlock.builder() + .content( + ToolResultContentBlock.builder() + .json( + Document.mapBuilder() + .putString("weather", "50 degrees and raining") + .build()) + .build()) + .toolUseId(seattleToolUseId) + .build()), + ContentBlock.fromToolResult( + ToolResultBlock.builder() + .content( + ToolResultContentBlock.builder() + .json( + Document.mapBuilder() + .putString("weather", "70 degrees and sunny") + .build()) + .build()) + .toolUseId(sanFranciscoToolUseId) + .build())) + .build()); + + ConverseResponse response1 = + client.converse( + ConverseRequest.builder() + .modelId(modelId) + .messages(messages) + .toolConfig(currentWeatherToolConfig()) + .build()); + + assertThat(response1.output().message().content().get(0).text()) + .contains( + "The current weather in Seattle is 50 degrees and raining. " + + "In San Francisco, the weather is 70 degrees and sunny."); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("chat amazon.nova-micro-v1:0") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes.GenAiOperationNameIncubatingValues + .CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId), + equalTo(GEN_AI_USAGE_INPUT_TOKENS, 554), + equalTo(GEN_AI_USAGE_OUTPUT_TOKENS, 57), + equalTo(GEN_AI_RESPONSE_FINISH_REASONS, asList("end_turn"))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.token.usage") + .hasUnit("{token}") + .hasDescription("Measures number of input and output tokens used") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(554) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_TOKEN_TYPE, + GenAiIncubatingAttributes + .GenAiTokenTypeIncubatingValues.INPUT), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)), + point -> + point + .hasSum(57) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_TOKEN_TYPE, + GenAiIncubatingAttributes + .GenAiTokenTypeIncubatingValues.COMPLETION), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)))), + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasUnit("s") + .hasDescription("GenAI operation duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId))))); + + SpanContext spanCtx1 = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(emptyMap())), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.assistant.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "toolCalls", + Value.of( + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of("id", Value.of(seattleToolUseId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of("id", Value.of(sanFranciscoToolUseId)), + KeyValue.of("type", Value.of("function"))))))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(KeyValue.of("id", Value.of(seattleToolUseId)))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody(Value.of(KeyValue.of("id", Value.of(sanFranciscoToolUseId)))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("end_turn")), + KeyValue.of("index", Value.of(0))))); + } + + private static ToolConfiguration currentWeatherToolConfig() { + return ToolConfiguration.builder() + .tools( + Tool.builder() + .toolSpec( + ToolSpecification.builder() + .name("get_current_weather") + .description("Get the current weather in a given location.") + .inputSchema( + ToolInputSchema.builder() + .json( + Document.mapBuilder() + .putString("type", "object") + .putDocument( + "properties", + Document.mapBuilder() + .putDocument( + "location", + Document.mapBuilder() + .putString("type", "string") + .putString( + "description", "The name of the city") + .build()) + .build()) + .putList( + "required", + singletonList(Document.fromString("location"))) + .build()) + .build()) + .build()) + .build()) + .build(); + } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2BedrockRuntimeTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2BedrockRuntimeTest.java index 4bca9b5e30df..5652c45d5e97 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2BedrockRuntimeTest.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2BedrockRuntimeTest.java @@ -18,20 +18,29 @@ import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_TOKEN_TYPE; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_INPUT_TOKENS; import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GEN_AI_USAGE_OUTPUT_TOKENS; +import static io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes.GenAiSystemIncubatingValues.AWS_BEDROCK; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.within; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.KeyValue; +import io.opentelemetry.api.common.Value; +import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.instrumentation.awssdk.v2_2.recording.RecordingExtension; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.semconv.incubating.GenAiIncubatingAttributes; import java.net.URI; +import java.util.ArrayList; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.document.Document; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient; import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClientBuilder; @@ -41,19 +50,27 @@ import software.amazon.awssdk.services.bedrockruntime.model.ConverseResponse; import software.amazon.awssdk.services.bedrockruntime.model.InferenceConfiguration; import software.amazon.awssdk.services.bedrockruntime.model.Message; +import software.amazon.awssdk.services.bedrockruntime.model.Tool; +import software.amazon.awssdk.services.bedrockruntime.model.ToolConfiguration; +import software.amazon.awssdk.services.bedrockruntime.model.ToolInputSchema; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolResultContentBlock; +import software.amazon.awssdk.services.bedrockruntime.model.ToolSpecification; public abstract class AbstractAws2BedrockRuntimeTest { - private static final String INSTRUMENTATION_NAME = "io.opentelemetry.aws-sdk-2.2"; + protected static final String INSTRUMENTATION_NAME = "io.opentelemetry.aws-sdk-2.2"; private static final String API_URL = "https://bedrock-runtime.us-east-1.amazonaws.com"; + protected static final AttributeKey EVENT_NAME = AttributeKey.stringKey("event.name"); + @RegisterExtension static final RecordingExtension recording = new RecordingExtension(API_URL); protected abstract InstrumentationExtension getTesting(); protected abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); - private static void configureClient(BedrockRuntimeClientBuilder builder) { + protected static void configureClient(BedrockRuntimeClientBuilder builder) { builder .region(Region.US_EAST_1) .endpointOverride(URI.create("http://localhost:" + recording.getPort())); @@ -95,10 +112,7 @@ void testConverseBasic() { span.hasName("chat amazon.titan-text-lite-v1") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfying( - equalTo( - GEN_AI_SYSTEM, - GenAiIncubatingAttributes.GenAiSystemIncubatingValues - .AWS_BEDROCK), + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_OPERATION_NAME, GenAiIncubatingAttributes.GenAiOperationNameIncubatingValues @@ -124,10 +138,7 @@ void testConverseBasic() { .hasSum(8) .hasCount(1) .hasAttributesSatisfyingExactly( - equalTo( - GEN_AI_SYSTEM, - GenAiIncubatingAttributes - .GenAiSystemIncubatingValues.AWS_BEDROCK), + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_TOKEN_TYPE, GenAiIncubatingAttributes @@ -142,10 +153,7 @@ void testConverseBasic() { .hasSum(14) .hasCount(1) .hasAttributesSatisfyingExactly( - equalTo( - GEN_AI_SYSTEM, - GenAiIncubatingAttributes - .GenAiSystemIncubatingValues.AWS_BEDROCK), + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_TOKEN_TYPE, GenAiIncubatingAttributes @@ -167,15 +175,33 @@ void testConverseBasic() { point .hasSumGreaterThan(0.0) .hasAttributesSatisfyingExactly( - equalTo( - GEN_AI_SYSTEM, - GenAiIncubatingAttributes - .GenAiSystemIncubatingValues.AWS_BEDROCK), + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_OPERATION_NAME, GenAiIncubatingAttributes .GenAiOperationNameIncubatingValues.CHAT), equalTo(GEN_AI_REQUEST_MODEL, modelId))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(KeyValue.of("content", Value.of("Say this is a test")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("end_turn")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "content", Value.of("Hi there! How can I help you today?"))))); } @Test @@ -214,10 +240,7 @@ void testConverseOptions() { span.hasName("chat amazon.titan-text-lite-v1") .hasKind(SpanKind.CLIENT) .hasAttributesSatisfying( - equalTo( - GEN_AI_SYSTEM, - GenAiIncubatingAttributes.GenAiSystemIncubatingValues - .AWS_BEDROCK), + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_OPERATION_NAME, GenAiIncubatingAttributes.GenAiOperationNameIncubatingValues @@ -249,10 +272,7 @@ void testConverseOptions() { .hasSum(8) .hasCount(1) .hasAttributesSatisfyingExactly( - equalTo( - GEN_AI_SYSTEM, - GenAiIncubatingAttributes - .GenAiSystemIncubatingValues.AWS_BEDROCK), + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_TOKEN_TYPE, GenAiIncubatingAttributes @@ -267,10 +287,150 @@ void testConverseOptions() { .hasSum(10) .hasCount(1) .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( - GEN_AI_SYSTEM, + GEN_AI_TOKEN_TYPE, GenAiIncubatingAttributes - .GenAiSystemIncubatingValues.AWS_BEDROCK), + .GenAiTokenTypeIncubatingValues.COMPLETION), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)))), + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasUnit("s") + .hasDescription("GenAI operation duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId))))); + + SpanContext spanCtx = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx) + .hasBody(Value.of(KeyValue.of("content", Value.of("Say this is a test")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("max_tokens")), + KeyValue.of("index", Value.of(0)), + KeyValue.of("content", Value.of("This is an LLM ("))))); + } + + @Test + void testConverseToolCall() { + BedrockRuntimeClientBuilder builder = BedrockRuntimeClient.builder(); + builder.overrideConfiguration(createOverrideConfigurationBuilder().build()); + configureClient(builder); + BedrockRuntimeClient client = builder.build(); + + String modelId = "amazon.nova-micro-v1:0"; + List messages = new ArrayList<>(); + messages.add( + Message.builder() + .role(ConversationRole.USER) + .content( + ContentBlock.fromText("What is the weather in Seattle and San Francisco today?")) + .build()); + ConverseResponse response0 = + client.converse( + ConverseRequest.builder() + .modelId(modelId) + .messages(messages) + .toolConfig(currentWeatherToolConfig()) + .build()); + + String seattleToolUseId0 = ""; + String sanFranciscoToolUseId0 = ""; + for (ContentBlock content : response0.output().message().content()) { + if (content.toolUse() == null) { + continue; + } + String toolUseId = content.toolUse().toolUseId(); + switch (content.toolUse().input().asMap().get("location").asString()) { + case "Seattle": + seattleToolUseId0 = toolUseId; + break; + case "San Francisco": + sanFranciscoToolUseId0 = toolUseId; + break; + default: + throw new IllegalArgumentException("Invalid tool use: " + content.toolUse()); + } + } + String seattleToolUseId = seattleToolUseId0; + String sanFranciscoToolUseId = sanFranciscoToolUseId0; + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("chat amazon.nova-micro-v1:0") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes.GenAiOperationNameIncubatingValues + .CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId), + equalTo(GEN_AI_USAGE_INPUT_TOKENS, 415), + equalTo(GEN_AI_USAGE_OUTPUT_TOKENS, 162), + equalTo(GEN_AI_RESPONSE_FINISH_REASONS, asList("tool_use"))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.token.usage") + .hasUnit("{token}") + .hasDescription("Measures number of input and output tokens used") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(415) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_TOKEN_TYPE, + GenAiIncubatingAttributes + .GenAiTokenTypeIncubatingValues.INPUT), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)), + point -> + point + .hasSum(162) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_TOKEN_TYPE, GenAiIncubatingAttributes @@ -292,14 +452,288 @@ void testConverseOptions() { point .hasSumGreaterThan(0.0) .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId))))); + + SpanContext spanCtx0 = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx0) + .hasBody( + Value.of( + KeyValue.of( + "content", + Value.of( + "What is the weather in Seattle and San Francisco today?")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx0) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("tool_use")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "toolCalls", + Value.of( + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of( + "arguments", Value.of("{\"location\":\"Seattle\"}")), + KeyValue.of("id", Value.of(seattleToolUseId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of( + "arguments", + Value.of("{\"location\":\"San Francisco\"}")), + KeyValue.of("id", Value.of(sanFranciscoToolUseId)), + KeyValue.of("type", Value.of("function"))))), + KeyValue.of( + "content", + Value.of( + " The User has asked for the current weather in two locations: Seattle and San Francisco. To provide the requested information, I will use the \"get_current_weather\" tool for each location separately. \n"))))); + + getTesting().clearData(); + + messages.add(response0.output().message()); + messages.add( + Message.builder() + .role(ConversationRole.USER) + .content( + ContentBlock.fromToolResult( + ToolResultBlock.builder() + .content( + ToolResultContentBlock.builder() + .json( + Document.mapBuilder() + .putString("weather", "50 degrees and raining") + .build()) + .build()) + .toolUseId(seattleToolUseId) + .build()), + ContentBlock.fromToolResult( + ToolResultBlock.builder() + .content( + ToolResultContentBlock.builder() + .json( + Document.mapBuilder() + .putString("weather", "70 degrees and sunny") + .build()) + .build()) + .toolUseId(sanFranciscoToolUseId) + .build())) + .build()); + + ConverseResponse response1 = + client.converse( + ConverseRequest.builder() + .modelId(modelId) + .messages(messages) + .toolConfig(currentWeatherToolConfig()) + .build()); + + assertThat(response1.output().message().content().get(0).text()) + .contains( + "The current weather in Seattle is 50 degrees and raining. " + + "In San Francisco, the weather is 70 degrees and sunny."); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("chat amazon.nova-micro-v1:0") + .hasKind(SpanKind.CLIENT) + .hasAttributesSatisfying( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes.GenAiOperationNameIncubatingValues + .CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId), + equalTo(GEN_AI_USAGE_INPUT_TOKENS, 554), + equalTo(GEN_AI_USAGE_OUTPUT_TOKENS, 57), + equalTo(GEN_AI_RESPONSE_FINISH_REASONS, asList("end_turn"))))); + + getTesting() + .waitAndAssertMetrics( + INSTRUMENTATION_NAME, + metric -> + metric + .hasName("gen_ai.client.token.usage") + .hasUnit("{token}") + .hasDescription("Measures number of input and output tokens used") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSum(554) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( - GEN_AI_SYSTEM, + GEN_AI_TOKEN_TYPE, GenAiIncubatingAttributes - .GenAiSystemIncubatingValues.AWS_BEDROCK), + .GenAiTokenTypeIncubatingValues.INPUT), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)), + point -> + point + .hasSum(57) + .hasCount(1) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo( + GEN_AI_TOKEN_TYPE, + GenAiIncubatingAttributes + .GenAiTokenTypeIncubatingValues.COMPLETION), + equalTo( + GEN_AI_OPERATION_NAME, + GenAiIncubatingAttributes + .GenAiOperationNameIncubatingValues.CHAT), + equalTo(GEN_AI_REQUEST_MODEL, modelId)))), + metric -> + metric + .hasName("gen_ai.client.operation.duration") + .hasUnit("s") + .hasDescription("GenAI operation duration") + .hasHistogramSatisfying( + histogram -> + histogram.hasPointsSatisfying( + point -> + point + .hasSumGreaterThan(0.0) + .hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo( GEN_AI_OPERATION_NAME, GenAiIncubatingAttributes .GenAiOperationNameIncubatingValues.CHAT), equalTo(GEN_AI_REQUEST_MODEL, modelId))))); + + SpanContext spanCtx1 = getTesting().waitForTraces(1).get(0).get(0).getSpanContext(); + + getTesting() + .waitAndAssertLogRecords( + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.user.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "content", + Value.of( + "What is the weather in Seattle and San Francisco today?")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.assistant.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of( + "toolCalls", + Value.of( + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of( + "arguments", Value.of("{\"location\":\"Seattle\"}")), + KeyValue.of("id", Value.of(seattleToolUseId)), + KeyValue.of("type", Value.of("function"))), + Value.of( + KeyValue.of("name", Value.of("get_current_weather")), + KeyValue.of( + "arguments", + Value.of("{\"location\":\"San Francisco\"}")), + KeyValue.of("id", Value.of(sanFranciscoToolUseId)), + KeyValue.of("type", Value.of("function"))))), + KeyValue.of( + "content", + Value.of( + " The User has asked for the current weather in two locations: Seattle and San Francisco. To provide the requested information, I will use the \"get_current_weather\" tool for each location separately. \n")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("id", Value.of(seattleToolUseId)), + KeyValue.of( + "content", Value.of("{\"weather\":\"50 degrees and raining\"}")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), + equalTo(EVENT_NAME, "gen_ai.tool.message")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("id", Value.of(sanFranciscoToolUseId)), + KeyValue.of( + "content", Value.of("{\"weather\":\"70 degrees and sunny\"}")))), + log -> + log.hasAttributesSatisfyingExactly( + equalTo(GEN_AI_SYSTEM, AWS_BEDROCK), equalTo(EVENT_NAME, "gen_ai.choice")) + .hasSpanContext(spanCtx1) + .hasBody( + Value.of( + KeyValue.of("finish_reason", Value.of("end_turn")), + KeyValue.of("index", Value.of(0)), + KeyValue.of( + "content", + Value.of( + " The tool has provided the current weather for both locations. Now I will compile this information and present it to the User. \n" + + "\n" + + "The current weather in Seattle is 50 degrees and raining. In San Francisco, the weather is 70 degrees and sunny."))))); + } + + private static ToolConfiguration currentWeatherToolConfig() { + return ToolConfiguration.builder() + .tools( + Tool.builder() + .toolSpec( + ToolSpecification.builder() + .name("get_current_weather") + .description("Get the current weather in a given location.") + .inputSchema( + ToolInputSchema.builder() + .json( + Document.mapBuilder() + .putString("type", "object") + .putDocument( + "properties", + Document.mapBuilder() + .putDocument( + "location", + Document.mapBuilder() + .putString("type", "string") + .putString( + "description", "The name of the city") + .build()) + .build()) + .putList( + "required", + singletonList(Document.fromString("location"))) + .build()) + .build()) + .build()) + .build()) + .build(); } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FixedHostAwsV4AuthScheme.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FixedHostAwsV4AuthScheme.java index 5a0db02a2e7f..681d5fe92ec6 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FixedHostAwsV4AuthScheme.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/FixedHostAwsV4AuthScheme.java @@ -5,6 +5,7 @@ package io.opentelemetry.instrumentation.awssdk.v2_2; +import java.net.URI; import java.util.concurrent.CompletableFuture; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.auth.aws.internal.scheme.DefaultAwsV4AuthScheme; @@ -49,7 +50,7 @@ private static class FixedHostAwsV4HttpSigner implements AwsV4HttpSigner { private final String apiUrl; FixedHostAwsV4HttpSigner(String apiUrl) { - this.apiUrl = apiUrl; + this.apiUrl = URI.create(apiUrl).getHost(); } @Override diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.awssdk.v2_2.abstractaws2bedrockruntimetest.testconversetoolcall.yaml b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.awssdk.v2_2.abstractaws2bedrockruntimetest.testconversetoolcall.yaml new file mode 100644 index 000000000000..3c06f9468922 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/resources/mappings/io.opentelemetry.instrumentation.awssdk.v2_2.abstractaws2bedrockruntimetest.testconversetoolcall.yaml @@ -0,0 +1,153 @@ +--- +id: 8d28eabd-85de-42e5-9759-4122febc3b8f +name: model_amazonnova-micro-v10_converse +request: + url: /model/amazon.nova-micro-v1%3A0/converse + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "role" : "user", + "content" : [ { + "text" : "What is the weather in Seattle and San Francisco today?" + } ] + } ], + "toolConfig" : { + "tools" : [ { + "toolSpec" : { + "name" : "get_current_weather", + "description" : "Get the current weather in a given location.", + "inputSchema" : { + "json" : { + "type" : "object", + "properties" : { + "location" : { + "type" : "string", + "description" : "The name of the city" + } + }, + "required" : [ "location" ] + } + } + } + } ] + } + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: "{\"metrics\":{\"latencyMs\":436},\"output\":{\"message\":{\"content\":[{\"\ + text\":\" The User has asked for the current weather in two locations:\ + \ Seattle and San Francisco. To provide the requested information, I will use\ + \ the \\\"get_current_weather\\\" tool for each location separately. \\\ + n\"},{\"toolUse\":{\"input\":{\"location\":\"Seattle\"},\"name\":\"get_current_weather\"\ + ,\"toolUseId\":\"tooluse_oJSvwNoiR6eGZt2lZ_omwA\"}},{\"toolUse\":{\"input\":{\"\ + location\":\"San Francisco\"},\"name\":\"get_current_weather\",\"toolUseId\":\"\ + tooluse_Kz6EtoqZSyK9_Se61LTMqQ\"}}],\"role\":\"assistant\"}},\"stopReason\":\"\ + tool_use\",\"usage\":{\"inputTokens\":415,\"outputTokens\":162,\"totalTokens\"\ + :577}}" + headers: + Date: "Fri, 07 Mar 2025 07:05:41 GMT" + Content-Type: application/json + x-amzn-RequestId: 1f7f8390-8993-4933-a640-0d0d3205318e +uuid: 8d28eabd-85de-42e5-9759-4122febc3b8f +persistent: true +insertionIndex: 12 +--- +id: 2d7c4ed1-1eca-4c36-96cb-381cbc6cc9c0 +name: model_amazonnova-micro-v10_converse +request: + url: /model/amazon.nova-micro-v1%3A0/converse + method: POST + bodyPatterns: + - equalToJson: |- + { + "messages" : [ { + "role" : "user", + "content" : [ { + "text" : "What is the weather in Seattle and San Francisco today?" + } ] + }, { + "role" : "assistant", + "content" : [ { + "text" : " The User has asked for the current weather in two locations: Seattle and San Francisco. To provide the requested information, I will use the \"get_current_weather\" tool for each location separately. \n" + }, { + "toolUse" : { + "toolUseId" : "tooluse_oJSvwNoiR6eGZt2lZ_omwA", + "name" : "get_current_weather", + "input" : { + "location" : "Seattle" + } + } + }, { + "toolUse" : { + "toolUseId" : "tooluse_Kz6EtoqZSyK9_Se61LTMqQ", + "name" : "get_current_weather", + "input" : { + "location" : "San Francisco" + } + } + } ] + }, { + "role" : "user", + "content" : [ { + "toolResult" : { + "toolUseId" : "tooluse_oJSvwNoiR6eGZt2lZ_omwA", + "content" : [ { + "json" : { + "weather" : "50 degrees and raining" + } + } ] + } + }, { + "toolResult" : { + "toolUseId" : "tooluse_Kz6EtoqZSyK9_Se61LTMqQ", + "content" : [ { + "json" : { + "weather" : "70 degrees and sunny" + } + } ] + } + } ] + } ], + "toolConfig" : { + "tools" : [ { + "toolSpec" : { + "name" : "get_current_weather", + "description" : "Get the current weather in a given location.", + "inputSchema" : { + "json" : { + "type" : "object", + "properties" : { + "location" : { + "type" : "string", + "description" : "The name of the city" + } + }, + "required" : [ "location" ] + } + } + } + } ] + } + } + ignoreArrayOrder: false + ignoreExtraElements: false +response: + status: 200 + body: "{\"metrics\":{\"latencyMs\":433},\"output\":{\"message\":{\"content\":[{\"\ + text\":\" The tool has provided the current weather for both locations.\ + \ Now I will compile this information and present it to the User. \\\ + n\\nThe current weather in Seattle is 50 degrees and raining. In San Francisco,\ + \ the weather is 70 degrees and sunny.\"}],\"role\":\"assistant\"}},\"stopReason\"\ + :\"end_turn\",\"usage\":{\"inputTokens\":554,\"outputTokens\":57,\"totalTokens\"\ + :611}}" + headers: + Date: "Fri, 07 Mar 2025 07:05:43 GMT" + Content-Type: application/json + x-amzn-RequestId: 5d08615f-4860-487d-bfaf-a8e82932aea1 +uuid: 2d7c4ed1-1eca-4c36-96cb-381cbc6cc9c0 +persistent: true +insertionIndex: 13