Files
2020-04-08 10:39:44 -07:00

302 lines
13 KiB
Python

import opentracing
from opentracing import Format
from opentracing.scope_managers import ThreadLocalScopeManager
import ddtrace
from ddtrace import Tracer as DatadogTracer
from ddtrace.constants import FILTERS_KEY
from ddtrace.settings import ConfigException
from ddtrace.utils import merge_dicts
from ddtrace.utils.config import get_application_name
from ..internal.logger import get_logger
from .propagation import HTTPPropagator
from .span import Span
from .span_context import SpanContext
from .settings import ConfigKeys as keys, config_invalid_keys
from .utils import get_context_provider_for_scope_manager
log = get_logger(__name__)
DEFAULT_CONFIG = {
keys.AGENT_HOSTNAME: 'localhost',
keys.AGENT_HTTPS: False,
keys.AGENT_PORT: 8126,
keys.DEBUG: False,
keys.ENABLED: True,
keys.GLOBAL_TAGS: {},
keys.SAMPLER: None,
keys.PRIORITY_SAMPLING: None,
keys.SETTINGS: {
FILTERS_KEY: [],
},
}
class Tracer(opentracing.Tracer):
"""A wrapper providing an OpenTracing API for the Datadog tracer."""
def __init__(self, service_name=None, config=None, scope_manager=None, dd_tracer=None):
"""Initialize a new Datadog opentracer.
:param service_name: (optional) the name of the service that this
tracer will be used with. Note if not provided, a service name will
try to be determined based off of ``sys.argv``. If this fails a
:class:`ddtrace.settings.ConfigException` will be raised.
:param config: (optional) a configuration object to specify additional
options. See the documentation for further information.
:param scope_manager: (optional) the scope manager for this tracer to
use. The available managers are listed in the Python OpenTracing repo
here: https://github.com/opentracing/opentracing-python#scope-managers.
If ``None`` is provided, defaults to
:class:`opentracing.scope_managers.ThreadLocalScopeManager`.
:param dd_tracer: (optional) the Datadog tracer for this tracer to use. This
should only be passed if a custom Datadog tracer is being used. Defaults
to the global ``ddtrace.tracer`` tracer.
"""
# Merge the given config with the default into a new dict
config = config or {}
self._config = merge_dicts(DEFAULT_CONFIG, config)
# Pull out commonly used properties for performance
self._service_name = service_name or get_application_name()
self._enabled = self._config.get(keys.ENABLED)
self._debug = self._config.get(keys.DEBUG)
if self._debug:
# Ensure there are no typos in any of the keys
invalid_keys = config_invalid_keys(self._config)
if invalid_keys:
str_invalid_keys = ','.join(invalid_keys)
raise ConfigException('invalid key(s) given (%s)'.format(str_invalid_keys))
if not self._service_name:
raise ConfigException(""" Cannot detect the \'service_name\'.
Please set the \'service_name=\'
keyword argument.
""")
self._scope_manager = scope_manager or ThreadLocalScopeManager()
dd_context_provider = get_context_provider_for_scope_manager(self._scope_manager)
self._dd_tracer = dd_tracer or ddtrace.tracer or DatadogTracer()
self._dd_tracer.set_tags(self._config.get(keys.GLOBAL_TAGS))
self._dd_tracer.configure(enabled=self._enabled,
hostname=self._config.get(keys.AGENT_HOSTNAME),
https=self._config.get(keys.AGENT_HTTPS),
port=self._config.get(keys.AGENT_PORT),
sampler=self._config.get(keys.SAMPLER),
settings=self._config.get(keys.SETTINGS),
priority_sampling=self._config.get(keys.PRIORITY_SAMPLING),
context_provider=dd_context_provider,
)
self._propagators = {
Format.HTTP_HEADERS: HTTPPropagator(),
Format.TEXT_MAP: HTTPPropagator(),
}
@property
def scope_manager(self):
"""Returns the scope manager being used by this tracer."""
return self._scope_manager
def start_active_span(self, operation_name, child_of=None, references=None,
tags=None, start_time=None, ignore_active_span=False,
finish_on_close=True):
"""Returns a newly started and activated `Scope`.
The returned `Scope` supports with-statement contexts. For example::
with tracer.start_active_span('...') as scope:
scope.span.set_tag('http.method', 'GET')
do_some_work()
# Span.finish() is called as part of Scope deactivation through
# the with statement.
It's also possible to not finish the `Span` when the `Scope` context
expires::
with tracer.start_active_span('...',
finish_on_close=False) as scope:
scope.span.set_tag('http.method', 'GET')
do_some_work()
# Span.finish() is not called as part of Scope deactivation as
# `finish_on_close` is `False`.
:param operation_name: name of the operation represented by the new
span from the perspective of the current service.
:param child_of: (optional) a Span or SpanContext instance representing
the parent in a REFERENCE_CHILD_OF Reference. If specified, the
`references` parameter must be omitted.
:param references: (optional) a list of Reference objects that identify
one or more parent SpanContexts. (See the Reference documentation
for detail).
:param tags: an optional dictionary of Span Tags. The caller gives up
ownership of that dictionary, because the Tracer may use it as-is
to avoid extra data copying.
:param start_time: an explicit Span start time as a unix timestamp per
time.time().
:param ignore_active_span: (optional) an explicit flag that ignores
the current active `Scope` and creates a root `Span`.
:param finish_on_close: whether span should automatically be finished
when `Scope.close()` is called.
:return: a `Scope`, already registered via the `ScopeManager`.
"""
otspan = self.start_span(
operation_name=operation_name,
child_of=child_of,
references=references,
tags=tags,
start_time=start_time,
ignore_active_span=ignore_active_span,
)
# activate this new span
scope = self._scope_manager.activate(otspan, finish_on_close)
return scope
def start_span(self, operation_name=None, child_of=None, references=None,
tags=None, start_time=None, ignore_active_span=False):
"""Starts and returns a new Span representing a unit of work.
Starting a root Span (a Span with no causal references)::
tracer.start_span('...')
Starting a child Span (see also start_child_span())::
tracer.start_span(
'...',
child_of=parent_span)
Starting a child Span in a more verbose way::
tracer.start_span(
'...',
references=[opentracing.child_of(parent_span)])
Note: the precedence when defining a relationship is the following, from highest to lowest:
1. *child_of*
2. *references*
3. `scope_manager.active` (unless *ignore_active_span* is True)
4. None
Currently Datadog only supports `child_of` references.
:param operation_name: name of the operation represented by the new
span from the perspective of the current service.
:param child_of: (optional) a Span or SpanContext instance representing
the parent in a REFERENCE_CHILD_OF Reference. If specified, the
`references` parameter must be omitted.
:param references: (optional) a list of Reference objects that identify
one or more parent SpanContexts. (See the Reference documentation
for detail)
:param tags: an optional dictionary of Span Tags. The caller gives up
ownership of that dictionary, because the Tracer may use it as-is
to avoid extra data copying.
:param start_time: an explicit Span start time as a unix timestamp per
time.time()
:param ignore_active_span: an explicit flag that ignores the current
active `Scope` and creates a root `Span`.
:return: an already-started Span instance.
"""
ot_parent = None # 'ot_parent' is more readable than 'child_of'
ot_parent_context = None # the parent span's context
dd_parent = None # the child_of to pass to the ddtracer
if child_of is not None:
ot_parent = child_of # 'ot_parent' is more readable than 'child_of'
elif references and isinstance(references, list):
# we currently only support child_of relations to one span
ot_parent = references[0].referenced_context
# - whenever child_of is not None ddspans with parent-child
# relationships will share a ddcontext which maintains a hierarchy of
# ddspans for the execution flow
# - when child_of is a ddspan then the ddtracer uses this ddspan to
# create the child ddspan
# - when child_of is a ddcontext then the ddtracer uses the ddcontext to
# get_current_span() for the parent
if ot_parent is None and not ignore_active_span:
# attempt to get the parent span from the scope manager
scope = self._scope_manager.active
parent_span = getattr(scope, 'span', None)
ot_parent_context = getattr(parent_span, 'context', None)
# we want the ddcontext of the active span in order to maintain the
# ddspan hierarchy
dd_parent = getattr(ot_parent_context, '_dd_context', None)
# if we cannot get the context then try getting it from the DD tracer
# this emulates the behaviour of tracer.trace()
if dd_parent is None:
dd_parent = self._dd_tracer.get_call_context()
elif ot_parent is not None and isinstance(ot_parent, Span):
# a span is given to use as a parent
ot_parent_context = ot_parent.context
dd_parent = ot_parent._dd_span
elif ot_parent is not None and isinstance(ot_parent, SpanContext):
# a span context is given to use to find the parent ddspan
dd_parent = ot_parent._dd_context
elif ot_parent is None:
# user wants to create a new parent span we don't have to do
# anything
pass
else:
raise TypeError('invalid span configuration given')
# create a new otspan and ddspan using the ddtracer and associate it
# with the new otspan
ddspan = self._dd_tracer.start_span(
name=operation_name,
child_of=dd_parent,
service=self._service_name,
)
# set the start time if one is specified
ddspan.start = start_time or ddspan.start
otspan = Span(self, ot_parent_context, operation_name)
# sync up the OT span with the DD span
otspan._associate_dd_span(ddspan)
if tags is not None:
for k in tags:
# Make sure we set the tags on the otspan to ensure that the special compatibility tags
# are handled correctly (resource name, span type, sampling priority, etc).
otspan.set_tag(k, tags[k])
return otspan
def inject(self, span_context, format, carrier): # noqa: A002
"""Injects a span context into a carrier.
:param span_context: span context to inject.
:param format: format to encode the span context with.
:param carrier: the carrier of the encoded span context.
"""
propagator = self._propagators.get(format, None)
if propagator is None:
raise opentracing.UnsupportedFormatException
propagator.inject(span_context, carrier)
def extract(self, format, carrier): # noqa: A002
"""Extracts a span context from a carrier.
:param format: format that the carrier is encoded with.
:param carrier: the carrier to extract from.
"""
propagator = self._propagators.get(format, None)
if propagator is None:
raise opentracing.UnsupportedFormatException
# we have to manually activate the returned context from a distributed
# trace
ot_span_ctx = propagator.extract(carrier)
dd_span_ctx = ot_span_ctx._dd_context
self._dd_tracer.context_provider.activate(dd_span_ctx)
return ot_span_ctx