diff --git a/src/instana/instrumentation/aiohttp/client.py b/src/instana/instrumentation/aiohttp/client.py index 4b307dc4..667c2620 100644 --- a/src/instana/instrumentation/aiohttp/client.py +++ b/src/instana/instrumentation/aiohttp/client.py @@ -12,7 +12,7 @@ from instana.propagators.format import Format from instana.singletons import agent from instana.util.secrets import strip_secrets_from_query -from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.util.traceutils import get_tracer_tuple, tracing_is_off, extract_custom_headers try: import aiohttp @@ -21,6 +21,7 @@ from aiohttp.client import ClientSession from instana.span.span import InstanaSpan + async def stan_request_start( session: "ClientSession", trace_config_ctx: SimpleNamespace, params ) -> Awaitable[None]: @@ -35,6 +36,8 @@ async def stan_request_start( span = tracer.start_span("aiohttp-client", span_context=parent_context) + extract_custom_headers(span, params.headers) + tracer.inject(span.context, Format.HTTP_HEADERS, params.headers) parts = str(params.url).split("?") @@ -59,13 +62,7 @@ async def stan_request_end( SpanAttributes.HTTP_STATUS_CODE, params.response.status ) - if agent.options.extra_http_headers: - for custom_header in agent.options.extra_http_headers: - if custom_header in params.response.headers: - span.set_attribute( - "http.header.%s" % custom_header, - params.response.headers[custom_header], - ) + extract_custom_headers(span, params.response.headers) if 500 <= params.response.status: span.mark_as_errored({"http.error": params.response.reason}) diff --git a/src/instana/instrumentation/aiohttp/server.py b/src/instana/instrumentation/aiohttp/server.py index d658641b..ff22ae6b 100644 --- a/src/instana/instrumentation/aiohttp/server.py +++ b/src/instana/instrumentation/aiohttp/server.py @@ -11,6 +11,7 @@ from instana.propagators.format import Format from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import extract_custom_headers if TYPE_CHECKING: from instana.span.span import InstanaSpan @@ -46,14 +47,7 @@ async def stan_middleware( span.set_attribute(SpanAttributes.HTTP_URL, parts[0]) span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) - # Custom header tracking support - if agent.options.extra_http_headers: - for custom_header in agent.options.extra_http_headers: - if custom_header in request.headers: - span.set_attribute( - "http.header.%s" % custom_header, - request.headers[custom_header], - ) + extract_custom_headers(span, request.headers) response = None try: @@ -69,6 +63,9 @@ async def stan_middleware( span.mark_as_errored() span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) + + extract_custom_headers(span, response.headers) + tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) return response diff --git a/src/instana/instrumentation/asgi.py b/src/instana/instrumentation/asgi.py index ed0866ae..2831bb92 100644 --- a/src/instana/instrumentation/asgi.py +++ b/src/instana/instrumentation/asgi.py @@ -14,6 +14,7 @@ from instana.propagators.format import Format from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import extract_custom_headers if TYPE_CHECKING: from starlette.middleware.exceptions import ExceptionMiddleware @@ -28,23 +29,6 @@ class InstanaASGIMiddleware: def __init__(self, app: "ExceptionMiddleware") -> None: self.app = app - def _extract_custom_headers( - self, span: "InstanaSpan", headers: Dict[str, Any] - ) -> None: - if agent.options.extra_http_headers is None: - return - 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_attribute( - f"http.header.{custom_header}", - header_pair[1].decode("utf-8"), - ) - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - def _collect_kvs(self, scope: Dict[str, Any], span: "InstanaSpan") -> None: try: span.set_attribute("span.kind", SpanKind.SERVER) @@ -93,8 +77,8 @@ async def __call__( with tracer.start_as_current_span("asgi", span_context=request_context) as span: self._collect_kvs(scope, span) - if "headers" in scope and agent.options.extra_http_headers: - self._extract_custom_headers(span, scope["headers"]) + if "headers" in scope: + extract_custom_headers(span, scope["headers"]) instana_send = self._send_with_instana( span, @@ -125,7 +109,7 @@ async def send_wrapper(response: Dict[str, Any]) -> Awaitable[None]: headers = response.get("headers") if headers: - self._extract_custom_headers(current_span, headers) + extract_custom_headers(current_span, headers) tracer.inject(current_span.context, Format.BINARY, headers) except Exception: logger.debug("ASGI send_wrapper error: ", exc_info=True) diff --git a/src/instana/instrumentation/boto3_inst.py b/src/instana/instrumentation/boto3_inst.py index fb7a3233..88e1c33f 100644 --- a/src/instana/instrumentation/boto3_inst.py +++ b/src/instana/instrumentation/boto3_inst.py @@ -10,7 +10,7 @@ from instana.log import logger from instana.singletons import tracer, agent -from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.util.traceutils import get_tracer_tuple, tracing_is_off, extract_custom_headers from instana.propagators.format import Format from instana.span.span import get_current_span @@ -23,21 +23,6 @@ import boto3 from boto3.s3 import inject - def extract_custom_headers( - span: "InstanaSpan", headers: Optional[Dict[str, Any]] = None - ) -> None: - if not agent.options.extra_http_headers or not headers: - return - try: - for custom_header in agent.options.extra_http_headers: - if custom_header in headers: - span.set_attribute( - "http.header.%s" % custom_header, headers[custom_header] - ) - - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - def lambda_inject_context(payload: Dict[str, Any], span: "InstanaSpan") -> None: """ When boto3 lambda client 'Invoke' is called, we want to inject the tracing context. diff --git a/src/instana/instrumentation/django/middleware.py b/src/instana/instrumentation/django/middleware.py index 4dc2e621..5e5b8419 100644 --- a/src/instana/instrumentation/django/middleware.py +++ b/src/instana/instrumentation/django/middleware.py @@ -13,10 +13,10 @@ from instana.log import logger from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format if TYPE_CHECKING: - from instana.span.span import InstanaSpan from django.core.handlers.base import BaseHandler from django.http import HttpRequest, HttpResponse @@ -53,29 +53,6 @@ def __init__( super(InstanaMiddleware, self).__init__(get_response) self.get_response = get_response - def _extract_custom_headers( - self, span: "InstanaSpan", headers: Dict[str, Any], format: bool - ) -> None: - if agent.options.extra_http_headers is None: - return - - try: - for custom_header in agent.options.extra_http_headers: - # Headers are available in this format: HTTP_X_CAPTURE_THIS - django_header = ( - ("HTTP_" + custom_header.upper()).replace("-", "_") - if format - else custom_header - ) - - if django_header in headers: - span.set_attribute( - f"http.header.{custom_header}", headers[django_header] - ) - - except Exception: - logger.debug("Instana middleware @ extract_custom_headers: ", exc_info=True) - def process_request(self, request: Type["HttpRequest"]) -> None: try: env = request.META @@ -89,7 +66,7 @@ def process_request(self, request: Type["HttpRequest"]) -> None: token = context.attach(ctx) request.token = token - self._extract_custom_headers(span, env, format=True) + extract_custom_headers(span, env, format=True) request.span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) if "PATH_INFO" in env: @@ -138,7 +115,7 @@ def process_response( SpanAttributes.HTTP_STATUS_CODE, response.status_code ) if hasattr(response, "headers"): - self._extract_custom_headers( + extract_custom_headers( request.span, response.headers, format=False ) tracer.inject(request.span.context, Format.HTTP_HEADERS, response) diff --git a/src/instana/instrumentation/flask/common.py b/src/instana/instrumentation/flask/common.py index d55c2432..a0e6f6fb 100644 --- a/src/instana/instrumentation/flask/common.py +++ b/src/instana/instrumentation/flask/common.py @@ -10,23 +10,15 @@ from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger -from instana.singletons import tracer, agent +from instana.singletons import tracer from instana.propagators.format import Format -from instana.instrumentation.flask import signals_available if TYPE_CHECKING: - from instana.span.span import InstanaSpan from werkzeug.exceptions import HTTPException from flask.typing import ResponseReturnValue from jinja2.environment import Template - if signals_available: - from werkzeug.datastructures.headers import Headers - else: - from werkzeug.datastructures import Headers - - @wrapt.patch_function_wrapper('flask', 'templating._render') def render_with_instana( wrapped: Callable[..., str], @@ -97,21 +89,3 @@ def handle_user_exception_with_instana( logger.debug("handle_user_exception_with_instana:", exc_info=True) return response - - -def extract_custom_headers( - span: "InstanaSpan", headers: Union[Dict[str, Any], "Headers"], format: bool -) -> None: - if agent.options.extra_http_headers is None: - return - try: - for custom_header in agent.options.extra_http_headers: - # Headers are available in this format: HTTP_X_CAPTURE_THIS - flask_header = ('HTTP_' + custom_header.upper()).replace('-', '_') if format else custom_header - if flask_header in headers: - span.set_attribute( - "http.header.%s" % custom_header, headers[flask_header] - ) - - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) diff --git a/src/instana/instrumentation/flask/vanilla.py b/src/instana/instrumentation/flask/vanilla.py index 0dd49795..fed13f16 100644 --- a/src/instana/instrumentation/flask/vanilla.py +++ b/src/instana/instrumentation/flask/vanilla.py @@ -13,7 +13,7 @@ from instana.log import logger from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query -from instana.instrumentation.flask.common import extract_custom_headers +from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format path_tpl_re = re.compile('<.*>') diff --git a/src/instana/instrumentation/flask/with_blinker.py b/src/instana/instrumentation/flask/with_blinker.py index cebe2ef3..df3af703 100644 --- a/src/instana/instrumentation/flask/with_blinker.py +++ b/src/instana/instrumentation/flask/with_blinker.py @@ -12,7 +12,7 @@ from instana.log import logger from instana.util.secrets import strip_secrets_from_query from instana.singletons import agent, tracer -from instana.instrumentation.flask.common import extract_custom_headers +from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format import flask @@ -78,7 +78,7 @@ def request_finished_with_instana( extract_custom_headers(span, response.headers, format=False) tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) - except: + except Exception: logger.debug("Flask request_finished_with_instana", exc_info=True) finally: if span and span.is_recording(): diff --git a/src/instana/instrumentation/pyramid.py b/src/instana/instrumentation/pyramid.py index 230ebcc5..88c3e419 100644 --- a/src/instana/instrumentation/pyramid.py +++ b/src/instana/instrumentation/pyramid.py @@ -16,12 +16,12 @@ from instana.log import logger from instana.singletons import tracer, agent from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format if TYPE_CHECKING: from pyramid.request import Request from pyramid.response import Response - from instana.span.span import InstanaSpan from pyramid.registry import Registry class InstanaTweenFactory(object): @@ -32,21 +32,6 @@ def __init__( ) -> None: self.handler = handler - def _extract_custom_headers( - self, span: "InstanaSpan", headers: Dict[str, Any] - ) -> None: - if not agent.options.extra_http_headers: - return - try: - for custom_header in agent.options.extra_http_headers: - if custom_header in headers: - span.set_attribute( - f"http.header.{custom_header}", headers[custom_header] - ) - - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) - def __call__(self, request: "Request") -> "Response": ctx = tracer.extract(Format.HTTP_HEADERS, dict(request.headers)) @@ -56,7 +41,7 @@ def __call__(self, request: "Request") -> "Response": span.set_attribute(SpanAttributes.HTTP_METHOD, request.method) span.set_attribute(SpanAttributes.HTTP_URL, request.path) - self._extract_custom_headers(span, request.headers) + extract_custom_headers(span, request.headers) if len(request.query_string): scrubbed_params = strip_secrets_from_query( @@ -74,7 +59,7 @@ def __call__(self, request: "Request") -> "Response": "http.path_tpl", request.matched_route.pattern ) - self._extract_custom_headers(span, response.headers) + extract_custom_headers(span, response.headers) tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) except HTTPException as e: diff --git a/src/instana/instrumentation/sanic_inst.py b/src/instana/instrumentation/sanic_inst.py index 97bdb8b9..72b0dc26 100644 --- a/src/instana/instrumentation/sanic_inst.py +++ b/src/instana/instrumentation/sanic_inst.py @@ -75,8 +75,7 @@ def request_with_instana(request: Request) -> None: ) span.set_attribute("http.params", scrubbed_params) - if agent.options.extra_http_headers: - extract_custom_headers(span, headers) + extract_custom_headers(span, headers) if hasattr(request, "uri_template") and request.uri_template: span.set_attribute("http.path_tpl", request.uri_template) except Exception: @@ -113,8 +112,7 @@ def response_with_instana(request: Request, response: HTTPResponse) -> None: span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, status_code) if hasattr(response, "headers"): - if agent.options.extra_http_headers: - extract_custom_headers(span, response.headers) + extract_custom_headers(span, response.headers) tracer.inject(span.context, Format.HTTP_HEADERS, response.headers) if span.is_recording(): diff --git a/src/instana/instrumentation/tornado/client.py b/src/instana/instrumentation/tornado/client.py index e937db68..134c7f7e 100644 --- a/src/instana/instrumentation/tornado/client.py +++ b/src/instana/instrumentation/tornado/client.py @@ -6,15 +6,18 @@ import wrapt import functools + from typing import TYPE_CHECKING, Dict, Any from opentelemetry.semconv.trace import SpanAttributes from instana.log import logger from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format from instana.span.span import get_current_span + @wrapt.patch_function_wrapper('tornado.httpclient', 'AsyncHTTPClient.fetch') def fetch_with_instana(wrapped, instance, argv, kwargs): try: @@ -41,6 +44,9 @@ def fetch_with_instana(wrapped, instance, argv, kwargs): parent_context = parent_span.get_span_context() if parent_span else None span = tracer.start_span("tornado-client", span_context=parent_context) + + extract_custom_headers(span, request.headers) + tracer.inject(span.context, Format.HTTP_HEADERS, request.headers) # Query param scrubbing @@ -68,6 +74,8 @@ def finish_tracing(future, span): try: response = future.result() span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.code) + + extract_custom_headers(span, response.headers) except tornado.httpclient.HTTPClientError as e: span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, e.code) span.record_exception(e) diff --git a/src/instana/instrumentation/tornado/server.py b/src/instana/instrumentation/tornado/server.py index dc373bc9..82266961 100644 --- a/src/instana/instrumentation/tornado/server.py +++ b/src/instana/instrumentation/tornado/server.py @@ -12,18 +12,9 @@ from instana.log import logger from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query + from instana.util.traceutils import extract_custom_headers from instana.propagators.format import Format - def extract_custom_headers(span, headers): - if not agent.options.extra_http_headers or not headers: - return - try: - for custom_header in agent.options.extra_http_headers: - if custom_header in headers: - span.set_attribute("http.header.%s" % custom_header, headers[custom_header]) - - except Exception: - logger.debug("extract_custom_headers: ", exc_info=True) @wrapt.patch_function_wrapper('tornado.web', 'RequestHandler._execute') diff --git a/src/instana/instrumentation/urllib3.py b/src/instana/instrumentation/urllib3.py index 00ee7648..4536d2be 100644 --- a/src/instana/instrumentation/urllib3.py +++ b/src/instana/instrumentation/urllib3.py @@ -11,7 +11,7 @@ from instana.propagators.format import Format from instana.singletons import agent from instana.util.secrets import strip_secrets_from_query -from instana.util.traceutils import get_tracer_tuple, tracing_is_off +from instana.util.traceutils import get_tracer_tuple, tracing_is_off, extract_custom_headers if TYPE_CHECKING: from instana.span.span import InstanaSpan @@ -19,19 +19,6 @@ try: import urllib3 - def _extract_custom_headers(span: "InstanaSpan", headers: Dict[str, Any]) -> None: - if agent.options.extra_http_headers is None: - return - - try: - for custom_header in agent.options.extra_http_headers: - if custom_header in headers: - span.set_attribute( - f"http.header.{custom_header}", headers[custom_header] - ) - except Exception: - logger.debug("urllib3 _extract_custom_headers error: ", exc_info=True) - def _collect_kvs( instance: Union[ urllib3.connectionpool.HTTPConnectionPool, @@ -82,7 +69,7 @@ def collect_response( try: span.set_attribute(SpanAttributes.HTTP_STATUS_CODE, response.status) - _extract_custom_headers(span, response.headers) + extract_custom_headers(span, response.headers) if 500 <= response.status: span.mark_as_errored() @@ -121,7 +108,7 @@ def urlopen_with_instana( if "method" in kvs: span.set_attribute(SpanAttributes.HTTP_METHOD, kvs["method"]) if "headers" in kwargs: - _extract_custom_headers(span, kwargs["headers"]) + extract_custom_headers(span, kwargs["headers"]) tracer.inject(span.context, Format.HTTP_HEADERS, kwargs["headers"]) response = wrapped(*args, **kwargs) diff --git a/src/instana/instrumentation/wsgi.py b/src/instana/instrumentation/wsgi.py index 5700b252..5ab7a2f7 100644 --- a/src/instana/instrumentation/wsgi.py +++ b/src/instana/instrumentation/wsgi.py @@ -4,6 +4,7 @@ """ Instana WSGI Middleware """ + from typing import Dict, Any, Callable, List, Tuple, Optional from opentelemetry.semconv.trace import SpanAttributes @@ -12,6 +13,7 @@ from instana.propagators.format import Format from instana.singletons import agent, tracer from instana.util.secrets import strip_secrets_from_query +from instana.util.traceutils import extract_custom_headers class InstanaWSGIMiddleware(object): @@ -23,11 +25,22 @@ def __init__(self, app: object) -> None: def __call__(self, environ: Dict[str, Any], start_response: Callable) -> object: env = environ - def new_start_response(status: str, headers: List[Tuple[object, ...]], exc_info: Optional[Exception] = None) -> object: + def new_start_response( + status: str, + headers: List[Tuple[object, ...]], + exc_info: Optional[Exception] = None, + ) -> object: """Modified start response with additional headers.""" + extract_custom_headers(self.span, headers) + tracer.inject(self.span.context, Format.HTTP_HEADERS, headers) - headers_str = [(header[0], str(header[1])) if not isinstance(header[1], str) else header for header in headers] + headers_str = [ + (header[0], str(header[1])) + if not isinstance(header[1], str) + else header + for header in headers + ] res = start_response(status, headers_str, exc_info) sc = status.split(" ")[0] @@ -47,14 +60,7 @@ def new_start_response(status: str, headers: List[Tuple[object, ...]], exc_info: ctx = trace.set_span_in_context(self.span) self.token = context.attach(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.span.set_attribute( - "http.header.%s" % custom_header, env[wsgi_header] - ) + extract_custom_headers(self.span, env, format=True) if "PATH_INFO" in env: self.span.set_attribute("http.path", env["PATH_INFO"]) diff --git a/src/instana/util/traceutils.py b/src/instana/util/traceutils.py index 06b821ca..a9f1849b 100644 --- a/src/instana/util/traceutils.py +++ b/src/instana/util/traceutils.py @@ -1,21 +1,37 @@ # (c) Copyright IBM Corp. 2021 # (c) Copyright Instana Inc. 2021 -from typing import Optional, Tuple +from typing import Optional, Tuple, TYPE_CHECKING, Union, Dict, List, Any, Iterable from instana.log import logger from instana.singletons import agent, tracer -from instana.span.span import InstanaSpan, get_current_span +from instana.span.span import get_current_span from instana.tracer import InstanaTracer +if TYPE_CHECKING: + from instana.span.span import InstanaSpan -def extract_custom_headers(tracing_span, headers) -> None: +def extract_custom_headers(span: "InstanaSpan", headers: Optional[Union[Dict[str, Any], List[Tuple[object, ...]], Iterable]] = None, format: Optional[bool] = False) -> None: + if not headers: + return try: for custom_header in agent.options.extra_http_headers: - # Headers are in the following format: b'x-header-1' - for header_key, value in headers.items(): - if header_key.lower() == custom_header.lower(): - tracing_span.set_attribute(f"http.header.{custom_header}", value) + # Headers are available in the following formats: HTTP_X_CAPTURE_THIS, b'x-header-1', X-Capture-That + expected_header = ( + ("HTTP_" + custom_header.upper()).replace("-", "_") + if format + else custom_header + ) + for header in headers: + if isinstance(header, tuple): + header_key = header[0].decode("utf-8") if isinstance(header[0], bytes) else header[0] + header_val = header[1].decode("utf-8") if isinstance(header[1], bytes) else header[1] + if header_key.lower() == expected_header.lower(): + span.set_attribute( + f"http.header.{custom_header}", header_val, + ) + elif header.lower() == expected_header.lower(): + span.set_attribute(f"http.header.{custom_header}", headers[expected_header]) except Exception: logger.debug("extract_custom_headers: ", exc_info=True) @@ -36,7 +52,7 @@ def get_active_tracer() -> Optional[InstanaTracer]: def get_tracer_tuple() -> ( - Tuple[Optional[InstanaTracer], Optional[InstanaSpan], Optional[str]] + Tuple[Optional[InstanaTracer], Optional["InstanaSpan"], Optional[str]] ): active_tracer = get_active_tracer() current_span = get_current_span() diff --git a/tests/apps/aiohttp_app/app.py b/tests/apps/aiohttp_app/app.py index 44bdb1a3..f43ee59d 100755 --- a/tests/apps/aiohttp_app/app.py +++ b/tests/apps/aiohttp_app/app.py @@ -34,6 +34,11 @@ def raise_exception(request): raise Exception("Simulated exception") +def response_headers(request): + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} + return web.Response(text="Stan wuz here with headers!", headers=headers) + + def aiohttp_server(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) @@ -44,6 +49,7 @@ def aiohttp_server(): app.add_routes([web.get('/401', four_hundred_one)]) app.add_routes([web.get('/500', five_hundred)]) app.add_routes([web.get('/exception', raise_exception)]) + app.add_routes([web.get('/response_headers', response_headers)]) runner = web.AppRunner(app) loop.run_until_complete(runner.setup()) diff --git a/tests/apps/bottle_app/app.py b/tests/apps/bottle_app/app.py index cd56c138..c0d29a3e 100644 --- a/tests/apps/bottle_app/app.py +++ b/tests/apps/bottle_app/app.py @@ -6,7 +6,7 @@ import logging from wsgiref.simple_server import make_server -from bottle import default_app +from bottle import default_app, response from tests.helpers import testenv from instana.middleware import InstanaWSGIMiddleware @@ -23,6 +23,12 @@ def hello(): return "

🐍 Hello Stan! 🦄

" +@app.route("/response_headers") +def response_headers(): + response.set_header("X-Capture-This", "this") + response.set_header("X-Capture-That", "that") + return "Stan wuz here with headers!" + # Wrap the application with the Instana WSGI Middleware app = InstanaWSGIMiddleware(app) bottle_server = make_server('127.0.0.1', testenv["wsgi_port"], app) diff --git a/tests/apps/starlette_app/app.py b/tests/apps/starlette_app/app.py index 04878c12..baaf7f66 100644 --- a/tests/apps/starlette_app/app.py +++ b/tests/apps/starlette_app/app.py @@ -20,6 +20,11 @@ def user(request): return PlainTextResponse("Hello, user id %s!" % user_id) +def response_headers(request): + headers = {"X-Capture-This-Too": "this too", "X-Capture-That-Too": "that too"} + return PlainTextResponse("Stan wuz here with headers!", headers=headers) + + async def websocket_endpoint(websocket): await websocket.accept() await websocket.send_text("Hello, websocket!") @@ -33,6 +38,7 @@ def startup(): routes = [ Route("/", homepage), Route("/users/{user_id}", user), + Route("/response_headers", response_headers), WebSocketRoute("/ws", websocket_endpoint), Mount("/static", StaticFiles(directory=dir_path + "/static")), ] diff --git a/tests/clients/test_urllib3.py b/tests/clients/test_urllib3.py index 77b49ece..642edd9a 100644 --- a/tests/clients/test_urllib3.py +++ b/tests/clients/test_urllib3.py @@ -12,7 +12,7 @@ import urllib3 from instana.instrumentation.urllib3 import ( _collect_kvs as collect_kvs, - _extract_custom_headers as extract_custom_headers, + extract_custom_headers, collect_response, ) from instana.singletons import agent, tracer @@ -971,7 +971,7 @@ def test_extract_custom_headers_exception( monkeypatch.setattr(span, "set_attribute", Exception("mocked error")) caplog.set_level(logging.DEBUG, logger="instana") extract_custom_headers(span, request_headers) - assert "urllib3 _extract_custom_headers error: " in caplog.messages + assert "extract_custom_headers: " in caplog.messages def test_collect_response_exception( self, span: "InstanaSpan", caplog: "LogCaptureFixture", monkeypatch diff --git a/tests/frameworks/test_aiohttp_client.py b/tests/frameworks/test_aiohttp_client.py index fd66ba17..3a2b29ea 100644 --- a/tests/frameworks/test_aiohttp_client.py +++ b/tests/frameworks/test_aiohttp_client.py @@ -501,3 +501,63 @@ async def test(): spans = self.recorder.queued_spans() assert len(spans) == 3 + + def test_client_request_header_capture(self) -> None: + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ["X-Capture-This-Too"] + + request_headers = { + "X-Capture-This-Too": "Ok too", + } + + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession() as session: + return await self.fetch( + session, testenv["flask_server"] + "/", headers=request_headers + ) + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + wsgi_span = spans[0] + aiohttp_span = spans[1] + test_span = spans[2] + + # Same traceId + traceId = test_span.t + assert aiohttp_span.t == traceId + assert wsgi_span.t == traceId + + # Parent relationships + assert aiohttp_span.p == test_span.s + assert wsgi_span.p == aiohttp_span.s + + # Error logging + assert not test_span.ec + assert not aiohttp_span.ec + assert not wsgi_span.ec + + assert aiohttp_span.n == "aiohttp-client" + assert aiohttp_span.data["http"]["status"] == 200 + assert aiohttp_span.data["http"]["url"] == testenv["flask_server"] + "/" + assert aiohttp_span.data["http"]["method"] == "GET" + assert aiohttp_span.stack + assert isinstance(aiohttp_span.stack, list) + assert len(aiohttp_span.stack) > 1 + + assert "X-Capture-This-Too" in aiohttp_span.data["http"]["header"] + assert aiohttp_span.data["http"]["header"]["X-Capture-This-Too"] == "Ok too" + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == hex_id(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" + + agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/frameworks/test_aiohttp_server.py b/tests/frameworks/test_aiohttp_server.py index f9cb01e5..6c2ca672 100644 --- a/tests/frameworks/test_aiohttp_server.py +++ b/tests/frameworks/test_aiohttp_server.py @@ -207,7 +207,9 @@ async def test(): assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" - def test_server_custom_header_capture(self): + def test_server_request_header_capture(self): + original_extra_http_headers = agent.options.extra_http_headers + async def test(): with tracer.start_as_current_span("test"): async with aiohttp.ClientSession() as session: @@ -272,6 +274,70 @@ async def test(): assert "X-Capture-That" in aioserver_span.data["http"]["header"] assert aioserver_span.data["http"]["header"]["X-Capture-That"] == "that" + agent.options.extra_http_headers = original_extra_http_headers + + def test_server_response_header_capture(self): + original_extra_http_headers = agent.options.extra_http_headers + + async def test(): + with tracer.start_as_current_span("test"): + async with aiohttp.ClientSession() as session: + # Hack together a manual custom headers list + agent.options.extra_http_headers = [ + "X-Capture-This-Too", + "X-Capture-That-Too", + ] + + return await self.fetch( + session, + testenv["aiohttp_server"] + "/response_headers" + ) + + response = self.loop.run_until_complete(test()) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + aioserver_span = spans[0] + aioclient_span = spans[1] + test_span = spans[2] + + # Same traceId + traceId = test_span.t + assert aioclient_span.t == traceId + assert aioserver_span.t == traceId + + # Parent relationships + assert aioclient_span.p == test_span.s + assert aioserver_span.p == aioclient_span.s + + # Error logging + assert not test_span.ec + assert not aioclient_span.ec + assert not aioserver_span.ec + + assert aioserver_span.n == "aiohttp-server" + assert aioserver_span.data["http"]["status"] == 200 + assert aioserver_span.data["http"]["url"] == f"{testenv['aiohttp_server']}/response_headers" + assert aioserver_span.data["http"]["method"] == "GET" + assert not aioserver_span.stack + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == hex_id(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == hex_id(aioserver_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == "1" + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" + + assert "X-Capture-This-Too" in aioserver_span.data["http"]["header"] + assert aioserver_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in aioserver_span.data["http"]["header"] + assert aioserver_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" + + agent.options.extra_http_headers = original_extra_http_headers + def test_server_get_401(self): async def test(): with tracer.start_as_current_span("test"): diff --git a/tests/frameworks/test_flask.py b/tests/frameworks/test_flask.py index 876fd2ba..d3a5f10e 100644 --- a/tests/frameworks/test_flask.py +++ b/tests/frameworks/test_flask.py @@ -20,7 +20,7 @@ from opentelemetry.trace import SpanKind import tests.apps.flask_app -from instana.singletons import tracer +from instana.singletons import tracer, agent from instana.span.span import get_current_span from tests.helpers import testenv @@ -1002,11 +1002,58 @@ def test_path_templates(self) -> None: # We should have a reported path template for this route assert "/users/{username}/sayhello" == wsgi_span.data["http"]["path_tpl"] + def test_request_header_capture(self) -> None: + # Hack together a manual custom headers list + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["flask_server"] + "/", headers=request_headers + ) + + assert response + assert response.status == 200 + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + wsgi_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + # Same traceId + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + + assert wsgi_span.ec is None + assert wsgi_span.stack is None + + assert "/" == wsgi_span.data["http"]["url"] + assert "GET" == wsgi_span.data["http"]["method"] + assert 200 == wsgi_span.data["http"]["status"] + + assert "X-Capture-This-Too" in wsgi_span.data["http"]["header"] + assert "this too" == wsgi_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in wsgi_span.data["http"]["header"] + assert "that too" == wsgi_span.data["http"]["header"]["X-Capture-That-Too"] + + agent.options.extra_http_headers = original_extra_http_headers + + def test_response_header_capture(self) -> None: # Hack together a manual custom headers list - from instana.singletons import agent original_extra_http_headers = agent.options.extra_http_headers - agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["flask_server"] + '/response_headers') diff --git a/tests/frameworks/test_starlette.py b/tests/frameworks/test_starlette.py index 1dd079f8..e332e024 100644 --- a/tests/frameworks/test_starlette.py +++ b/tests/frameworks/test_starlette.py @@ -23,6 +23,8 @@ def _resource(self) -> Generator[None, None, None]: agent.options.extra_http_headers = [ "X-Capture-This", "X-Capture-That", + "X-Capture-This-Too", + "X-Capture-That-Too" ] # Clear all spans before a test run. self.recorder = tracer.span_processor @@ -245,7 +247,7 @@ def test_synthetic_request(self) -> None: assert asgi_span.sy assert not test_span.sy - def test_custom_header_capture(self) -> None: + def test_request_header_capture(self) -> None: with tracer.start_as_current_span("test") as span: # As TestClient() is based on httpx, and we don't support it yet, # we must pass the SDK trace_id and span_id to the ASGI server. @@ -299,3 +301,56 @@ def test_custom_header_capture(self) -> None: assert "this" == asgi_span.data["http"]["header"]["X-Capture-This"] assert "X-Capture-That" in asgi_span.data["http"]["header"] assert "that" == asgi_span.data["http"]["header"]["X-Capture-That"] + + def test_response_header_capture(self) -> None: + with tracer.start_as_current_span("test") as span: + # As TestClient() is based on httpx, and we don't support it yet, + # we must pass the SDK trace_id and span_id to the ASGI server. + span_context = span.get_span_context() + headers = { + "X-INSTANA-T": hex_id(span_context.trace_id), + "X-INSTANA-S": hex_id(span_context.span_id), + } + result = self.client.get("/response_headers", headers=headers) + + assert result + assert "X-INSTANA-T" in result.headers + assert "X-INSTANA-S" in result.headers + assert "X-INSTANA-L" in result.headers + assert "Server-Timing" in result.headers + assert result.headers["X-INSTANA-L"] == "1" + + spans = self.recorder.queued_spans() + assert len(spans) == 2 + + span_filter = ( # noqa: E731 + lambda span: span.n == "sdk" and span.data["sdk"]["name"] == "test" + ) + test_span = get_first_span_by_filter(spans, span_filter) + assert test_span + + span_filter = lambda span: span.n == "asgi" # noqa: E731 + asgi_span = get_first_span_by_filter(spans, span_filter) + assert asgi_span + + assert test_span.t == asgi_span.t + assert test_span.s == asgi_span.p + + assert result.headers["X-INSTANA-T"] == hex_id(asgi_span.t) + assert result.headers["X-INSTANA-S"] == hex_id(asgi_span.s) + assert result.headers["X-INSTANA-L"] == "1" + assert result.headers["Server-Timing"] == f"intid;desc={hex_id(asgi_span.t)}" + + assert not asgi_span.ec + assert asgi_span.data["http"]["host"] == "testserver" + assert asgi_span.data["http"]["path"] == "/response_headers" + assert asgi_span.data["http"]["path_tpl"] == "/response_headers" + assert asgi_span.data["http"]["method"] == "GET" + assert asgi_span.data["http"]["status"] == 200 + assert not asgi_span.data["http"]["error"] + assert not asgi_span.data["http"]["params"] + + assert "X-Capture-This-Too" in asgi_span.data["http"]["header"] + assert "this too" == asgi_span.data["http"]["header"]["X-Capture-This-Too"] + assert "X-Capture-That-Too" in asgi_span.data["http"]["header"] + assert "that too" == asgi_span.data["http"]["header"]["X-Capture-That-Too"] diff --git a/tests/frameworks/test_tornado_client.py b/tests/frameworks/test_tornado_client.py index 20ba1f0f..2c93afdc 100644 --- a/tests/frameworks/test_tornado_client.py +++ b/tests/frameworks/test_tornado_client.py @@ -8,7 +8,7 @@ import tornado from tornado.httpclient import AsyncHTTPClient -from instana.singletons import tracer +from instana.singletons import tracer, agent from instana.span.span import get_current_span from instana.util.ids import hex_id @@ -460,3 +460,142 @@ async def test(): assert response.headers["X-INSTANA-L"] == '1' assert "Server-Timing" in response.headers assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" + + def test_request_header_capture(self) -> None: + # Hack together a manual custom headers list + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] + + request_headers = { + "X-Capture-This": "this", + "X-Capture-That": "that", + } + + async def test(): + with tracer.start_as_current_span("test"): + return await self.http_client.fetch(testenv["tornado_server"] + "/", headers=request_headers) + + response = tornado.ioloop.IOLoop.current().run_sync(test) + assert isinstance(response, tornado.httpclient.HTTPResponse) + + time.sleep(0.5) + spans = self.recorder.queued_spans() + + assert len(spans) == 3 + + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") + + assert not get_current_span().is_recording() + + # Same traceId + traceId = test_span.t + assert traceId == client_span.t + assert traceId == server_span.t + + # Parent relationships + assert client_span.p == test_span.s + assert server_span.p == client_span.s + + # Error logging + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == hex_id(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == '1' + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" + + assert "X-Capture-This" in client_span.data["http"]["header"] + assert client_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in client_span.data["http"]["header"] + assert client_span.data["http"]["header"]["X-Capture-That"] == "that" + + agent.options.extra_http_headers = original_extra_http_headers + + def test_response_header_capture(self) -> None: + # Hack together a manual custom headers list + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + + async def test(): + with tracer.start_as_current_span("test"): + return await self.http_client.fetch(testenv["tornado_server"] + "/response_headers") + + response = tornado.ioloop.IOLoop.current().run_sync(test) + assert isinstance(response, tornado.httpclient.HTTPResponse) + + time.sleep(0.5) + spans = self.recorder.queued_spans() + + assert len(spans) == 3 + + server_span = get_first_span_by_name(spans, "tornado-server") + client_span = get_first_span_by_name(spans, "tornado-client") + test_span = get_first_span_by_name(spans, "sdk") + + assert not get_current_span().is_recording() + + # Same traceId + traceId = test_span.t + assert traceId == client_span.t + assert traceId == server_span.t + + # Parent relationships + assert client_span.p == test_span.s + assert server_span.p == client_span.s + + # Error logging + assert not test_span.ec + assert not client_span.ec + assert not server_span.ec + + assert server_span.n == "tornado-server" + assert server_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/response_headers" == server_span.data["http"]["url"] + assert not server_span.data["http"]["params"] + assert server_span.data["http"]["method"] == "GET" + + assert client_span.n == "tornado-client" + assert client_span.data["http"]["status"] == 200 + assert testenv["tornado_server"] + "/response_headers" == client_span.data["http"]["url"] + assert client_span.data["http"]["method"] == "GET" + assert client_span.stack + assert type(client_span.stack) is list + assert len(client_span.stack) > 1 + + assert "X-INSTANA-T" in response.headers + assert response.headers["X-INSTANA-T"] == hex_id(traceId) + assert "X-INSTANA-S" in response.headers + assert response.headers["X-INSTANA-S"] == hex_id(server_span.s) + assert "X-INSTANA-L" in response.headers + assert response.headers["X-INSTANA-L"] == '1' + assert "Server-Timing" in response.headers + assert response.headers["Server-Timing"] == f"intid;desc={hex_id(traceId)}" + + assert "X-Capture-This-Too" in client_span.data["http"]["header"] + assert client_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in client_span.data["http"]["header"] + assert client_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" + + agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/frameworks/test_wsgi.py b/tests/frameworks/test_wsgi.py index e57f0485..882c5cfd 100644 --- a/tests/frameworks/test_wsgi.py +++ b/tests/frameworks/test_wsgi.py @@ -107,72 +107,6 @@ def test_synthetic_request(self) -> None: assert urllib3_span.sy is None assert test_span.sy is None - - def test_custom_header_capture(self) -> None: - # Hack together a manual custom headers list - agent.options.extra_http_headers = [u'X-Capture-This', u'X-Capture-That'] - - request_headers = {} - request_headers['X-Capture-This'] = 'this' - request_headers['X-Capture-That'] = 'that' - - with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/', headers=request_headers) - - spans = self.recorder.queued_spans() - - assert 3 == len(spans) - assert get_current_span().is_recording() is False - - wsgi_span = spans[0] - urllib3_span = spans[1] - test_span = spans[2] - - assert response - assert 200 == response.status - - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) - assert response.headers["X-INSTANA-T"] == hex_id(wsgi_span.t) - - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) - assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) - - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' - - assert 'Server-Timing' in response.headers - server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" - assert response.headers['Server-Timing'] == server_timing_value - - # Same traceId - assert test_span.t == urllib3_span.t - assert urllib3_span.t == wsgi_span.t - - # Parent relationships - assert urllib3_span.p == test_span.s - assert wsgi_span.p == urllib3_span.s - - # Error logging - assert test_span.ec is None - assert urllib3_span.ec is None - assert wsgi_span.ec is None - - # wsgi - assert "wsgi" == wsgi_span.n - assert '127.0.0.1:' + str(testenv["wsgi_port"]) == wsgi_span.data["http"]["host"] - assert '/' == wsgi_span.data["http"]["path"] - assert 'GET' == wsgi_span.data["http"]["method"] - assert "200" == wsgi_span.data["http"]["status"] - assert wsgi_span.data["http"]["error"] is None - assert wsgi_span.stack is None - - assert "X-Capture-This" in wsgi_span.data["http"]["header"] - assert "this" == wsgi_span.data["http"]["header"]["X-Capture-This"] - assert "X-Capture-That" in wsgi_span.data["http"]["header"] - assert "that" == wsgi_span.data["http"]["header"]["X-Capture-That"] - def test_secret_scrubbing(self) -> None: with tracer.start_as_current_span("test"): response = self.http.request('GET', testenv["wsgi_server"] + '/?secret=shhh') @@ -297,33 +231,114 @@ def test_with_incoming_mixed_case_context(self) -> None: server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" assert response.headers['Server-Timing'] == server_timing_value - def test_response_headers(self) -> None: + def test_response_header_capture(self) -> None: + # Hack together a manual custom headers list + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ["X-Capture-This", "X-Capture-That"] + with tracer.start_as_current_span("test"): - response = self.http.request('GET', testenv["wsgi_server"] + '/') + response = self.http.request( + "GET", testenv["wsgi_server"] + "/response_headers" + ) spans = self.recorder.queued_spans() - - assert 3 == len(spans) - assert get_current_span().is_recording() is False + assert len(spans) == 3 wsgi_span = spans[0] urllib3_span = spans[1] test_span = spans[2] assert response - assert 200 == response.status + assert response.status == 200 - assert 'X-INSTANA-T' in response.headers - assert int(response.headers['X-INSTANA-T'], 16) - assert response.headers["X-INSTANA-T"] == hex_id(wsgi_span.t) + # Same traceId + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t - assert 'X-INSTANA-S' in response.headers - assert int(response.headers['X-INSTANA-S'], 16) - assert response.headers["X-INSTANA-S"] == hex_id(wsgi_span.s) + # Parent relationships + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s - assert 'X-INSTANA-L' in response.headers - assert response.headers['X-INSTANA-L'] == '1' + # Synthetic + assert not wsgi_span.sy + assert not urllib3_span.sy + assert not test_span.sy - assert 'Server-Timing' in response.headers - server_timing_value = f"intid;desc={hex_id(wsgi_span.t)}" - assert response.headers['Server-Timing'] == server_timing_value + # Error logging + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec + + # wsgi + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["wsgi_port"]) + assert wsgi_span.data["http"]["path"] == "/response_headers" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == "200" + assert not wsgi_span.data["http"]["error"] + + # custom headers + assert "X-Capture-This" in wsgi_span.data["http"]["header"] + assert wsgi_span.data["http"]["header"]["X-Capture-This"] == "this" + assert "X-Capture-That" in wsgi_span.data["http"]["header"] + assert wsgi_span.data["http"]["header"]["X-Capture-That"] == "that" + + agent.options.extra_http_headers = original_extra_http_headers + + def test_request_header_capture(self) -> None: + original_extra_http_headers = agent.options.extra_http_headers + agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] + + request_headers = { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + } + + with tracer.start_as_current_span("test"): + response = self.http.request( + "GET", testenv["wsgi_server"] + "/", headers=request_headers + ) + + spans = self.recorder.queued_spans() + assert len(spans) == 3 + + wsgi_span = spans[0] + urllib3_span = spans[1] + test_span = spans[2] + + assert response.status == 200 + + # Same traceId + assert test_span.t == urllib3_span.t + assert urllib3_span.t == wsgi_span.t + + # Parent relationships + assert urllib3_span.p == test_span.s + assert wsgi_span.p == urllib3_span.s + + # Synthetic + assert not wsgi_span.sy + assert not urllib3_span.sy + assert not test_span.sy + + # Error logging + assert not test_span.ec + assert not urllib3_span.ec + assert not wsgi_span.ec + + # wsgi + assert wsgi_span.n == "wsgi" + assert wsgi_span.data["http"]["host"] == "127.0.0.1:" + str(testenv["wsgi_port"]) + assert wsgi_span.data["http"]["path"] == "/" + assert wsgi_span.data["http"]["method"] == "GET" + assert wsgi_span.data["http"]["status"] == "200" + assert not wsgi_span.data["http"]["error"] + assert not wsgi_span.stack + + # custom headers + assert "X-Capture-This-Too" in wsgi_span.data["http"]["header"] + assert wsgi_span.data["http"]["header"]["X-Capture-This-Too"] == "this too" + assert "X-Capture-That-Too" in wsgi_span.data["http"]["header"] + assert wsgi_span.data["http"]["header"]["X-Capture-That-Too"] == "that too" + + agent.options.extra_http_headers = original_extra_http_headers diff --git a/tests/util/test_traceutils.py b/tests/util/test_traceutils.py index a5505c1f..3cfc87c0 100644 --- a/tests/util/test_traceutils.py +++ b/tests/util/test_traceutils.py @@ -1,6 +1,6 @@ # (c) Copyright IBM Corp. 2024 -from unittest.mock import patch +import pytest from instana.singletons import agent, tracer from instana.tracer import InstanaTracer @@ -12,26 +12,61 @@ ) -def test_extract_custom_headers(span) -> None: +@pytest.mark.parametrize( + "custom_headers, format", + [ + ( + { + "X-Capture-This-Too": "this too", + "X-Capture-That-Too": "that too", + }, + False, + ), + ( + { + "HTTP_X_CAPTURE_THIS_TOO": "this too", + "HTTP_X_CAPTURE_THAT_TOO": "that too", + }, + True, + ), + ( + [("X-CAPTURE-THIS-TOO", "this too"), ("x-capture-that-too", "that too")], + False, + ), + ( + [ + (b"X-Capture-This-Too", b"this too"), + (b"X-Capture-That-Too", b"that too"), + ], + False, + ), + ( + [ + ("HTTP_X_CAPTURE_THIS_TOO", "this too"), + ("HTTP_X_CAPTURE_THAT_TOO", "that too"), + ], + True, + ), + ], +) +def test_extract_custom_headers(span, custom_headers, format) -> None: agent.options.extra_http_headers = ["X-Capture-This-Too", "X-Capture-That-Too"] - request_headers = { - "X-Capture-This-Too": "this too", - "X-Capture-That-Too": "that too", - } - extract_custom_headers(span, request_headers) + extract_custom_headers(span, custom_headers, format=format) assert len(span.attributes) == 2 assert span.attributes["http.header.X-Capture-This-Too"] == "this too" assert span.attributes["http.header.X-Capture-That-Too"] == "that too" -def test_get_activate_tracer() -> None: +def test_get_activate_tracer(mocker) -> None: assert not get_active_tracer() with tracer.start_as_current_span("test"): response = get_active_tracer() assert isinstance(response, InstanaTracer) assert response == tracer - with patch("instana.span.span.InstanaSpan.is_recording", return_value=False): + with mocker.patch( + "instana.span.span.InstanaSpan.is_recording", return_value=False + ): assert not get_active_tracer()