diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..65e4f3dc --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: python +python: + - "2.7" + - "3.6" +install: "pip install -r requirements.txt" +script: nosetests diff --git a/README.md b/README.md index 0df274df..99f244ca 100644 --- a/README.md +++ b/README.md @@ -1 +1,37 @@ -# python-sensor +
+ +
+ +# Instana + +The instana package provides Python metrics and traces (request, queue & cross-host) for [Instana](https://www.instana.com/). + +## Note + +This package supports Python 2.7 or greater. + +Any and all feedback is welcome. Happy Python visibility. + +## Installation + + $ pip install instana + +## Usage + +The instana package is a zero configuration tool that will automatically collect key metrics from your Python processes. Just install and go. + +## Tracing + +This Python package supports [OpenTracing](http://opentracing.io/). + +## Documentation + +You can find more documentation covering supported components and minimum versions in the Instana [documentation portal](https://instana.atlassian.net/wiki/display/DOCS/Python). + +## Contributing + +Bug reports and pull requests are welcome on GitHub at https://github.com/instana/python-sensor. + +## More + +Want to instrument other languages? See our [Nodejs instrumentation](https://github.com/instana/nodejs-sensor), [Go instrumentation](https://github.com/instana/golang-sensor), [Ruby instrumentation](https://github.com/instana/ruby-sensor) or [many other supported technologies](https://www.instana.com/supported-technologies/). diff --git a/instana/agent.py b/instana/agent.py index 79121836..4dd4ee95 100644 --- a/instana/agent.py +++ b/instana/agent.py @@ -8,6 +8,7 @@ except ImportError: import urllib2 + class From(object): pid = "" hostId = "" @@ -42,10 +43,11 @@ def __init__(self, sensor): self.reset() def to_json(self, o): - return json.dumps(o, default=lambda o: o.__dict__, sort_keys=False,separators=(',', ':')).encode() + return json.dumps(o, default=lambda o: o.__dict__, + sort_keys=False, separators=(',', ':')).encode() def can_send(self): - return self.fsm.fsm.current == "ready" + return self.fsm.fsm.current == "good2go" def head(self, url): return self.request(url, "HEAD", None) @@ -94,7 +96,7 @@ def full_request_response(self, url, method, o, body, header): if method == "HEAD": b = True except Exception as e: - l.error(str(e)) + l.error("full_request_response: " + str(e)) return (b, h) diff --git a/instana/custom.py b/instana/custom.py deleted file mode 100644 index 5bf5dcba..00000000 --- a/instana/custom.py +++ /dev/null @@ -1,6 +0,0 @@ -class CustomData(object): - tags = None - logs = None - - def __init__(self, **kwds): - self.__dict__.update(kwds) diff --git a/instana/data.py b/instana/data.py deleted file mode 100644 index 2b126853..00000000 --- a/instana/data.py +++ /dev/null @@ -1,8 +0,0 @@ -class Data(object): - service = None - http = None - baggage = None - custom = None - - def __init__(self, **kwds): - self.__dict__.update(kwds) diff --git a/instana/fsm.py b/instana/fsm.py index fb1717fc..50f14b29 100644 --- a/instana/fsm.py +++ b/instana/fsm.py @@ -1,6 +1,6 @@ import subprocess import os -import sys +import psutil import threading as t import fysom as f import instana.log as l @@ -17,35 +17,34 @@ def __init__(self, **kwds): class Fsm(object): - E_START = "start" - E_LOOKUP = "lookup" - E_ANNOUNCE = "announce" - E_TEST = "test" - RETRY_PERIOD = 30 agent = None fsm = None + timer = None def __init__(self, agent): l.debug("initializing fsm") self.agent = agent self.fsm = f.Fysom({ - "initial": "none", + "initial": "lostandalone", "events": [ - {"name": self.E_START, "src": [ - "none", "unannounced", "announced", "ready"], "dst": "init"}, - {"name": self.E_LOOKUP, "src": "init", "dst": "unannounced"}, - {"name": self.E_ANNOUNCE, "src": "unannounced", "dst": "announced"}, - {"name": self.E_TEST, "src": "announced", "dst": "ready"}], + ("startup", "*", "lostandalone"), + ("lookup", "lostandalone", "found"), + ("announce", "found", "announced"), + ("ready", "announced", "good2go")], "callbacks": { - "onstart": self.lookup_agent_host, - "onenterunannounced": self.announce_sensor, - "onenterannounced": self.test_agent}}) + "onlookup": self.lookup_agent_host, + "onannounce": self.announce_sensor, + "onchangestate": self.printstatechange}}) + + def printstatechange(self, e): + l.debug('========= (%i#%s) FSM event: %s, src: %s, dst: %s ==========' % \ + (os.getpid(), t.current_thread().name, e.event, e.src, e.dst)) def reset(self): - self.fsm.start() + self.fsm.lookup() def lookup_agent_host(self, e): if self.agent.sensor.options.agent_host != "": @@ -56,17 +55,19 @@ def lookup_agent_host(self, e): h = self.check_host(host) if h == a.AGENT_HEADER: self.agent.set_host(host) - self.fsm.lookup() + self.fsm.announce() else: host = self.get_default_gateway() if host: self.check_host(host) if h == a.AGENT_HEADER: self.agent.set_host(host) - self.fsm.lookup() + self.fsm.announce() else: l.error("Cannot lookup agent host. Scheduling retry.") - self.schedule_retry(self.lookup_agent_host, e) + self.schedule_retry(self.lookup_agent_host, e, "agent_lookup") + return False + return True def get_default_gateway(self): l.debug("checking default gateway") @@ -91,22 +92,29 @@ def check_host(self, host): def announce_sensor(self, e): l.debug("announcing sensor to the agent") - - d = Discovery(pid=os.getpid(), - name=sys.executable, - args=sys.argv[0:]) + p = psutil.Process(os.getpid()) + d = Discovery(pid=p.pid, + name=p.cmdline()[0], + args=p.cmdline()[1:]) (b, _) = self.agent.request_response( self.agent.make_url(a.AGENT_DISCOVERY_URL), "PUT", d) if not b: l.error("Cannot announce sensor. Scheduling retry.") - self.schedule_retry(self.announce_sensor, e) + self.schedule_retry(self.announce_sensor, e, "announce") + return False else: self.agent.set_from(b) - self.fsm.announce() + self.fsm.ready() + return True - def schedule_retry(self, fun, e): - t.Timer(self.RETRY_PERIOD, fun, [e]).start() + def schedule_retry(self, fun, e, name): + l.error("Scheduling: " + name) + self.timer = t.Timer(self.RETRY_PERIOD, fun, [e]) + self.timer.daemon = True + self.timer.name = name + self.timer.start() + l.debug('Threadlist: %s', str(t.enumerate())) def test_agent(self, e): l.debug("testing communication with the agent") @@ -114,6 +122,6 @@ def test_agent(self, e): (b, _) = self.agent.head(self.agent.make_url(a.AGENT_DATA_URL)) if not b: - self.schedule_retry(self.test_agent, e) + self.schedule_retry(self.test_agent, e, "agent test") else: self.fsm.test() diff --git a/instana/http.py b/instana/http.py deleted file mode 100644 index d7871a78..00000000 --- a/instana/http.py +++ /dev/null @@ -1,11 +0,0 @@ -HTTP_CLIENT = "g.hc" -HTTP_SERVER = "g.http" - -class HttpData(object): - host = None - url = None - status = 0 - method = None - - def __init__(self, **kwds): - self.__dict__.update(kwds) diff --git a/instana/log.py b/instana/log.py index baad4911..d16029f7 100644 --- a/instana/log.py +++ b/instana/log.py @@ -1,6 +1,7 @@ import logging as l +import os -logger = l.getLogger('instana') +logger = l.getLogger('instana(' + str(os.getpid()) + ')') def init(level): diff --git a/instana/meter.py b/instana/meter.py index d6b48870..96d33f8a 100644 --- a/instana/meter.py +++ b/instana/meter.py @@ -5,6 +5,8 @@ import gc as gc_ import sys import instana.agent_const as a +import copy +import time class Snapshot(object): @@ -63,6 +65,17 @@ class Metrics(object): def __init__(self, **kwds): self.__dict__.update(kwds) + def delta_data(self, delta): + data = self.__dict__ + if delta is None: + return data + + unchanged_items = set(data.items()) & set(delta.items()) + for x in unchanged_items: + data.pop(x[0]) + + return data + class EntityData(object): pid = 0 @@ -75,33 +88,44 @@ def __init__(self, **kwds): class Meter(object): SNAPSHOT_PERIOD = 600 - snapshot_countdown = 1 + snapshot_countdown = 30 sensor = None last_usage = None last_collect = None + timer = None + last_metrics = None def __init__(self, sensor): self.sensor = sensor - self.tick() + self.run() + + def run(self): + self.timer = t.Thread(target=self.collect_and_report) + self.timer.daemon = True + self.timer.name = "Instana Metric Collection" + self.timer.start() - def tick(self): - t.Timer(1, self.process).start() + def collect_and_report(self): + while 1: + self.process() + time.sleep(1) def process(self): if self.sensor.agent.can_send(): self.snapshot_countdown = self.snapshot_countdown - 1 s = None + cm = self.collect_metrics() if self.snapshot_countdown == 0: self.snapshot_countdown = self.SNAPSHOT_PERIOD s = self.collect_snapshot() - - m = self.collect_metrics() - d = EntityData(pid=os.getpid(), snapshot=s, metrics=m) - - t.Thread(target=self.sensor.agent.request, - args=(self.sensor.agent.make_url(a.AGENT_DATA_URL), "POST", d)).start() - - self.tick() + md = cm.delta_data(None) + else: + md = copy.deepcopy(cm).delta_data(self.last_metrics) + + d = EntityData(pid=os.getpid(), snapshot=s, metrics=md) + self.sensor.agent.request( + self.sensor.agent.make_url(a.AGENT_DATA_URL), "POST", d) + self.last_metrics = cm.__dict__ def collect_snapshot(self): try: diff --git a/instana/recorder.py b/instana/recorder.py index c83bd1cb..5e038d2f 100644 --- a/instana/recorder.py +++ b/instana/recorder.py @@ -1,53 +1,127 @@ from basictracer import Sampler, SpanRecorder import instana.agent_const as a -import instana.http as http -import instana.custom as c import threading as t import opentracing.ext.tags as ext import socket -import instana.data as d +import instana.span as sd +import Queue +import time -class InstanaSpan(object): - t = 0 - p = None - s = 0 - ts = 0 - d = 0 - n = None - f = None - data = None - - def __init__(self, **kwds): - self.__dict__.update(kwds) class InstanaRecorder(SpanRecorder): sensor = None + registered_spans = ("memcache", "rpc-client", "rpc-server") + entry_kind = ["entry", "server", "consumer"] + exit_kind = ["exit", "client", "producer"] + queue = Queue.Queue() def __init__(self, sensor): super(InstanaRecorder, self).__init__() self.sensor = sensor + self.run() + + def run(self): + """ Span a background thread to periodically report queued spans """ + self.timer = t.Thread(target=self.report_spans) + self.timer.daemon = True + self.timer.name = "Instana Span Reporting" + self.timer.start() + + def report_spans(self): + """ Periodically report the queued spans """ + while 1: + if self.sensor.agent.can_send() and self.queue.qsize: + url = self.sensor.agent.make_url(a.AGENT_TRACES_URL) + self.sensor.agent.request(url, "POST", self.queued_spans()) + time.sleep(2) + + def queue_size(self): + """ Return the size of the queue; how may spans are queued, """ + return self.queue.qsize() + + def queued_spans(self): + """ Get all of the spans in the queue """ + spans = [] + while True: + try: + s = self.queue.get(False) + except Queue.Empty: + break + else: + spans.append(s) + return spans + + def clear_spans(self): + """ Clear the queue of spans """ + self.queued_spans() def record_span(self, span): + """ + Convert the passed BasicSpan into an InstanaSpan and + add it to the span queue + """ if self.sensor.agent.can_send(): - data = d.Data(service=self.get_service_name(span), - http=http.HttpData(host=self.get_host_name(span), - url=self.get_string_tag(span, ext.HTTP_URL), - method=self.get_string_tag(span, ext.HTTP_METHOD), - status=self.get_tag(span, ext.HTTP_STATUS_CODE)), - baggage=span.context.baggage, - custom=c.CustomData(tags=span.tags, - logs=self.collect_logs(span))) - - t.Thread(target=self.sensor.agent.request, - args=(self.sensor.agent.make_url(a.AGENT_TRACES_URL), "POST", - [InstanaSpan(t=span.context.trace_id, - p=span.parent_id, - s=span.context.span_id, - ts=int(round(span.start_time * 1000)), - d=int(round(span.duration * 1000)), - n=self.get_http_type(span), - f=self.sensor.agent.from_, - data=data)])).start() + instana_span = None + + if span.operation_name in self.registered_spans: + instana_span = self.build_registered_span(span) + else: + instana_span = self.build_sdk_span(span) + + self.queue.put(instana_span) + + def build_registered_span(self, span): + """ Takes a BasicSpan and converts it into a registered InstanaSpan """ + data = sd.Data(service=self.get_service_name(span), + http=sd.HttpData(host=self.get_host_name(span), + url=self.get_string_tag(span, ext.HTTP_URL), + method=self.get_string_tag(span, ext.HTTP_METHOD), + status=self.get_tag(span, ext.HTTP_STATUS_CODE)), + baggage=span.context.baggage, + custom=sd.CustomData(tags=span.tags, + logs=self.collect_logs(span))) + return sd.InstanaSpan( + t=span.context.trace_id, + p=span.parent_id, + s=span.context.span_id, + ts=int(round(span.start_time * 1000)), + d=int(round(span.duration * 1000)), + n=self.get_http_type(span), + f=self.sensor.agent.from_, + data=data) + + def build_sdk_span(self, span): + """ Takes a BasicSpan and converts into an SDK type InstanaSpan """ + + custom_data = sd.CustomData( + tags=span.tags, + logs=self.collect_logs(span)) + + sdk_data = sd.SDKData( + name=span.operation_name, + custom=custom_data + ) + + if "span.kind" in span.tags: + if span.tags["span.kind"] in self.entry_kind: + sdk_data.Type = "entry" + elif span.tags["span.kind"] in self.exit_kind: + sdk_data.Type = "exit" + else: + sdk_data.Type = "local" + + data = sd.Data(service=self.get_service_name(span), + sdk=sdk_data) + + return sd.InstanaSpan( + t=span.context.trace_id, + p=span.parent_id, + s=span.context.span_id, + ts=int(round(span.start_time * 1000)), + d=int(round(span.duration * 1000)), + n="sdk", + f=self.sensor.agent.from_, + data=data) def get_tag(self, span, tag): if tag in span.tags: diff --git a/instana/span.py b/instana/span.py new file mode 100644 index 00000000..442a1809 --- /dev/null +++ b/instana/span.py @@ -0,0 +1,53 @@ +HTTP_CLIENT = "g.hc" +HTTP_SERVER = "g.http" + +class InstanaSpan(object): + t = 0 + p = None + s = 0 + ts = 0 + ta = "py" + d = 0 + n = None + f = None + ec = 0 + data = None + + def __init__(self, **kwds): + self.__dict__.update(kwds) + +class Data(object): + service = None + http = None + baggage = None + custom = None + sdk = None + + def __init__(self, **kwds): + self.__dict__.update(kwds) + +class HttpData(object): + host = None + url = None + status = 0 + method = None + + def __init__(self, **kwds): + self.__dict__.update(kwds) + +class CustomData(object): + tags = None + logs = None + + def __init__(self, **kwds): + self.__dict__.update(kwds) + +class SDKData(object): + name = None + Type = None + arguments = None + Return = None + custom = None + + def __init__(self, **kwds): + self.__dict__.update(kwds) diff --git a/instana/tracer.py b/instana/tracer.py index 1e018110..6d0ab7ac 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -1,18 +1,76 @@ +import time from basictracer import BasicTracer import instana.recorder as r import opentracing import instana.options as o import instana.sensor as s +import instana.log as ilog + +from basictracer.context import SpanContext +from basictracer.span import BasicSpan +from instana.util import generate_id + +# In case a user or app creates multiple tracers, we limit to just +# one sensor per process otherwise metrics collection is duplicated, +# triplicated etc. +gSensor = None class InstanaTracer(BasicTracer): sensor = None def __init__(self, options=o.Options()): - self.sensor = s.Sensor(options) + global gSensor + if gSensor is None: + self.sensor = s.Sensor(options) + gSensor = self.sensor + else: + self.sensor = gSensor super(InstanaTracer, self).__init__( r.InstanaRecorder(self.sensor), r.InstanaSampler()) + def start_span( + self, + operation_name=None, + child_of=None, + references=None, + tags=None, + start_time=None): + "Taken from BasicTracer so we can override generate_id calls to ours" + + start_time = time.time() if start_time is None else start_time + + # See if we have a parent_ctx in `references` + parent_ctx = None + if child_of is not None: + parent_ctx = ( + child_of if isinstance(child_of, opentracing.SpanContext) + else child_of.context) + elif references is not None and len(references) > 0: + # TODO only the first reference is currently used + parent_ctx = references[0].referenced_context + + # Assemble the child ctx + instana_id = generate_id() + ctx = SpanContext(span_id=instana_id) + if parent_ctx is not None: + if parent_ctx._baggage is not None: + ctx._baggage = parent_ctx._baggage.copy() + ctx.trace_id = parent_ctx.trace_id + ctx.sampled = parent_ctx.sampled + else: + ctx.trace_id = instana_id + ctx.sampled = self.sampler.sampled(ctx.trace_id) + + # Tie it all together + return BasicSpan( + self, + operation_name=operation_name, + context=ctx, + parent_id=(None if parent_ctx is None else parent_ctx.span_id), + tags=tags, + start_time=start_time) + def init(options): opentracing.tracer = InstanaTracer(options) diff --git a/instana/util.py b/instana/util.py new file mode 100644 index 00000000..8eb6ff5e --- /dev/null +++ b/instana/util.py @@ -0,0 +1,41 @@ +import random +import os +import time +import struct +import binascii + +_rnd = random.Random() +_current_pid = 0 + + +def generate_id(): + """ Generate a 64bit signed integer for use as a Span or Trace ID """ + global _current_pid + + pid = os.getpid() + if (_current_pid != pid): + _current_pid = pid + _rnd.seed(int(1000000 * time.time()) ^ pid) + return _rnd.randint(-9223372036854775808, 9223372036854775807) + + +def id_to_header(id): + """ Convert a 64bit signed integer to an unsigned base 16 hex string """ + + if not isinstance(id, int): + return "" + + byteString = struct.pack('>q', id) + return binascii.hexlify(byteString).lstrip('0') + + +def header_to_id(header): + """ Convert an unsigned base 16 hex string into a 64bit signed integer """ + + if not isinstance(header, basestring): + return 0 + + # Pad the header to 16 chars + header = header.zfill(16) + r = binascii.unhexlify(header) + return struct.unpack('>q', r)[0] diff --git a/requirements.txt b/requirements.txt index 3b0983cc..80a82923 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ nose>=1.0 fysom>=2.1.2 opentracing>=1.2.1 basictracer>=2.2.0 +psutil>=5.1.3 diff --git a/setup.cfg b/setup.cfg index 5061eeff..1e9fde20 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [nosetests] verbose=1 nocapture=1 + +[metadata] +description-file = README.md diff --git a/setup.py b/setup.py index bdac1e4f..3ac334da 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ url='https://github.com/instana/python-sensor', license='MIT', author='Instana Inc.', - author_email='pavlo.baron@instana.com', + author_email='peter.lombardo@instana.com', description='Metrics sensor and trace collector for Instana', packages=find_packages(exclude=['tests', 'examples']), long_description=open('README.md').read(), @@ -13,5 +13,7 @@ setup_requires=['nose>=1.0', 'fysom>=2.1.2', 'opentracing>=1.2.1,<1.3', - 'basictracer>=2.2.0'], - test_suite='nose.collector') + 'basictracer>=2.2.0', + 'psutil>=5.1.3'], + test_suite='nose.collector', + keywords=['performance', 'opentracing', 'metrics', 'monitoring']) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e95723bf --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,6 @@ +import os +import opentracing +import instana.tracer +os.environ["INSTANA_TEST"] = "true" + +opentracing.global_tracer = instana.tracer.InstanaTracer() diff --git a/tests/test_id_management.py b/tests/test_id_management.py new file mode 100644 index 00000000..0c2877be --- /dev/null +++ b/tests/test_id_management.py @@ -0,0 +1,130 @@ +import instana.util +import string +from nose.tools import assert_equals + + +def test_id_generation(): + count = 0 + while count <= 1000: + id = instana.util.generate_id() + assert id >= -9223372036854775808 + assert id <= 9223372036854775807 + count += 1 + + +def test_id_max_value_and_conversion(): + max_id = 9223372036854775807 + min_id = -9223372036854775808 + max_hex = "7fffffffffffffff" + min_hex = "8000000000000000" + + assert_equals(max_hex, instana.util.id_to_header(max_id)) + assert_equals(min_hex, instana.util.id_to_header(min_id)) + + assert_equals(max_id, instana.util.header_to_id(max_hex)) + assert_equals(min_id, instana.util.header_to_id(min_hex)) + + +def test_id_conversion_back_and_forth(): + # id --> header --> id + original_id = instana.util.generate_id() + header_id = instana.util.id_to_header(original_id) + converted_back_id = instana.util.header_to_id(header_id) + assert original_id == converted_back_id + + # header --> id --> header + original_header_id = "c025ee93b1aeda7b" + id = instana.util.header_to_id(original_header_id) + converted_back_header_id = instana.util.id_to_header(id) + assert_equals(original_header_id, converted_back_header_id) + + # Test a random value + id = -7815363404733516491 + header = "938a406416457535" + + result = instana.util.header_to_id(header) + assert_equals(id, result) + + result = instana.util.id_to_header(id) + assert_equals(header, result) + + +def test_that_leading_zeros_handled_correctly(): + header = instana.util.id_to_header(16) + assert_equals("10", header) + + id = instana.util.header_to_id("10") + assert_equals(16, id) + + id = instana.util.header_to_id("0000000000000010") + assert_equals(16, id) + + id = instana.util.header_to_id("88b6c735206ca42") + assert_equals(615705016619420226, id) + + id = instana.util.header_to_id("088b6c735206ca42") + assert_equals(615705016619420226, id) + + +def test_id_to_header_conversion(): + # Test passing a standard Integer ID + original_id = instana.util.generate_id() + converted_id = instana.util.id_to_header(original_id) + + # Assert that it is a string and there are no non-hex characters + assert isinstance(converted_id, basestring) + assert all(c in string.hexdigits for c in converted_id) + + # Test passing a standard Integer ID as a String + original_id = instana.util.generate_id() + converted_id = instana.util.id_to_header(original_id) + + # Assert that it is a string and there are no non-hex characters + assert isinstance(converted_id, basestring) + assert all(c in string.hexdigits for c in converted_id) + + +def test_id_to_header_conversion_with_bogus_id(): + # Test passing an empty String + converted_id = instana.util.id_to_header('') + + # Assert that it is a string and there are no non-hex characters + assert isinstance(converted_id, basestring) + assert converted_id == '' + + # Test passing a nil + converted_id = instana.util.id_to_header(None) + + # Assert that it is a string and there are no non-hex characters + assert isinstance(converted_id, basestring) + assert converted_id == '' + + # Test passing an Array + converted_id = instana.util.id_to_header([]) + + # Assert that it is a string and there are no non-hex characters + assert isinstance(converted_id, basestring) + assert converted_id == '' + + +def test_header_to_id_conversion(): + # Get a hex string to test against & convert + header_id = instana.util.id_to_header(instana.util.generate_id) + converted_id = instana.util.header_to_id(header_id) + + # Assert that it is an Integer + assert isinstance(converted_id, int) + + +def test_header_to_id_conversion_with_bogus_header(): + # Bogus nil arg + bogus_result = instana.util.header_to_id(None) + assert_equals(0, bogus_result) + + # Bogus Integer arg + bogus_result = instana.util.header_to_id(1234) + assert_equals(0, bogus_result) + + # Bogus Array arg + bogus_result = instana.util.header_to_id([1234]) + assert_equals(0, bogus_result) diff --git a/tests/test_ot_span.py b/tests/test_ot_span.py new file mode 100644 index 00000000..170d3b88 --- /dev/null +++ b/tests/test_ot_span.py @@ -0,0 +1,129 @@ +import opentracing +from nose.tools import assert_equals +import time + + +class TestOTSpan: + def setUp(self): + """ Clear all spans before a test run """ + recorder = opentracing.global_tracer.recorder + recorder.clear_spans() + + def tearDown(self): + """ Do nothing for now """ + return None + + def test_span_interface(self): + span = opentracing.global_tracer.start_span("blah") + assert hasattr(span, "finish") + assert hasattr(span, "set_tag") + assert hasattr(span, "tags") + assert hasattr(span, "operation_name") + assert hasattr(span, "set_baggage_item") + assert hasattr(span, "get_baggage_item") + assert hasattr(span, "context") + assert hasattr(span, "log") + + def test_span_ids(self): + count = 0 + while count <= 1000: + count += 1 + span = opentracing.global_tracer.start_span("test_span_ids") + context = span.context + assert -9223372036854775808 <= context.span_id <= 9223372036854775807 + assert -9223372036854775808 <= context.trace_id <= 9223372036854775807 + + def test_span_fields(self): + span = opentracing.global_tracer.start_span("mycustom") + assert_equals("mycustom", span.operation_name) + assert span.context + + span.set_tag("tagone", "string") + span.set_tag("tagtwo", 150) + + assert_equals("string", span.tags['tagone']) + assert_equals(150, span.tags['tagtwo']) + + def test_span_queueing(self): + recorder = opentracing.global_tracer.recorder + + count = 1 + while count <= 20: + count += 1 + span = opentracing.global_tracer.start_span("queuethisplz") + span.set_tag("tagone", "string") + span.set_tag("tagtwo", 150) + span.finish() + + assert_equals(20, recorder.queue_size()) + + def test_sdk_spans(self): + recorder = opentracing.global_tracer.recorder + + span = opentracing.global_tracer.start_span("custom_sdk_span") + span.set_tag("tagone", "string") + span.set_tag("tagtwo", 150) + span.set_tag('span.kind', "entry") + time.sleep(0.5) + span.finish() + + spans = recorder.queued_spans() + assert 1, len(spans) + + sdk_span = spans[0] + assert_equals('sdk', sdk_span.n) + assert_equals(None, sdk_span.p) + assert_equals(sdk_span.s, sdk_span.t) + assert sdk_span.ts + assert sdk_span.ts > 0 + assert sdk_span.d + assert sdk_span.d > 0 + assert_equals("py", sdk_span.ta) + + assert sdk_span.data + assert sdk_span.data.sdk + assert_equals('entry', sdk_span.data.sdk.Type) + assert_equals('custom_sdk_span', sdk_span.data.sdk.name) + assert sdk_span.data.sdk.custom + assert sdk_span.data.sdk.custom.tags + + def test_span_kind(self): + recorder = opentracing.global_tracer.recorder + + span = opentracing.global_tracer.start_span("custom_sdk_span") + span.set_tag('span.kind', "consumer") + span.finish() + + span = opentracing.global_tracer.start_span("custom_sdk_span") + span.set_tag('span.kind', "server") + span.finish() + + span = opentracing.global_tracer.start_span("custom_sdk_span") + span.set_tag('span.kind', "producer") + span.finish() + + span = opentracing.global_tracer.start_span("custom_sdk_span") + span.set_tag('span.kind', "client") + span.finish() + + span = opentracing.global_tracer.start_span("custom_sdk_span") + span.set_tag('span.kind', "blah") + span.finish() + + spans = recorder.queued_spans() + assert 5, len(spans) + + span = spans[0] + assert_equals('entry', span.data.sdk.Type) + + span = spans[1] + assert_equals('entry', span.data.sdk.Type) + + span = spans[2] + assert_equals('exit', span.data.sdk.Type) + + span = spans[3] + assert_equals('exit', span.data.sdk.Type) + + span = spans[4] + assert_equals('local', span.data.sdk.Type) diff --git a/tests/test_ot_tracer.py b/tests/test_ot_tracer.py new file mode 100644 index 00000000..1e3706d4 --- /dev/null +++ b/tests/test_ot_tracer.py @@ -0,0 +1,10 @@ +import opentracing +import instana.tracer +from nose.tools import assert_equals + + +def test_tracer_basics(): + assert hasattr(instana.tracer, 'InstanaTracer') + assert hasattr(opentracing.global_tracer, "start_span") + assert hasattr(opentracing.global_tracer, "inject") + assert hasattr(opentracing.global_tracer, "extract")