mirror of
https://github.com/open-telemetry/opentelemetry-python-contrib.git
synced 2025-07-31 22:23:12 +08:00
Move DD code into its own directory (#6)
This commit is contained in:
394
reference/ddtrace/span.py
Normal file
394
reference/ddtrace/span.py
Normal file
@ -0,0 +1,394 @@
|
||||
import math
|
||||
import random
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from .compat import StringIO, stringify, iteritems, numeric_types, time_ns, is_integer
|
||||
from .constants import NUMERIC_TAGS, MANUAL_DROP_KEY, MANUAL_KEEP_KEY
|
||||
from .ext import SpanTypes, errors, priority, net, http
|
||||
from .internal.logger import get_logger
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
if sys.version_info.major < 3:
|
||||
_getrandbits = random.SystemRandom().getrandbits
|
||||
else:
|
||||
_getrandbits = random.getrandbits
|
||||
|
||||
|
||||
class Span(object):
|
||||
|
||||
__slots__ = [
|
||||
# Public span attributes
|
||||
'service',
|
||||
'name',
|
||||
'resource',
|
||||
'span_id',
|
||||
'trace_id',
|
||||
'parent_id',
|
||||
'meta',
|
||||
'error',
|
||||
'metrics',
|
||||
'span_type',
|
||||
'start_ns',
|
||||
'duration_ns',
|
||||
'tracer',
|
||||
# Sampler attributes
|
||||
'sampled',
|
||||
# Internal attributes
|
||||
'_context',
|
||||
'finished',
|
||||
'_parent',
|
||||
'__weakref__',
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tracer,
|
||||
name,
|
||||
|
||||
service=None,
|
||||
resource=None,
|
||||
span_type=None,
|
||||
trace_id=None,
|
||||
span_id=None,
|
||||
parent_id=None,
|
||||
start=None,
|
||||
context=None,
|
||||
):
|
||||
"""
|
||||
Create a new span. Call `finish` once the traced operation is over.
|
||||
|
||||
:param ddtrace.Tracer tracer: the tracer that will submit this span when
|
||||
finished.
|
||||
:param str name: the name of the traced operation.
|
||||
|
||||
:param str service: the service name
|
||||
:param str resource: the resource name
|
||||
:param str span_type: the span type
|
||||
|
||||
:param int trace_id: the id of this trace's root span.
|
||||
:param int parent_id: the id of this span's direct parent span.
|
||||
:param int span_id: the id of this span.
|
||||
|
||||
:param int start: the start time of request as a unix epoch in seconds
|
||||
:param object context: the Context of the span.
|
||||
"""
|
||||
# required span info
|
||||
self.name = name
|
||||
self.service = service
|
||||
self.resource = resource or name
|
||||
self.span_type = span_type.value if isinstance(span_type, SpanTypes) else span_type
|
||||
|
||||
# tags / metatdata
|
||||
self.meta = {}
|
||||
self.error = 0
|
||||
self.metrics = {}
|
||||
|
||||
# timing
|
||||
self.start_ns = time_ns() if start is None else int(start * 1e9)
|
||||
self.duration_ns = None
|
||||
|
||||
# tracing
|
||||
self.trace_id = trace_id or _new_id()
|
||||
self.span_id = span_id or _new_id()
|
||||
self.parent_id = parent_id
|
||||
self.tracer = tracer
|
||||
|
||||
# sampling
|
||||
self.sampled = True
|
||||
|
||||
self._context = context
|
||||
self._parent = None
|
||||
|
||||
# state
|
||||
self.finished = False
|
||||
|
||||
@property
|
||||
def start(self):
|
||||
"""The start timestamp in Unix epoch seconds."""
|
||||
return self.start_ns / 1e9
|
||||
|
||||
@start.setter
|
||||
def start(self, value):
|
||||
self.start_ns = int(value * 1e9)
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
"""The span duration in seconds."""
|
||||
if self.duration_ns is not None:
|
||||
return self.duration_ns / 1e9
|
||||
|
||||
@duration.setter
|
||||
def duration(self, value):
|
||||
self.duration_ns = value * 1e9
|
||||
|
||||
def finish(self, finish_time=None):
|
||||
"""Mark the end time of the span and submit it to the tracer.
|
||||
If the span has already been finished don't do anything
|
||||
|
||||
:param int finish_time: The end time of the span in seconds.
|
||||
Defaults to now.
|
||||
"""
|
||||
if self.finished:
|
||||
return
|
||||
self.finished = True
|
||||
|
||||
if self.duration_ns is None:
|
||||
ft = time_ns() if finish_time is None else int(finish_time * 1e9)
|
||||
# be defensive so we don't die if start isn't set
|
||||
self.duration_ns = ft - (self.start_ns or ft)
|
||||
|
||||
if self._context:
|
||||
try:
|
||||
self._context.close_span(self)
|
||||
except Exception:
|
||||
log.exception('error recording finished trace')
|
||||
else:
|
||||
# if a tracer is available to process the current context
|
||||
if self.tracer:
|
||||
try:
|
||||
self.tracer.record(self._context)
|
||||
except Exception:
|
||||
log.exception('error recording finished trace')
|
||||
|
||||
def set_tag(self, key, value=None):
|
||||
""" Set the given key / value tag pair on the span. Keys and values
|
||||
must be strings (or stringable). If a casting error occurs, it will
|
||||
be ignored.
|
||||
"""
|
||||
# Special case, force `http.status_code` as a string
|
||||
# DEV: `http.status_code` *has* to be in `meta` for metrics
|
||||
# calculated in the trace agent
|
||||
if key == http.STATUS_CODE:
|
||||
value = str(value)
|
||||
|
||||
# Determine once up front
|
||||
is_an_int = is_integer(value)
|
||||
|
||||
# Explicitly try to convert expected integers to `int`
|
||||
# DEV: Some integrations parse these values from strings, but don't call `int(value)` themselves
|
||||
INT_TYPES = (net.TARGET_PORT, )
|
||||
if key in INT_TYPES and not is_an_int:
|
||||
try:
|
||||
value = int(value)
|
||||
is_an_int = True
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Set integers that are less than equal to 2^53 as metrics
|
||||
if is_an_int and abs(value) <= 2 ** 53:
|
||||
self.set_metric(key, value)
|
||||
return
|
||||
|
||||
# All floats should be set as a metric
|
||||
elif isinstance(value, float):
|
||||
self.set_metric(key, value)
|
||||
return
|
||||
|
||||
# Key should explicitly be converted to a float if needed
|
||||
elif key in NUMERIC_TAGS:
|
||||
try:
|
||||
# DEV: `set_metric` will try to cast to `float()` for us
|
||||
self.set_metric(key, value)
|
||||
except (TypeError, ValueError):
|
||||
log.debug('error setting numeric metric %s:%s', key, value)
|
||||
|
||||
return
|
||||
|
||||
elif key == MANUAL_KEEP_KEY:
|
||||
self.context.sampling_priority = priority.USER_KEEP
|
||||
return
|
||||
elif key == MANUAL_DROP_KEY:
|
||||
self.context.sampling_priority = priority.USER_REJECT
|
||||
return
|
||||
|
||||
try:
|
||||
self.meta[key] = stringify(value)
|
||||
if key in self.metrics:
|
||||
del self.metrics[key]
|
||||
except Exception:
|
||||
log.debug('error setting tag %s, ignoring it', key, exc_info=True)
|
||||
|
||||
def _remove_tag(self, key):
|
||||
if key in self.meta:
|
||||
del self.meta[key]
|
||||
|
||||
def get_tag(self, key):
|
||||
""" Return the given tag or None if it doesn't exist.
|
||||
"""
|
||||
return self.meta.get(key, None)
|
||||
|
||||
def set_tags(self, tags):
|
||||
""" Set a dictionary of tags on the given span. Keys and values
|
||||
must be strings (or stringable)
|
||||
"""
|
||||
if tags:
|
||||
for k, v in iter(tags.items()):
|
||||
self.set_tag(k, v)
|
||||
|
||||
def set_meta(self, k, v):
|
||||
self.set_tag(k, v)
|
||||
|
||||
def set_metas(self, kvs):
|
||||
self.set_tags(kvs)
|
||||
|
||||
def set_metric(self, key, value):
|
||||
# This method sets a numeric tag value for the given key. It acts
|
||||
# like `set_meta()` and it simply add a tag without further processing.
|
||||
|
||||
# FIXME[matt] we could push this check to serialization time as well.
|
||||
# only permit types that are commonly serializable (don't use
|
||||
# isinstance so that we convert unserializable types like numpy
|
||||
# numbers)
|
||||
if type(value) not in numeric_types:
|
||||
try:
|
||||
value = float(value)
|
||||
except (ValueError, TypeError):
|
||||
log.debug('ignoring not number metric %s:%s', key, value)
|
||||
return
|
||||
|
||||
# don't allow nan or inf
|
||||
if math.isnan(value) or math.isinf(value):
|
||||
log.debug('ignoring not real metric %s:%s', key, value)
|
||||
return
|
||||
|
||||
if key in self.meta:
|
||||
del self.meta[key]
|
||||
self.metrics[key] = value
|
||||
|
||||
def set_metrics(self, metrics):
|
||||
if metrics:
|
||||
for k, v in iteritems(metrics):
|
||||
self.set_metric(k, v)
|
||||
|
||||
def get_metric(self, key):
|
||||
return self.metrics.get(key)
|
||||
|
||||
def to_dict(self):
|
||||
d = {
|
||||
'trace_id': self.trace_id,
|
||||
'parent_id': self.parent_id,
|
||||
'span_id': self.span_id,
|
||||
'service': self.service,
|
||||
'resource': self.resource,
|
||||
'name': self.name,
|
||||
'error': self.error,
|
||||
}
|
||||
|
||||
# a common mistake is to set the error field to a boolean instead of an
|
||||
# int. let's special case that here, because it's sure to happen in
|
||||
# customer code.
|
||||
err = d.get('error')
|
||||
if err and type(err) == bool:
|
||||
d['error'] = 1
|
||||
|
||||
if self.start_ns:
|
||||
d['start'] = self.start_ns
|
||||
|
||||
if self.duration_ns:
|
||||
d['duration'] = self.duration_ns
|
||||
|
||||
if self.meta:
|
||||
d['meta'] = self.meta
|
||||
|
||||
if self.metrics:
|
||||
d['metrics'] = self.metrics
|
||||
|
||||
if self.span_type:
|
||||
d['type'] = self.span_type
|
||||
|
||||
return d
|
||||
|
||||
def set_traceback(self, limit=20):
|
||||
""" If the current stack has an exception, tag the span with the
|
||||
relevant error info. If not, set the span to the current python stack.
|
||||
"""
|
||||
(exc_type, exc_val, exc_tb) = sys.exc_info()
|
||||
|
||||
if (exc_type and exc_val and exc_tb):
|
||||
self.set_exc_info(exc_type, exc_val, exc_tb)
|
||||
else:
|
||||
tb = ''.join(traceback.format_stack(limit=limit + 1)[:-1])
|
||||
self.set_tag(errors.ERROR_STACK, tb) # FIXME[gabin] Want to replace 'error.stack' tag with 'python.stack'
|
||||
|
||||
def set_exc_info(self, exc_type, exc_val, exc_tb):
|
||||
""" Tag the span with an error tuple as from `sys.exc_info()`. """
|
||||
if not (exc_type and exc_val and exc_tb):
|
||||
return # nothing to do
|
||||
|
||||
self.error = 1
|
||||
|
||||
# get the traceback
|
||||
buff = StringIO()
|
||||
traceback.print_exception(exc_type, exc_val, exc_tb, file=buff, limit=20)
|
||||
tb = buff.getvalue()
|
||||
|
||||
# readable version of type (e.g. exceptions.ZeroDivisionError)
|
||||
exc_type_str = '%s.%s' % (exc_type.__module__, exc_type.__name__)
|
||||
|
||||
self.set_tag(errors.ERROR_MSG, exc_val)
|
||||
self.set_tag(errors.ERROR_TYPE, exc_type_str)
|
||||
self.set_tag(errors.ERROR_STACK, tb)
|
||||
|
||||
def _remove_exc_info(self):
|
||||
""" Remove all exception related information from the span. """
|
||||
self.error = 0
|
||||
self._remove_tag(errors.ERROR_MSG)
|
||||
self._remove_tag(errors.ERROR_TYPE)
|
||||
self._remove_tag(errors.ERROR_STACK)
|
||||
|
||||
def pprint(self):
|
||||
""" Return a human readable version of the span. """
|
||||
lines = [
|
||||
('name', self.name),
|
||||
('id', self.span_id),
|
||||
('trace_id', self.trace_id),
|
||||
('parent_id', self.parent_id),
|
||||
('service', self.service),
|
||||
('resource', self.resource),
|
||||
('type', self.span_type),
|
||||
('start', self.start),
|
||||
('end', '' if not self.duration else self.start + self.duration),
|
||||
('duration', '%fs' % (self.duration or 0)),
|
||||
('error', self.error),
|
||||
('tags', '')
|
||||
]
|
||||
|
||||
lines.extend((' ', '%s:%s' % kv) for kv in sorted(self.meta.items()))
|
||||
return '\n'.join('%10s %s' % l for l in lines)
|
||||
|
||||
@property
|
||||
def context(self):
|
||||
"""
|
||||
Property that provides access to the ``Context`` associated with this ``Span``.
|
||||
The ``Context`` contains state that propagates from span to span in a
|
||||
larger trace.
|
||||
"""
|
||||
return self._context
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
try:
|
||||
if exc_type:
|
||||
self.set_exc_info(exc_type, exc_val, exc_tb)
|
||||
self.finish()
|
||||
except Exception:
|
||||
log.exception('error closing trace')
|
||||
|
||||
def __repr__(self):
|
||||
return '<Span(id=%s,trace_id=%s,parent_id=%s,name=%s)>' % (
|
||||
self.span_id,
|
||||
self.trace_id,
|
||||
self.parent_id,
|
||||
self.name,
|
||||
)
|
||||
|
||||
|
||||
def _new_id():
|
||||
"""Generate a random trace_id or span_id"""
|
||||
return _getrandbits(64)
|
Reference in New Issue
Block a user