|
| 1 | +# (c) Copyright IBM Corp. 2021 |
| 2 | +# (c) Copyright Instana Inc. 2021 |
| 3 | + |
| 4 | +""" |
| 5 | +Instrumentation for Sanic |
| 6 | +https://sanicframework.org/en/ |
| 7 | +""" |
| 8 | +try: |
| 9 | + import sanic |
| 10 | + import wrapt |
| 11 | + import opentracing |
| 12 | + from ..log import logger |
| 13 | + from ..singletons import async_tracer, agent |
| 14 | + from ..util.secrets import strip_secrets_from_query |
| 15 | + from ..util.traceutils import extract_custom_headers |
| 16 | + |
| 17 | + |
| 18 | + @wrapt.patch_function_wrapper('sanic.exceptions', 'SanicException.__init__') |
| 19 | + def exception_with_instana(wrapped, instance, args, kwargs): |
| 20 | + message = kwargs.get("message", args[0]) |
| 21 | + status_code = kwargs.get("status_code") |
| 22 | + span = async_tracer.active_span |
| 23 | + |
| 24 | + if all([span, status_code, message]) and (500 <= status_code <= 599): |
| 25 | + span.set_tag("http.error", message) |
| 26 | + try: |
| 27 | + wrapped(*args, **kwargs) |
| 28 | + except Exception as exc: |
| 29 | + span.log_exception(exc) |
| 30 | + else: |
| 31 | + wrapped(*args, **kwargs) |
| 32 | + |
| 33 | + |
| 34 | + def response_details(span, response): |
| 35 | + try: |
| 36 | + status_code = response.status |
| 37 | + if status_code is not None: |
| 38 | + if 500 <= int(status_code) <= 511: |
| 39 | + span.mark_as_errored() |
| 40 | + span.set_tag('http.status_code', status_code) |
| 41 | + |
| 42 | + if response.headers is not None: |
| 43 | + async_tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, response.headers) |
| 44 | + response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id |
| 45 | + except Exception: |
| 46 | + logger.debug("send_wrapper: ", exc_info=True) |
| 47 | + |
| 48 | + |
| 49 | + if hasattr(sanic.response.BaseHTTPResponse, "send"): |
| 50 | + @wrapt.patch_function_wrapper('sanic.response', 'BaseHTTPResponse.send') |
| 51 | + async def send_with_instana(wrapped, instance, args, kwargs): |
| 52 | + span = async_tracer.active_span |
| 53 | + if span is None: |
| 54 | + await wrapped(*args, **kwargs) |
| 55 | + else: |
| 56 | + response_details(span=span, response=instance) |
| 57 | + try: |
| 58 | + await wrapped(*args, **kwargs) |
| 59 | + except Exception as exc: |
| 60 | + span.log_exception(exc) |
| 61 | + raise |
| 62 | + else: |
| 63 | + @wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.write_response') |
| 64 | + def write_with_instana(wrapped, instance, args, kwargs): |
| 65 | + response = args[0] |
| 66 | + span = async_tracer.active_span |
| 67 | + if span is None: |
| 68 | + wrapped(*args, **kwargs) |
| 69 | + else: |
| 70 | + response_details(span=span, response=response) |
| 71 | + try: |
| 72 | + wrapped(*args, **kwargs) |
| 73 | + except Exception as exc: |
| 74 | + span.log_exception(exc) |
| 75 | + raise |
| 76 | + |
| 77 | + |
| 78 | + @wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.stream_response') |
| 79 | + async def stream_with_instana(wrapped, instance, args, kwargs): |
| 80 | + response = args[0] |
| 81 | + span = async_tracer.active_span |
| 82 | + if span is None: |
| 83 | + await wrapped(*args, **kwargs) |
| 84 | + else: |
| 85 | + response_details(span=span, response=response) |
| 86 | + try: |
| 87 | + await wrapped(*args, **kwargs) |
| 88 | + except Exception as exc: |
| 89 | + span.log_exception(exc) |
| 90 | + raise |
| 91 | + |
| 92 | + |
| 93 | + @wrapt.patch_function_wrapper('sanic.app', 'Sanic.handle_request') |
| 94 | + async def handle_request_with_instana(wrapped, instance, args, kwargs): |
| 95 | + |
| 96 | + try: |
| 97 | + request = args[0] |
| 98 | + try: # scheme attribute is calculated in the sanic handle_request method for v19, not yet present |
| 99 | + if "http" not in request.scheme: |
| 100 | + return await wrapped(*args, **kwargs) |
| 101 | + except AttributeError: |
| 102 | + pass |
| 103 | + headers = request.headers.copy() |
| 104 | + ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, headers) |
| 105 | + with async_tracer.start_active_span("asgi", child_of=ctx) as scope: |
| 106 | + scope.span.set_tag('span.kind', 'entry') |
| 107 | + scope.span.set_tag('http.path', request.path) |
| 108 | + scope.span.set_tag('http.method', request.method) |
| 109 | + scope.span.set_tag('http.host', request.host) |
| 110 | + scope.span.set_tag("http.url", request.url) |
| 111 | + |
| 112 | + query = request.query_string |
| 113 | + |
| 114 | + if isinstance(query, (str, bytes)) and len(query): |
| 115 | + if isinstance(query, bytes): |
| 116 | + query = query.decode('utf-8') |
| 117 | + scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher, |
| 118 | + agent.options.secrets_list) |
| 119 | + scope.span.set_tag("http.params", scrubbed_params) |
| 120 | + |
| 121 | + if agent.options.extra_http_headers is not None: |
| 122 | + extract_custom_headers(scope, headers) |
| 123 | + await wrapped(*args, **kwargs) |
| 124 | + if hasattr(request, "uri_template"): |
| 125 | + scope.span.set_tag("http.path_tpl", request.uri_template) |
| 126 | + if hasattr(request, "ctx"): # ctx attribute added in the latest v19 versions |
| 127 | + request.ctx.iscope = scope |
| 128 | + except Exception as e: |
| 129 | + logger.debug("Sanic framework @ handle_request", exc_info=True) |
| 130 | + return await wrapped(*args, **kwargs) |
| 131 | + |
| 132 | + |
| 133 | + logger.debug("Instrumenting Sanic") |
| 134 | + |
| 135 | +except ImportError: |
| 136 | + pass |
| 137 | +except AttributeError: |
| 138 | + logger.debug("Not supported Sanic version") |
0 commit comments