Skip to content

Commit 47ae9a3

Browse files
a single hook for all aws services
1 parent f0c9dcb commit 47ae9a3

File tree

3 files changed

+82
-59
lines changed

3 files changed

+82
-59
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
([#670](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/670))
1515
- `opentelemetry-instrumentation-redis` added request_hook and response_hook callbacks passed as arguments to the instrument method.
1616
([#669](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/669))
17-
- `opentelemetry-instrumentation-botocore` add request_hooks and response_hooks
17+
- `opentelemetry-instrumentation-botocore` add `request_hook` and `response_hook` callbacks
1818
([679](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/679))
1919

2020
### Changed

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/__init__.py

+17-39
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
The `instrument` method accepts the following keyword args:
4949
5050
tracer_provider (TracerProvider) - an optional tracer provider
51-
request_hooks (dict) - a mapping between service names their respective callable request hooks
52-
* a request hook signature is: def request_hook(span: Span, operation_name: str, api_params: dict) -> None
53-
response_hooks (dict) - a mapping between service names their respective callable response hooks
54-
* a response hook signature is: def response_hook(span: Span, operation_name: str, result: dict) -> None
51+
request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request
52+
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, api_params: dict) -> None
53+
response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request
54+
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None
5555
5656
for example:
5757
@@ -60,16 +60,14 @@
6060
from opentelemetry.instrumentation.botocore import BotocoreInstrumentor
6161
import botocore
6262
63-
def ec2_request_hook(span, operation_name, api_params):
63+
def request_hook(span, service_name, operation_name, api_params):
6464
# request hook logic
6565
66-
def ec2_response_hook(span, operation_name, result):
66+
def response_hook(span, service_name, operation_name, result):
6767
# response hook logic
6868
6969
# Instrument Botocore with hooks
70-
BotocoreInstrumentor().instrument(
71-
request_hooks={"ec2": ec2_request_hook}, response_hooks={"ec2": ec2_response_hook}
72-
)
70+
BotocoreInstrumentor().instrument(request_hook=request_hook, response_hooks=response_hook)
7371
7472
# This will create a span with Botocore-specific attributes, including custom attributes added from the hooks
7573
session = botocore.session.get_session()
@@ -127,8 +125,8 @@ class BotocoreInstrumentor(BaseInstrumentor):
127125

128126
def __init__(self):
129127
super().__init__()
130-
self.request_hooks = dict()
131-
self.response_hooks = dict()
128+
self.request_hook = None
129+
self.response_hook = None
132130

133131
def instrumentation_dependencies(self) -> Collection[str]:
134132
return _instruments
@@ -139,14 +137,8 @@ def _instrument(self, **kwargs):
139137
__name__, __version__, kwargs.get("tracer_provider")
140138
)
141139

142-
request_hooks = kwargs.get("request_hooks")
143-
response_hooks = kwargs.get("response_hooks")
144-
145-
if isinstance(request_hooks, dict):
146-
self.request_hooks = request_hooks
147-
148-
if isinstance(response_hooks, dict):
149-
self.response_hooks = response_hooks
140+
self.request_hook = kwargs.get("request_hook")
141+
self.response_hook = kwargs.get("response_hook")
150142

151143
wrap_function_wrapper(
152144
"botocore.client",
@@ -214,9 +206,10 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
214206
context_api.set_value(_SUPPRESS_HTTP_INSTRUMENTATION_KEY, True)
215207
)
216208

217-
self.apply_request_hook(
218-
span, service_name, operation_name, api_params
219-
)
209+
if callable(self.request_hook):
210+
self.request_hook(
211+
span, service_name, operation_name, api_params
212+
)
220213

221214
try:
222215
result = original_func(*args, **kwargs)
@@ -228,9 +221,8 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
228221
if error:
229222
result = error.response
230223

231-
self.apply_response_hook(
232-
span, service_name, operation_name, result
233-
)
224+
if callable(self.response_hook):
225+
self.response_hook(span, service_name, operation_name, result)
234226

235227
self._set_api_call_result_attributes(span, result)
236228

@@ -284,17 +276,3 @@ def _set_api_call_result_attributes(span, result):
284276
SpanAttributes.HTTP_STATUS_CODE,
285277
metadata["HTTPStatusCode"],
286278
)
287-
288-
def apply_request_hook(
289-
self, span, service_name, operation_name, api_params
290-
):
291-
if service_name in self.request_hooks:
292-
request_hook = self.request_hooks.get(service_name)
293-
if callable(request_hook):
294-
request_hook(span, operation_name, api_params)
295-
296-
def apply_response_hook(self, span, service_name, operation_name, result):
297-
if service_name in self.response_hooks:
298-
response_hook = self.response_hooks.get(service_name)
299-
if callable(response_hook):
300-
response_hook(span, operation_name, result)

instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_instrumentation.py

+64-19
Original file line numberDiff line numberDiff line change
@@ -631,34 +631,22 @@ def test_dynamodb_client(self):
631631
)
632632

633633
@mock_dynamodb2
634-
def test_hooks(self):
634+
def test__request_hook(self):
635+
request_hook_service_attribute_name = "request_hook.service_name"
635636
request_hook_operation_attribute_name = "request_hook.operation_name"
636637
request_hook_api_params_attribute_name = "request_hook.api_params"
637-
response_hook_operation_attribute_name = "response_hook.operation_name"
638-
response_hook_result_attribute_name = "response_hook.result"
639638

640-
def request_hook(span, operation_name, api_params):
639+
def request_hook(span, service_name, operation_name, api_params):
641640
hook_attributes = {
641+
request_hook_service_attribute_name: service_name,
642642
request_hook_operation_attribute_name: operation_name,
643643
request_hook_api_params_attribute_name: json.dumps(api_params),
644644
}
645645
if span and span.is_recording():
646646
span.set_attributes(hook_attributes)
647647

648-
def response_hook(span, operation_name, result):
649-
if span and span.is_recording():
650-
span.set_attribute(
651-
response_hook_operation_attribute_name, operation_name,
652-
)
653-
span.set_attribute(
654-
response_hook_result_attribute_name, list(result.keys()),
655-
)
656-
657648
BotocoreInstrumentor().uninstrument()
658-
BotocoreInstrumentor().instrument(
659-
request_hooks={"dynamodb": request_hook},
660-
response_hooks={"dynamodb": response_hook},
661-
)
649+
BotocoreInstrumentor().instrument(request_hook=request_hook,)
662650

663651
self.session = botocore.session.get_session()
664652
self.session.set_credentials(
@@ -694,8 +682,10 @@ def response_hook(span, operation_name, result):
694682
{"TableName": test_table_name, "Item": item}
695683
)
696684

697-
expected_result_keys = ("ConsumedCapacity", "ResponseMetadata")
698-
685+
self.assertEqual(
686+
"dynamodb",
687+
get_item_attributes.get(request_hook_service_attribute_name),
688+
)
699689
self.assertEqual(
700690
"PutItem",
701691
get_item_attributes.get(request_hook_operation_attribute_name),
@@ -704,6 +694,61 @@ def response_hook(span, operation_name, result):
704694
expected_api_params,
705695
get_item_attributes.get(request_hook_api_params_attribute_name),
706696
)
697+
698+
@mock_dynamodb2
699+
def test__response_hook(self):
700+
response_hook_service_attribute_name = "request_hook.service_name"
701+
response_hook_operation_attribute_name = "response_hook.operation_name"
702+
response_hook_result_attribute_name = "response_hook.result"
703+
704+
def response_hook(span, service_name, operation_name, result):
705+
hook_attributes = {
706+
response_hook_service_attribute_name: service_name,
707+
response_hook_operation_attribute_name: operation_name,
708+
response_hook_result_attribute_name: list(result.keys()),
709+
}
710+
if span and span.is_recording():
711+
span.set_attributes(hook_attributes)
712+
713+
BotocoreInstrumentor().uninstrument()
714+
BotocoreInstrumentor().instrument(response_hook=response_hook,)
715+
716+
self.session = botocore.session.get_session()
717+
self.session.set_credentials(
718+
access_key="access-key", secret_key="secret-key"
719+
)
720+
721+
ddb = self.session.create_client("dynamodb", region_name="us-west-2")
722+
723+
test_table_name = "test_table_name"
724+
725+
ddb.create_table(
726+
AttributeDefinitions=[
727+
{"AttributeName": "id", "AttributeType": "S"},
728+
],
729+
KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
730+
ProvisionedThroughput={
731+
"ReadCapacityUnits": 5,
732+
"WriteCapacityUnits": 5,
733+
},
734+
TableName=test_table_name,
735+
)
736+
737+
item = {"id": {"S": "test_key"}}
738+
739+
ddb.put_item(TableName=test_table_name, Item=item)
740+
741+
spans = self.memory_exporter.get_finished_spans()
742+
assert spans
743+
self.assertEqual(len(spans), 2)
744+
get_item_attributes = spans[1].attributes
745+
746+
expected_result_keys = ("ConsumedCapacity", "ResponseMetadata")
747+
748+
self.assertEqual(
749+
"dynamodb",
750+
get_item_attributes.get(response_hook_service_attribute_name),
751+
)
707752
self.assertEqual(
708753
"PutItem",
709754
get_item_attributes.get(response_hook_operation_attribute_name),

0 commit comments

Comments
 (0)