|
| 1 | +""" |
| 2 | +Instana ASGI Middleware |
| 3 | +""" |
| 4 | +import opentracing |
| 5 | + |
| 6 | +from ..log import logger |
| 7 | +from ..singletons import async_tracer, agent |
| 8 | +from ..util import strip_secrets_from_query |
| 9 | + |
| 10 | +class InstanaASGIMiddleware: |
| 11 | + """ |
| 12 | + Instana ASGI Middleware |
| 13 | + """ |
| 14 | + def __init__(self, app): |
| 15 | + self.app = app |
| 16 | + |
| 17 | + def _extract_custom_headers(self, span, headers): |
| 18 | + try: |
| 19 | + for custom_header in agent.options.extra_http_headers: |
| 20 | + # Headers are in the following format: b'x-header-1' |
| 21 | + for header_pair in headers: |
| 22 | + if header_pair[0].decode('utf-8').lower() == custom_header.lower(): |
| 23 | + span.set_tag("http.%s" % custom_header, header_pair[1].decode('utf-8')) |
| 24 | + except Exception: |
| 25 | + logger.debug("extract_custom_headers: ", exc_info=True) |
| 26 | + |
| 27 | + def _collect_kvs(self, scope, span): |
| 28 | + try: |
| 29 | + span.set_tag('http.path', scope.get('path')) |
| 30 | + span.set_tag('http.method', scope.get('method')) |
| 31 | + |
| 32 | + server = scope.get('server') |
| 33 | + if isinstance(server, tuple): |
| 34 | + span.set_tag('http.host', server[0]) |
| 35 | + |
| 36 | + query = scope.get('query_string') |
| 37 | + if isinstance(query, (str, bytes)) and len(query): |
| 38 | + if isinstance(query, bytes): |
| 39 | + query = query.decode('utf-8') |
| 40 | + scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, agent.options.secrets_list) |
| 41 | + span.set_tag("http.params", scrubbed_params) |
| 42 | + |
| 43 | + app = scope.get('app') |
| 44 | + if app is not None and hasattr(app, 'routes'): |
| 45 | + # Attempt to detect the Starlette routes registered. |
| 46 | + # If Starlette isn't present, we harmlessly dump out. |
| 47 | + from starlette.routing import Match |
| 48 | + for route in scope['app'].routes: |
| 49 | + if route.matches(scope)[0] == Match.FULL: |
| 50 | + span.set_tag("http.path_tpl", route.path) |
| 51 | + except Exception: |
| 52 | + logger.debug("ASGI collect_kvs: ", exc_info=True) |
| 53 | + |
| 54 | + |
| 55 | + async def __call__(self, scope, receive, send): |
| 56 | + request_context = None |
| 57 | + |
| 58 | + if scope["type"] not in ("http", "websocket"): |
| 59 | + await self.app(scope, receive, send) |
| 60 | + return |
| 61 | + |
| 62 | + request_headers = scope.get('headers') |
| 63 | + if isinstance(request_headers, list): |
| 64 | + request_context = async_tracer.extract(opentracing.Format.BINARY, request_headers) |
| 65 | + |
| 66 | + async def send_wrapper(response): |
| 67 | + span = async_tracer.active_span |
| 68 | + if span is None: |
| 69 | + await send(response) |
| 70 | + else: |
| 71 | + if response['type'] == 'http.response.start': |
| 72 | + try: |
| 73 | + status_code = response.get('status') |
| 74 | + if status_code is not None: |
| 75 | + if 500 <= int(status_code) <= 511: |
| 76 | + span.mark_as_errored() |
| 77 | + span.set_tag('http.status_code', status_code) |
| 78 | + |
| 79 | + headers = response.get('headers') |
| 80 | + if headers is not None: |
| 81 | + async_tracer.inject(span.context, opentracing.Format.BINARY, headers) |
| 82 | + except Exception: |
| 83 | + logger.debug("send_wrapper: ", exc_info=True) |
| 84 | + |
| 85 | + try: |
| 86 | + await send(response) |
| 87 | + except Exception as exc: |
| 88 | + span.log_exception(exc) |
| 89 | + raise |
| 90 | + |
| 91 | + with async_tracer.start_active_span("asgi", child_of=request_context) as tracing_scope: |
| 92 | + self._collect_kvs(scope, tracing_scope.span) |
| 93 | + if 'headers' in scope and agent.options.extra_http_headers is not None: |
| 94 | + self._extract_custom_headers(tracing_scope.span, scope['headers']) |
| 95 | + |
| 96 | + try: |
| 97 | + await self.app(scope, receive, send_wrapper) |
| 98 | + except Exception as exc: |
| 99 | + tracing_scope.span.log_exception(exc) |
| 100 | + raise exc |
0 commit comments