diff --git a/instana/__init__.py b/instana/__init__.py index d8ccaac4..f89b8f7f 100644 --- a/instana/__init__.py +++ b/instana/__init__.py @@ -125,6 +125,10 @@ def boot_agent(): # Import & initialize instrumentation from .instrumentation.aws import lambda_inst + if sys.version_info >= (3, 6, 0): + from .instrumentation import fastapi_inst + from .instrumentation import starlette_inst + if sys.version_info >= (3, 5, 3): from .instrumentation import asyncio from .instrumentation.aiohttp import client diff --git a/instana/agent/host.py b/instana/agent/host.py index ad1b249a..75ac7385 100644 --- a/instana/agent/host.py +++ b/instana/agent/host.py @@ -102,6 +102,9 @@ def can_send(self): Are we in a state where we can send data? @return: Boolean """ + if "INSTANA_TEST" in os.environ: + return True + # Watch for pid change (fork) self.last_fork_check = datetime.now() current_pid = os.getpid() diff --git a/instana/collector/aws_fargate.py b/instana/collector/aws_fargate.py index 8c13eec5..3e014f16 100644 --- a/instana/collector/aws_fargate.py +++ b/instana/collector/aws_fargate.py @@ -1,5 +1,5 @@ """ -Snapshot & metrics collection for AWS Fargate +AWS Fargate Collector: Manages the periodic collection of metrics & snapshot data """ import os import json diff --git a/instana/collector/aws_lambda.py b/instana/collector/aws_lambda.py index ad018363..33724393 100644 --- a/instana/collector/aws_lambda.py +++ b/instana/collector/aws_lambda.py @@ -1,5 +1,5 @@ """ -Snapshot & metrics collection for AWS Lambda +AWS Lambda Collector: Manages the periodic collection of metrics & snapshot data """ from ..log import logger from .base import BaseCollector diff --git a/instana/collector/base.py b/instana/collector/base.py index 2bb13fdf..aaa9c56a 100644 --- a/instana/collector/base.py +++ b/instana/collector/base.py @@ -29,7 +29,15 @@ def __init__(self, agent): self.THREAD_NAME = "Instana Collector" # The Queue where we store finished spans before they are sent - self.span_queue = queue.Queue() + if env_is_test: + # Override span queue with a multiprocessing version + # The test suite runs background applications - some in background threads, + # others in background processes. This multiprocess queue allows us to collect + # up spans from all sources. + import multiprocessing + self.span_queue = multiprocessing.Queue() + else: + self.span_queue = queue.Queue() # The Queue where we store finished profiles before they are sent self.profile_queue = queue.Queue() diff --git a/instana/collector/host.py b/instana/collector/host.py index 3681c919..a714dc08 100644 --- a/instana/collector/host.py +++ b/instana/collector/host.py @@ -1,11 +1,10 @@ """ -Snapshot & metrics collection for AWS Fargate +Host Collector: Manages the periodic collection of metrics & snapshot data """ from time import time from ..log import logger from .base import BaseCollector from ..util import DictionaryOfStan -from ..singletons import env_is_test from .helpers.runtime import RuntimeHelper diff --git a/instana/instrumentation/asgi.py b/instana/instrumentation/asgi.py new file mode 100644 index 00000000..63cd04ad --- /dev/null +++ b/instana/instrumentation/asgi.py @@ -0,0 +1,100 @@ +""" +Instana ASGI Middleware +""" +import opentracing + +from ..log import logger +from ..singletons import async_tracer, agent +from ..util import strip_secrets_from_query + +class InstanaASGIMiddleware: + """ + Instana ASGI Middleware + """ + def __init__(self, app): + self.app = app + + def _extract_custom_headers(self, span, headers): + try: + for custom_header in agent.options.extra_http_headers: + # Headers are in the following format: b'x-header-1' + for header_pair in headers: + if header_pair[0].decode('utf-8').lower() == custom_header.lower(): + span.set_tag("http.%s" % custom_header, header_pair[1].decode('utf-8')) + except Exception: + logger.debug("extract_custom_headers: ", exc_info=True) + + def _collect_kvs(self, scope, span): + try: + span.set_tag('http.path', scope.get('path')) + span.set_tag('http.method', scope.get('method')) + + server = scope.get('server') + if isinstance(server, tuple): + span.set_tag('http.host', server[0]) + + query = scope.get('query_string') + if isinstance(query, (str, bytes)) and len(query): + if isinstance(query, bytes): + query = query.decode('utf-8') + scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, agent.options.secrets_list) + span.set_tag("http.params", scrubbed_params) + + app = scope.get('app') + if app is not None and hasattr(app, 'routes'): + # Attempt to detect the Starlette routes registered. + # If Starlette isn't present, we harmlessly dump out. + from starlette.routing import Match + for route in scope['app'].routes: + if route.matches(scope)[0] == Match.FULL: + span.set_tag("http.path_tpl", route.path) + except Exception: + logger.debug("ASGI collect_kvs: ", exc_info=True) + + + async def __call__(self, scope, receive, send): + request_context = None + + if scope["type"] not in ("http", "websocket"): + await self.app(scope, receive, send) + return + + request_headers = scope.get('headers') + if isinstance(request_headers, list): + request_context = async_tracer.extract(opentracing.Format.BINARY, request_headers) + + async def send_wrapper(response): + span = async_tracer.active_span + if span is None: + await send(response) + else: + if response['type'] == 'http.response.start': + try: + status_code = response.get('status') + if status_code is not None: + if 500 <= int(status_code) <= 511: + span.mark_as_errored() + span.set_tag('http.status_code', status_code) + + headers = response.get('headers') + if headers is not None: + async_tracer.inject(span.context, opentracing.Format.BINARY, headers) + except Exception: + logger.debug("send_wrapper: ", exc_info=True) + + try: + await send(response) + except Exception as exc: + span.log_exception(exc) + raise + + with async_tracer.start_active_span("asgi", child_of=request_context) as tracing_scope: + self._collect_kvs(scope, tracing_scope.span) + if 'headers' in scope and agent.options.extra_http_headers is not None: + self._extract_custom_headers(tracing_scope.span, scope['headers']) + + try: + await self.app(scope, receive, send_wrapper) + except Exception as exc: + tracing_scope.span.log_exception(exc) + raise exc diff --git a/instana/instrumentation/fastapi_inst.py b/instana/instrumentation/fastapi_inst.py new file mode 100644 index 00000000..18c00a3e --- /dev/null +++ b/instana/instrumentation/fastapi_inst.py @@ -0,0 +1,33 @@ +""" +Instrumentation for FastAPI +https://fastapi.tiangolo.com/ +""" +try: + import fastapi + import wrapt + import signal + import os + + from ..log import logger + from ..util import running_in_gunicorn + from .asgi import InstanaASGIMiddleware + from starlette.middleware import Middleware + + @wrapt.patch_function_wrapper('fastapi.applications', 'FastAPI.__init__') + def init_with_instana(wrapped, instance, args, kwargs): + middleware = kwargs.get('middleware') + if middleware is None: + kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)] + elif isinstance(middleware, list): + middleware.append(Middleware(InstanaASGIMiddleware)) + + return wrapped(*args, **kwargs) + + logger.debug("Instrumenting FastAPI") + + # Reload GUnicorn when we are instrumenting an already running application + if "INSTANA_MAGIC" in os.environ and running_in_gunicorn(): + os.kill(os.getpid(), signal.SIGHUP) + +except ImportError: + pass \ No newline at end of file diff --git a/instana/instrumentation/pyramid/tweens.py b/instana/instrumentation/pyramid/tweens.py index 5ac1d3ec..a75c9d74 100644 --- a/instana/instrumentation/pyramid/tweens.py +++ b/instana/instrumentation/pyramid/tweens.py @@ -12,12 +12,12 @@ class InstanaTweenFactory(object): """A factory that provides Instana instrumentation tween for Pyramid apps""" - + def __init__(self, handler, registry): self.handler = handler def __call__(self, request): - ctx = tracer.extract(ot.Format.HTTP_HEADERS, request.headers) + ctx = tracer.extract(ot.Format.HTTP_HEADERS, dict(request.headers)) scope = tracer.start_active_span('http', child_of=ctx) scope.span.set_tag(ext.SPAN_KIND, ext.SPAN_KIND_RPC_SERVER) @@ -42,7 +42,7 @@ def __call__(self, request): response = None try: response = self.handler(request) - + tracer.inject(scope.span.context, ot.Format.HTTP_HEADERS, response.headers) response.headers['Server-Timing'] = "intid;desc=%s" % scope.span.context.trace_id except HTTPException as e: @@ -53,21 +53,21 @@ def __call__(self, request): # we need to explicitly populate the `message` tag with an error here # so that it's picked up from an SDK span - scope.span.set_tag("message", str(e)) + scope.span.set_tag("message", str(e)) scope.span.log_exception(e) - + logger.debug("Pyramid Instana tween", exc_info=True) finally: if response: scope.span.set_tag("http.status", response.status_int) - + if 500 <= response.status_int <= 511: if response.exception is not None: message = str(response.exception) scope.span.log_exception(response.exception) else: message = response.status - + scope.span.set_tag("message", message) scope.span.assure_errored() diff --git a/instana/instrumentation/starlette_inst.py b/instana/instrumentation/starlette_inst.py new file mode 100644 index 00000000..f033a430 --- /dev/null +++ b/instana/instrumentation/starlette_inst.py @@ -0,0 +1,24 @@ +""" +Instrumentation for Starlette +https://www.starlette.io/ +""" +try: + import starlette + import wrapt + from ..log import logger + from .asgi import InstanaASGIMiddleware + from starlette.middleware import Middleware + + @wrapt.patch_function_wrapper('starlette.applications', 'Starlette.__init__') + def init_with_instana(wrapped, instance, args, kwargs): + middleware = kwargs.get('middleware') + if middleware is None: + kwargs['middleware'] = [Middleware(InstanaASGIMiddleware)] + elif isinstance(middleware, list): + middleware.append(Middleware(InstanaASGIMiddleware)) + + return wrapped(*args, **kwargs) + + logger.debug("Instrumenting Starlette") +except ImportError: + pass diff --git a/instana/instrumentation/tornado/server.py b/instana/instrumentation/tornado/server.py index d563ef00..264734f8 100644 --- a/instana/instrumentation/tornado/server.py +++ b/instana/instrumentation/tornado/server.py @@ -24,7 +24,9 @@ def execute_with_instana(wrapped, instance, argv, kwargs): try: with tracer_stack_context(): - ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS, instance.request.headers) + ctx = None + if hasattr(instance.request.headers, '__dict__') and '_dict' in instance.request.headers.__dict__: + ctx = tornado_tracer.extract(opentracing.Format.HTTP_HEADERS, instance.request.headers.__dict__['_dict']) scope = tornado_tracer.start_active_span('tornado-server', child_of=ctx) # Query param scrubbing diff --git a/instana/instrumentation/wsgi.py b/instana/instrumentation/wsgi.py new file mode 100644 index 00000000..f729ed5b --- /dev/null +++ b/instana/instrumentation/wsgi.py @@ -0,0 +1,55 @@ +""" +Instana WSGI Middleware +""" +import opentracing as ot +import opentracing.ext.tags as tags + +from ..singletons import agent, tracer +from ..util import strip_secrets_from_query + + +class InstanaWSGIMiddleware(object): + """ Instana WSGI middleware """ + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + env = environ + + def new_start_response(status, headers, exc_info=None): + """Modified start response with additional headers.""" + tracer.inject(self.scope.span.context, ot.Format.HTTP_HEADERS, headers) + headers.append(('Server-Timing', "intid;desc=%s" % self.scope.span.context.trace_id)) + + res = start_response(status, headers, exc_info) + + sc = status.split(' ')[0] + if 500 <= int(sc) <= 511: + self.scope.span.mark_as_errored() + + self.scope.span.set_tag(tags.HTTP_STATUS_CODE, sc) + self.scope.close() + return res + + ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) + self.scope = tracer.start_active_span("wsgi", child_of=ctx) + + if agent.options.extra_http_headers is not None: + for custom_header in agent.options.extra_http_headers: + # Headers are available in this format: HTTP_X_CAPTURE_THIS + wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_') + if wsgi_header in env: + self.scope.span.set_tag("http.%s" % custom_header, env[wsgi_header]) + + if 'PATH_INFO' in env: + self.scope.span.set_tag('http.path', env['PATH_INFO']) + if 'QUERY_STRING' in env and len(env['QUERY_STRING']): + scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) + self.scope.span.set_tag("http.params", scrubbed_params) + if 'REQUEST_METHOD' in env: + self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD']) + if 'HTTP_HOST' in env: + self.scope.span.set_tag("http.host", env['HTTP_HOST']) + + return self.app(environ, new_start_response) diff --git a/instana/log.py b/instana/log.py index 9b117121..77c83b82 100644 --- a/instana/log.py +++ b/instana/log.py @@ -32,16 +32,31 @@ def get_aws_lambda_logger(): aws_lambda_logger.setLevel(logging.INFO) return aws_lambda_logger +def glogging_available(): + """ + Determines if the gunicorn.glogging package is available + + @return: Boolean + """ + package_check = False + + # Is the glogging package available? + try: + from gunicorn import glogging + except ImportError: + pass + else: + package_check = True + + return package_check def running_in_gunicorn(): """ - Determines if we are running inside of a gunicorn process and that the gunicorn logging package - is available. + Determines if we are running inside of a gunicorn process. @return: Boolean """ process_check = False - package_check = False try: # Is this a gunicorn process? @@ -60,25 +75,16 @@ def running_in_gunicorn(): if cmdline.find('gunicorn') >= 0: process_check = True - # Is the glogging package available? - try: - from gunicorn import glogging - except ImportError: - pass - else: - package_check = True - - # Both have to be true for gunicorn logging - return process_check and package_check - except Exception as e: - print("Instana.log.running_in_gunicorn: %s", e, file=sys.stderr) + return process_check + except Exception: + logger.debug("Instana.log.running_in_gunicorn: ", exc_info=True) return False aws_env = os.environ.get("AWS_EXECUTION_ENV", "") env_is_aws_lambda = "AWS_Lambda_" in aws_env -if running_in_gunicorn(): +if running_in_gunicorn() and glogging_available(): logger = logging.getLogger("gunicorn.error") elif env_is_aws_lambda is True: logger = get_aws_lambda_logger() diff --git a/instana/middleware.py b/instana/middleware.py new file mode 100644 index 00000000..7b2eeedb --- /dev/null +++ b/instana/middleware.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import + +from .instrumentation.wsgi import InstanaWSGIMiddleware +from .instrumentation.asgi import InstanaASGIMiddleware \ No newline at end of file diff --git a/instana/propagators/__init__.py b/instana/propagators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/instana/http_propagator.py b/instana/propagators/base_propagator.py similarity index 59% rename from instana/http_propagator.py rename to instana/propagators/base_propagator.py index 467286f4..caf92614 100644 --- a/instana/http_propagator.py +++ b/instana/propagators/base_propagator.py @@ -1,10 +1,13 @@ from __future__ import absolute_import -import opentracing as ot +import sys -from .log import logger -from .util import header_to_id -from .span_context import SpanContext +from ..log import logger +from ..util import header_to_id +from ..span_context import SpanContext + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 # The carrier can be a dict or a list. # Using the trace header as an example, it can be in the following forms @@ -19,12 +22,17 @@ # X-Instana-T -class HTTPPropagator(): - """A Propagator for Format.HTTP_HEADERS. """ +class BasePropagator(): + UC_HEADER_KEY_T = 'X-INSTANA-T' + UC_HEADER_KEY_S = 'X-INSTANA-S' + UC_HEADER_KEY_L = 'X-INSTANA-L' + UC_HEADER_KEY_SYNTHETIC = 'X-INSTANA-SYNTHETIC' HEADER_KEY_T = 'X-Instana-T' HEADER_KEY_S = 'X-Instana-S' HEADER_KEY_L = 'X-Instana-L' + HEADER_KEY_SYNTHETIC = 'X-Instana-Synthetic' + LC_HEADER_KEY_T = 'x-instana-t' LC_HEADER_KEY_S = 'x-instana-s' LC_HEADER_KEY_L = 'x-instana-l' @@ -38,49 +46,46 @@ class HTTPPropagator(): ALT_LC_HEADER_KEY_L = 'http_x_instana_l' ALT_LC_HEADER_KEY_SYNTHETIC = 'http_x_instana_synthetic' - def inject(self, span_context, carrier): - try: - trace_id = span_context.trace_id - span_id = span_context.span_id - - 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 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 hasattr(carrier, '__setitem__'): - carrier.__setitem__(self.HEADER_KEY_T, trace_id) - carrier.__setitem__(self.HEADER_KEY_S, span_id) - carrier.__setitem__(self.HEADER_KEY_L, "1") - else: - raise Exception("Unsupported carrier type", type(carrier)) - - except Exception: - logger.debug("inject error:", exc_info=True) + def extract(self, carrier): + """ + Search carrier for the *HEADER* keys and return a SpanContext or None + + Note: Extract is on the base class since it never really varies in task regardless + of the propagator in uses. - def extract(self, carrier): # noqa + :param carrier: The dict or list potentially containing context + :return: SpanContext or None + """ trace_id = None span_id = None level = 1 synthetic = False + dc = None try: - if isinstance(carrier, dict) or hasattr(carrier, "__getitem__"): - dc = carrier - elif hasattr(carrier, "__dict__"): - dc = carrier.__dict__ - elif isinstance(carrier, list): - dc = dict(carrier) - else: - raise ot.SpanContextCorruptedException() + # Attempt to convert incoming into a dict + try: + if isinstance(carrier, dict): + dc = carrier + elif hasattr(carrier, "__dict__"): + dc = carrier.__dict__ + else: + dc = dict(carrier) + except Exception: + logger.debug("extract: Couln't convert %s", carrier) + + if dc is None: + return None # Headers can exist in the standard X-Instana-T/S format or the alternate HTTP_X_INSTANA_T/S style # We do a case insensitive search to cover all possible variations of incoming headers. for key in dc.keys(): - lc_key = key.lower() + lc_key = None + + if PY3 is True and isinstance(key, bytes): + lc_key = key.decode("utf-8").lower() + else: + lc_key = key.lower() if self.LC_HEADER_KEY_T == lc_key: trace_id = header_to_id(dc[key]) @@ -89,7 +94,7 @@ def extract(self, carrier): # noqa elif self.LC_HEADER_KEY_L == lc_key: level = dc[key] elif self.LC_HEADER_KEY_SYNTHETIC == lc_key: - synthetic = dc[key] == "1" + synthetic = dc[key] in ['1', b'1'] elif self.ALT_LC_HEADER_KEY_T == lc_key: trace_id = header_to_id(dc[key]) @@ -98,7 +103,7 @@ def extract(self, carrier): # noqa elif self.ALT_LC_HEADER_KEY_L == lc_key: level = dc[key] elif self.ALT_LC_HEADER_KEY_SYNTHETIC == lc_key: - synthetic = dc[key] == "1" + synthetic = dc[key] in ['1', b'1'] ctx = None if trace_id is not None and span_id is not None: @@ -114,4 +119,4 @@ def extract(self, carrier): # noqa return ctx except Exception: - logger.debug("extract error:", exc_info=True) + logger.debug("extract error:", exc_info=True) \ No newline at end of file diff --git a/instana/binary_propagator.py b/instana/propagators/binary_propagator.py similarity index 50% rename from instana/binary_propagator.py rename to instana/propagators/binary_propagator.py index fbccfcb0..08981adf 100644 --- a/instana/binary_propagator.py +++ b/instana/propagators/binary_propagator.py @@ -1,83 +1,51 @@ from __future__ import absolute_import -import opentracing as ot +from ..log import logger +from .base_propagator import BasePropagator -from .log import logger -from .util import header_to_id -from .span_context import SpanContext - -class BinaryPropagator(): +class BinaryPropagator(BasePropagator): """ - A Propagator for TEXT_MAP. + A Propagator for BINARY. + The BINARY format represents SpanContexts in an opaque bytearray carrier. """ + + # ByteArray variations from base class HEADER_KEY_T = b'x-instana-t' HEADER_KEY_S = b'x-instana-s' HEADER_KEY_L = b'x-instana-l' + HEADER_SERVER_TIMING = b'server-timing' def inject(self, span_context, carrier): try: trace_id = str.encode(span_context.trace_id) span_id = str.encode(span_context.span_id) level = str.encode("1") + server_timing = str.encode("intid;desc=%s" % span_context.trace_id) 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 + carrier[self.HEADER_SERVER_TIMING] = server_timing 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)) + carrier.append((self.HEADER_SERVER_TIMING, server_timing)) 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),)) + carrier = carrier.__add__(((self.HEADER_SERVER_TIMING, server_timing),)) elif hasattr(carrier, '__setitem__'): carrier.__setitem__(self.HEADER_KEY_T, trace_id) carrier.__setitem__(self.HEADER_KEY_S, span_id) carrier.__setitem__(self.HEADER_KEY_L, level) + carrier.__setitem__(self.HEADER_SERVER_TIMING, server_timing) else: raise Exception("Unsupported carrier type", type(carrier)) return carrier except Exception: logger.debug("inject error:", exc_info=True) - - def extract(self, carrier): # noqa - trace_id = None - span_id = None - level = None - - try: - if isinstance(carrier, dict) or hasattr(carrier, "__getitem__"): - dc = carrier - elif hasattr(carrier, "__dict__"): - dc = carrier.__dict__ - elif isinstance(carrier, list): - dc = dict(carrier) - else: - raise ot.SpanContextCorruptedException() - - for key, value in dc.items(): - if isinstance(key, str): - key = str.encode(key) - - if self.HEADER_KEY_T == key: - trace_id = header_to_id(value) - elif self.HEADER_KEY_S == key: - span_id = header_to_id(value) - elif self.HEADER_KEY_L == key: - level = value - - ctx = None - if trace_id is not None and span_id is not None: - ctx = SpanContext(span_id=span_id, - trace_id=trace_id, - level=level, - baggage={}, - sampled=True) - return ctx - - except Exception: - logger.debug("extract error:", exc_info=True) diff --git a/instana/propagators/http_propagator.py b/instana/propagators/http_propagator.py new file mode 100644 index 00000000..944a642e --- /dev/null +++ b/instana/propagators/http_propagator.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import + +import sys + +from ..log import logger +from .base_propagator import BasePropagator + +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +class HTTPPropagator(BasePropagator): + """ + Instana Propagator for Format.HTTP_HEADERS. + + The HTTP_HEADERS format deals with key-values with string to string mapping. + The character set should be restricted to HTTP compatible. + """ + def inject(self, span_context, carrier): + try: + trace_id = span_context.trace_id + span_id = span_context.span_id + + 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 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 hasattr(carrier, '__setitem__'): + carrier.__setitem__(self.HEADER_KEY_T, trace_id) + carrier.__setitem__(self.HEADER_KEY_S, span_id) + carrier.__setitem__(self.HEADER_KEY_L, "1") + else: + raise Exception("Unsupported carrier type", type(carrier)) + + except Exception: + logger.debug("inject error:", exc_info=True) diff --git a/instana/propagators/text_propagator.py b/instana/propagators/text_propagator.py new file mode 100644 index 00000000..f872a0b6 --- /dev/null +++ b/instana/propagators/text_propagator.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import + +from ..log import logger +from .base_propagator import BasePropagator + + +class TextPropagator(BasePropagator): + """ + Instana context propagator for TEXT_MAP. + + The TEXT_MAP deals with key-values with string to string mapping. + The character set is unrestricted. + """ + + def inject(self, span_context, carrier): + try: + trace_id = span_context.trace_id + span_id = span_context.span_id + + if isinstance(carrier, dict) or hasattr(carrier, "__dict__"): + carrier[self.UC_HEADER_KEY_T] = trace_id + carrier[self.UC_HEADER_KEY_S] = span_id + carrier[self.UC_HEADER_KEY_L] = "1" + elif isinstance(carrier, list): + carrier.append((self.UC_HEADER_KEY_T, trace_id)) + carrier.append((self.UC_HEADER_KEY_S, span_id)) + carrier.append((self.UC_HEADER_KEY_L, "1")) + elif isinstance(carrier, tuple): + carrier = carrier.__add__(((self.UC_HEADER_KEY_T, trace_id),)) + carrier = carrier.__add__(((self.UC_HEADER_KEY_S, span_id),)) + carrier = carrier.__add__(((self.UC_HEADER_KEY_L, "1"),)) + elif hasattr(carrier, '__setitem__'): + carrier.__setitem__(self.UC_HEADER_KEY_T, trace_id) + carrier.__setitem__(self.UC_HEADER_KEY_S, span_id) + carrier.__setitem__(self.UC_HEADER_KEY_L, "1") + else: + raise Exception("Unsupported carrier type", type(carrier)) + + return carrier + except Exception: + logger.debug("inject error:", exc_info=True) diff --git a/instana/recorder.py b/instana/recorder.py index 70f366bb..c2fbd666 100644 --- a/instana/recorder.py +++ b/instana/recorder.py @@ -17,7 +17,7 @@ class StanRecorder(object): THREAD_NAME = "Instana Span Reporting" - REGISTERED_SPANS = ("aiohttp-client", "aiohttp-server", "aws.lambda.entry", "boto3", "cassandra", + REGISTERED_SPANS = ("aiohttp-client", "aiohttp-server", "asgi", "aws.lambda.entry", "boto3", "cassandra", "celery-client", "celery-worker", "couchbase", "django", "gcs", "log", "memcache", "mongo", "mysql", "postgres", "pymongo", "rabbitmq", "redis", "render", "rpc-client", "rpc-server", "sqlalchemy", "soap", "tornado-client", @@ -43,6 +43,15 @@ def queued_spans(self): """ Get all of the spans in the queue """ span = None spans = [] + + import time + from .singletons import env_is_test + if env_is_test is True: + time.sleep(1) + + if self.agent.collector.span_queue.empty() is True: + return spans + while True: try: span = self.agent.collector.span_queue.get(False) @@ -54,7 +63,8 @@ def queued_spans(self): def clear_spans(self): """ Clear the queue of spans """ - self.queued_spans() + if self.agent.collector.span_queue.empty() == False: + self.queued_spans() def record_span(self, span): """ diff --git a/instana/span.py b/instana/span.py index 80a82322..2baa94c0 100644 --- a/instana/span.py +++ b/instana/span.py @@ -227,14 +227,14 @@ def get_span_kind(self, span): class RegisteredSpan(BaseSpan): - HTTP_SPANS = ("aiohttp-client", "aiohttp-server", "django", "http", "soap", "tornado-client", + HTTP_SPANS = ("aiohttp-client", "aiohttp-server", "asgi", "django", "http", "soap", "tornado-client", "tornado-server", "urllib3", "wsgi") EXIT_SPANS = ("aiohttp-client", "boto3", "cassandra", "celery-client", "couchbase", "log", "memcache", "mongo", "mysql", "postgres", "rabbitmq", "redis", "rpc-client", "sqlalchemy", "soap", "tornado-client", "urllib3", "pymongo", "gcs") - ENTRY_SPANS = ("aiohttp-server", "aws.lambda.entry", "celery-worker", "django", "wsgi", "rabbitmq", + ENTRY_SPANS = ("aiohttp-server", "asgi", "aws.lambda.entry", "celery-worker", "django", "wsgi", "rabbitmq", "rpc-server", "tornado-server") LOCAL_SPANS = ("render") diff --git a/instana/text_propagator.py b/instana/text_propagator.py deleted file mode 100644 index 7da9e495..00000000 --- a/instana/text_propagator.py +++ /dev/null @@ -1,79 +0,0 @@ -from __future__ import absolute_import - -import opentracing as ot - -from .log import logger -from .util import header_to_id -from .span_context import SpanContext - - -class TextPropagator(): - """ - A Propagator for TEXT_MAP. - """ - HEADER_KEY_T = 'X-INSTANA-T' - HEADER_KEY_S = 'X-INSTANA-S' - HEADER_KEY_L = 'X-INSTANA-L' - - def inject(self, span_context, carrier): - try: - trace_id = span_context.trace_id - span_id = span_context.span_id - - 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 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 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"),)) - elif hasattr(carrier, '__setitem__'): - carrier.__setitem__(self.HEADER_KEY_T, trace_id) - carrier.__setitem__(self.HEADER_KEY_S, span_id) - carrier.__setitem__(self.HEADER_KEY_L, "1") - else: - raise Exception("Unsupported carrier type", type(carrier)) - - return carrier - except Exception: - logger.debug("inject error:", exc_info=True) - - def extract(self, carrier): # noqa - trace_id = None - span_id = None - level = 1 - - try: - if isinstance(carrier, dict) or hasattr(carrier, "__getitem__"): - dc = carrier - elif hasattr(carrier, "__dict__"): - dc = carrier.__dict__ - elif isinstance(carrier, list): - dc = dict(carrier) - else: - raise ot.SpanContextCorruptedException() - - for key in dc.keys(): - if self.HEADER_KEY_T == key: - trace_id = header_to_id(dc[key]) - elif self.HEADER_KEY_S == key: - span_id = header_to_id(dc[key]) - elif self.HEADER_KEY_L == key: - level = dc[key] - - ctx = None - if trace_id is not None and span_id is not None: - ctx = SpanContext(span_id=span_id, - trace_id=trace_id, - level=level, - baggage={}, - sampled=True) - return ctx - - except Exception: - logger.debug("extract error:", exc_info=True) diff --git a/instana/tracer.py b/instana/tracer.py index 68ffe0fa..3a04950a 100644 --- a/instana/tracer.py +++ b/instana/tracer.py @@ -10,11 +10,11 @@ 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 .propagators.http_propagator import HTTPPropagator +from .propagators.text_propagator import TextPropagator +from .propagators.binary_propagator import BinaryPropagator class InstanaTracer(BasicTracer): diff --git a/instana/util.py b/instana/util.py index 658354e7..1b7eda12 100644 --- a/instana/util.py +++ b/instana/util.py @@ -21,13 +21,19 @@ else: string_types = str +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + _rnd = random.Random() _current_pid = 0 BAD_ID = "BADCAFFE" # Bad Caffe +def nested_dictionary(): + return defaultdict(DictionaryOfStan) + # Simple implementation of a nested dictionary. -DictionaryOfStan = lambda: defaultdict(DictionaryOfStan) +DictionaryOfStan = nested_dictionary def generate_id(): @@ -49,12 +55,15 @@ def generate_id(): def header_to_id(header): """ We can receive headers in the following formats: - 1. unsigned base 16 hex string of variable length + 1. unsigned base 16 hex string (or bytes) of variable length 2. [eventual] :param header: the header to analyze, validate and convert (if needed) :return: a valid ID to be used internal to the tracer """ + if PY3 is True and isinstance(header, bytes): + header = header.decode('utf-8') + if not isinstance(header, string_types): return BAD_ID @@ -450,8 +459,8 @@ def determine_service_name(): pass except Exception: logger.debug("non-fatal get_application_name: ", exc_info=True) - finally: - return app_name + + return app_name def normalize_aws_lambda_arn(context): @@ -476,7 +485,7 @@ def normalize_aws_lambda_arn(context): logger.debug("Unexpected ARN parse issue: %s", arn) return arn - except: + except Exception: logger.debug("normalize_arn: ", exc_info=True) @@ -495,5 +504,38 @@ def validate_url(url): try: result = parse.urlparse(url) return all([result.scheme, result.netloc]) - except: + except Exception: + pass + + return False + + +def running_in_gunicorn(): + """ + Determines if we are running inside of a gunicorn process. + + @return: Boolean + """ + process_check = False + + try: + # Is this a gunicorn process? + if hasattr(sys, 'argv'): + for arg in sys.argv: + if arg.find('gunicorn') >= 0: + process_check = True + elif os.path.isfile("/proc/self/cmdline"): + with open("/proc/self/cmdline") as cmd: + contents = cmd.read() + + parts = contents.split('\0') + parts.pop() + cmdline = " ".join(parts) + + if cmdline.find('gunicorn') >= 0: + process_check = True + + return process_check + except Exception: + logger.debug("Instana.log.running_in_gunicorn: ", exc_info=True) return False diff --git a/instana/wsgi.py b/instana/wsgi.py index 77dfa8b9..318863fb 100644 --- a/instana/wsgi.py +++ b/instana/wsgi.py @@ -1,61 +1,6 @@ from __future__ import absolute_import -import opentracing as ot -import opentracing.ext.tags as tags +from .instrumentation.wsgi import InstanaWSGIMiddleware -from .singletons import agent, tracer -from .util import strip_secrets_from_query - - -class iWSGIMiddleware(object): - """ Instana WSGI middleware """ - - def __init__(self, app): - self.app = app - self - - def __call__(self, environ, start_response): - env = environ - - def new_start_response(status, headers, exc_info=None): - """Modified start response with additional headers.""" - tracer.inject(self.scope.span.context, ot.Format.HTTP_HEADERS, headers) - headers.append(('Server-Timing', "intid;desc=%s" % self.scope.span.context.trace_id)) - - res = start_response(status, headers, exc_info) - - sc = status.split(' ')[0] - if 500 <= int(sc) <= 511: - self.scope.span.mark_as_errored() - - self.scope.span.set_tag(tags.HTTP_STATUS_CODE, sc) - self.scope.close() - return res - - ctx = tracer.extract(ot.Format.HTTP_HEADERS, env) - self.scope = tracer.start_active_span("wsgi", child_of=ctx) - - if agent.options.extra_http_headers is not None: - for custom_header in agent.options.extra_http_headers: - # Headers are available in this format: HTTP_X_CAPTURE_THIS - wsgi_header = ('HTTP_' + custom_header.upper()).replace('-', '_') - if wsgi_header in env: - self.scope.span.set_tag("http.%s" % custom_header, env[wsgi_header]) - - if 'PATH_INFO' in env: - self.scope.span.set_tag('http.path', env['PATH_INFO']) - if 'QUERY_STRING' in env and len(env['QUERY_STRING']): - scrubbed_params = strip_secrets_from_query(env['QUERY_STRING'], agent.options.secrets_matcher, agent.options.secrets_list) - self.scope.span.set_tag("http.params", scrubbed_params) - if 'REQUEST_METHOD' in env: - self.scope.span.set_tag(tags.HTTP_METHOD, env['REQUEST_METHOD']) - if 'HTTP_HOST' in env: - self.scope.span.set_tag("http.host", env['HTTP_HOST']) - - return self.app(environ, new_start_response) - - -def make_middleware(app=None, *args, **kw): - """ Given an app, return that app wrapped in iWSGIMiddleware """ - app = iWSGIMiddleware(app, *args, **kw) - return app +# Alias for historical name +iWSGIMiddleware = InstanaWSGIMiddleware diff --git a/setup.py b/setup.py index ab8ebc05..4565aa5f 100644 --- a/setup.py +++ b/setup.py @@ -56,10 +56,10 @@ def check_setuptools(): long_description_content_type='text/markdown', zip_safe=False, install_requires=['autowrapt>=1.0', - 'basictracer>=3.0.0', + 'basictracer>=3.1.0', 'certifi>=2018.4.16', 'fysom>=2.1.2', - 'opentracing>=2.0.0', + 'opentracing>=2.3.0', 'requests>=2.8.0', 'six>=1.12.0', 'urllib3>=1.18.1'], @@ -91,11 +91,13 @@ def check_setuptools(): 'couchbase==2.5.9', ], 'test': [ + 'aiofiles>=0.5.0;python_version>="3.5"', 'aiohttp>=3.5.4;python_version>="3.5"', 'asynqp>=0.4;python_version>="3.5"', 'boto3>=1.10.0', 'celery>=4.1.1', 'django>=1.11,<2.2', + 'fastapi>=0.61.1;python_version>="3.6"', 'flask>=0.12.2', 'grpcio>=1.18.0', 'google-cloud-storage>=1.24.0;python_version>="3.5"', @@ -118,6 +120,7 @@ def check_setuptools(): 'spyne>=2.9,<=2.12.14', 'suds-jurko>=0.6', 'tornado>=4.5.3,<6.0', + 'uvicorn>=0.12.2;python_version>="3.6"', 'urllib3[secure]>=1.15' ], }, diff --git a/tests/apps/__init__.py b/tests/apps/__init__.py index 325625d1..e69de29b 100644 --- a/tests/apps/__init__.py +++ b/tests/apps/__init__.py @@ -1,21 +0,0 @@ -import os -import sys -import time -import threading - -if 'GEVENT_TEST' not in os.environ and 'CASSANDRA_TEST' not in os.environ: - if sys.version_info >= (3, 5, 3): - # Background RPC application - # - # Spawn the background RPC app that the tests will throw - # requests at. - import tests.apps.grpc_server - from .grpc_server.stan_server import StanServicer - stan_servicer = StanServicer() - rpc_server_thread = threading.Thread(target=stan_servicer.start_server) - rpc_server_thread.daemon = True - rpc_server_thread.name = "Background RPC app" - print("Starting background RPC app...") - rpc_server_thread.start() - -time.sleep(1) diff --git a/tests/apps/fastapi_app/README.md b/tests/apps/fastapi_app/README.md new file mode 100644 index 00000000..de3f58a6 --- /dev/null +++ b/tests/apps/fastapi_app/README.md @@ -0,0 +1,12 @@ +To launch manually from an iPython console: + +```python +from tests.apps.fastapi_app import launch_fastapi +launch_fastapi() +``` + +Then you can launch requests: + +```bash +curl -i localhost:10816/ +``` diff --git a/tests/apps/fastapi_app/__init__.py b/tests/apps/fastapi_app/__init__.py new file mode 100644 index 00000000..bc3dd79d --- /dev/null +++ b/tests/apps/fastapi_app/__init__.py @@ -0,0 +1,15 @@ +import uvicorn +from ...helpers import testenv +from instana.log import logger + +testenv["fastapi_port"] = 10816 +testenv["fastapi_server"] = ("http://127.0.0.1:" + str(testenv["fastapi_port"])) + +def launch_fastapi(): + from .app import fastapi_server + from instana.singletons import agent + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] + + uvicorn.run(fastapi_server, host='127.0.0.1', port=testenv['fastapi_port'], log_level="critical") diff --git a/tests/apps/fastapi_app/app.py b/tests/apps/fastapi_app/app.py new file mode 100644 index 00000000..ef63a707 --- /dev/null +++ b/tests/apps/fastapi_app/app.py @@ -0,0 +1,38 @@ +from fastapi import FastAPI, HTTPException +from fastapi.exceptions import RequestValidationError +from fastapi.responses import PlainTextResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +fastapi_server = FastAPI() + +# @fastapi_server.exception_handler(StarletteHTTPException) +# async def http_exception_handler(request, exc): +# return PlainTextResponse(str(exc.detail), status_code=exc.status_code) + +# @fastapi_server.exception_handler(RequestValidationError) +# async def validation_exception_handler(request, exc): +# return PlainTextResponse(str(exc), status_code=400) + +@fastapi_server.get("/") +async def root(): + return {"message": "Hello World"} + +@fastapi_server.get("/users/{user_id}") +async def user(user_id): + return {"user": user_id} + +@fastapi_server.get("/400") +async def four_zero_zero(): + raise HTTPException(status_code=400, detail="400 response") + +@fastapi_server.get("/404") +async def four_zero_four(): + raise HTTPException(status_code=404, detail="Item not found") + +@fastapi_server.get("/500") +async def five_hundred(): + raise HTTPException(status_code=500, detail="500 response") + +@fastapi_server.get("/starlette_exception") +async def starlette_exception(): + raise StarletteHTTPException(status_code=500, detail="500 response") \ No newline at end of file diff --git a/tests/apps/flask_app/app.py b/tests/apps/flask_app/app.py index 24a47dfe..d3b7cef6 100644 --- a/tests/apps/flask_app/app.py +++ b/tests/apps/flask_app/app.py @@ -18,7 +18,7 @@ from ...helpers import testenv from instana.singletons import tracer -logging.basicConfig(level=logging.INFO) +logging.basicConfig(level=logging.WARNING) logger = logging.getLogger(__name__) testenv["wsgi_port"] = 10811 diff --git a/tests/apps/grpc_server/__init__.py b/tests/apps/grpc_server/__init__.py index 779dcbfa..736ebf98 100644 --- a/tests/apps/grpc_server/__init__.py +++ b/tests/apps/grpc_server/__init__.py @@ -1 +1,19 @@ -# __all__ = ["digestor_pb2", "digestor_pb2_grpc"] \ No newline at end of file +import os +import sys +import time +import threading + +if 'GEVENT_TEST' not in os.environ and 'CASSANDRA_TEST' not in os.environ and sys.version_info >= (3, 5, 3): + # Background RPC application + # + # Spawn the background RPC app that the tests will throw + # requests at. + import tests.apps.grpc_server + from .stan_server import StanServicer + stan_servicer = StanServicer() + rpc_server_thread = threading.Thread(target=stan_servicer.start_server) + rpc_server_thread.daemon = True + rpc_server_thread.name = "Background RPC app" + print("Starting background RPC app...") + rpc_server_thread.start() + time.sleep(1) \ No newline at end of file diff --git a/tests/apps/starlette_app/__init__.py b/tests/apps/starlette_app/__init__.py new file mode 100644 index 00000000..9a1359d8 --- /dev/null +++ b/tests/apps/starlette_app/__init__.py @@ -0,0 +1,15 @@ +import uvicorn +from ...helpers import testenv +from instana.log import logger + +testenv["starlette_port"] = 10817 +testenv["starlette_server"] = ("http://127.0.0.1:" + str(testenv["starlette_port"])) + +def launch_starlette(): + from .app import starlette_server + from instana.singletons import agent + + # Hack together a manual custom headers list; We'll use this in tests + agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] + + uvicorn.run(starlette_server, host='127.0.0.1', port=testenv['starlette_port'], log_level="critical") diff --git a/tests/apps/starlette_app/app.py b/tests/apps/starlette_app/app.py new file mode 100644 index 00000000..44aef731 --- /dev/null +++ b/tests/apps/starlette_app/app.py @@ -0,0 +1,32 @@ +from starlette.applications import Starlette +from starlette.responses import PlainTextResponse +from starlette.routing import Route, Mount, WebSocketRoute +from starlette.staticfiles import StaticFiles + +import os +dir_path = os.path.dirname(os.path.realpath(__file__)) + +def homepage(request): + return PlainTextResponse('Hello, world!') + +def user(request): + user_id = request.path_params['user_id'] + return PlainTextResponse('Hello, user id %s!' % user_id) + +async def websocket_endpoint(websocket): + await websocket.accept() + await websocket.send_text('Hello, websocket!') + await websocket.close() + +def startup(): + print('Ready to go') + + +routes = [ + Route('/', homepage), + Route('/users/{user_id}', user), + WebSocketRoute('/ws', websocket_endpoint), + Mount('/static', StaticFiles(directory=dir_path + "/static")), +] + +starlette_server = Starlette(debug=True, routes=routes, on_startup=[startup]) \ No newline at end of file diff --git a/tests/apps/starlette_app/static/stan.png b/tests/apps/starlette_app/static/stan.png new file mode 100644 index 00000000..53890285 Binary files /dev/null and b/tests/apps/starlette_app/static/stan.png differ diff --git a/tests/apps/utils.py b/tests/apps/utils.py index 774f1482..d62b6c5e 100644 --- a/tests/apps/utils.py +++ b/tests/apps/utils.py @@ -1,10 +1,12 @@ import threading -def launch_background_thread(app, name): - app_thread = threading.Thread(target=app) +def launch_background_thread(app, app_name, fun_args=(), fun_kwargs={}): + print("Starting background %s app..." % app_name) + app_thread = threading.Thread(target=app, + name=app_name, + args=fun_args, + kwargs=fun_kwargs) app_thread.daemon = True - app_thread.name = "Background %s app" % name - print("Starting background %s app..." % name) app_thread.start() return app_thread diff --git a/tests/conftest.py b/tests/conftest.py index d00ba39d..c8f4343d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,9 +25,19 @@ collect_ignore_glob.append("*test_grpc*") collect_ignore_glob.append("*test_boto3*") +if LooseVersion(sys.version) < LooseVersion('3.6.0'): + collect_ignore_glob.append("*test_fastapi*") + collect_ignore_glob.append("*test_starlette*") + if LooseVersion(sys.version) >= LooseVersion('3.7.0'): collect_ignore_glob.append("*test_sudsjurko*") +# Set our testing flags +os.environ["INSTANA_TEST"] = "true" +# os.environ["INSTANA_DEBUG"] = "true" + +# Make sure the instana package is fully loaded +import instana @pytest.fixture(scope='session') def celery_config(): diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py new file mode 100644 index 00000000..e4d99204 --- /dev/null +++ b/tests/frameworks/test_aiohttp_client.py @@ -0,0 +1,443 @@ +from __future__ import absolute_import + +import aiohttp +import asyncio +import unittest + +from instana.singletons import async_tracer, agent + +import tests.apps.flask_app +import tests.apps.aiohttp_app +from ..helpers import testenv + + +class TestAiohttp(unittest.TestCase): + + async def fetch(self, session, url, headers=None): + try: + async with session.get(url, headers=headers) as response: + return response + except aiohttp.web_exceptions.HTTPException: + pass + + def setUp(self): + """ Clear all spans before a test run """ + self.recorder = async_tracer.recorder + self.recorder.clear_spans() + + # New event loop for every test + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + + def tearDown(self): + pass + + def test_client_get(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertIsNone(aiohttp_span.ec) + self.assertIsNone(wsgi_span.ec) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(200, aiohttp_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + def test_client_get_301(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/301") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(4, len(spans)) + + wsgi_span1 = spans[0] + wsgi_span2 = spans[1] + aiohttp_span = spans[2] + test_span = spans[3] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span1.t) + self.assertEqual(traceId, wsgi_span2.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span1.p, aiohttp_span.s) + self.assertEqual(wsgi_span2.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertIsNone(aiohttp_span.ec) + self.assertIsNone(wsgi_span1.ec) + self.assertIsNone(wsgi_span2.ec) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(200, aiohttp_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/301", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span2.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + def test_client_get_405(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/405") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertIsNone(aiohttp_span.ec) + self.assertIsNone(wsgi_span.ec) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(405, aiohttp_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/405", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + def test_client_get_500(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/500") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertEqual(aiohttp_span.ec, 1) + self.assertEqual(wsgi_span.ec, 1) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(500, aiohttp_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/500", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertEqual('INTERNAL SERVER ERROR', + aiohttp_span.data["http"]["error"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + def test_client_get_504(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/504") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertEqual(aiohttp_span.ec, 1) + self.assertEqual(wsgi_span.ec, 1) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(504, aiohttp_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/504", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertEqual('GATEWAY TIMEOUT', aiohttp_span.data["http"]["error"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + def test_client_get_with_params_to_scrub(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/?secret=yeah") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertIsNone(aiohttp_span.ec) + self.assertIsNone(wsgi_span.ec) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(200, aiohttp_span.data["http"]["status"]) + self.assertEqual(testenv["wsgi_server"] + "/", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertEqual("secret=", + aiohttp_span.data["http"]["params"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + def test_client_response_header_capture(self): + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ['X-Capture-This'] + + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, testenv["wsgi_server"] + "/response_headers") + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + self.assertEqual(3, len(spans)) + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + self.assertEqual(traceId, wsgi_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + self.assertEqual(wsgi_span.p, aiohttp_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertIsNone(aiohttp_span.ec) + self.assertIsNone(wsgi_span.ec) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertEqual(200, aiohttp_span.data["http"]["status"]) + self.assertEqual( + testenv["wsgi_server"] + "/response_headers", aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + self.assertTrue( + 'http.X-Capture-This' in aiohttp_span.data["custom"]["tags"]) + + assert "X-Instana-T" in response.headers + self.assertEqual(response.headers["X-Instana-T"], traceId) + assert "X-Instana-S" in response.headers + self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) + assert "X-Instana-L" in response.headers + self.assertEqual(response.headers["X-Instana-L"], '1') + assert "Server-Timing" in response.headers + self.assertEqual( + response.headers["Server-Timing"], "intid;desc=%s" % traceId) + + agent.options.extra_http_headers = original_extra_http_headers + + def test_client_error(self): + async def test(): + with async_tracer.start_active_span('test'): + async with aiohttp.ClientSession() as session: + return await self.fetch(session, 'http://doesnotexist:10/') + + response = None + try: + response = self.loop.run_until_complete(test()) + except: + pass + + spans = self.recorder.queued_spans() + self.assertEqual(2, len(spans)) + + aiohttp_span = spans[0] + test_span = spans[1] + + self.assertIsNone(async_tracer.active_span) + + # Same traceId + traceId = test_span.t + self.assertEqual(traceId, aiohttp_span.t) + + # Parent relationships + self.assertEqual(aiohttp_span.p, test_span.s) + + # Error logging + self.assertIsNone(test_span.ec) + self.assertEqual(aiohttp_span.ec, 1) + + self.assertEqual("aiohttp-client", aiohttp_span.n) + self.assertIsNone(aiohttp_span.data["http"]["status"]) + self.assertEqual("http://doesnotexist:10/", + aiohttp_span.data["http"]["url"]) + self.assertEqual("GET", aiohttp_span.data["http"]["method"]) + self.assertIsNotNone(aiohttp_span.data["http"]["error"]) + assert(len(aiohttp_span.data["http"]["error"])) + self.assertIsNotNone(aiohttp_span.stack) + self.assertTrue(type(aiohttp_span.stack) is list) + self.assertTrue(len(aiohttp_span.stack) > 1) + + self.assertIsNone(response) diff --git a/tests/frameworks/test_aiohttp.py b/tests/frameworks/test_aiohttp_server.py similarity index 55% rename from tests/frameworks/test_aiohttp.py rename to tests/frameworks/test_aiohttp_server.py index bfc26808..3f636960 100644 --- a/tests/frameworks/test_aiohttp.py +++ b/tests/frameworks/test_aiohttp_server.py @@ -4,14 +4,13 @@ import asyncio import unittest -from instana.singletons import async_tracer, agent - -import tests.apps.flask_app import tests.apps.aiohttp_app from ..helpers import testenv +from instana.singletons import async_tracer, agent + -class TestAiohttp(unittest.TestCase): +class TestAiohttpServer(unittest.TestCase): async def fetch(self, session, url, headers=None): try: @@ -32,416 +31,6 @@ def setUp(self): def tearDown(self): pass - def test_client_get(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - wsgi_span = spans[0] - aiohttp_span = spans[1] - test_span = spans[2] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_301(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/301") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(4, len(spans)) - - wsgi_span1 = spans[0] - wsgi_span2 = spans[1] - aiohttp_span = spans[2] - test_span = spans[3] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span1.t) - self.assertEqual(traceId, wsgi_span2.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span1.p, aiohttp_span.s) - self.assertEqual(wsgi_span2.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span1.ec) - self.assertIsNone(wsgi_span2.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/301", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span2.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_405(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/405") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - wsgi_span = spans[0] - aiohttp_span = spans[1] - test_span = spans[2] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(405, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/405", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_500(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/500") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - wsgi_span = spans[0] - aiohttp_span = spans[1] - test_span = spans[2] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(wsgi_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(500, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/500", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual('INTERNAL SERVER ERROR', - aiohttp_span.data["http"]["error"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_504(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/504") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - wsgi_span = spans[0] - aiohttp_span = spans[1] - test_span = spans[2] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - self.assertEqual(wsgi_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(504, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/504", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual('GATEWAY TIMEOUT', aiohttp_span.data["http"]["error"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_get_with_params_to_scrub(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/?secret=yeah") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - wsgi_span = spans[0] - aiohttp_span = spans[1] - test_span = spans[2] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual(testenv["wsgi_server"] + "/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertEqual("secret=", - aiohttp_span.data["http"]["params"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - def test_client_response_header_capture(self): - original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = ['X-Capture-This'] - - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, testenv["wsgi_server"] + "/response_headers") - - response = self.loop.run_until_complete(test()) - - spans = self.recorder.queued_spans() - self.assertEqual(3, len(spans)) - - wsgi_span = spans[0] - aiohttp_span = spans[1] - test_span = spans[2] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - self.assertEqual(traceId, wsgi_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - self.assertEqual(wsgi_span.p, aiohttp_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertIsNone(aiohttp_span.ec) - self.assertIsNone(wsgi_span.ec) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertEqual(200, aiohttp_span.data["http"]["status"]) - self.assertEqual( - testenv["wsgi_server"] + "/response_headers", aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - self.assertTrue( - 'http.X-Capture-This' in aiohttp_span.data["custom"]["tags"]) - - assert "X-Instana-T" in response.headers - self.assertEqual(response.headers["X-Instana-T"], traceId) - assert "X-Instana-S" in response.headers - self.assertEqual(response.headers["X-Instana-S"], wsgi_span.s) - assert "X-Instana-L" in response.headers - self.assertEqual(response.headers["X-Instana-L"], '1') - assert "Server-Timing" in response.headers - self.assertEqual( - response.headers["Server-Timing"], "intid;desc=%s" % traceId) - - agent.options.extra_http_headers = original_extra_http_headers - - def test_client_error(self): - async def test(): - with async_tracer.start_active_span('test'): - async with aiohttp.ClientSession() as session: - return await self.fetch(session, 'http://doesnotexist:10/') - - response = None - try: - response = self.loop.run_until_complete(test()) - except: - pass - - spans = self.recorder.queued_spans() - self.assertEqual(2, len(spans)) - - aiohttp_span = spans[0] - test_span = spans[1] - - self.assertIsNone(async_tracer.active_span) - - # Same traceId - traceId = test_span.t - self.assertEqual(traceId, aiohttp_span.t) - - # Parent relationships - self.assertEqual(aiohttp_span.p, test_span.s) - - # Error logging - self.assertIsNone(test_span.ec) - self.assertEqual(aiohttp_span.ec, 1) - - self.assertEqual("aiohttp-client", aiohttp_span.n) - self.assertIsNone(aiohttp_span.data["http"]["status"]) - self.assertEqual("http://doesnotexist:10/", - aiohttp_span.data["http"]["url"]) - self.assertEqual("GET", aiohttp_span.data["http"]["method"]) - self.assertIsNotNone(aiohttp_span.data["http"]["error"]) - assert(len(aiohttp_span.data["http"]["error"])) - self.assertIsNotNone(aiohttp_span.stack) - self.assertTrue(type(aiohttp_span.stack) is list) - self.assertTrue(len(aiohttp_span.stack) > 1) - - self.assertIsNone(response) - def test_server_get(self): async def test(): with async_tracer.start_active_span('test'): diff --git a/tests/frameworks/test_fastapi.py b/tests/frameworks/test_fastapi.py new file mode 100644 index 00000000..8d4163d7 --- /dev/null +++ b/tests/frameworks/test_fastapi.py @@ -0,0 +1,370 @@ +from __future__ import absolute_import + +import time +import pytest +import requests +import multiprocessing +from instana.singletons import tracer +from ..helpers import testenv +from ..helpers import get_first_span_by_filter + +@pytest.fixture(scope="module") +def server(): + from tests.apps.fastapi_app import launch_fastapi + proc = multiprocessing.Process(target=launch_fastapi, args=(), daemon=True) + proc.start() + time.sleep(2) + yield + proc.kill() # Kill server after tests + +def test_vanilla_get(server): + result = requests.get(testenv["fastapi_server"] + '/') + + assert result.status_code is 200 + assert "X-Instana-T" in result.headers + assert "X-Instana-S" in result.headers + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + + spans = tracer.recorder.queued_spans() + # FastAPI instrumentation (like all instrumentation) _always_ traces unless told otherwise + assert len(spans) == 1 + assert spans[0].n == 'asgi' + + +def test_basic_get(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/') + + assert result.status_code == 200 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + +def test_400(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/400') + + assert result.status_code == 400 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/400') + assert(asgi_span.data['http']['path_tpl'] == '/400') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 400) + assert(asgi_span.data['http']['error'] == None) + +def test_500(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/500') + + assert result.status_code == 500 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == 1) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/500') + assert(asgi_span.data['http']['path_tpl'] == '/500') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 500) + assert(asgi_span.data['http']['error'] == None) + + +def test_path_templates(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/users/1') + + assert result.status_code == 200 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/users/1') + assert(asgi_span.data['http']['path_tpl'] == '/users/{user_id}') + assert(asgi_span.data['http']['params'] == None) + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + +def test_secret_scrubbing(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/?secret=shhh') + + assert result.status_code == 200 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['params'] == 'secret=') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + +def test_synthetic_request(server): + request_headers = { + 'X-Instana-Synthetic': '1' + } + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/', headers=request_headers) + + assert result.status_code == 200 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + + assert(asgi_span.sy) + assert(urllib3_span.sy is None) + assert(test_span.sy is None) + +def test_custom_header_capture(server): + from instana.singletons import agent + + # The background FastAPI server is pre-configured with custom headers to capture + + request_headers = { + 'X-Capture-This': 'this', + 'X-Capture-That': 'that' + } + with tracer.start_active_span('test'): + result = requests.get(testenv["fastapi_server"] + '/', headers=request_headers) + + assert result.status_code == 200 + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + + assert("http.X-Capture-This" in asgi_span.data["custom"]['tags']) + assert("this" == asgi_span.data["custom"]['tags']["http.X-Capture-This"]) + assert("http.X-Capture-That" in asgi_span.data["custom"]['tags']) + assert("that" == asgi_span.data["custom"]['tags']["http.X-Capture-That"]) diff --git a/tests/frameworks/test_grpcio.py b/tests/frameworks/test_grpcio.py index c0c9e4d6..373edf18 100644 --- a/tests/frameworks/test_grpcio.py +++ b/tests/frameworks/test_grpcio.py @@ -6,6 +6,7 @@ import grpc +import tests.apps.grpc_server import tests.apps.grpc_server.stan_pb2 as stan_pb2 import tests.apps.grpc_server.stan_pb2_grpc as stan_pb2_grpc diff --git a/tests/frameworks/test_starlette.py b/tests/frameworks/test_starlette.py new file mode 100644 index 00000000..991de2a7 --- /dev/null +++ b/tests/frameworks/test_starlette.py @@ -0,0 +1,277 @@ +from __future__ import absolute_import + +import time +import pytest +import requests +import multiprocessing +from ..helpers import testenv +from instana.singletons import tracer +from ..helpers import get_first_span_by_filter + +@pytest.fixture(scope="module") +def server(): + from tests.apps.starlette_app import launch_starlette + proc = multiprocessing.Process(target=launch_starlette, args=(), daemon=True) + proc.start() + time.sleep(2) + yield + proc.kill() # Kill server after tests + +def test_vanilla_get(server): + result = requests.get(testenv["starlette_server"] + '/') + assert(result) + spans = tracer.recorder.queued_spans() + # Starlette instrumentation (like all instrumentation) _always_ traces unless told otherwise + assert len(spans) == 1 + assert spans[0].n == 'asgi' + + assert "X-Instana-T" in result.headers + assert "X-Instana-S" in result.headers + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + +def test_basic_get(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["starlette_server"] + '/') + + assert(result) + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + +def test_path_templates(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["starlette_server"] + '/users/1') + + assert(result) + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/users/1') + assert(asgi_span.data['http']['path_tpl'] == '/users/{user_id}') + assert(asgi_span.data['http']['params'] == None) + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + +def test_secret_scrubbing(server): + result = None + with tracer.start_active_span('test'): + result = requests.get(testenv["starlette_server"] + '/?secret=shhh') + + assert(result) + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['params'] == 'secret=') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + +def test_synthetic_request(server): + request_headers = { + 'X-Instana-Synthetic': '1' + } + with tracer.start_active_span('test'): + result = requests.get(testenv["starlette_server"] + '/', headers=request_headers) + + assert(result) + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + + assert(asgi_span.sy) + assert(urllib3_span.sy is None) + assert(test_span.sy is None) + +def test_custom_header_capture(server): + from instana.singletons import agent + + # The background Starlette server is pre-configured with custom headers to capture + + request_headers = { + 'X-Capture-This': 'this', + 'X-Capture-That': 'that' + } + with tracer.start_active_span('test'): + result = requests.get(testenv["starlette_server"] + '/', headers=request_headers) + + assert(result) + + spans = tracer.recorder.queued_spans() + assert len(spans) == 3 + + span_filter = lambda span: span.n == "sdk" + test_span = get_first_span_by_filter(spans, span_filter) + assert(test_span) + + span_filter = lambda span: span.n == "urllib3" + urllib3_span = get_first_span_by_filter(spans, span_filter) + assert(urllib3_span) + + span_filter = lambda span: span.n == "asgi" + asgi_span = get_first_span_by_filter(spans, span_filter) + assert(asgi_span) + + assert(test_span.t == urllib3_span.t == asgi_span.t) + assert(asgi_span.p == urllib3_span.s) + assert(urllib3_span.p == test_span.s) + + assert "X-Instana-T" in result.headers + assert result.headers["X-Instana-T"] == asgi_span.t + assert "X-Instana-S" in result.headers + assert result.headers["X-Instana-S"] == asgi_span.s + assert "X-Instana-L" in result.headers + assert result.headers["X-Instana-L"] == '1' + assert "Server-Timing" in result.headers + assert result.headers["Server-Timing"] == ("intid;desc=%s" % asgi_span.t) + + assert('http' in asgi_span.data) + assert(asgi_span.ec == None) + assert(isinstance(asgi_span.stack, list)) + assert(asgi_span.data['http']['host'] == '127.0.0.1') + assert(asgi_span.data['http']['path'] == '/') + assert(asgi_span.data['http']['path_tpl'] == '/') + assert(asgi_span.data['http']['method'] == 'GET') + assert(asgi_span.data['http']['status'] == 200) + assert(asgi_span.data['http']['error'] == None) + + assert("http.X-Capture-This" in asgi_span.data["custom"]['tags']) + assert("this" == asgi_span.data["custom"]['tags']["http.X-Capture-This"]) + assert("http.X-Capture-That" in asgi_span.data["custom"]['tags']) + assert("that" == asgi_span.data["custom"]['tags']["http.X-Capture-That"]) diff --git a/tests/frameworks/test_tornado_server.py b/tests/frameworks/test_tornado_server.py index b93ee72e..4d9e5bd2 100644 --- a/tests/frameworks/test_tornado_server.py +++ b/tests/frameworks/test_tornado_server.py @@ -52,7 +52,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -116,8 +115,7 @@ async def test(): return await self.post(session, testenv["tornado_server"] + "/") response = tornado.ioloop.IOLoop.current().run_sync(test) - - time.sleep(0.5) + spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -185,7 +183,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -205,7 +202,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(4, len(spans)) @@ -284,7 +280,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -348,7 +343,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -413,7 +407,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -478,7 +471,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) @@ -550,7 +542,6 @@ async def test(): response = tornado.ioloop.IOLoop.current().run_sync(test) - time.sleep(0.5) spans = self.recorder.queued_spans() self.assertEqual(3, len(spans)) diff --git a/tests/helpers.py b/tests/helpers.py index dc9d17e4..789db728 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -140,3 +140,15 @@ def get_spans_by_filter(spans, filter): if filter(span) is True: results.append(span) return results + +def launch_traced_request(url): + import requests + from instana.log import logger + from instana.singletons import tracer + + logger.warn("Launching request with a root SDK span name of 'launch_traced_request'") + + with tracer.start_active_span('launch_traced_request'): + response = requests.get(url) + + return response \ No newline at end of file diff --git a/tests/opentracing/test_ot_propagators.py b/tests/opentracing/test_ot_propagators.py index 0e2f2df3..bdcca890 100644 --- a/tests/opentracing/test_ot_propagators.py +++ b/tests/opentracing/test_ot_propagators.py @@ -2,9 +2,9 @@ import opentracing as ot -import instana.http_propagator as ihp -import instana.text_propagator as itp -from instana import span +import instana.propagators.http_propagator as ihp +import instana.propagators.text_propagator as itp +import instana.propagators.binary_propagator as ibp from instana.span_context import SpanContext from instana.tracer import InstanaTracer @@ -29,11 +29,11 @@ def test_http_inject_with_dict(): ot.tracer.inject(span.context, ot.Format.HTTP_HEADERS, carrier) assert 'X-Instana-T' in carrier - assert(carrier['X-Instana-T'] == span.context.trace_id) + assert carrier['X-Instana-T'] == span.context.trace_id assert 'X-Instana-S' in carrier - assert(carrier['X-Instana-S'] == span.context.span_id) + assert carrier['X-Instana-S'] == span.context.span_id assert 'X-Instana-L' in carrier - assert(carrier['X-Instana-L'] == "1") + assert carrier['X-Instana-L'] == "1" def test_http_inject_with_list(): @@ -55,8 +55,34 @@ def test_http_basic_extract(): ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) assert isinstance(ctx, SpanContext) - assert('0000000000000001' == ctx.trace_id) - assert('0000000000000001' == ctx.span_id) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' + assert ctx.synthetic + + +def test_http_extract_with_byte_keys(): + ot.tracer = InstanaTracer() + + carrier = {b'X-Instana-T': '1', b'X-Instana-S': '1', b'X-Instana-L': '1', b'X-Instana-Synthetic': '1'} + ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) + + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' + assert ctx.synthetic + + +def test_http_extract_from_list_of_tuples(): + ot.tracer = InstanaTracer() + + carrier = [(b'user-agent', b'python-requests/2.23.0'), (b'accept-encoding', b'gzip, deflate'), + (b'accept', b'*/*'), (b'connection', b'keep-alive'), + (b'x-instana-t', b'1'), (b'x-instana-s', b'1'), (b'x-instana-l', b'1'), (b'X-Instana-Synthetic', '1')] + ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) + + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' assert ctx.synthetic @@ -67,8 +93,8 @@ def test_http_mixed_case_extract(): ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) assert isinstance(ctx, SpanContext) - assert('0000000000000001' == ctx.trace_id) - assert('0000000000000001' == ctx.span_id) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' assert not ctx.synthetic @@ -101,8 +127,8 @@ def test_http_128bit_headers(): ctx = ot.tracer.extract(ot.Format.HTTP_HEADERS, carrier) assert isinstance(ctx, SpanContext) - assert('b0789916ff8f319f' == ctx.trace_id) - assert('b0789916ff8f319f' == ctx.span_id) + assert ctx.trace_id == 'b0789916ff8f319f' + assert ctx.span_id == 'b0789916ff8f319f' def test_text_basics(): @@ -125,11 +151,11 @@ def test_text_inject_with_dict(): ot.tracer.inject(span.context, ot.Format.TEXT_MAP, carrier) assert 'X-INSTANA-T' in carrier - assert(carrier['X-INSTANA-T'] == span.context.trace_id) + assert carrier['X-INSTANA-T'] == span.context.trace_id assert 'X-INSTANA-S' in carrier - assert(carrier['X-INSTANA-S'] == span.context.span_id) + assert carrier['X-INSTANA-S'] == span.context.span_id assert 'X-INSTANA-L' in carrier - assert(carrier['X-INSTANA-L'] == "1") + assert carrier['X-INSTANA-L'] == "1" def test_text_inject_with_list(): @@ -151,8 +177,8 @@ def test_text_basic_extract(): ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) assert isinstance(ctx, SpanContext) - assert('0000000000000001' == ctx.trace_id) - assert('0000000000000001' == ctx.span_id) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' def test_text_mixed_case_extract(): @@ -161,7 +187,9 @@ def test_text_mixed_case_extract(): carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1'} ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) - assert(ctx is None) + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' def test_text_no_context_extract(): @@ -181,5 +209,89 @@ def test_text_128bit_headers(): ctx = ot.tracer.extract(ot.Format.TEXT_MAP, carrier) assert isinstance(ctx, SpanContext) - assert('b0789916ff8f319f' == ctx.trace_id) assert('b0789916ff8f319f' == ctx.span_id) + assert ctx.trace_id == 'b0789916ff8f319f' + assert ctx.span_id == 'b0789916ff8f319f' + +def test_binary_basics(): + inspect.isclass(ibp.BinaryPropagator) + + inject_func = getattr(ibp.BinaryPropagator, "inject", None) + assert inject_func + assert callable(inject_func) + + extract_func = getattr(ibp.BinaryPropagator, "extract", None) + assert extract_func + assert callable(extract_func) + + +def test_binary_inject_with_dict(): + ot.tracer = InstanaTracer() + + carrier = {} + span = ot.tracer.start_span("nosetests") + ot.tracer.inject(span.context, ot.Format.BINARY, carrier) + + assert b'x-instana-t' in carrier + assert carrier[b'x-instana-t'] == str.encode(span.context.trace_id) + assert b'x-instana-s' in carrier + assert carrier[b'x-instana-s'] == str.encode(span.context.span_id) + assert b'x-instana-l' in carrier + assert carrier[b'x-instana-l'] == b'1' + + +def test_binary_inject_with_list(): + ot.tracer = InstanaTracer() + + carrier = [] + span = ot.tracer.start_span("nosetests") + ot.tracer.inject(span.context, ot.Format.BINARY, carrier) + + assert (b'x-instana-t', str.encode(span.context.trace_id)) in carrier + assert (b'x-instana-s', str.encode(span.context.span_id)) in carrier + assert (b'x-instana-l', b'1') in carrier + + +def test_binary_basic_extract(): + ot.tracer = InstanaTracer() + + carrier = {b'X-INSTANA-T': b'1', b'X-INSTANA-S': b'1', b'X-INSTANA-L': b'1', b'X-INSTANA-SYNTHETIC': b'1'} + ctx = ot.tracer.extract(ot.Format.BINARY, carrier) + + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' + assert ctx.synthetic + + +def test_binary_mixed_case_extract(): + ot.tracer = InstanaTracer() + + carrier = {'x-insTana-T': '1', 'X-inSTANa-S': '1', 'X-INstana-l': '1', b'X-inStaNa-SYNtheTIC': b'1'} + ctx = ot.tracer.extract(ot.Format.BINARY, carrier) + + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == '0000000000000001' + assert ctx.span_id == '0000000000000001' + assert ctx.synthetic + + +def test_binary_no_context_extract(): + ot.tracer = InstanaTracer() + + carrier = {} + ctx = ot.tracer.extract(ot.Format.BINARY, carrier) + + assert ctx is None + + +def test_binary_128bit_headers(): + ot.tracer = InstanaTracer() + + carrier = {'X-INSTANA-T': '0000000000000000b0789916ff8f319f', + 'X-INSTANA-S': ' 0000000000000000b0789916ff8f319f', 'X-INSTANA-L': '1'} + ctx = ot.tracer.extract(ot.Format.BINARY, carrier) + + assert isinstance(ctx, SpanContext) + assert ctx.trace_id == 'b0789916ff8f319f' + assert ctx.span_id == 'b0789916ff8f319f' diff --git a/tests/platforms/test_lambda.py b/tests/platforms/test_lambda.py index 5861c928..cd45e98b 100644 --- a/tests/platforms/test_lambda.py +++ b/tests/platforms/test_lambda.py @@ -3,6 +3,7 @@ import os import sys import json +import time import wrapt import logging import unittest @@ -181,6 +182,7 @@ def test_custom_service_name(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload) @@ -247,6 +249,7 @@ def test_api_gateway_trigger_tracing(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload) @@ -312,6 +315,7 @@ def test_application_lb_trigger_tracing(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload) @@ -376,6 +380,7 @@ def test_cloudwatch_trigger_tracing(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload) @@ -440,6 +445,7 @@ def test_cloudwatch_logs_trigger_tracing(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload) @@ -506,6 +512,7 @@ def test_s3_trigger_tracing(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload) @@ -571,6 +578,7 @@ def test_sqs_trigger_tracing(self): assert 'headers' in result assert 'Server-Timing' in result['headers'] + time.sleep(1) payload = self.agent.collector.prepare_payload() self.assertTrue("metrics" in payload)