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 <owais@users.noreply.github.com>
Co-authored-by: Diego Hurtado <ocelotl@users.noreply.github.com>
This commit is contained in:
Mario Jonke 2021-10-12 17:29:35 +02:00 committed by GitHub
parent b41a91713e
commit c3df816ad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 3 deletions

View File

@ -35,6 +35,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/664)) ([#664](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/664))
- `opentelemetry-instrumentation-botocore` Fix span injection for lambda invoke - `opentelemetry-instrumentation-botocore` Fix span injection for lambda invoke
([#663](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/663)) ([#663](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/663))
- `opentelemetry-instrumentation-botocore` Introduce instrumentation extensions
([#718](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/718))
### Changed ### Changed

View File

@ -80,7 +80,7 @@ for example:
import json import json
import logging import logging
from typing import Any, Collection, Dict, Optional, Tuple from typing import Any, Callable, Collection, Dict, Optional, Tuple
from botocore.client import BaseClient from botocore.client import BaseClient
from botocore.endpoint import Endpoint from botocore.endpoint import Endpoint
@ -88,6 +88,7 @@ from botocore.exceptions import ClientError
from wrapt import wrap_function_wrapper from wrapt import wrap_function_wrapper
from opentelemetry import context as context_api from opentelemetry import context as context_api
from opentelemetry.instrumentation.botocore.extensions import _find_extension
from opentelemetry.instrumentation.botocore.extensions.types import ( from opentelemetry.instrumentation.botocore.extensions.types import (
_AwsSdkCallContext, _AwsSdkCallContext,
) )
@ -190,6 +191,10 @@ class BotocoreInstrumentor(BaseInstrumentor):
if call_context is None: if call_context is None:
return original_func(*args, **kwargs) return original_func(*args, **kwargs)
extension = _find_extension(call_context)
if not extension.should_trace_service_call():
return original_func(*args, **kwargs)
attributes = { attributes = {
SpanAttributes.RPC_SYSTEM: "aws-api", SpanAttributes.RPC_SYSTEM: "aws-api",
SpanAttributes.RPC_SERVICE: call_context.service_id, SpanAttributes.RPC_SERVICE: call_context.service_id,
@ -198,6 +203,8 @@ class BotocoreInstrumentor(BaseInstrumentor):
"aws.region": call_context.region, "aws.region": call_context.region,
} }
_safe_invoke(extension.extract_attributes, attributes)
with self._tracer.start_as_current_span( with self._tracer.start_as_current_span(
call_context.span_name, call_context.span_name,
kind=call_context.span_kind, kind=call_context.span_kind,
@ -208,6 +215,7 @@ class BotocoreInstrumentor(BaseInstrumentor):
BotocoreInstrumentor._patch_lambda_invoke(call_context.params) BotocoreInstrumentor._patch_lambda_invoke(call_context.params)
_set_api_call_attributes(span, call_context) _set_api_call_attributes(span, call_context)
_safe_invoke(extension.before_service_call, span)
self._call_request_hook(span, call_context) self._call_request_hook(span, call_context)
token = context_api.attach( token = context_api.attach(
@ -220,11 +228,14 @@ class BotocoreInstrumentor(BaseInstrumentor):
except ClientError as error: except ClientError as error:
result = getattr(error, "response", None) result = getattr(error, "response", None)
_apply_response_attributes(span, result) _apply_response_attributes(span, result)
_safe_invoke(extension.on_error, span, error)
raise raise
else: else:
_apply_response_attributes(span, result) _apply_response_attributes(span, result)
_safe_invoke(extension.on_success, span, result)
finally: finally:
context_api.detach(token) context_api.detach(token)
_safe_invoke(extension.after_service_call)
self._call_response_hook(span, call_context, result) self._call_response_hook(span, call_context, result)
@ -254,8 +265,6 @@ def _set_api_call_attributes(span, call_context: _AwsSdkCallContext):
if not span.is_recording(): if not span.is_recording():
return return
if "QueueUrl" in call_context.params:
span.set_attribute("aws.queue_url", call_context.params["QueueUrl"])
if "TableName" in call_context.params: if "TableName" in call_context.params:
span.set_attribute("aws.table_name", call_context.params["TableName"]) span.set_attribute("aws.table_name", call_context.params["TableName"])
@ -309,3 +318,14 @@ def _determine_call_context(
# extracting essential attributes ('service' and 'operation') failed. # extracting essential attributes ('service' and 'operation') failed.
logger.error("Error when initializing call context", exc_info=ex) logger.error("Error when initializing call context", exc_info=ex)
return None return None
def _safe_invoke(function: Callable, *args):
function_name = "<unknown>"
try:
function_name = function.__name__
function(*args)
except Exception as ex: # pylint:disable=broad-except
logger.error(
"Error when invoking function '%s'", function_name, exc_info=ex
)

View File

@ -0,0 +1,35 @@
import importlib
import logging
from opentelemetry.instrumentation.botocore.extensions.types import (
_AwsSdkCallContext,
_AwsSdkExtension,
)
_logger = logging.getLogger(__name__)
def _lazy_load(module, cls):
def loader():
imported_mod = importlib.import_module(module, __name__)
return getattr(imported_mod, cls, None)
return loader
_KNOWN_EXTENSIONS = {
"sqs": _lazy_load(".sqs", "_SqsExtension"),
}
def _find_extension(call_context: _AwsSdkCallContext) -> _AwsSdkExtension:
try:
loader = _KNOWN_EXTENSIONS.get(call_context.service)
if loader is None:
return _AwsSdkExtension(call_context)
extension_cls = loader()
return extension_cls(call_context)
except Exception as ex: # pylint: disable=broad-except
_logger.error("Error when loading extension: %s", ex)
return _AwsSdkExtension(call_context)

View File

@ -0,0 +1,12 @@
from opentelemetry.instrumentation.botocore.extensions.types import (
_AttributeMapT,
_AwsSdkExtension,
)
class _SqsExtension(_AwsSdkExtension):
def extract_attributes(self, attributes: _AttributeMapT):
queue_url = self._call_context.params.get("QueueUrl")
if queue_url:
# TODO: update when semantic conventions exist
attributes["aws.queue_url"] = queue_url

View File

@ -2,12 +2,17 @@ import logging
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
from opentelemetry.trace import SpanKind from opentelemetry.trace import SpanKind
from opentelemetry.trace.span import Span
from opentelemetry.util.types import AttributeValue
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_BotoClientT = "botocore.client.BaseClient" _BotoClientT = "botocore.client.BaseClient"
_BotoResultT = Dict[str, Any]
_BotoClientErrorT = "botocore.exceptions.ClientError"
_OperationParamsT = Dict[str, Any] _OperationParamsT = Dict[str, Any]
_AttributeMapT = Dict[str, AttributeValue]
class _AwsSdkCallContext: class _AwsSdkCallContext:
@ -70,3 +75,49 @@ class _AwsSdkCallContext:
except AttributeError: except AttributeError:
_logger.warning("Could not get attribute '%s'", name) _logger.warning("Could not get attribute '%s'", name)
return default return default
class _AwsSdkExtension:
def __init__(self, call_context: _AwsSdkCallContext):
self._call_context = call_context
def should_trace_service_call(self) -> bool: # pylint:disable=no-self-use
"""Returns if the AWS SDK service call should be traced or not
Extensions might override this function to disable tracing for certain
operations.
"""
return True
def extract_attributes(self, attributes: _AttributeMapT):
"""Callback which gets invoked before the span is created.
Extensions might override this function to extract additional attributes.
"""
def before_service_call(self, span: Span):
"""Callback which gets invoked after the span is created but before the
AWS SDK service is called.
Extensions might override this function e.g. for injecting the span into
a carrier.
"""
def on_success(self, span: Span, result: _BotoResultT):
"""Callback that gets invoked when the AWS SDK call returns
successfully.
Extensions might override this function e.g. to extract and set response
attributes on the span.
"""
def on_error(self, span: Span, exception: _BotoClientErrorT):
"""Callback that gets invoked when the AWS SDK service call raises a
ClientError.
"""
def after_service_call(self):
"""Callback that gets invoked after the AWS SDK service was called.
Extensions might override this function to do some cleanup tasks.
"""