diff --git a/instana/binary_propagator.py b/instana/binary_propagator.py index 93cf3bd1..fbccfcb0 100644 --- a/instana/binary_propagator.py +++ b/instana/binary_propagator.py @@ -4,7 +4,7 @@ from .log import logger from .util import header_to_id -from .span import SpanContext +from .span_context import SpanContext class BinaryPropagator(): @@ -21,15 +21,15 @@ def inject(self, span_context, carrier): span_id = str.encode(span_context.span_id) level = str.encode("1") - if type(carrier) is dict or hasattr(carrier, "__dict__"): + if isinstance(carrier, dict) or hasattr(carrier, "__dict__"): carrier[self.HEADER_KEY_T] = trace_id carrier[self.HEADER_KEY_S] = span_id carrier[self.HEADER_KEY_L] = level - elif type(carrier) is list: + elif isinstance(carrier, list): carrier.append((self.HEADER_KEY_T, trace_id)) carrier.append((self.HEADER_KEY_S, span_id)) carrier.append((self.HEADER_KEY_L, level)) - elif type(carrier) is tuple: + elif isinstance(carrier, tuple): carrier = carrier.__add__(((self.HEADER_KEY_T, trace_id),)) carrier = carrier.__add__(((self.HEADER_KEY_S, span_id),)) carrier = carrier.__add__(((self.HEADER_KEY_L, level),)) @@ -50,17 +50,17 @@ def extract(self, carrier): # noqa level = None try: - if type(carrier) is dict or hasattr(carrier, "__getitem__"): + if isinstance(carrier, dict) or hasattr(carrier, "__getitem__"): dc = carrier elif hasattr(carrier, "__dict__"): dc = carrier.__dict__ - elif type(carrier) is list: + elif isinstance(carrier, list): dc = dict(carrier) else: raise ot.SpanContextCorruptedException() for key, value in dc.items(): - if type(key) is str: + if isinstance(key, str): key = str.encode(key) if self.HEADER_KEY_T == key: diff --git a/instana/http_propagator.py b/instana/http_propagator.py index 00dab833..467286f4 100644 --- a/instana/http_propagator.py +++ b/instana/http_propagator.py @@ -3,8 +3,8 @@ import opentracing as ot from .log import logger -from .span import SpanContext from .util import header_to_id +from .span_context import SpanContext # The carrier can be a dict or a list. # Using the trace header as an example, it can be in the following forms @@ -43,11 +43,11 @@ def inject(self, span_context, carrier): trace_id = span_context.trace_id span_id = span_context.span_id - if type(carrier) is dict or hasattr(carrier, "__dict__"): + if isinstance(carrier, dict) or hasattr(carrier, "__dict__"): carrier[self.HEADER_KEY_T] = trace_id carrier[self.HEADER_KEY_S] = span_id carrier[self.HEADER_KEY_L] = "1" - elif type(carrier) is list: + elif isinstance(carrier, list): carrier.append((self.HEADER_KEY_T, trace_id)) carrier.append((self.HEADER_KEY_S, span_id)) carrier.append((self.HEADER_KEY_L, "1")) @@ -68,11 +68,11 @@ def extract(self, carrier): # noqa synthetic = False try: - if type(carrier) is dict or hasattr(carrier, "__getitem__"): + if isinstance(carrier, dict) or hasattr(carrier, "__getitem__"): dc = carrier elif hasattr(carrier, "__dict__"): dc = carrier.__dict__ - elif type(carrier) is list: + elif isinstance(carrier, list): dc = dict(carrier) else: raise ot.SpanContextCorruptedException() diff --git a/instana/span.py b/instana/span.py index 43bab65a..21f154d3 100644 --- a/instana/span.py +++ b/instana/span.py @@ -1,75 +1,102 @@ +""" +This module contains the classes that represents spans. + +InstanaSpan - the OpenTracing based span used during tracing + +When an InstanaSpan is finished, it is converted into either an SDKSpan +or RegisteredSpan depending on type. + +BaseSpan: Base class containing the commonalities for the two descendants + - SDKSpan: Class that represents an SDK type span + - RegisteredSpan: Class that represents a Registered type span +""" import six -import sys -from .log import logger -from .util import DictionaryOfStan + from basictracer.span import BasicSpan import opentracing.ext.tags as ot_tags - -class SpanContext(): - def __init__( - self, - trace_id=None, - span_id=None, - baggage=None, - sampled=True, - level=1, - synthetic=False): - - self.level = level - self.trace_id = trace_id - self.span_id = span_id - self.sampled = sampled - self.synthetic = synthetic - self._baggage = baggage or {} - - @property - def baggage(self): - return self._baggage - - def with_baggage_item(self, key, value): - new_baggage = self._baggage.copy() - new_baggage[key] = value - return SpanContext( - trace_id=self.trace_id, - span_id=self.span_id, - sampled=self.sampled, - baggage=new_baggage) +from .log import logger +from .util import DictionaryOfStan class InstanaSpan(BasicSpan): stack = None synthetic = False - def finish(self, finish_time=None): - super(InstanaSpan, self).finish(finish_time) + def __init__(self, tracer, operation_name=None, context=None, parent_id=None, tags=None, start_time=None): + # Tag validation + filtered_tags = {} + if tags is not None: + for key in tags.keys(): + validated_key, validated_value = self._validate_tag(key, tags[key]) + if validated_key is not None: + filtered_tags[validated_key] = validated_value + + super(InstanaSpan, self).__init__(tracer, operation_name, context, parent_id, filtered_tags, start_time) + + def _validate_tag(self, key, value): + """ + This method will assure that and are valid to set as a tag. + If fails the check, an attempt will be made to convert it into + something useful. + + On check failure, this method will return None values indicating that the tag is + not valid and could not be converted into something useful + + :param key: The tag key + :param value: The tag value + :return: Tuple (key, value) + """ + validated_key = None + validated_value = None + + try: + # Tag keys must be some type of text or string type + if isinstance(key, (six.text_type, six.string_types)): + validated_key = key[0:1024] # Max key length of 1024 characters + + if isinstance(value, (bool, float, int, list, dict, six.text_type, six.string_types)): + validated_value = value + else: + validated_value = self._convert_tag_value(value) + else: + logger.debug("(non-fatal) tag names must be strings. tag discarded for %s", type(key)) + except Exception: + logger.debug("instana.span._validate_tag: ", exc_info=True) + + return (validated_key, validated_value) + + def _convert_tag_value(self, value): + final_value = None + + try: + final_value = repr(value) + except Exception: + final_value = "(non-fatal) span.set_tag: values must be one of these types: bool, float, int, list, " \ + "set, str or alternatively support 'repr'. tag discarded" + logger.debug(final_value, exc_info=True) + return None + return final_value def set_tag(self, key, value): - # Key validation - if not isinstance(key, six.text_type) and not isinstance(key, six.string_types) : - logger.debug("(non-fatal) span.set_tag: tag names must be strings. tag discarded for %s", type(key)) - return self + validated_key, validated_value = self._validate_tag(key, value) - final_value = value - value_type = type(value) + if validated_key is not None and validated_value is not None: + return super(InstanaSpan, self).set_tag(validated_key, validated_value) - # Value validation - if value_type in [bool, float, int, list, str]: - return super(InstanaSpan, self).set_tag(key, final_value) + return self - elif isinstance(value, six.text_type): - final_value = str(value) + def log_kv(self, key_values, timestamp=None): + validated_key = None + validated_value = None - else: - try: - final_value = repr(value) - except: - final_value = "(non-fatal) span.set_tag: values must be one of these types: bool, float, int, list, " \ - "set, str or alternatively support 'repr'. tag discarded" - logger.debug(final_value, exc_info=True) - return self + for key in key_values.keys(): + validated_key, validated_value = self._validate_tag(key, key_values[key]) + + if validated_key is not None and validated_value is not None: + return super(InstanaSpan, self).log_kv({validated_key: validated_value}, timestamp) - return super(InstanaSpan, self).set_tag(key, final_value) + return self def mark_as_errored(self, tags = None): """ @@ -81,7 +108,7 @@ def mark_as_errored(self, tags = None): ec = self.tags.get('ec', 0) self.set_tag('ec', ec + 1) - if tags is not None and type(tags) is dict: + if tags is not None and isinstance(tags, dict): for key in tags: self.set_tag(key, tags[key]) except Exception: @@ -99,7 +126,7 @@ def assure_errored(self): except Exception: logger.debug('span.assure_errored', exc_info=True) - def log_exception(self, e): + def log_exception(self, exc): """ Log an exception onto this span. This will log pertinent info from the exception and assure that this span is marked as errored. @@ -110,12 +137,12 @@ def log_exception(self, e): message = "" self.mark_as_errored() - if hasattr(e, '__str__') and len(str(e)) > 0: - message = str(e) - elif hasattr(e, 'message') and e.message is not None: - message = e.message + if hasattr(exc, '__str__') and len(str(exc)) > 0: + message = str(exc) + elif hasattr(exc, 'message') and exc.message is not None: + message = exc.message else: - message = repr(e) + message = repr(exc) if self.operation_name in ['rpc-server', 'rpc-client']: self.set_tag('rpc.error', message) @@ -133,32 +160,10 @@ def log_exception(self, e): logger.debug("span.log_exception", exc_info=True) raise - def collect_logs(self): - """ - Collect up log data and feed it to the Instana brain. - - :param span: The span to search for logs in - :return: Logs ready for consumption by the Instana brain. - """ - logs = {} - for log in self.logs: - ts = int(round(log.timestamp * 1000)) - if ts not in logs: - logs[ts] = {} - - if 'message' in log.key_values: - logs[ts]['message'] = log.key_values['message'] - if 'event' in log.key_values: - logs[ts]['event'] = log.key_values['event'] - if 'parameters' in log.key_values: - logs[ts]['parameters'] = log.key_values['parameters'] - - return logs - class BaseSpan(object): sy = None - + def __str__(self): return "BaseSpan(%s)" % self.__dict__.__str__() @@ -166,6 +171,7 @@ def __repr__(self): return self.__dict__.__str__() def __init__(self, span, source, service_name, **kwargs): + # pylint: disable=invalid-name self.t = span.context.trace_id self.p = span.parent_id self.s = span.context.span_id @@ -189,6 +195,7 @@ class SDKSpan(BaseSpan): EXIT_KIND = ["exit", "client", "producer"] def __init__(self, span, source, service_name, **kwargs): + # pylint: disable=invalid-name super(SDKSpan, self).__init__(span, source, service_name, **kwargs) span_kind = self.get_span_kind(span) @@ -202,13 +209,18 @@ def __init__(self, span, source, service_name, **kwargs): self.data["sdk"]["name"] = span.operation_name self.data["sdk"]["type"] = span_kind[0] self.data["sdk"]["custom"]["tags"] = span.tags - self.data["sdk"]["custom"]["logs"] = span.logs + + if span.logs is not None and len(span.logs) > 0: + logs = DictionaryOfStan() + for log in span.logs: + logs[repr(log.timestamp)] = log.key_values + self.data["sdk"]["custom"]["logs"] = logs if "arguments" in span.tags: - self.data.sdk.arguments = span.tags["arguments"] + self.data['sdk']['arguments'] = span.tags["arguments"] if "return" in span.tags: - self.data.sdk.Return = span.tags["return"] + self.data['sdk']['return'] = span.tags["return"] if len(span.context.baggage) > 0: self.data["baggage"] = span.context.baggage @@ -244,6 +256,7 @@ class RegisteredSpan(BaseSpan): LOCAL_SPANS = ("render") def __init__(self, span, source, service_name, **kwargs): + # pylint: disable=invalid-name super(RegisteredSpan, self).__init__(span, source, service_name, **kwargs) self.n = span.operation_name @@ -263,7 +276,7 @@ def __init__(self, span, source, service_name, **kwargs): self.k = 1 # entry # Store any leftover tags in the custom section - if len(span.tags): + if len(span.tags) > 0: self.data["custom"]["tags"] = span.tags def _populate_entry_span_data(self, span): diff --git a/instana/span_context.py b/instana/span_context.py new file mode 100644 index 00000000..001b3101 --- /dev/null +++ b/instana/span_context.py @@ -0,0 +1,30 @@ + +class SpanContext(): + def __init__( + self, + trace_id=None, + span_id=None, + baggage=None, + sampled=True, + level=1, + synthetic=False): + + self.level = level + self.trace_id = trace_id + self.span_id = span_id + self.sampled = sampled + self.synthetic = synthetic + self._baggage = baggage or {} + + @property + def baggage(self): + return self._baggage + + def with_baggage_item(self, key, value): + new_baggage = self._baggage.copy() + new_baggage[key] = value + return SpanContext( + trace_id=self.trace_id, + span_id=self.span_id, + sampled=self.sampled, + baggage=new_baggage) \ No newline at end of file diff --git a/instana/text_propagator.py b/instana/text_propagator.py index 94fefb2d..7da9e495 100644 --- a/instana/text_propagator.py +++ b/instana/text_propagator.py @@ -3,8 +3,8 @@ import opentracing as ot from .log import logger -from .span import SpanContext from .util import header_to_id +from .span_context import SpanContext class TextPropagator(): @@ -20,15 +20,15 @@ def inject(self, span_context, carrier): trace_id = span_context.trace_id span_id = span_context.span_id - if type(carrier) is dict or hasattr(carrier, "__dict__"): + if isinstance(carrier, dict) or hasattr(carrier, "__dict__"): carrier[self.HEADER_KEY_T] = trace_id carrier[self.HEADER_KEY_S] = span_id carrier[self.HEADER_KEY_L] = "1" - elif type(carrier) is list: + elif isinstance(carrier, list): carrier.append((self.HEADER_KEY_T, trace_id)) carrier.append((self.HEADER_KEY_S, span_id)) carrier.append((self.HEADER_KEY_L, "1")) - elif type(carrier) is tuple: + elif isinstance(carrier, tuple): carrier = carrier.__add__(((self.HEADER_KEY_T, trace_id),)) carrier = carrier.__add__(((self.HEADER_KEY_S, span_id),)) carrier = carrier.__add__(((self.HEADER_KEY_L, "1"),)) @@ -49,11 +49,11 @@ def extract(self, carrier): # noqa level = 1 try: - if type(carrier) is dict or hasattr(carrier, "__getitem__"): + if isinstance(carrier, dict) or hasattr(carrier, "__getitem__"): dc = carrier elif hasattr(carrier, "__dict__"): dc = carrier.__dict__ - elif type(carrier) is list: + elif isinstance(carrier, list): dc = dict(carrier) else: raise ot.SpanContextCorruptedException() diff --git a/instana/tracer.py b/instana/tracer.py index fa12fd46..68ffe0fa 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -8,12 +8,13 @@ import opentracing as ot from basictracer import BasicTracer -from .binary_propagator import BinaryPropagator +from .util import generate_id +from .span_context import SpanContext from .http_propagator import HTTPPropagator from .text_propagator import TextPropagator +from .span import InstanaSpan, RegisteredSpan +from .binary_propagator import BinaryPropagator from .recorder import StanRecorder, InstanaSampler -from .span import InstanaSpan, RegisteredSpan, SpanContext -from .util import generate_id class InstanaTracer(BasicTracer): @@ -112,14 +113,14 @@ def start_span(self, def inject(self, span_context, format, carrier): if format in self._propagators: return self._propagators[format].inject(span_context, carrier) - else: - raise ot.UnsupportedFormatException() + + raise ot.UnsupportedFormatException() def extract(self, format, carrier): if format in self._propagators: return self._propagators[format].extract(carrier) - else: - raise ot.UnsupportedFormatException() + + raise ot.UnsupportedFormatException() def __add_stack(self, span, limit=None): """ Adds a backtrace to this span """ diff --git a/tests/opentracing/test_ot_propagators.py b/tests/opentracing/test_ot_propagators.py index 0bd786f7..0e2f2df3 100644 --- a/tests/opentracing/test_ot_propagators.py +++ b/tests/opentracing/test_ot_propagators.py @@ -5,6 +5,7 @@ import instana.http_propagator as ihp import instana.text_propagator as itp from instana import span +from instana.span_context import SpanContext from instana.tracer import InstanaTracer @@ -53,7 +54,7 @@ def test_http_basic_extract(): carrier = {'X-Instana-T': '1', 'X-Instana-S': '1', 'X-Instana-L': '1', 'X-Instana-Synthetic': '1'} ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - assert isinstance(ctx, span.SpanContext) + assert isinstance(ctx, SpanContext) assert('0000000000000001' == ctx.trace_id) assert('0000000000000001' == ctx.span_id) assert ctx.synthetic @@ -65,7 +66,7 @@ def test_http_mixed_case_extract(): carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1'} ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - assert isinstance(ctx, span.SpanContext) + assert isinstance(ctx, SpanContext) assert('0000000000000001' == ctx.trace_id) assert('0000000000000001' == ctx.span_id) assert not ctx.synthetic @@ -77,7 +78,7 @@ def test_http_extract_synthetic_only(): carrier = {'X-Instana-Synthetic': '1'} ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - assert isinstance(ctx, span.SpanContext) + assert isinstance(ctx, SpanContext) assert ctx.trace_id is None assert ctx.span_id is None assert ctx.synthetic @@ -99,7 +100,7 @@ def test_http_128bit_headers(): 'X-Instana-S': '0000000000000000b0789916ff8f319f', 'X-Instana-L': '1'} ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) - assert isinstance(ctx, span.SpanContext) + assert isinstance(ctx, SpanContext) assert('b0789916ff8f319f' == ctx.trace_id) assert('b0789916ff8f319f' == ctx.span_id) @@ -149,7 +150,7 @@ def test_text_basic_extract(): carrier = {'X-INSTANA-T': '1', 'X-INSTANA-S': '1', 'X-INSTANA-L': '1'} ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - assert isinstance(ctx, span.SpanContext) + assert isinstance(ctx, SpanContext) assert('0000000000000001' == ctx.trace_id) assert('0000000000000001' == ctx.span_id) @@ -179,6 +180,6 @@ def test_text_128bit_headers(): 'X-INSTANA-S': ' 0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - assert isinstance(ctx, span.SpanContext) + assert isinstance(ctx, SpanContext) assert('b0789916ff8f319f' == ctx.trace_id) assert('b0789916ff8f319f' == ctx.span_id) diff --git a/tests/opentracing/test_ot_span.py b/tests/opentracing/test_ot_span.py index 1e8c3771..a8184c0a 100644 --- a/tests/opentracing/test_ot_span.py +++ b/tests/opentracing/test_ot_span.py @@ -267,3 +267,20 @@ def test_custom_service_name(self): assert("service" not in intermediate_span.data) assert(exit_span.k == 2) + def test_span_log(self): + with tracer.start_active_span('mylogspan') as scope: + scope.span.log_kv({'Don McLean': 'American Pie'}) + scope.span.log_kv({'Elton John': 'Your Song'}) + + spans = tracer.recorder.queued_spans() + assert len(spans) == 1 + + my_log_span = spans[0] + assert my_log_span.n == 'sdk' + + log_data = my_log_span.data['sdk']['custom']['logs'] + assert len(log_data) == 2 + + + +