Skip to content

Commit 9f4655a

Browse files
authored
Add Bedrock, Bedrock Agent, Bedrock Agent Runtime Support. (#209)
Add bedrock support as following: 1. Instrumentation enhancement: A. **Bedrock**: Extract `guardrailId` from API response, and add into `"aws.bedrock.guardrail.id"` span attribute. B. **Bedrock Agent**: Extract `agentId` from both API request and response, and add into `"aws.bedrock.agent.id"` span attribute. Extract `knowledgeBaseId` from API request, and add into `"aws.bedrock.knowledgebase.id"` span attribute. Extract `dataSourceId` from both API request and response,, and add into `"aws.bedrock.datasource.id"` span attribute. The instrumentation is on API operation level, we make sure only one attribute is extracted per API call, there will be no overlap/conflict to identify the resouce. C. **Bedrock Agent Runtime**: Extract `agentId` from API request, and add into `"aws.bedrock.agent.id"` span attribute. Extract `knowledgeBaseId` from API request, and add into `"aws.bedrock.knowledgebase.id"` span attribute. We have checked all APIs and make sure there is no API call having both attributes. D. **Bedrock Runtime**: extract the following attributes and add into span according to [ Gen AI semantic-conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/attributes-registry/gen-ai.md): ``` gen_ai.request.model gen_ai.system ``` 2. Populate `RemoteResourceType` and `RemoteResourceIdentifier` with: ``` "RemoteResourceType": "AWS::Bedrock::Agent" "RemoteResourceIdentifier": "<agent_id>" "RemoteResourceType": "AWS::Bedrock::DataSource" "RemoteResourceIdentifier": "<datasource_id>" "RemoteResourceType": "AWS::Bedrock::Guardrail" "RemoteResourceIdentifier": "<guardrail_id>" "RemoteResourceType": "AWS::Bedrock::KnowledgeBase" "RemoteResourceIdentifier": "<knowledgeBase_id>" "RemoteResourceType": "AWS::Bedrock::Model" "RemoteResourceIdentifier": "<modelId>" ``` Testing: E2E test performed to confirm trace, metrics are generated: Traces: ![Screenshot 2024-07-08 at 3 52 21 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/ca2321d0-65b8-4061-bbce-d9f9cf86cf31) ![Screenshot 2024-07-08 at 3 52 54 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/de6eca34-f2af-4237-91af-0e997ebede3f) ![Screenshot 2024-07-08 at 3 53 23 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/f0eaa2ee-3c86-4ad6-be02-0eb272584e26) ![Screenshot 2024-07-08 at 4 06 29 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/9fab0cbf-ae5f-47ff-93e5-35253404ef4d) ![Screenshot 2024-07-08 at 4 06 44 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/04e85441-dd24-4b19-bc56-e5d0c5edb180) metrics: ![Screenshot 2024-07-08 at 4 17 27 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/4e108573-07f4-419d-a6b6-9b4f426c40df) ![Screenshot 2024-07-08 at 4 16 47 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/6fd769ff-abd0-4887-8144-36793c077471) ![Screenshot 2024-07-08 at 4 18 24 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/1d158819-1893-4301-b386-42b083522d48) ![Screenshot 2024-07-08 at 4 18 50 PM](https://github.com/aws-observability/aws-otel-python-instrumentation/assets/146124015/099c0bfb-50e3-4776-a2ab-7b8afbfcd527) By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 7f25ce2 commit 9f4655a

File tree

7 files changed

+499
-3
lines changed

7 files changed

+499
-3
lines changed

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_attribute_keys.py

+4
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@
1717
AWS_SQS_QUEUE_URL: str = "aws.sqs.queue.url"
1818
AWS_SQS_QUEUE_NAME: str = "aws.sqs.queue.name"
1919
AWS_KINESIS_STREAM_NAME: str = "aws.kinesis.stream.name"
20+
AWS_BEDROCK_DATA_SOURCE_ID: str = "aws.bedrock.data_source.id"
21+
AWS_BEDROCK_KNOWLEDGE_BASE_ID: str = "aws.bedrock.knowledge_base.id"
22+
AWS_BEDROCK_AGENT_ID: str = "aws.bedrock.agent.id"
23+
AWS_BEDROCK_GUARDRAIL_ID: str = "aws.bedrock.guardrail.id"

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_metric_attribute_generator.py

+33-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
from urllib.parse import ParseResult, urlparse
77

88
from amazon.opentelemetry.distro._aws_attribute_keys import (
9+
AWS_BEDROCK_AGENT_ID,
10+
AWS_BEDROCK_DATA_SOURCE_ID,
11+
AWS_BEDROCK_GUARDRAIL_ID,
12+
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
913
AWS_KINESIS_STREAM_NAME,
1014
AWS_LOCAL_OPERATION,
1115
AWS_LOCAL_SERVICE,
@@ -19,6 +23,7 @@
1923
AWS_SQS_QUEUE_URL,
2024
)
2125
from amazon.opentelemetry.distro._aws_span_processing_util import (
26+
GEN_AI_REQUEST_MODEL,
2227
LOCAL_ROOT,
2328
MAX_KEYWORD_LENGTH,
2429
SQL_KEYWORD_PATTERN,
@@ -80,6 +85,8 @@
8085
_NORMALIZED_KINESIS_SERVICE_NAME: str = "AWS::Kinesis"
8186
_NORMALIZED_S3_SERVICE_NAME: str = "AWS::S3"
8287
_NORMALIZED_SQS_SERVICE_NAME: str = "AWS::SQS"
88+
_NORMALIZED_BEDROCK_SERVICE_NAME: str = "AWS::Bedrock"
89+
_NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME: str = "AWS::BedrockRuntime"
8390
_DB_CONNECTION_STRING_TYPE: str = "DB::Connection"
8491

8592
# Special DEPENDENCY attribute value if GRAPHQL_OPERATION_TYPE attribute key is present.
@@ -291,9 +298,18 @@ def _normalize_remote_service_name(span: ReadableSpan, service_name: str) -> str
291298
If the span is an AWS SDK span, normalize the name to align with <a
292299
href="https://docs.aws.amazon.com/cloudcontrolapi/latest/userguide/supported-resources.html">AWS Cloud Control
293300
resource format</a> as much as possible. Long term, we would like to normalize service name in the upstream.
301+
302+
For Bedrock, Bedrock Agent, and Bedrock Agent Runtime, we can align with AWS Cloud Control and use
303+
AWS::Bedrock for RemoteService. For BedrockRuntime, we are using AWS::BedrockRuntime
304+
as the associated remote resource (Model) is not listed in Cloud Control.
294305
"""
295306
if is_aws_sdk_span(span):
296-
return "AWS::" + service_name
307+
aws_sdk_service_mapping = {
308+
"Bedrock Agent": _NORMALIZED_BEDROCK_SERVICE_NAME,
309+
"Bedrock Agent Runtime": _NORMALIZED_BEDROCK_SERVICE_NAME,
310+
"Bedrock Runtime": _NORMALIZED_BEDROCK_RUNTIME_SERVICE_NAME,
311+
}
312+
return aws_sdk_service_mapping.get(service_name, "AWS::" + service_name)
297313
return service_name
298314

299315

@@ -342,6 +358,7 @@ def _generate_remote_operation(span: ReadableSpan) -> str:
342358
return remote_operation
343359

344360

361+
# pylint: disable=too-many-branches
345362
def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttributes) -> None:
346363
"""
347364
Remote resource attributes {@link AwsAttributeKeys#AWS_REMOTE_RESOURCE_TYPE} and {@link
@@ -375,6 +392,21 @@ def _set_remote_type_and_identifier(span: ReadableSpan, attributes: BoundedAttri
375392
remote_resource_identifier = _escape_delimiters(
376393
SqsUrlParser.get_queue_name(span.attributes.get(AWS_SQS_QUEUE_URL))
377394
)
395+
elif is_key_present(span, AWS_BEDROCK_AGENT_ID):
396+
remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::Agent"
397+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_BEDROCK_AGENT_ID))
398+
elif is_key_present(span, AWS_BEDROCK_DATA_SOURCE_ID):
399+
remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::DataSource"
400+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_BEDROCK_DATA_SOURCE_ID))
401+
elif is_key_present(span, AWS_BEDROCK_GUARDRAIL_ID):
402+
remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::Guardrail"
403+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_BEDROCK_GUARDRAIL_ID))
404+
elif is_key_present(span, AWS_BEDROCK_KNOWLEDGE_BASE_ID):
405+
remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::KnowledgeBase"
406+
remote_resource_identifier = _escape_delimiters(span.attributes.get(AWS_BEDROCK_KNOWLEDGE_BASE_ID))
407+
elif is_key_present(span, GEN_AI_REQUEST_MODEL):
408+
remote_resource_type = _NORMALIZED_BEDROCK_SERVICE_NAME + "::Model"
409+
remote_resource_identifier = _escape_delimiters(span.attributes.get(GEN_AI_REQUEST_MODEL))
378410
elif is_db_span(span):
379411
remote_resource_type = _DB_CONNECTION_STRING_TYPE
380412
remote_resource_identifier = _get_db_connection(span)

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/_aws_span_processing_util.py

+4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
# Max keyword length supported by parsing into remote_operation from DB_STATEMENT
2626
MAX_KEYWORD_LENGTH = 27
2727

28+
# TODO: Use Semantic Conventions once upgrade to 0.47b0
29+
GEN_AI_REQUEST_MODEL: str = "gen_ai.request.model"
30+
GEN_AI_SYSTEM: str = "gen_ai.system"
31+
2832

2933
# Get dialect keywords retrieved from dialect_keywords.json file.
3034
# Only meant to be invoked by SQL_KEYWORD_PATTERN and unit tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
import abc
4+
import inspect
5+
from typing import Dict, Optional
6+
7+
from amazon.opentelemetry.distro._aws_attribute_keys import (
8+
AWS_BEDROCK_AGENT_ID,
9+
AWS_BEDROCK_DATA_SOURCE_ID,
10+
AWS_BEDROCK_GUARDRAIL_ID,
11+
AWS_BEDROCK_KNOWLEDGE_BASE_ID,
12+
)
13+
from amazon.opentelemetry.distro._aws_span_processing_util import GEN_AI_REQUEST_MODEL, GEN_AI_SYSTEM
14+
from opentelemetry.instrumentation.botocore.extensions.types import (
15+
_AttributeMapT,
16+
_AwsSdkCallContext,
17+
_AwsSdkExtension,
18+
_BotoResultT,
19+
)
20+
from opentelemetry.trace.span import Span
21+
22+
_AGENT_ID: str = "agentId"
23+
_KNOWLEDGE_BASE_ID: str = "knowledgeBaseId"
24+
_DATA_SOURCE_ID: str = "dataSourceId"
25+
_GUARDRAIL_ID: str = "guardrailId"
26+
_MODEL_ID: str = "modelId"
27+
_AWS_BEDROCK_SYSTEM: str = "aws_bedrock"
28+
29+
30+
class _BedrockAgentOperation(abc.ABC):
31+
"""
32+
We use subclasses and operation names to handle specific Bedrock Agent operations.
33+
- Only operations involving Agent, DataSource, or KnowledgeBase resources are supported.
34+
- Operations without these specified resources are not covered.
35+
- When an operation involves multiple resources (e.g., AssociateAgentKnowledgeBase),
36+
we map it to one resource based on some judgement classification of rules.
37+
38+
For detailed API documentation on Bedrock Agent operations, visit:
39+
https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Agents_for_Amazon_Bedrock.html
40+
"""
41+
42+
request_attributes: Optional[Dict[str, str]] = None
43+
response_attributes: Optional[Dict[str, str]] = None
44+
45+
@classmethod
46+
@abc.abstractmethod
47+
def operation_names(cls):
48+
pass
49+
50+
51+
class _AgentOperation(_BedrockAgentOperation):
52+
"""
53+
This class covers BedrockAgent API related to <a
54+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_Agent.html">Agents</a>,
55+
and extracts agent-related attributes.
56+
"""
57+
58+
request_attributes = {
59+
AWS_BEDROCK_AGENT_ID: _AGENT_ID,
60+
}
61+
response_attributes = {
62+
AWS_BEDROCK_AGENT_ID: _AGENT_ID,
63+
}
64+
65+
@classmethod
66+
def operation_names(cls):
67+
return [
68+
"CreateAgentActionGroup",
69+
"CreateAgentAlias",
70+
"DeleteAgentActionGroup",
71+
"DeleteAgentAlias",
72+
"DeleteAgent",
73+
"DeleteAgentVersion",
74+
"GetAgentActionGroup",
75+
"GetAgentAlias",
76+
"GetAgent",
77+
"GetAgentVersion",
78+
"ListAgentActionGroups",
79+
"ListAgentAliases",
80+
"ListAgentKnowledgeBases",
81+
"ListAgentVersions",
82+
"PrepareAgent",
83+
"UpdateAgentActionGroup",
84+
"UpdateAgentAlias",
85+
"UpdateAgent",
86+
]
87+
88+
89+
class _KnowledgeBaseOperation(_BedrockAgentOperation):
90+
"""
91+
This class covers BedrockAgent API related to <a
92+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_KnowledgeBase.html">KnowledgeBases</a>,
93+
and extracts knowledge base-related attributes.
94+
95+
Note: The 'CreateDataSource' operation does not have a 'dataSourceId' in the context,
96+
but it always comes with a 'knowledgeBaseId'. Therefore, we categorize it under 'knowledgeBaseId' operations.
97+
"""
98+
99+
request_attributes = {
100+
AWS_BEDROCK_KNOWLEDGE_BASE_ID: _KNOWLEDGE_BASE_ID,
101+
}
102+
response_attributes = {
103+
AWS_BEDROCK_KNOWLEDGE_BASE_ID: _KNOWLEDGE_BASE_ID,
104+
}
105+
106+
@classmethod
107+
def operation_names(cls):
108+
return [
109+
"AssociateAgentKnowledgeBase",
110+
"CreateDataSource",
111+
"DeleteKnowledgeBase",
112+
"DisassociateAgentKnowledgeBase",
113+
"GetAgentKnowledgeBase",
114+
"GetKnowledgeBase",
115+
"ListDataSources",
116+
"UpdateAgentKnowledgeBase",
117+
]
118+
119+
120+
class _DataSourceOperation(_BedrockAgentOperation):
121+
"""
122+
This class covers BedrockAgent API related to <a
123+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_agent_DataSource.html">DataSources</a>,
124+
and extracts data source-related attributes.
125+
"""
126+
127+
request_attributes = {
128+
AWS_BEDROCK_DATA_SOURCE_ID: _DATA_SOURCE_ID,
129+
}
130+
response_attributes = {
131+
AWS_BEDROCK_DATA_SOURCE_ID: _DATA_SOURCE_ID,
132+
}
133+
134+
@classmethod
135+
def operation_names(cls):
136+
return ["DeleteDataSource", "GetDataSource", "UpdateDataSource"]
137+
138+
139+
# _OPERATION_NAME_TO_CLASS_MAPPING maps operation names to their corresponding classes
140+
# by iterating over all subclasses of _BedrockAgentOperation and extract operations
141+
# by calling operation_names() function.
142+
_OPERATION_NAME_TO_CLASS_MAPPING = {
143+
op_name: op_class
144+
for op_class in [_KnowledgeBaseOperation, _DataSourceOperation, _AgentOperation]
145+
for op_name in op_class.operation_names()
146+
if inspect.isclass(op_class) and issubclass(op_class, _BedrockAgentOperation) and not inspect.isabstract(op_class)
147+
}
148+
149+
150+
class _BedrockAgentExtension(_AwsSdkExtension):
151+
"""
152+
This class is an extension for <a
153+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Agents_for_Amazon_Bedrock.html">
154+
Agents for Amazon Bedrock</a>.
155+
156+
This class primarily identify three types of resource based operations: _AgentOperation, _KnowledgeBaseOperation,
157+
and _DataSourceOperation. We only support operations that are related to the resource
158+
and where the context contains the resource ID.
159+
"""
160+
161+
def __init__(self, call_context: _AwsSdkCallContext):
162+
super().__init__(call_context)
163+
self._operation_class = _OPERATION_NAME_TO_CLASS_MAPPING.get(call_context.operation)
164+
165+
def extract_attributes(self, attributes: _AttributeMapT):
166+
if self._operation_class is None:
167+
return
168+
for attribute_key, request_param_key in self._operation_class.request_attributes.items():
169+
request_param_value = self._call_context.params.get(request_param_key)
170+
if request_param_value:
171+
attributes[attribute_key] = request_param_value
172+
173+
def on_success(self, span: Span, result: _BotoResultT):
174+
if self._operation_class is None:
175+
return
176+
177+
for attribute_key, response_param_key in self._operation_class.response_attributes.items():
178+
response_param_value = result.get(response_param_key)
179+
if response_param_value:
180+
span.set_attribute(
181+
attribute_key,
182+
response_param_value,
183+
)
184+
185+
186+
class _BedrockAgentRuntimeExtension(_AwsSdkExtension):
187+
"""
188+
This class is an extension for <a
189+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Agents_for_Amazon_Bedrock_Runtime.html">
190+
Agents for Amazon Bedrock Runtime</a>.
191+
"""
192+
193+
def extract_attributes(self, attributes: _AttributeMapT):
194+
agent_id = self._call_context.params.get(_AGENT_ID)
195+
if agent_id:
196+
attributes[AWS_BEDROCK_AGENT_ID] = agent_id
197+
198+
knowledge_base_id = self._call_context.params.get(_KNOWLEDGE_BASE_ID)
199+
if knowledge_base_id:
200+
attributes[AWS_BEDROCK_KNOWLEDGE_BASE_ID] = knowledge_base_id
201+
202+
203+
class _BedrockExtension(_AwsSdkExtension):
204+
"""
205+
This class is an extension for <a
206+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Amazon_Bedrock.html">Bedrock</a>.
207+
"""
208+
209+
# pylint: disable=no-self-use
210+
def on_success(self, span: Span, result: _BotoResultT):
211+
# _GUARDRAIL_ID can only be retrieved from the response, not from the request
212+
guardrail_id = result.get(_GUARDRAIL_ID)
213+
if guardrail_id:
214+
span.set_attribute(
215+
AWS_BEDROCK_GUARDRAIL_ID,
216+
guardrail_id,
217+
)
218+
219+
220+
class _BedrockRuntimeExtension(_AwsSdkExtension):
221+
"""
222+
This class is an extension for <a
223+
href="https://docs.aws.amazon.com/bedrock/latest/APIReference/API_Operations_Amazon_Bedrock_Runtime.html">
224+
Amazon Bedrock Runtime</a>.
225+
"""
226+
227+
def extract_attributes(self, attributes: _AttributeMapT):
228+
attributes[GEN_AI_SYSTEM] = _AWS_BEDROCK_SYSTEM
229+
230+
model_id = self._call_context.params.get(_MODEL_ID)
231+
if model_id:
232+
attributes[GEN_AI_REQUEST_MODEL] = model_id

aws-opentelemetry-distro/src/amazon/opentelemetry/distro/patches/_botocore_patches.py

+21
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
AWS_SQS_QUEUE_NAME,
99
AWS_SQS_QUEUE_URL,
1010
)
11+
from amazon.opentelemetry.distro.patches._bedrock_patches import ( # noqa # pylint: disable=unused-import
12+
_BedrockAgentExtension,
13+
_BedrockAgentRuntimeExtension,
14+
_BedrockExtension,
15+
_BedrockRuntimeExtension,
16+
)
1117
from opentelemetry.instrumentation.botocore.extensions import _KNOWN_EXTENSIONS
1218
from opentelemetry.instrumentation.botocore.extensions.sqs import _SqsExtension
1319
from opentelemetry.instrumentation.botocore.extensions.types import _AttributeMapT, _AwsSdkExtension
@@ -22,6 +28,7 @@ def _apply_botocore_instrumentation_patches() -> None:
2228
_apply_botocore_kinesis_patch()
2329
_apply_botocore_s3_patch()
2430
_apply_botocore_sqs_patch()
31+
_apply_botocore_bedrock_patch()
2532

2633

2734
def _apply_botocore_kinesis_patch() -> None:
@@ -70,6 +77,20 @@ def patch_extract_attributes(self, attributes: _AttributeMapT):
7077
_SqsExtension.extract_attributes = patch_extract_attributes
7178

7279

80+
def _apply_botocore_bedrock_patch() -> None:
81+
"""Botocore instrumentation patch for Bedrock, Bedrock Agent, Bedrock Runtime and Bedrock Agent Runtime
82+
83+
This patch adds an extension to the upstream's list of known extension for Bedrock.
84+
Extensions allow for custom logic for adding service-specific information to spans, such as attributes.
85+
Specifically, we are adding logic to add the AWS_BEDROCK attributes referenced in _aws_attribute_keys,
86+
GEN_AI_REQUEST_MODEL and GEN_AI_SYSTEM attributes referenced in _aws_span_processing_util.
87+
"""
88+
_KNOWN_EXTENSIONS["bedrock"] = _lazy_load(".", "_BedrockExtension")
89+
_KNOWN_EXTENSIONS["bedrock-agent"] = _lazy_load(".", "_BedrockAgentExtension")
90+
_KNOWN_EXTENSIONS["bedrock-agent-runtime"] = _lazy_load(".", "_BedrockAgentRuntimeExtension")
91+
_KNOWN_EXTENSIONS["bedrock-runtime"] = _lazy_load(".", "_BedrockRuntimeExtension")
92+
93+
7394
# The OpenTelemetry Authors code
7495
def _lazy_load(module, cls):
7596
"""Clone of upstream opentelemetry.instrumentation.botocore.extensions.lazy_load

0 commit comments

Comments
 (0)