Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ For any change that affects end users of this package, please add an entry under
If your change does not need a CHANGELOG entry, add the "skip changelog" label to your PR.

## Unreleased
- Add Resource and CFN Attributes for Bedrock AgentCore spans
([#495](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/495))
- Add botocore instrumentation extension for Bedrock AgentCore services with span attributes
([#490](https://github.com/aws-observability/aws-otel-python-instrumentation/pull/490))
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@

from amazon.opentelemetry.distro._aws_attribute_keys import (
AWS_AUTH_ACCESS_KEY,
AWS_AUTH_CREDENTIAL_PROVIDER_ARN,
AWS_AUTH_REGION,
AWS_BEDROCK_AGENT_ID,
AWS_BEDROCK_AGENTCORE_BROWSER_ARN,
AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN,
AWS_BEDROCK_AGENTCORE_GATEWAY_ARN,
AWS_BEDROCK_AGENTCORE_MEMORY_ARN,
AWS_BEDROCK_AGENTCORE_RUNTIME_ARN,
AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN,
AWS_BEDROCK_DATA_SOURCE_ID,
AWS_BEDROCK_GUARDRAIL_ARN,
AWS_BEDROCK_GUARDRAIL_ID,
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
AWS_CLOUDFORMATION_PRIMARY_IDENTIFIER,
AWS_DYNAMODB_TABLE_ARN,
AWS_GATEWAY_TARGET_ID,
AWS_KINESIS_STREAM_ARN,
AWS_KINESIS_STREAM_NAME,
AWS_LAMBDA_FUNCTION_ARN,
Expand Down Expand Up @@ -63,6 +71,13 @@
SERVICE_METRIC,
MetricAttributeGenerator,
)
from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import (
GEN_AI_BROWSER_ID,
GEN_AI_CODE_INTERPRETER_ID,
GEN_AI_GATEWAY_ID,
GEN_AI_MEMORY_ID,
GEN_AI_RUNTIME_ID,
)
from amazon.opentelemetry.distro.regional_resource_arn_parser import RegionalResourceArnParser
from amazon.opentelemetry.distro.sqs_url_parser import SqsUrlParser
from opentelemetry.sdk.resources import Resource
Expand Down Expand Up @@ -105,6 +120,7 @@
_NORMALIZED_SQS_SERVICE_NAME: str = "AWS::SQS"
_NORMALIZED_BEDROCK_SERVICE_NAME: str = "AWS::Bedrock"
_NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME: str = "AWS::BedrockRuntime"
_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME: str = "AWS::BedrockAgentCore"
_NORMALIZED_SECRETSMANAGER_SERVICE_NAME: str = "AWS::SecretsManager"
_NORMALIZED_SNS_SERVICE_NAME: str = "AWS::SNS"
_NORMALIZED_STEPFUNCTIONS_SERVICE_NAME: str = "AWS::StepFunctions"
Expand All @@ -118,6 +134,19 @@
# Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
_GRAPHQL: str = "graphql"

# AWS SDK service mapping for normalization
_AWS_SDK_SERVICE_MAPPING = {
"Bedrock Agent": _NORMALIZED_BEDROCK_SERVICE_NAME,
"Bedrock Agent Runtime": _NORMALIZED_BEDROCK_SERVICE_NAME,
"Bedrock Runtime": _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME,
"Bedrock AgentCore Control": _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME,
"Bedrock AgentCore": _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME,
"Secrets Manager": _NORMALIZED_SECRETSMANAGER_SERVICE_NAME,
"SNS": _NORMALIZED_SNS_SERVICE_NAME,
"SFN": _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME,
"Lambda": _NORMALIZED_LAMBDA_SERVICE_NAME,
}

_logger: Logger = getLogger(__name__)


Expand Down Expand Up @@ -327,16 +356,6 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str
as the associated remote resource (Model) is not listed in Cloud Control.
"""
if is_aws_sdk_span(span):
aws_sdk_service_mapping = {
"Bedrock Agent": _NORMALIZED_BEDROCK_SERVICE_NAME,
"Bedrock Agent Runtime": _NORMALIZED_BEDROCK_SERVICE_NAME,
"Bedrock Runtime": _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME,
"Secrets Manager": _NORMALIZED_SECRETSMANAGER_SERVICE_NAME,
"SNS": _NORMALIZED_SNS_SERVICE_NAME,
"SFN": _NORMALIZED_STEPFUNCTIONS_SERVICE_NAME,
"Lambda": _NORMALIZED_LAMBDA_SERVICE_NAME,
}

# Special handling for Lambda invoke operations
if _is_lambda_invoke_operation(span):
lambda_function_name = span.attributes.get(AWS_LAMBDA_FUNCTION_NAME)
Expand All @@ -345,7 +364,7 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str
# is missing rather than falling back to a generic service name
return lambda_function_name if lambda_function_name else UNKNOWN_REMOTE_SERVICE

return aws_sdk_service_mapping.get(service_name, "AWS::" + service_name)
return _AWS_SDK_SERVICE_MAPPING.get(service_name, "AWS::" + service_name)
return service_name


Expand Down Expand Up @@ -466,6 +485,16 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
elif is_key_present(span, GEN_AI_REQUEST_MODEL):
remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::Model"
remote_resource_identifier = _escape_delimiters(span.attributes.get(GEN_AI_REQUEST_MODEL))
elif (
_AWS_SDK_SERVICE_MAPPING.get(str(span.attributes.get(_RPC_SERVICE)))
== _NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME
):
agentcore_type, agentcore_identifier, agentcore_cfn_id = (
_get_bedrock_agentcore_resource_type_and_identifier(span)
)
remote_resource_type = agentcore_type
remote_resource_identifier = _escape_delimiters(agentcore_identifier) if agentcore_identifier else None
cloudformation_primary_identifier = _escape_delimiters(agentcore_cfn_id) if agentcore_cfn_id else None
elif is_key_present(span, AWS_SECRETSMANAGER_SECRET_ARN):
remote_resource_type = _NORMALIZED_SECRETSMANAGER_SERVICE_NAME + "::Secret"
remote_resource_identifier = _escape_delimiters(
Expand Down Expand Up @@ -674,6 +703,147 @@ def _set_span_kind_for_dependency(span: ReadableSpan, attributes: BoundedAttribu
attributes[AWS_SPAN_KIND] = span_kind


def _get_bedrock_agentcore_resource_type_and_identifier(
span: ReadableSpan,
) -> tuple[Optional[str], Optional[str], Optional[str]]:
"""Get BedrockAgentCore resource type, identifier, and CFN primary identifier based on span attributes."""
attrs = span.attributes
if not attrs:
return None, None, None

def format_resource_type(resource_type: Optional[str]) -> Optional[str]:
return f"{_NORMALIZED_BEDROCK_AGENTCORE_SERVICE_NAME}::{resource_type}" if resource_type else None

for handler in [
_handle_browser_attrs,
_handle_gateway_attrs,
_handle_runtime_attrs,
_handle_code_interpreter_attrs,
_handle_identity_attrs,
_handle_memory_attrs,
]:
resource_type, resource_identifier, cfn_primary_identifier = handler(attrs)
if resource_type:
return format_resource_type(resource_type), resource_identifier, cfn_primary_identifier

return None, None, None


def _handle_browser_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]:
browser_id = attrs.get(GEN_AI_BROWSER_ID)
browser_arn = attrs.get(AWS_BEDROCK_AGENTCORE_BROWSER_ARN)
if browser_id or browser_arn:
agentcore_cfn_identifier = None
if browser_id:
agentcore_cfn_identifier = str(browser_id)
elif browser_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(browser_arn))
resource_type = "Browser" if agentcore_cfn_identifier == "aws.browser.v1" else "BrowserCustom"
return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier
return None, None, None


def _handle_gateway_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]:
gateway_id = attrs.get(GEN_AI_GATEWAY_ID)
gateway_arn = attrs.get(AWS_BEDROCK_AGENTCORE_GATEWAY_ARN)
gateway_target_id = attrs.get(AWS_GATEWAY_TARGET_ID)

if gateway_target_id:
agentcore_cfn_identifier = str(gateway_target_id)
if gateway_id:
agentcore_cfn_identifier = str(gateway_id)
elif gateway_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn))
return "GatewayTarget", agentcore_cfn_identifier, agentcore_cfn_identifier

if gateway_arn or gateway_id:
agentcore_cfn_identifier = None
if gateway_id:
agentcore_cfn_identifier = str(gateway_id)
elif gateway_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(gateway_arn))
return "Gateway", agentcore_cfn_identifier, agentcore_cfn_identifier

return None, None, None


def _handle_runtime_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]:
runtime_id = attrs.get(GEN_AI_RUNTIME_ID)
runtime_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ARN)
runtime_endpoint_arn = attrs.get(AWS_BEDROCK_AGENTCORE_RUNTIME_ENDPOINT_ARN)

if runtime_endpoint_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_endpoint_arn))
return "RuntimeEndpoint", agentcore_cfn_identifier, str(runtime_endpoint_arn)

if runtime_arn or runtime_id:
agentcore_cfn_identifier = None
if runtime_id:
agentcore_cfn_identifier = str(runtime_id)
elif runtime_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(runtime_arn))
return "Runtime", agentcore_cfn_identifier, agentcore_cfn_identifier

return None, None, None


def _handle_code_interpreter_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]:
code_interpreter_id = attrs.get(GEN_AI_CODE_INTERPRETER_ID)
code_interpreter_arn = attrs.get(AWS_BEDROCK_AGENTCORE_CODE_INTERPRETER_ARN)

if code_interpreter_id or code_interpreter_arn:
agentcore_cfn_identifier = None
if code_interpreter_id:
agentcore_cfn_identifier = str(code_interpreter_id)
elif code_interpreter_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(code_interpreter_arn))
resource_type = "CodeInterpreter" if agentcore_cfn_identifier == "aws.codeinterpreter.v1" else "CodeInterpreterCustom"
return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier

return None, None, None


def _handle_identity_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]:
credential_arn = attrs.get(AWS_AUTH_CREDENTIAL_PROVIDER_ARN)

if credential_arn:
credential_arn_str = str(credential_arn)
resource_type = None
if "apikeycredentialprovider" in credential_arn_str:
resource_type = "APIKeyCredentialProvider"
elif "oauth2credentialprovider" in credential_arn_str:
resource_type = "OAuth2CredentialProvider"
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(credential_arn_str)
return resource_type, agentcore_cfn_identifier, agentcore_cfn_identifier

return None, None, None


def _handle_memory_attrs(attrs) -> tuple[Optional[str], Optional[str], Optional[str]]:
memory_id = attrs.get(GEN_AI_MEMORY_ID)
memory_arn = attrs.get(AWS_BEDROCK_AGENTCORE_MEMORY_ARN)

if memory_id or memory_arn:
agentcore_cfn_identifier = None
agentcore_cfn_primary_identifier = None
if memory_id:
agentcore_cfn_identifier = str(memory_id)
elif memory_arn:
agentcore_cfn_identifier = extract_bedrock_agentcore_resource_id_from_arn(str(memory_arn))
agentcore_cfn_primary_identifier = str(memory_arn)
return "Memory", agentcore_cfn_identifier, agentcore_cfn_primary_identifier

return None, None, None


def extract_bedrock_agentcore_resource_id_from_arn(arn: str) -> Optional[str]:
"""Extract resource ID from ARN resource part."""
resource_part = RegionalResourceArnParser.extract_resource_name_from_arn(arn)
if not resource_part:
return None
return resource_part.split("/")[-1] if "/" in resource_part else resource_part


def _log_unknown_attribute(attribute_key: str, span: ReadableSpan) -> None:
message: str = "No valid %s value found for %s span %s"
if _logger.isEnabledFor(DEBUG):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN,
AWS_GATEWAY_TARGET_ID,
)
from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import (
GEN_AI_BROWSER_ID,
GEN_AI_CODE_INTERPRETER_ID,
GEN_AI_GATEWAY_ID,
GEN_AI_MEMORY_ID,
GEN_AI_RUNTIME_ID,
)
from opentelemetry.instrumentation.botocore.extensions.types import (
_AttributeMapT,
_AwsSdkExtension,
Expand All @@ -21,12 +28,6 @@
)
from opentelemetry.trace.span import Span

GEN_AI_RUNTIME_ID = "gen_ai.runtime.id"
GEN_AI_BROWSER_ID = "gen_ai.browser.id"
GEN_AI_CODE_INTERPRETER_ID = "gen_ai.code_interpreter.id"
GEN_AI_MEMORY_ID = "gen_ai.memory.id"
GEN_AI_GATEWAY_ID = "gen_ai.gateway.id"

# Mapping of flattened JSON paths to attribute keys
_ATTRIBUTE_MAPPING = {
"agentRuntimeArn": AWS_BEDROCK_AGENTCORE_RUNTIME_ARN,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Utility module holding attribute keys for incubating Gen AI semantic conventions.

Remove this once we've contributed them to upstream.
"""

GEN_AI_RUNTIME_ID = "gen_ai.runtime.id"
GEN_AI_BROWSER_ID = "gen_ai.browser.id"
GEN_AI_CODE_INTERPRETER_ID = "gen_ai.code_interpreter.id"
GEN_AI_MEMORY_ID = "gen_ai.memory.id"
GEN_AI_GATEWAY_ID = "gen_ai.gateway.id"
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@
AWS_BEDROCK_AGENTCORE_WORKLOAD_IDENTITY_ARN,
AWS_GATEWAY_TARGET_ID,
)
from amazon.opentelemetry.distro.patches._bedrock_agentcore_patches import (
from amazon.opentelemetry.distro.patches._instrumentation_patch import (
AWS_GEVENT_PATCH_MODULES,
apply_instrumentation_patches,
)
from amazon.opentelemetry.distro.patches.semconv._incubating.attributes.gen_ai_attributes import (
GEN_AI_BROWSER_ID,
GEN_AI_CODE_INTERPRETER_ID,
GEN_AI_GATEWAY_ID,
GEN_AI_MEMORY_ID,
GEN_AI_RUNTIME_ID,
)
from amazon.opentelemetry.distro.patches._instrumentation_patch import (
AWS_GEVENT_PATCH_MODULES,
apply_instrumentation_patches,
)
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS, bedrock_utils
from opentelemetry.propagate import get_global_textmap
Expand Down
Loading
Loading