diff --git a/src/OpenTelemetry.Instrumentation.AWS/AWSInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.AWS/AWSInstrumentationEventSource.cs new file mode 100644 index 0000000000..e971d1dd06 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AWS/AWSInstrumentationEventSource.cs @@ -0,0 +1,30 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.AWS; + +[EventSource(Name = "OpenTelemetry-Instrumentation-AWS")] +internal sealed class AWSInstrumentationEventSource : EventSource +{ + public static AWSInstrumentationEventSource Log = new(); + + private const int EventIdFailedToParseJson = 1; + + [NonEvent] + public void JsonParserException(string format, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.FailedToPraseJson(format, ex.ToInvariantString()); + } + } + + [Event(EventIdFailedToParseJson, Message = "Failed to parse Json in {0}. Error Message: '{1}'", Level = EventLevel.Warning)] + public void FailedToPraseJson(string format, string exception) + { + this.WriteEvent(EventIdFailedToParseJson, format, exception); + } +} diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSLlmModelProcessor.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSLlmModelProcessor.cs new file mode 100644 index 0000000000..11207bc303 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSLlmModelProcessor.cs @@ -0,0 +1,425 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NET +using System.Diagnostics.CodeAnalysis; +#endif +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using OpenTelemetry.AWS; + +namespace OpenTelemetry.Instrumentation.AWS.Implementation; + +internal class AWSLlmModelProcessor +{ +#if NET + [UnconditionalSuppressMessage( + "Specify StringComparison for clarity", + "CA1307", + Justification = "Adding StringComparison only works for NET Core but not the framework.")] +#endif + internal static void ProcessGenAiAttributes(Activity activity, MemoryStream body, string modelName, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + // message can be either a request or a response. isRequest is used by the model-specific methods to determine + // whether to extract the request or response attributes. + + // Currently, the .NET SDK does not expose "X-Amzn-Bedrock-*" HTTP headers in the response metadata, as per + // https://github.com/aws/aws-sdk-net/issues/3171. As a result, we can only extract attributes given what is in + // the response body. For the Claude, Command, and Mistral models, the input and output tokens are not provided + // in the response body, so we approximate their values by dividing the input and output lengths by 6, based on + // the Bedrock documentation here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-customization-prepare.html + try + { + var jsonString = Encoding.UTF8.GetString(body.ToArray()); +#if NET + var jsonObject = JsonSerializer.Deserialize(jsonString, SourceGenerationContext.Default.DictionaryStringJsonElement); +#else + var jsonObject = JsonSerializer.Deserialize>(jsonString); +#endif + if (jsonObject == null) + { + return; + } + + // extract model specific attributes based on model name + if (modelName.Contains("amazon.nova")) + { + ProcessNovaModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + else if (modelName.Contains("amazon.titan")) + { + ProcessTitanModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + else if (modelName.Contains("anthropic.claude")) + { + ProcessClaudeModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + else if (modelName.Contains("meta.llama3")) + { + ProcessLlamaModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + else if (modelName.Contains("cohere.command")) + { + ProcessCommandModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + else if (modelName.Contains("ai21.jamba")) + { + ProcessJambaModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + else if (modelName.Contains("mistral.mistral")) + { + ProcessMistralModelAttributes(activity, jsonObject, isRequest, awsSemanticConventions); + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessNovaModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("inferenceConfig", out var inferenceConfig)) + { + if (inferenceConfig.TryGetProperty("top_p", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (inferenceConfig.TryGetProperty("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (inferenceConfig.TryGetProperty("max_new_tokens", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + } + } + else + { + if (jsonBody.TryGetValue("usage", out var usage)) + { + if (usage.TryGetProperty("inputTokens", out var inputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, inputTokens.GetInt32()); + } + + if (usage.TryGetProperty("outputTokens", out var outputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, outputTokens.GetInt32()); + } + } + + if (jsonBody.TryGetValue("stopReason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessTitanModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("textGenerationConfig", out var textGenerationConfig)) + { + if (textGenerationConfig.TryGetProperty("topP", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (textGenerationConfig.TryGetProperty("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (textGenerationConfig.TryGetProperty("maxTokenCount", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + } + } + else + { + if (jsonBody.TryGetValue("inputTextTokenCount", out var inputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, inputTokens.GetInt32()); + } + + if (jsonBody.TryGetValue("results", out var resultsArray)) + { + var results = resultsArray[0]; + if (results.TryGetProperty("tokenCount", out var outputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, outputTokens.GetInt32()); + } + + if (results.TryGetProperty("completionReason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessClaudeModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("top_p", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (jsonBody.TryGetValue("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (jsonBody.TryGetValue("max_tokens", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + } + else + { + if (jsonBody.TryGetValue("usage", out var usage)) + { + if (usage.TryGetProperty("input_tokens", out var inputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, inputTokens.GetInt32()); + } + + if (usage.TryGetProperty("output_tokens", out var outputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, outputTokens.GetInt32()); + } + } + + if (jsonBody.TryGetValue("stop_reason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessLlamaModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("top_p", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (jsonBody.TryGetValue("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (jsonBody.TryGetValue("max_gen_len", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + } + else + { + if (jsonBody.TryGetValue("prompt_token_count", out var inputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, inputTokens.GetInt32()); + } + + if (jsonBody.TryGetValue("generation_token_count", out var outputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, outputTokens.GetInt32()); + } + + if (jsonBody.TryGetValue("stop_reason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessCommandModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("p", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (jsonBody.TryGetValue("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (jsonBody.TryGetValue("max_tokens", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + + // input tokens not provided in Command response body, so we estimate the value based on input length + if (jsonBody.TryGetValue("message", out var input)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, Convert.ToInt32(Math.Ceiling((double)(input.GetString()?.Length ?? 0) / 6))); + } + } + else + { + if (jsonBody.TryGetValue("finish_reason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + + // completion tokens not provided in Command response body, so we estimate the value based on output length + if (jsonBody.TryGetValue("text", out var output)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, Convert.ToInt32(Math.Ceiling((double)(output.GetString()?.Length ?? 0) / 6))); + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessJambaModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("top_p", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (jsonBody.TryGetValue("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (jsonBody.TryGetValue("max_tokens", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + } + else + { + if (jsonBody.TryGetValue("usage", out var usage)) + { + if (usage.TryGetProperty("prompt_tokens", out var inputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, inputTokens.GetInt32()); + } + + if (usage.TryGetProperty("completion_tokens", out var outputTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, outputTokens.GetInt32()); + } + } + + if (jsonBody.TryGetValue("choices", out var choices)) + { + if (choices[0].TryGetProperty("finish_reason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } + + private static void ProcessMistralModelAttributes(Activity activity, Dictionary jsonBody, bool isRequest, AWSSemanticConventions awsSemanticConventions) + { + try + { + if (isRequest) + { + if (jsonBody.TryGetValue("top_p", out var topP)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTopP(activity, topP.GetDouble()); + } + + if (jsonBody.TryGetValue("temperature", out var temperature)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiTemperature(activity, temperature.GetDouble()); + } + + if (jsonBody.TryGetValue("max_tokens", out var maxTokens)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiMaxTokens(activity, maxTokens.GetInt32()); + } + + // input tokens not provided in Mistral response body, so we estimate the value based on input length + if (jsonBody.TryGetValue("prompt", out var input)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiInputTokens(activity, Convert.ToInt32(Math.Ceiling((double)(input.GetString()?.Length ?? 0) / 6))); + } + } + else + { + if (jsonBody.TryGetValue("outputs", out var outputsArray)) + { + var output = outputsArray[0]; + if (output.TryGetProperty("stop_reason", out var finishReasons)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiFinishReasons(activity, [finishReasons.GetString() ?? string.Empty]); + } + + // output tokens not provided in Mistral response body, so we estimate the value based on output length + if (output.TryGetProperty("text", out var text)) + { + awsSemanticConventions.TagBuilder.SetTagAttributeGenAiOutputTokens(activity, Convert.ToInt32(Math.Ceiling((double)(text.GetString()?.Length ?? 0) / 6))); + } + } + } + } + catch (Exception ex) + { + AWSInstrumentationEventSource.Log.JsonParserException(nameof(AWSLlmModelProcessor), ex); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs index 67ea5d94df..ecadbd6bd8 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceHelper.cs @@ -20,90 +20,47 @@ public AWSServiceHelper(AWSSemanticConventions semanticConventions) .AddAttributeAWSBedrockDataSourceId("DataSourceId") .AddAttributeAWSBedrockGuardrailId("GuardrailId") .AddAttributeAWSBedrockKnowledgeBaseId("KnowledgeBaseId") + .AddAttributeAWSSQSQueueName("QueueName") + .AddAttributeAwsS3Bucket("BucketName") + .AddAttributeAwsKinesisStreamName("StreamName") + .AddAttributeAwsSnsTopicArn("TopicArn") + .AddAttributeAwsSecretsmanagerSecretArn("ARN") + .AddAttributeAwsSecretsmanagerSecretArn("SecretId") + .AddAttributeAwsStepFunctionsActivityArn("ActivityArn") + .AddAttributeAwsStepFunctionsStateMachineArn("StateMachineArn") + .AddAttributeAwsLambdaResourceMappingId("UUID") + .AddAttributeAWSLambdaFunctionName("FunctionName") + .AddAttributeAWSKinesisStreamArn("StreamARN") + .AddAttributeAWSDynamoTableArn("TableArn") + .AddAttributeAWSBedrockGuardrailArn("GuardrailArn") .Build(); } internal static IReadOnlyDictionary> ServiceRequestParameterMap { get; } = new Dictionary>() { - { AWSServiceType.DynamoDbService, ["TableName"] }, - { AWSServiceType.SQSService, ["QueueUrl"] }, + { AWSServiceType.DynamoDbService, ["TableName", "TableArn"] }, + { AWSServiceType.SQSService, ["QueueUrl", "QueueName"] }, { AWSServiceType.BedrockAgentService, ["AgentId", "KnowledgeBaseId", "DataSourceId"] }, { AWSServiceType.BedrockAgentRuntimeService, ["AgentId", "KnowledgeBaseId"] }, { AWSServiceType.BedrockRuntimeService, ["ModelId"] }, + { AWSServiceType.S3Service, ["BucketName"] }, + { AWSServiceType.KinesisService, ["StreamName", "StreamARN"] }, + { AWSServiceType.LambdaService, ["UUID", "FunctionName"] }, + { AWSServiceType.SecretsManagerService, ["SecretId"] }, + { AWSServiceType.SNSService, ["TopicArn"] }, + { AWSServiceType.StepFunctionsService, ["ActivityArn", "StateMachineArn"] }, }; internal static IReadOnlyDictionary> ServiceResponseParameterMap { get; } = new Dictionary>() { - { AWSServiceType.BedrockService, ["GuardrailId"] }, + { AWSServiceType.BedrockService, ["GuardrailId", "GuardrailArn"] }, { AWSServiceType.BedrockAgentService, ["AgentId", "DataSourceId"] }, + { AWSServiceType.SecretsManagerService, ["ARN"] }, + { AWSServiceType.SQSService, ["QueueUrl"] }, }; - // for Bedrock Agent operations, we map each supported operation to one resource: Agent, DataSource, or KnowledgeBase - internal static List BedrockAgentAgentOps { get; } = - [ - "CreateAgentActionGroup", - "CreateAgentAlias", - "DeleteAgentActionGroup", - "DeleteAgentAlias", - "DeleteAgent", - "DeleteAgentVersion", - "GetAgentActionGroup", - "GetAgentAlias", - "GetAgent", - "GetAgentVersion", - "ListAgentActionGroups", - "ListAgentAliases", - "ListAgentKnowledgeBases", - "ListAgentVersions", - "PrepareAgent", - "UpdateAgentActionGroup", - "UpdateAgentAlias", - "UpdateAgent" - ]; - - internal static List BedrockAgentKnowledgeBaseOps { get; } = - [ - "AssociateAgentKnowledgeBase", - "CreateDataSource", - "DeleteKnowledgeBase", - "DisassociateAgentKnowledgeBase", - "GetAgentKnowledgeBase", - "GetKnowledgeBase", - "ListDataSources", - "UpdateAgentKnowledgeBase" - ]; - - internal static List BedrockAgentDataSourceOps { get; } = - [ - "DeleteDataSource", - "GetDataSource", - "UpdateDataSource" - ]; - internal IDictionary ParameterAttributeMap { get; } - internal static IReadOnlyDictionary OperationNameToResourceMap() - { - var operationClassMap = new Dictionary(); - - foreach (var op in BedrockAgentKnowledgeBaseOps) - { - operationClassMap[op] = "KnowledgeBaseId"; - } - - foreach (var op in BedrockAgentDataSourceOps) - { - operationClassMap[op] = "DataSourceId"; - } - - foreach (var op in BedrockAgentAgentOps) - { - operationClassMap[op] = "AgentId"; - } - - return operationClassMap; - } - internal static string GetAWSServiceName(IRequestContext requestContext) => Utils.RemoveAmazonPrefixFromServiceName(requestContext.ServiceMetaData.ServiceId); diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs index 4a69b87f17..3991747005 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSServiceType.cs @@ -12,6 +12,11 @@ internal class AWSServiceType internal const string BedrockAgentService = "Bedrock Agent"; internal const string BedrockAgentRuntimeService = "Bedrock Agent Runtime"; internal const string BedrockRuntimeService = "Bedrock Runtime"; + internal const string S3Service = "S3"; + internal const string KinesisService = "Kinesis"; + internal const string LambdaService = "Lambda"; + internal const string SecretsManagerService = "Secrets Manager"; + internal const string StepFunctionsService = "SFN"; internal static bool IsDynamoDbService(string service) => DynamoDbService.Equals(service, StringComparison.OrdinalIgnoreCase); @@ -33,4 +38,19 @@ internal static bool IsBedrockAgentRuntimeService(string service) internal static bool IsBedrockRuntimeService(string service) => BedrockRuntimeService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsS3Service(string service) + => S3Service.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsKinesisService(string service) + => KinesisService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsLambdaService(string service) + => LambdaService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsSecretsManagerService(string service) + => SecretsManagerService.Equals(service, StringComparison.OrdinalIgnoreCase); + + internal static bool IsStepFunctionsService(string service) + => StepFunctionsService.Equals(service, StringComparison.OrdinalIgnoreCase); } diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs index c2a9ee300e..a91b3292d9 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/AWSTracingPipelineHandler.cs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using Amazon.BedrockRuntime.Model; using Amazon.Runtime; using Amazon.Runtime.Internal; using Amazon.Runtime.Telemetry; +using Amazon.Util; using OpenTelemetry.AWS; using OpenTelemetry.Context.Propagation; @@ -46,6 +48,36 @@ public override async Task InvokeAsync(IExecutionContext executionContext) return ret; } + private static string FetchRequestId(IRequestContext requestContext, IResponseContext responseContext) + { + var request_id = string.Empty; + var response = responseContext.Response; + if (response != null) + { + request_id = response.ResponseMetadata.RequestId; + } + else + { + var request_headers = requestContext.Request.Headers; + if (string.IsNullOrEmpty(request_id) && request_headers.TryGetValue("x-amzn-RequestId", out var req_id)) + { + request_id = req_id; + } + + if (string.IsNullOrEmpty(request_id) && request_headers.TryGetValue("x-amz-request-id", out req_id)) + { + request_id = req_id; + } + + if (string.IsNullOrEmpty(request_id) && request_headers.TryGetValue("x-amz-id-2", out req_id)) + { + request_id = req_id; + } + } + + return request_id; + } + private static void AddPropagationDataToRequest(Activity activity, IRequestContext requestContext) { var service = requestContext.ServiceMetaData.ServiceId; @@ -72,6 +104,7 @@ private void AddResponseSpecificInformation(Activity activity, IExecutionContext { var service = executionContext.RequestContext.ServiceMetaData.ServiceId; var responseContext = executionContext.ResponseContext; + var requestContext = executionContext.RequestContext; if (AWSServiceHelper.ServiceResponseParameterMap.TryGetValue(service, out var parameters)) { @@ -81,16 +114,6 @@ private void AddResponseSpecificInformation(Activity activity, IExecutionContext { try { - // for bedrock agent, extract attribute from object in response. - if (AWSServiceType.IsBedrockAgentService(service)) - { - var operationName = Utils.RemoveSuffix(response.GetType().Name, "Response"); - if (AWSServiceHelper.OperationNameToResourceMap()[operationName] == parameter) - { - this.AddBedrockAgentResponseAttribute(activity, response, parameter); - } - } - var property = response.GetType().GetProperty(parameter); if (property != null) { @@ -107,32 +130,82 @@ private void AddResponseSpecificInformation(Activity activity, IExecutionContext } } } - } -#if NET - [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage( - "Trimming", - "IL2075", - Justification = "The reflected properties were already used by the AWS SDK's marshallers so the properties could not have been trimmed.")] -#endif - private void AddBedrockAgentResponseAttribute(Activity activity, AmazonWebServiceResponse response, string parameter) - { - var responseObject = response.GetType().GetProperty(Utils.RemoveSuffix(parameter, "Id")); - if (responseObject != null) + // for bedrock runtime, LLM specific attributes are extracted based on the model ID. + if (AWSServiceType.IsBedrockRuntimeService(service)) { - var attributeObject = responseObject.GetValue(response); - if (attributeObject != null) + var model = this.awsSemanticConventions.TagExtractor.GetTagAttributeGenAiModelId(activity); + if (model != null) { - var property = attributeObject.GetType().GetProperty(parameter); - if (property != null) + var modelString = model.ToString(); + if (modelString != null) { - if (this.awsServiceHelper.ParameterAttributeMap.TryGetValue(parameter, out var attribute)) + var response = (InvokeModelResponse)responseContext.Response; + AWSLlmModelProcessor.ProcessGenAiAttributes(activity, response.Body, modelString, false, this.awsSemanticConventions); + } + } + } + + // for Lambda, extract function ARN from response Configuration object. + if (AWSServiceType.IsLambdaService(service)) + { + var response = responseContext.Response; + var configuration = response.GetType().GetProperty("Configuration"); + if (configuration != null) + { + var configObject = configuration.GetValue(response); + if (configObject != null) + { + var functionArn = configObject.GetType().GetProperty("FunctionArn"); + if (functionArn != null) { - activity.SetTag(attribute, property.GetValue(attributeObject)); + var functionArnValue = functionArn.GetValue(configObject); + if (functionArnValue != null) + { + this.awsSemanticConventions.TagBuilder.SetTagAttributeAWSLambdaFunctionArn(activity, functionArnValue); + } } } } } + + if (AWSServiceType.IsDynamoDbService(service)) + { + var response = responseContext.Response; + var responseObject = response.GetType().GetProperty("Table"); + if (responseObject != null) + { + var tableObject = responseObject.GetValue(response); + if (tableObject != null) + { + var property = tableObject.GetType().GetProperty("TableArn"); + if (property != null) + { + if (this.awsServiceHelper.ParameterAttributeMap.TryGetValue("TableArn", out var attribute)) + { + activity.SetTag(attribute, property.GetValue(tableObject)); + } + } + } + } + } + + var httpResponse = responseContext.HttpResponse; + if (httpResponse != null) + { + var statusCode = (int)httpResponse.StatusCode; + + string? accessKey = requestContext.ClientConfig?.DefaultAWSCredentials?.GetCredentials()?.AccessKey; + string? determinedSigningRegion = requestContext.Request.DeterminedSigningRegion; + if (accessKey != null && determinedSigningRegion != null) + { + this.awsSemanticConventions.TagBuilder.SetTagAttributeAWSAuthAccessKey(activity, accessKey); + this.awsSemanticConventions.TagBuilder.SetTagAttributeAWSAuthRegion(activity, determinedSigningRegion); + } + + this.AddStatusCodeToActivity(activity, statusCode); + this.awsSemanticConventions.TagBuilder.SetTagAttributeHttpResponseHeaderContentLength(activity, httpResponse.ContentLength); + } } #if NET @@ -143,7 +216,7 @@ private void AddBedrockAgentResponseAttribute(Activity activity, AmazonWebServic #endif private void AddRequestSpecificInformation(Activity activity, IRequestContext requestContext) { - var service = requestContext.ServiceMetaData.ServiceId; + var service = AWSServiceHelper.GetAWSServiceName(requestContext); if (AWSServiceHelper.ServiceRequestParameterMap.TryGetValue(service, out var parameters)) { @@ -153,18 +226,58 @@ private void AddRequestSpecificInformation(Activity activity, IRequestContext re { try { - // for bedrock agent, we only extract one attribute based on the operation. - if (AWSServiceType.IsBedrockAgentService(service)) + var property = request.GetType().GetProperty(parameter); + if (property != null) { - if (AWSServiceHelper.OperationNameToResourceMap()[AWSServiceHelper.GetAWSOperationName(requestContext)] != parameter) + // for bedrock runtime, LLM specific attributes are extracted based on the model ID. + if (AWSServiceType.IsBedrockRuntimeService(service) && parameter == "ModelId") { - continue; + var model = property.GetValue(request); + if (model != null) + { + var modelString = model.ToString(); + if (modelString != null) + { + var invokeModelRequest = (InvokeModelRequest)request; + AWSLlmModelProcessor.ProcessGenAiAttributes(activity, invokeModelRequest.Body, modelString, true, this.awsSemanticConventions); + } + } + } + + // for secrets manager, only extract SecretId from request if it is a secret ARN. + if (AWSServiceType.IsSecretsManagerService(service) && parameter == "SecretId") + { + var secretId = property.GetValue(request); + if (secretId != null) + { + var secretIdString = secretId.ToString(); + if (secretIdString != null && !secretIdString.StartsWith("arn:aws:secretsmanager:", StringComparison.Ordinal)) + { + continue; + } + } + } + + if (AWSServiceType.IsLambdaService(service) && parameter == "FunctionName") + { + var functionName = property.GetValue(request); + if (functionName != null) + { + var functionNameString = functionName.ToString(); + if (functionNameString != null) + { + string[] parts = functionNameString.Split(':'); + var extractedFunctionName = parts.Length > 0 ? parts[parts.Length - 1] : null; + if (extractedFunctionName != null) + { + this.awsSemanticConventions.TagBuilder.SetTagAttributeAWSLambdaFunctionName(activity, extractedFunctionName); + } + + continue; + } + } } - } - var property = request.GetType().GetProperty(parameter); - if (property != null) - { if (this.awsServiceHelper.ParameterAttributeMap.TryGetValue(parameter, out var attribute)) { activity.SetTag(attribute, property.GetValue(request)); @@ -183,10 +296,27 @@ private void AddRequestSpecificInformation(Activity activity, IRequestContext re { this.awsSemanticConventions.TagBuilder.SetTagAttributeDbSystemToDynamoDb(activity); } + else if (AWSServiceType.IsSqsService(service)) + { + SqsRequestContextHelper.AddAttributes( + requestContext, AWSMessagingUtils.InjectIntoDictionary(new PropagationContext(activity.Context, Baggage.Current))); + } + else if (AWSServiceType.IsSnsService(service)) + { + SnsRequestContextHelper.AddAttributes( + requestContext, AWSMessagingUtils.InjectIntoDictionary(new PropagationContext(activity.Context, Baggage.Current))); + } else if (AWSServiceType.IsBedrockRuntimeService(service)) { this.awsSemanticConventions.TagBuilder.SetTagAttributeGenAiSystemToBedrock(activity); } + + var client = requestContext.ClientConfig; + if (client != null) + { + var region = client.RegionEndpoint?.SystemName; + this.awsSemanticConventions.TagBuilder.SetTagAttributeCloudRegion(activity, region ?? AWSSDKUtils.DetermineRegion(client.ServiceURL)); + } } private void ProcessEndRequest(Activity? activity, IExecutionContext executionContext) @@ -196,6 +326,13 @@ private void ProcessEndRequest(Activity? activity, IExecutionContext executionCo return; } + var responseContext = executionContext.ResponseContext; + var requestContext = executionContext.RequestContext; + if (this.awsSemanticConventions.TagExtractor.GetTagAttributeAwsRequestId == null) + { + this.awsSemanticConventions.TagBuilder.SetTagAttributeAwsRequestId(activity, FetchRequestId(requestContext, responseContext)); + } + this.AddResponseSpecificInformation(activity, executionContext); } @@ -225,4 +362,9 @@ private void ProcessEndRequest(Activity? activity, IExecutionContext executionCo return currentActivity; } + + private void AddStatusCodeToActivity(Activity activity, int status_code) + { + this.awsSemanticConventions.TagBuilder.SetTagAttributeHttpResponseStatusCode(activity, status_code); + } } diff --git a/src/OpenTelemetry.Instrumentation.AWS/Implementation/SourceGenerationContext.cs b/src/OpenTelemetry.Instrumentation.AWS/Implementation/SourceGenerationContext.cs new file mode 100644 index 0000000000..3cd27ad4f3 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AWS/Implementation/SourceGenerationContext.cs @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace OpenTelemetry.Instrumentation.AWS.Implementation; + +/// +/// "Source Generation" is feature added to System.Text.Json in .NET 6.0. +/// This is a performance optimization that avoids runtime reflection when performing serialization. +/// Serialization metadata will be computed at compile-time and included in the assembly. +/// . +/// . +/// . +/// +[JsonSerializable(typeof(Dictionary))] +internal sealed partial class SourceGenerationContext : JsonSerializerContext +{ +} diff --git a/src/OpenTelemetry.Instrumentation.AWS/OpenTelemetry.Instrumentation.AWS.csproj b/src/OpenTelemetry.Instrumentation.AWS/OpenTelemetry.Instrumentation.AWS.csproj index 894afb8ad7..6e28d3e3d1 100644 --- a/src/OpenTelemetry.Instrumentation.AWS/OpenTelemetry.Instrumentation.AWS.csproj +++ b/src/OpenTelemetry.Instrumentation.AWS/OpenTelemetry.Instrumentation.AWS.csproj @@ -11,6 +11,7 @@ $(TargetFrameworks);$(NetFrameworkMinimumSupportedVersion) AWS client instrumentation for OpenTelemetry .NET. Instrumentation.AWS- + $(SystemTextJsonLatestNet6OutOfBandPkgVer) INSTRUMENTATION_AWS;$(DefineConstants) @@ -25,6 +26,7 @@ + @@ -32,6 +34,7 @@ + diff --git a/src/Shared/AWS/AWSSemanticConventions.Base.cs b/src/Shared/AWS/AWSSemanticConventions.Base.cs index a8a6a39ec0..73103a6cfd 100644 --- a/src/Shared/AWS/AWSSemanticConventions.Base.cs +++ b/src/Shared/AWS/AWSSemanticConventions.Base.cs @@ -334,6 +334,113 @@ private abstract class AWSSemanticConventionsBase /// public virtual string AttributeAWSBedrock => string.Empty; + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSSQSQueueName => string.Empty; + + /// + /// The S3 bucket name the request refers to. Corresponds to the --bucket parameter of the S3 API operations. + /// + /// + /// The bucket attribute is applicable to all S3 operations that reference a bucket, i.e. that require the bucket name as a mandatory parameter. + /// This applies to almost all S3 operations except list-buckets. + /// + /// + /// AwsAttributes.AttributeAwsS3Bucket + /// + public virtual string AttributeAwsS3Bucket => string.Empty; + + /// + /// The name of the AWS Kinesis stream the request refers to. Corresponds to the --stream-name parameter of the Kinesis describe-stream operation. + /// + /// + /// AwsAttributes.AttributeAwsKinesisStreamName + /// + public virtual string AttributeAwsKinesisStreamName => string.Empty; + + /// + /// The AWS request ID as returned in the response headers x-amzn-requestid, x-amzn-request-id or x-amz-request-id. + /// + /// + /// AwsAttributes.AttributeAWSRequestId + /// + public virtual string AttributeAwsRequestId => string.Empty; + + /// + /// The UUID of the AWS Lambda EvenSource Mapping. An event source is mapped to a lambda function. It's contents are read by Lambda and used to trigger a function. This isn't available in the lambda execution context or the lambda runtime environtment. This is going to be populated by the AWS SDK for each language when that UUID is present. Some of these operations are Create/Delete/Get/List/Update EventSourceMapping. + /// + /// + /// AwsAttributes.AttributeAwsLambdaResourceMappingId + /// + public virtual string AttributeAwsLambdaResourceMappingId => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSLambdaFunctionName => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSKinesisStreamArn => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSDynamoTableArn => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSBedrockGuardrailArn => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSAuthRegion => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSAuthAccessKey => string.Empty; + + /// + /// Not yet incorporated in Semantic Conventions repository. + /// + public virtual string AttributeAWSLambdaFunctionArn => string.Empty; + + /// + /// The ARN of the Secret stored in the Secrets Mangger. + /// + /// + /// AwsAttributes.AttributeAwsSecretsmanagerSecretArn + /// + public virtual string AttributeAwsSecretsmanagerSecretArn => string.Empty; + + /// + /// The ARN of the AWS SNS Topic. An Amazon SNS topic is a logical access point that acts as a communication channel. + /// + /// + /// AwsAttributes.AttributeAwsSecretsmanagerSecretArn + /// + public virtual string AttributeAwsSnsTopicArn => string.Empty; + + /// + /// The ARN of the AWS Step Functions Activity. + /// + /// + /// AwsAttributes.AttributeAwsSecretsmanagerSecretArn + /// + public virtual string AttributeAwsStepFunctionsActivityArn => string.Empty; + + /// + /// The ARN of the AWS Step Functions State Machine. + /// + /// + /// AwsAttributes.AttributeAwsSecretsmanagerSecretArn + /// + public virtual string AttributeAwsStepFunctionsStateMachineArn => string.Empty; #endregion #region FAAS Attributes @@ -485,6 +592,53 @@ private abstract class AWSSemanticConventionsBase /// public virtual string AttributeGenAiSystem => string.Empty; + /// + /// The top_p sampling setting for the GenAI request. + /// + /// + /// GenAiAttributes.AttributeGenAiTopP + /// + public virtual string AttributeGenAiTopP => string.Empty; + + /// + /// The temperature setting for the GenAI request. + /// + /// + /// GenAiAttributes.AttributeGenAiTemperature + /// + public virtual string AttributeGenAiTemperature => string.Empty; + + /// + /// The maximum number of tokens the model generates for a request. + /// + /// + /// GenAiAttributes.AttributeGenAiMaxTokens + /// + public virtual string AttributeGenAiMaxTokens => string.Empty; + + /// + /// The number of tokens used in the GenAI input (prompt). + /// + /// + /// GenAiAttributes.AttributeGenAiInputTokens + /// + public virtual string AttributeGenAiInputTokens => string.Empty; + + /// + /// The number of tokens used in the GenAI response (completion). + /// + /// + /// GenAiAttributes.AttributeGenAiOutputTokens + /// + public virtual string AttributeGenAiOutputTokens => string.Empty; + + /// + /// Array of reasons the model stopped generating tokens, corresponding to each generation received. + /// + /// + /// GenAiAttributes.AttributeGenAiFinishReasons + /// + public virtual string AttributeGenAiFinishReasons => string.Empty; #endregion #region HOST Attributes @@ -585,6 +739,25 @@ private abstract class AWSSemanticConventionsBase /// public virtual string AttributeHttpRequestMethod => string.Empty; + /// + /// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, + /// present as the Content-Length header. For requests using transport encoding, this should be the compressed size. + /// + /// + /// HttpAttributes.AttributeHttpResponseContentLength + /// + [Obsolete("Replaced by http.response.header.content-length.")] + public virtual string AttributeHttpResponseContentLength => string.Empty; + + /// + /// The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and is often, but not always, + /// present as the Content-Length header. For requests using transport encoding, this should be the compressed size. + /// + /// + /// HttpAttributes.AttributeHttpResponseHeaderContentLength + /// + public virtual string AttributeHttpResponseHeaderContentLength => string.Empty; + #endregion #region NET Attributes diff --git a/src/Shared/AWS/AWSSemanticConventions.Legacy.cs b/src/Shared/AWS/AWSSemanticConventions.Legacy.cs index bc60e9f7e6..535370a1db 100644 --- a/src/Shared/AWS/AWSSemanticConventions.Legacy.cs +++ b/src/Shared/AWS/AWSSemanticConventions.Legacy.cs @@ -62,6 +62,22 @@ private abstract class AWSSemanticConventionsLegacy : AWSSemanticConventionsBase public override string AttributeAWSBedrockGuardrailId => "aws.bedrock.guardrail.id"; public override string AttributeAWSBedrockKnowledgeBaseId => "aws.bedrock.knowledge_base.id"; public override string AttributeAWSBedrock => "aws_bedrock"; + public override string AttributeAwsRequestId => "aws.request_id"; + public override string AttributeAWSSQSQueueName => "aws.sqs.queue_name"; + public override string AttributeAwsS3Bucket => "aws.s3.bucket"; + public override string AttributeAwsKinesisStreamName => "aws.kinesis.stream_name"; + public override string AttributeAwsLambdaResourceMappingId => "aws.lambda.resource_mapping.id"; + public override string AttributeAWSLambdaFunctionName => "aws.lambda.function.name"; + public override string AttributeAWSKinesisStreamArn => "aws.kinesis.stream.arn"; + public override string AttributeAWSDynamoTableArn => "aws.dynamodb.table.arn"; + public override string AttributeAWSBedrockGuardrailArn => "aws.bedrock.guardrail.arn"; + public override string AttributeAWSAuthRegion => "aws.auth.region"; + public override string AttributeAWSAuthAccessKey => "aws.auth.account.access_key"; + public override string AttributeAWSLambdaFunctionArn => "aws.lambda.function.arn"; + public override string AttributeAwsSecretsmanagerSecretArn => "aws.secretsmanager.secret.arn"; + public override string AttributeAwsSnsTopicArn => "aws.sns.topic.arn"; + public override string AttributeAwsStepFunctionsActivityArn => "aws.stepfunctions.activity.arn"; + public override string AttributeAwsStepFunctionsStateMachineArn => "aws.stepfunctions.state_machine.arn"; // FAAS Attributes public override string AttributeFaasID => "faas.id"; @@ -76,6 +92,12 @@ private abstract class AWSSemanticConventionsLegacy : AWSSemanticConventionsBase // GEN AI Attributes public override string AttributeGenAiModelId => "gen_ai.request.model"; public override string AttributeGenAiSystem => "gen_ai.system"; + public override string AttributeGenAiTopP => "gen_ai.request.top_p"; + public override string AttributeGenAiTemperature => "gen_ai.request.temperature"; + public override string AttributeGenAiMaxTokens => "gen_ai.request.max_tokens"; + public override string AttributeGenAiInputTokens => "gen_ai.usage.input_tokens"; + public override string AttributeGenAiOutputTokens => "gen_ai.usage.output_tokens"; + public override string AttributeGenAiFinishReasons => "gen_ai.response.finish_reasons"; // HOST Attributes public override string AttributeHostID => "host.id"; @@ -91,6 +113,9 @@ private abstract class AWSSemanticConventionsLegacy : AWSSemanticConventionsBase public override string AttributeHttpTarget => "http.target"; [Obsolete("Replaced by http.request.method.")] public override string AttributeHttpMethod => "http.method"; + [Obsolete("Replaced by http.response.header.content-length.")] + public override string AttributeHttpResponseContentLength => "http.response_content_length"; + public override string AttributeHttpResponseHeaderContentLength => "http.response.header.content-length"; // NET Attributes [Obsolete("Replaced by server.address.")] diff --git a/src/Shared/AWS/AWSSemanticConventions.cs b/src/Shared/AWS/AWSSemanticConventions.cs index 4fc56cf917..ce17c3dd30 100644 --- a/src/Shared/AWS/AWSSemanticConventions.cs +++ b/src/Shared/AWS/AWSSemanticConventions.cs @@ -79,6 +79,9 @@ internal partial class AWSSemanticConventions /// public TagBuilderImpl TagBuilder { get; } + /// + public TagExtractorImpl TagExtractor { get; } + /// /// /// Sets the that will be used to resolve attribute names. @@ -89,6 +92,7 @@ public AWSSemanticConventions(SemanticConventionVersion semanticConventionVersio this.AttributeBuilder = new(this); this.ParameterMappingBuilder = new(this); this.TagBuilder = new(this); + this.TagExtractor = new(this); } /// @@ -97,7 +101,7 @@ public AWSSemanticConventions(SemanticConventionVersion semanticConventionVersio public class ParameterMappingBuilderImpl { private readonly AWSSemanticConventions awsSemanticConventions; - private Dictionary state = new(); + private Dictionary state = []; public ParameterMappingBuilderImpl(AWSSemanticConventions semanticConventions) { @@ -110,7 +114,7 @@ public IDictionary Build() { var builtState = this.state; - this.state = new Dictionary(); + this.state = []; return builtState; } @@ -143,6 +147,66 @@ public ParameterMappingBuilderImpl AddAttributeAWSBedrockGuardrailId(string valu /// public ParameterMappingBuilderImpl AddAttributeAWSBedrockKnowledgeBaseId(string value) => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSBedrockKnowledgeBaseId, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSSQSQueueName(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSSQSQueueName, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsS3Bucket(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsS3Bucket, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsKinesisStreamName(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsKinesisStreamName, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsSnsTopicArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsSnsTopicArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsSecretsmanagerSecretArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsSecretsmanagerSecretArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsStepFunctionsActivityArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsStepFunctionsActivityArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsStepFunctionsStateMachineArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsStepFunctionsStateMachineArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAwsLambdaResourceMappingId(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAwsLambdaResourceMappingId, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSLambdaFunctionName(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSLambdaFunctionName, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSKinesisStreamArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSKinesisStreamArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSDynamoTableArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSDynamoTableArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSBedrockGuardrailArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSBedrockGuardrailArn, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSAuthRegion(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSAuthRegion, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSAuthAccessKey(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSAuthAccessKey, value); + + /// + public ParameterMappingBuilderImpl AddAttributeAWSLambdaFunctionArn(string value) + => this.awsSemanticConventions.AddDic(this, x => x.AttributeAWSLambdaFunctionArn, value); #endregion } @@ -419,6 +483,54 @@ public TagBuilderImpl(AWSSemanticConventions semanticConventions) /// public Activity? SetTagAttributeGenAiSystemToBedrock(Activity? activity) => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiSystem, x => x.AttributeAWSBedrock); + + /// + public Activity? SetTagAttributeAwsRequestId(Activity? activity, string operationName) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeAwsRequestId, operationName); + + /// + public Activity? SetTagAttributeAWSLambdaFunctionName(Activity? activity, string functionName) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeAWSLambdaFunctionName, functionName); + + /// + public Activity? SetTagAttributeAWSLambdaFunctionArn(Activity? activity, object functionArn) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeAWSLambdaFunctionArn, functionArn); + + /// + public Activity? SetTagAttributeAWSAuthAccessKey(Activity? activity, string authAccessKey) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeAWSAuthAccessKey, authAccessKey); + + /// + public Activity? SetTagAttributeAWSAuthRegion(Activity? activity, string authRegion) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeAWSAuthRegion, authRegion); + #endregion + + #region GEN AI + + /// + public Activity? SetTagAttributeGenAiTopP(Activity? activity, double topP) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiTopP, topP); + + /// + public Activity? SetTagAttributeGenAiTemperature(Activity? activity, double temperature) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiTemperature, temperature); + + /// + public Activity? SetTagAttributeGenAiMaxTokens(Activity? activity, int maxTokens) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiMaxTokens, maxTokens); + + /// + public Activity? SetTagAttributeGenAiInputTokens(Activity? activity, int inputTokens) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiInputTokens, inputTokens); + + /// + public Activity? SetTagAttributeGenAiOutputTokens(Activity? activity, int outputTokens) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiOutputTokens, outputTokens); + + /// + public Activity? SetTagAttributeGenAiFinishReasons(Activity? activity, string[] finishReasons) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeGenAiFinishReasons, finishReasons); + #endregion #region Http @@ -431,6 +543,42 @@ public TagBuilderImpl(AWSSemanticConventions semanticConventions) /// public Activity? SetTagAttributeHttpResponseStatusCode(Activity? activity, int value) => this.awsSemanticConventions.SetTag(activity, x => x.AttributeHttpResponseStatusCode, value); + + /// + public Activity? SetTagAttributeHttpResponseHeaderContentLength(Activity? activity, long value) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeHttpResponseHeaderContentLength, value); + #endregion + + #region Cloud + + /// + public Activity? SetTagAttributeCloudRegion(Activity? activity, string operationName) + => this.awsSemanticConventions.SetTag(activity, x => x.AttributeCloudRegion, operationName); + #endregion + } + + /// + /// Get Attributes from . + /// + public class TagExtractorImpl + { + private readonly AWSSemanticConventions awsSemanticConventions; + + public TagExtractorImpl(AWSSemanticConventions semanticConventions) + { + this.awsSemanticConventions = semanticConventions; + } + + #region AWS + /// + public object? GetTagAttributeAwsRequestId(Activity? activity) + => this.awsSemanticConventions.GetTag(activity, x => x.AttributeAwsRequestId); + #endregion + + #region GEN AI + /// + public object? GetTagAttributeGenAiModelId(Activity? activity) + => this.awsSemanticConventions.GetTag(activity, x => x.AttributeGenAiModelId); #endregion } @@ -475,6 +623,16 @@ private AttributeBuilderImpl Add(AttributeBuilderImpl attributes, Func attributeNameFunc) + { + var semanticConventionVersionImpl = this.GetSemanticConventionVersion(); + + var attributeName = attributeNameFunc(semanticConventionVersionImpl); + + // if attributeName is empty, exit + return string.IsNullOrEmpty(attributeName) ? null : activity?.GetTagItem(attributeName); + } + private ParameterMappingBuilderImpl AddDic(ParameterMappingBuilderImpl dict, Func attributeNameFunc, string value) { var semanticConventionVersionImpl = this.GetSemanticConventionVersion(); diff --git a/test/OpenTelemetry.Instrumentation.AWS.Tests/TestAWSClientInstrumentation.cs b/test/OpenTelemetry.Instrumentation.AWS.Tests/TestAWSClientInstrumentation.cs index 217512280d..46c4a3a826 100644 --- a/test/OpenTelemetry.Instrumentation.AWS.Tests/TestAWSClientInstrumentation.cs +++ b/test/OpenTelemetry.Instrumentation.AWS.Tests/TestAWSClientInstrumentation.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Text; +using System.Text.Json; using Amazon; using Amazon.Bedrock; using Amazon.Bedrock.Model; @@ -24,6 +26,8 @@ namespace OpenTelemetry.Instrumentation.AWS.Tests; public class TestAWSClientInstrumentation { + private static readonly string[] BedrockRuntimeExpectedFinishReasons = ["finish_reason"]; + [Fact] #if NETFRAMEWORK public void TestDDBScanSuccessful() @@ -341,9 +345,9 @@ public async Task TestBedrockGetGuardrailSuccessful() [Fact] #if NETFRAMEWORK - public void TestBedrockRuntimeInvokeModelSuccessful() + public void TestBedrockRuntimeInvokeModelNovaSuccessful() #else - public async Task TestBedrockRuntimeInvokeModelSuccessful() + public async Task TestBedrockRuntimeInvokeModelNovaSuccessful() #endif { var exportedItems = new List(); @@ -362,9 +366,404 @@ public async Task TestBedrockRuntimeInvokeModelSuccessful() .Build()) { var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); - var dummyResponse = "{}"; + var dummyResponse = @" + { + ""usage"": + { + ""inputTokens"": 12345, + ""outputTokens"": 67890 + }, + ""stopReason"": ""finish_reason"" + }"; + CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "amazon.nova-micro-v1:0", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + inferenceConfig = new + { + temperature = 0.123, + top_p = 0.456, + max_new_tokens = 789, + }, + }))), + }; +#if NETFRAMEWORK + var response = bedrockruntime.InvokeModel(invokeModelRequest); +#else + var response = await bedrockruntime.InvokeModelAsync(invokeModelRequest); +#endif + } + + Assert.NotEmpty(exportedItems); + var awssdk_activity = exportedItems.FirstOrDefault(e => e.DisplayName == "Bedrock Runtime.InvokeModel"); + Assert.NotNull(awssdk_activity); + + this.ValidateAWSActivity(awssdk_activity, parent); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "amazon.nova-micro-v1:0"); + + Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); + Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); + } + + [Fact] +#if NETFRAMEWORK + public void TestBedrockRuntimeInvokeModelTitanSuccessful() +#else + public async Task TestBedrockRuntimeInvokeModelTitanSuccessful() +#endif + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + var requestId = @"fakerequ-esti-dfak-ereq-uestidfakere"; + + using (Sdk.CreateTracerProviderBuilder() + .AddXRayTraceId() + .SetSampler(new AlwaysOnSampler()) + .AddAWSInstrumentation(o => + { + o.SemanticConventionVersion = SemanticConventionVersion.Latest; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); + var dummyResponse = @" + { + ""inputTextTokenCount"": 12345, + ""results"": [ + { + ""tokenCount"": 67890, + ""completionReason"": ""finish_reason"" + } + ] + }"; + CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "amazon.titan-text-express-v1", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + textGenerationConfig = new + { + temperature = 0.123, + topP = 0.456, + maxTokenCount = 789, + }, + }))), + }; +#if NETFRAMEWORK + var response = bedrockruntime.InvokeModel(invokeModelRequest); +#else + var response = await bedrockruntime.InvokeModelAsync(invokeModelRequest); +#endif + } + + Assert.NotEmpty(exportedItems); + var awssdk_activity = exportedItems.FirstOrDefault(e => e.DisplayName == "Bedrock Runtime.InvokeModel"); + Assert.NotNull(awssdk_activity); + + this.ValidateAWSActivity(awssdk_activity, parent); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "amazon.titan-text-express-v1"); + + Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); + Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); + } + + [Fact] +#if NETFRAMEWORK + public void TestBedrockRuntimeInvokeModelClaudeSuccessful() +#else + public async Task TestBedrockRuntimeInvokeModelClaudeSuccessful() +#endif + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + var requestId = @"fakerequ-esti-dfak-ereq-uestidfakere"; + + using (Sdk.CreateTracerProviderBuilder() + .AddXRayTraceId() + .SetSampler(new AlwaysOnSampler()) + .AddAWSInstrumentation(o => + { + o.SemanticConventionVersion = SemanticConventionVersion.Latest; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); + var dummyResponse = @" + { + ""usage"": + { + ""input_tokens"": 12345, + ""output_tokens"": 67890 + }, + ""stop_reason"": ""finish_reason"" + }"; CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); - var invokeModelRequest = new InvokeModelRequest { ModelId = "amazon.titan-text-express-v1" }; + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "anthropic.claude-3-5-haiku-202410-22-v1:0", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + temperature = 0.123, + top_p = 0.456, + max_tokens = 789, + }))), + }; +#if NETFRAMEWORK + var response = bedrockruntime.InvokeModel(invokeModelRequest); +#else + var response = await bedrockruntime.InvokeModelAsync(invokeModelRequest); +#endif + } + + Assert.NotEmpty(exportedItems); + var awssdk_activity = exportedItems.FirstOrDefault(e => e.DisplayName == "Bedrock Runtime.InvokeModel"); + Assert.NotNull(awssdk_activity); + + this.ValidateAWSActivity(awssdk_activity, parent); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "anthropic.claude-3-5-haiku-202410-22-v1:0"); + + Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); + Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); + } + + [Fact] +#if NETFRAMEWORK + public void TestBedrockRuntimeInvokeModelLlamaSuccessful() +#else + public async Task TestBedrockRuntimeInvokeModelLlamaSuccessful() +#endif + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + var requestId = @"fakerequ-esti-dfak-ereq-uestidfakere"; + + using (Sdk.CreateTracerProviderBuilder() + .AddXRayTraceId() + .SetSampler(new AlwaysOnSampler()) + .AddAWSInstrumentation(o => + { + o.SemanticConventionVersion = SemanticConventionVersion.Latest; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); + var dummyResponse = @" + { + ""prompt_token_count"": 12345, + ""generation_token_count"": 67890, + ""stop_reason"": ""finish_reason"" + }"; + CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "meta.llama3-8b-instruct-v1:0", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + temperature = 0.123, + top_p = 0.456, + max_gen_len = 789, + }))), + }; +#if NETFRAMEWORK + var response = bedrockruntime.InvokeModel(invokeModelRequest); +#else + var response = await bedrockruntime.InvokeModelAsync(invokeModelRequest); +#endif + } + + Assert.NotEmpty(exportedItems); + var awssdk_activity = exportedItems.FirstOrDefault(e => e.DisplayName == "Bedrock Runtime.InvokeModel"); + Assert.NotNull(awssdk_activity); + + this.ValidateAWSActivity(awssdk_activity, parent); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "meta.llama3-8b-instruct-v1:0"); + + Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); + Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); + } + + [Fact] +#if NETFRAMEWORK + public void TestBedrockRuntimeInvokeModelCommandSuccessful() +#else + public async Task TestBedrockRuntimeInvokeModelCommandSuccessful() +#endif + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + var requestId = @"fakerequ-esti-dfak-ereq-uestidfakere"; + + using (Sdk.CreateTracerProviderBuilder() + .AddXRayTraceId() + .SetSampler(new AlwaysOnSampler()) + .AddAWSInstrumentation(o => + { + o.SemanticConventionVersion = SemanticConventionVersion.Latest; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); + + // no input_tokens or output_tokens in response body, so we generate input and output text of the desired length + // (6 chars * number of tokens) to get the desired token estimation. + var dummyResponse = @" + { + ""text"": """ + string.Concat(Enumerable.Repeat("sample", 67890)) + @""", + ""finish_reason"": ""finish_reason"" + }"; + CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "cohere.command-r-v1:0", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + message = string.Concat(Enumerable.Repeat("sample", 12345)), + temperature = 0.123, + p = 0.456, + max_tokens = 789, + }))), + }; +#if NETFRAMEWORK + var response = bedrockruntime.InvokeModel(invokeModelRequest); +#else + var response = await bedrockruntime.InvokeModelAsync(invokeModelRequest); +#endif + } + + Assert.NotEmpty(exportedItems); + var awssdk_activity = exportedItems.FirstOrDefault(e => e.DisplayName == "Bedrock Runtime.InvokeModel"); + Assert.NotNull(awssdk_activity); + + this.ValidateAWSActivity(awssdk_activity, parent); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "cohere.command-r-v1:0"); + + Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); + Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); + } + + [Fact] +#if NETFRAMEWORK + public void TestBedrockRuntimeInvokeModelJambaSuccessful() +#else + public async Task TestBedrockRuntimeInvokeModelJambaSuccessful() +#endif + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + var requestId = @"fakerequ-esti-dfak-ereq-uestidfakere"; + + using (Sdk.CreateTracerProviderBuilder() + .AddXRayTraceId() + .SetSampler(new AlwaysOnSampler()) + .AddAWSInstrumentation(o => + { + o.SemanticConventionVersion = SemanticConventionVersion.Latest; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); + var dummyResponse = @" + { + ""usage"": + { + ""prompt_tokens"": 12345, + ""completion_tokens"": 67890 + }, + ""choices"": [ + { + ""finish_reason"": ""finish_reason"" + } + ] + }"; + CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "ai21.jamba-1-5-large-v1:0", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + temperature = 0.123, + top_p = 0.456, + max_tokens = 789, + }))), + }; +#if NETFRAMEWORK + var response = bedrockruntime.InvokeModel(invokeModelRequest); +#else + var response = await bedrockruntime.InvokeModelAsync(invokeModelRequest); +#endif + } + + Assert.NotEmpty(exportedItems); + var awssdk_activity = exportedItems.FirstOrDefault(e => e.DisplayName == "Bedrock Runtime.InvokeModel"); + Assert.NotNull(awssdk_activity); + + this.ValidateAWSActivity(awssdk_activity, parent); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "ai21.jamba-1-5-large-v1:0"); + + Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); + Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); + } + + [Fact] +#if NETFRAMEWORK + public void TestBedrockRuntimeInvokeModelMistralSuccessful() +#else + public async Task TestBedrockRuntimeInvokeModelMistralSuccessful() +#endif + { + var exportedItems = new List(); + + var parent = new Activity("parent").Start(); + var requestId = @"fakerequ-esti-dfak-ereq-uestidfakere"; + + using (Sdk.CreateTracerProviderBuilder() + .AddXRayTraceId() + .SetSampler(new AlwaysOnSampler()) + .AddAWSInstrumentation(o => + { + o.SemanticConventionVersion = SemanticConventionVersion.Latest; + }) + .AddInMemoryExporter(exportedItems) + .Build()) + { + var bedrockruntime = new AmazonBedrockRuntimeClient(new AnonymousAWSCredentials(), RegionEndpoint.USEast1); + + // no input_tokens or output_tokens in response body, so we generate input and output text of the desired length + // (6 chars * number of tokens) to get the desired token estimation. + var dummyResponse = @" + { + ""outputs"": [ + { + ""text"": """ + string.Concat(Enumerable.Repeat("sample", 67890)) + @""", + ""stop_reason"": ""finish_reason"" + } + ] + }"; + CustomResponses.SetResponse(bedrockruntime, dummyResponse, requestId, true); + var invokeModelRequest = new InvokeModelRequest + { + ModelId = "mistral.mistral-7b-instruct-v0:2", + Body = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new + { + prompt = string.Concat(Enumerable.Repeat("sample", 12345)), + temperature = 0.123, + top_p = 0.456, + max_tokens = 789, + }))), + }; #if NETFRAMEWORK var response = bedrockruntime.InvokeModel(invokeModelRequest); #else @@ -377,7 +776,7 @@ public async Task TestBedrockRuntimeInvokeModelSuccessful() Assert.NotNull(awssdk_activity); this.ValidateAWSActivity(awssdk_activity, parent); - this.ValidateBedrockRuntimeActivityTags(awssdk_activity); + this.ValidateBedrockRuntimeActivityTags(awssdk_activity, "mistral.mistral-7b-instruct-v0:2"); Assert.Equal(ActivityStatusCode.Unset, awssdk_activity.Status); Assert.Equal(requestId, Utils.GetTagValue(awssdk_activity, "aws.request_id")); @@ -643,11 +1042,17 @@ private void ValidateBedrockActivityTags(Activity bedrock_activity) Assert.Equal("GetGuardrail", Utils.GetTagValue(bedrock_activity, "rpc.method")); } - private void ValidateBedrockRuntimeActivityTags(Activity bedrock_activity) + private void ValidateBedrockRuntimeActivityTags(Activity bedrock_activity, string model_id) { Assert.Equal("Bedrock Runtime.InvokeModel", bedrock_activity.DisplayName); - Assert.Equal("amazon.titan-text-express-v1", Utils.GetTagValue(bedrock_activity, "gen_ai.request.model")); + Assert.Equal(model_id, Utils.GetTagValue(bedrock_activity, "gen_ai.request.model")); Assert.Equal("aws.bedrock", Utils.GetTagValue(bedrock_activity, "gen_ai.system")); + Assert.Equal(0.123, Utils.GetTagValue(bedrock_activity, "gen_ai.request.temperature")); + Assert.Equal(0.456, Utils.GetTagValue(bedrock_activity, "gen_ai.request.top_p")); + Assert.Equal(789, Utils.GetTagValue(bedrock_activity, "gen_ai.request.max_tokens")); + Assert.Equal(12345, Utils.GetTagValue(bedrock_activity, "gen_ai.usage.input_tokens")); + Assert.Equal(67890, Utils.GetTagValue(bedrock_activity, "gen_ai.usage.output_tokens")); + Assert.Equal(BedrockRuntimeExpectedFinishReasons, Utils.GetTagValue(bedrock_activity, "gen_ai.response.finish_reasons")); Assert.Equal("aws-api", Utils.GetTagValue(bedrock_activity, "rpc.system")); Assert.Equal("Bedrock Runtime", Utils.GetTagValue(bedrock_activity, "rpc.service")); Assert.Equal("InvokeModel", Utils.GetTagValue(bedrock_activity, "rpc.method"));