Skip to content

Commit c3df816

Browse files
mariojonkeowaisocelotl
authored
botocore: Introduce instrumentation extensions (#718)
* botocore: Introduce instrumentation extensions * add extensions that are invoked before and after an AWS SDK service call to enrich the span with service specific request and response attirbutes * move SQS specific parts to a separate extension * changelog Co-authored-by: Owais Lone <[email protected]> Co-authored-by: Diego Hurtado <[email protected]>
1 parent b41a917 commit c3df816

File tree

5 files changed

+123
-3
lines changed

5 files changed

+123
-3
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3535
([#664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/664))
3636
- `opentelemetry-instrumentation-botocore` Fix span injection for lambda invoke
3737
([#663](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/663))
38+
- `opentelemetry-instrumentation-botocore` Introduce instrumentation extensions
39+
([#718](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/718))
3840

3941
### Changed
4042

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

+23-3
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,15 @@ def response_hook(span, service_name, operation_name, result):
8080

8181
import json
8282
import logging
83-
from typing import Any, Collection, Dict, Optional, Tuple
83+
from typing import Any, Callable, Collection, Dict, Optional, Tuple
8484

8585
from botocore.client import BaseClient
8686
from botocore.endpoint import Endpoint
8787
from botocore.exceptions import ClientError
8888
from wrapt import wrap_function_wrapper
8989

9090
from opentelemetry import context as context_api
91+
from opentelemetry.instrumentation.botocore.extensions import _find_extension
9192
from opentelemetry.instrumentation.botocore.extensions.types import (
9293
_AwsSdkCallContext,
9394
)
@@ -190,6 +191,10 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
190191
if call_context is None:
191192
return original_func(*args, **kwargs)
192193

194+
extension = _find_extension(call_context)
195+
if not extension.should_trace_service_call():
196+
return original_func(*args, **kwargs)
197+
193198
attributes = {
194199
SpanAttributes.RPC_SYSTEM: "aws-api",
195200
SpanAttributes.RPC_SERVICE: call_context.service_id,
@@ -198,6 +203,8 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
198203
"aws.region": call_context.region,
199204
}
200205

206+
_safe_invoke(extension.extract_attributes, attributes)
207+
201208
with self._tracer.start_as_current_span(
202209
call_context.span_name,
203210
kind=call_context.span_kind,
@@ -208,6 +215,7 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
208215
BotocoreInstrumentor._patch_lambda_invoke(call_context.params)
209216

210217
_set_api_call_attributes(span, call_context)
218+
_safe_invoke(extension.before_service_call, span)
211219
self._call_request_hook(span, call_context)
212220

213221
token = context_api.attach(
@@ -220,11 +228,14 @@ def _patched_api_call(self, original_func, instance, args, kwargs):
220228
except ClientError as error:
221229
result = getattr(error, "response", None)
222230
_apply_response_attributes(span, result)
231+
_safe_invoke(extension.on_error, span, error)
223232
raise
224233
else:
225234
_apply_response_attributes(span, result)
235+
_safe_invoke(extension.on_success, span, result)
226236
finally:
227237
context_api.detach(token)
238+
_safe_invoke(extension.after_service_call)
228239

229240
self._call_response_hook(span, call_context, result)
230241

@@ -254,8 +265,6 @@ def _set_api_call_attributes(span, call_context: _AwsSdkCallContext):
254265
if not span.is_recording():
255266
return
256267

257-
if "QueueUrl" in call_context.params:
258-
span.set_attribute("aws.queue_url", call_context.params["QueueUrl"])
259268
if "TableName" in call_context.params:
260269
span.set_attribute("aws.table_name", call_context.params["TableName"])
261270

@@ -309,3 +318,14 @@ def _determine_call_context(
309318
# extracting essential attributes ('service' and 'operation') failed.
310319
logger.error("Error when initializing call context", exc_info=ex)
311320
return None
321+
322+
323+
def _safe_invoke(function: Callable, *args):
324+
function_name = "<unknown>"
325+
try:
326+
function_name = function.__name__
327+
function(*args)
328+
except Exception as ex: # pylint:disable=broad-except
329+
logger.error(
330+
"Error when invoking function '%s'", function_name, exc_info=ex
331+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import importlib
2+
import logging
3+
4+
from opentelemetry.instrumentation.botocore.extensions.types import (
5+
_AwsSdkCallContext,
6+
_AwsSdkExtension,
7+
)
8+
9+
_logger = logging.getLogger(__name__)
10+
11+
12+
def _lazy_load(module, cls):
13+
def loader():
14+
imported_mod = importlib.import_module(module, __name__)
15+
return getattr(imported_mod, cls, None)
16+
17+
return loader
18+
19+
20+
_KNOWN_EXTENSIONS = {
21+
"sqs": _lazy_load(".sqs", "_SqsExtension"),
22+
}
23+
24+
25+
def _find_extension(call_context: _AwsSdkCallContext) -> _AwsSdkExtension:
26+
try:
27+
loader = _KNOWN_EXTENSIONS.get(call_context.service)
28+
if loader is None:
29+
return _AwsSdkExtension(call_context)
30+
31+
extension_cls = loader()
32+
return extension_cls(call_context)
33+
except Exception as ex: # pylint: disable=broad-except
34+
_logger.error("Error when loading extension: %s", ex)
35+
return _AwsSdkExtension(call_context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from opentelemetry.instrumentation.botocore.extensions.types import (
2+
_AttributeMapT,
3+
_AwsSdkExtension,
4+
)
5+
6+
7+
class _SqsExtension(_AwsSdkExtension):
8+
def extract_attributes(self, attributes: _AttributeMapT):
9+
queue_url = self._call_context.params.get("QueueUrl")
10+
if queue_url:
11+
# TODO: update when semantic conventions exist
12+
attributes["aws.queue_url"] = queue_url

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/types.py

+51
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22
from typing import Any, Dict, Optional, Tuple
33

44
from opentelemetry.trace import SpanKind
5+
from opentelemetry.trace.span import Span
6+
from opentelemetry.util.types import AttributeValue
57

68
_logger = logging.getLogger(__name__)
79

810
_BotoClientT = "botocore.client.BaseClient"
11+
_BotoResultT = Dict[str, Any]
12+
_BotoClientErrorT = "botocore.exceptions.ClientError"
913

1014
_OperationParamsT = Dict[str, Any]
15+
_AttributeMapT = Dict[str, AttributeValue]
1116

1217

1318
class _AwsSdkCallContext:
@@ -70,3 +75,49 @@ def _get_attr(obj, name: str, default=None):
7075
except AttributeError:
7176
_logger.warning("Could not get attribute '%s'", name)
7277
return default
78+
79+
80+
class _AwsSdkExtension:
81+
def __init__(self, call_context: _AwsSdkCallContext):
82+
self._call_context = call_context
83+
84+
def should_trace_service_call(self) -> bool: # pylint:disable=no-self-use
85+
"""Returns if the AWS SDK service call should be traced or not
86+
87+
Extensions might override this function to disable tracing for certain
88+
operations.
89+
"""
90+
return True
91+
92+
def extract_attributes(self, attributes: _AttributeMapT):
93+
"""Callback which gets invoked before the span is created.
94+
95+
Extensions might override this function to extract additional attributes.
96+
"""
97+
98+
def before_service_call(self, span: Span):
99+
"""Callback which gets invoked after the span is created but before the
100+
AWS SDK service is called.
101+
102+
Extensions might override this function e.g. for injecting the span into
103+
a carrier.
104+
"""
105+
106+
def on_success(self, span: Span, result: _BotoResultT):
107+
"""Callback that gets invoked when the AWS SDK call returns
108+
successfully.
109+
110+
Extensions might override this function e.g. to extract and set response
111+
attributes on the span.
112+
"""
113+
114+
def on_error(self, span: Span, exception: _BotoClientErrorT):
115+
"""Callback that gets invoked when the AWS SDK service call raises a
116+
ClientError.
117+
"""
118+
119+
def after_service_call(self):
120+
"""Callback that gets invoked after the AWS SDK service was called.
121+
122+
Extensions might override this function to do some cleanup tasks.
123+
"""

0 commit comments

Comments
 (0)