Skip to content

Commit 49ea512

Browse files
GSVarshapvital
authored andcommitted
feat: enable auto-instrumentation with middleware for sanic
Signed-off-by: Varsha GS <[email protected]> (cherry picked from commit fb29c7931675f8508743917f7188eab199dca9f5)
1 parent 892992e commit 49ea512

File tree

3 files changed

+108
-124
lines changed

3 files changed

+108
-124
lines changed

src/instana/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def boot_agent():
181181
# redis, # noqa: F401
182182
# sqlalchemy, # noqa: F401
183183
starlette_inst, # noqa: F401
184-
# sanic_inst, # noqa: F401
184+
sanic_inst, # noqa: F401
185185
urllib3, # noqa: F401
186186
)
187187
from instana.instrumentation.aiohttp import (
Lines changed: 106 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
# (c) Copyright IBM Corp. 2021
2-
# (c) Copyright Instana Inc. 2021
1+
# (c) Copyright IBM Corp. 2024
32

43
"""
54
Instrumentation for Sanic
@@ -8,137 +7,122 @@
87
try:
98
import sanic
109
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-
try:
21-
message = kwargs.get("message") or args[0]
22-
status_code = kwargs.get("status_code")
23-
span = async_tracer.active_span
24-
25-
if all([span, status_code, message]) and 500 <= status_code:
26-
span.set_tag("http.error", message)
27-
try:
28-
wrapped(*args, **kwargs)
29-
except Exception as exc:
30-
span.log_exception(exc)
31-
else:
32-
wrapped(*args, **kwargs)
33-
except Exception:
34-
logger.debug("exception_with_instana: ", exc_info=True)
35-
wrapped(*args, **kwargs)
36-
37-
38-
def response_details(span, response):
39-
try:
40-
status_code = response.status
41-
if status_code is not None:
42-
if 500 <= int(status_code):
43-
span.mark_as_errored()
44-
span.set_tag('http.status_code', status_code)
45-
46-
if response.headers is not None:
47-
extract_custom_headers(span, response.headers)
48-
async_tracer.inject(span.context, opentracing.Format.HTTP_HEADERS, response.headers)
49-
response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id
50-
except Exception:
51-
logger.debug("send_wrapper: ", exc_info=True)
52-
53-
54-
if hasattr(sanic.response.BaseHTTPResponse, "send"):
55-
@wrapt.patch_function_wrapper('sanic.response', 'BaseHTTPResponse.send')
56-
async def send_with_instana(wrapped, instance, args, kwargs):
57-
span = async_tracer.active_span
58-
if span is None:
59-
await wrapped(*args, **kwargs)
60-
else:
61-
response_details(span=span, response=instance)
62-
try:
63-
await wrapped(*args, **kwargs)
64-
except Exception as exc:
65-
span.log_exception(exc)
66-
raise
10+
from typing import Callable, Tuple, Dict, Any
11+
from sanic.exceptions import SanicException
12+
13+
from opentelemetry import context, trace
14+
from opentelemetry.trace import SpanKind
15+
from opentelemetry.semconv.trace import SpanAttributes
16+
17+
from instana.log import logger
18+
from instana.singletons import tracer, agent
19+
from instana.util.secrets import strip_secrets_from_query
20+
from instana.util.traceutils import extract_custom_headers
21+
from instana.propagators.format import Format
22+
23+
if hasattr(sanic.request, "types"):
24+
from sanic.request.types import Request
25+
from sanic.response.types import HTTPResponse
6726
else:
68-
@wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.write_response')
69-
def write_with_instana(wrapped, instance, args, kwargs):
70-
response = args[0]
71-
span = async_tracer.active_span
72-
if span is None:
73-
wrapped(*args, **kwargs)
74-
else:
75-
response_details(span=span, response=response)
76-
try:
77-
wrapped(*args, **kwargs)
78-
except Exception as exc:
79-
span.log_exception(exc)
80-
raise
81-
82-
83-
@wrapt.patch_function_wrapper('sanic.server', 'HttpProtocol.stream_response')
84-
async def stream_with_instana(wrapped, instance, args, kwargs):
85-
response = args[0]
86-
span = async_tracer.active_span
87-
if span is None:
88-
await wrapped(*args, **kwargs)
89-
else:
90-
response_details(span=span, response=response)
91-
try:
92-
await wrapped(*args, **kwargs)
93-
except Exception as exc:
94-
span.log_exception(exc)
95-
raise
96-
97-
98-
@wrapt.patch_function_wrapper('sanic.app', 'Sanic.handle_request')
99-
async def handle_request_with_instana(wrapped, instance, args, kwargs):
100-
101-
try:
102-
request = args[0]
103-
try: # scheme attribute is calculated in the sanic handle_request method for v19, not yet present
27+
from sanic.request import Request
28+
from sanic.response import HTTPResponse
29+
30+
31+
@wrapt.patch_function_wrapper("sanic.app", "Sanic.__init__")
32+
def init_with_instana(
33+
wrapped: Callable[..., sanic.app.Sanic.__init__],
34+
instance: sanic.app.Sanic,
35+
args: Tuple[object, ...],
36+
kwargs: Dict[str, Any],
37+
) -> None:
38+
wrapped(*args, **kwargs)
39+
app = instance
40+
41+
@app.middleware("request")
42+
def request_with_instana(request: Request) -> None:
43+
try:
10444
if "http" not in request.scheme:
105-
return await wrapped(*args, **kwargs)
106-
except AttributeError:
107-
pass
108-
headers = request.headers.copy()
109-
ctx = async_tracer.extract(opentracing.Format.HTTP_HEADERS, headers)
110-
with async_tracer.start_active_span("asgi", child_of=ctx) as scope:
111-
scope.span.set_tag('span.kind', 'entry')
112-
scope.span.set_tag('http.path', request.path)
113-
scope.span.set_tag('http.method', request.method)
114-
scope.span.set_tag('http.host', request.host)
45+
return
46+
47+
headers = request.headers.copy()
48+
parent_context = tracer.extract(Format.HTTP_HEADERS, headers)
49+
50+
span = tracer.start_span("asgi", span_context=parent_context)
51+
request.ctx.span = span
52+
53+
ctx = trace.set_span_in_context(span)
54+
token = context.attach(ctx)
55+
request.ctx.token = token
56+
57+
span.set_attribute('span.kind', SpanKind.CLIENT)
58+
span.set_attribute('http.path', request.path)
59+
span.set_attribute(SpanAttributes.HTTP_METHOD, request.method)
60+
span.set_attribute(SpanAttributes.HTTP_HOST, request.host)
11561
if hasattr(request, "url"):
116-
scope.span.set_tag("http.url", request.url)
62+
span.set_attribute(SpanAttributes.HTTP_URL, request.url)
11763

11864
query = request.query_string
11965

12066
if isinstance(query, (str, bytes)) and len(query):
12167
if isinstance(query, bytes):
12268
query = query.decode('utf-8')
12369
scrubbed_params = strip_secrets_from_query(query, agent.options.secrets_matcher,
124-
agent.options.secrets_list)
125-
scope.span.set_tag("http.params", scrubbed_params)
70+
agent.options.secrets_list)
71+
span.set_attribute("http.params", scrubbed_params)
12672

127-
if agent.options.extra_http_headers is not None:
128-
extract_custom_headers(scope.span, headers)
129-
await wrapped(*args, **kwargs)
73+
if agent.options.extra_http_headers:
74+
extract_custom_headers(span, headers)
13075
if hasattr(request, "uri_template") and request.uri_template:
131-
scope.span.set_tag("http.path_tpl", request.uri_template)
132-
if hasattr(request, "ctx"): # ctx attribute added in the latest v19 versions
133-
request.ctx.iscope = scope
134-
except Exception as e:
135-
logger.debug("Sanic framework @ handle_request", exc_info=True)
136-
return await wrapped(*args, **kwargs)
137-
138-
139-
logger.debug("Instrumenting Sanic")
76+
span.set_attribute("http.path_tpl", request.uri_template)
77+
except Exception:
78+
logger.debug("request_with_instana: ", exc_info=True)
79+
80+
81+
@app.exception(Exception)
82+
def exception_with_instana(request: Request, exception: Exception) -> None:
83+
try:
84+
if not hasattr(request.ctx, "span"):
85+
return
86+
span = request.ctx.span
87+
88+
if isinstance(exception, SanicException):
89+
# Handle Sanic-specific exceptions
90+
status_code = exception.status_code
91+
message = str(exception)
92+
93+
if all([span, status_code, message]) and 500 <= status_code:
94+
span.set_attribute("http.error", message)
95+
except Exception:
96+
logger.debug("exception_with_instana: ", exc_info=True)
97+
98+
99+
@app.middleware("response")
100+
def response_with_instana(request: Request, response: HTTPResponse) -> None:
101+
try:
102+
if not hasattr(request.ctx, "span"):
103+
return
104+
span = request.ctx.span
105+
106+
status_code = response.status
107+
if status_code:
108+
if int(status_code) >= 500:
109+
span.mark_as_errored()
110+
span.set_attribute('http.status_code', status_code)
111+
112+
if hasattr(response, "headers"):
113+
extract_custom_headers(span, response.headers)
114+
tracer.inject(span.context, Format.HTTP_HEADERS, response.headers)
115+
response.headers['Server-Timing'] = "intid;desc=%s" % span.context.trace_id
116+
117+
if span.is_recording():
118+
span.end()
119+
request.ctx.span = None
120+
121+
if request.ctx.token:
122+
context.detach(request.ctx.token)
123+
request.ctx.token = None
124+
except Exception:
125+
logger.debug("response_with_instana: ", exc_info=True)
140126

141127
except ImportError:
142-
pass
143-
except AttributeError:
144-
logger.debug("Not supported Sanic version")
128+
pass

src/instana/util/traceutils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def extract_custom_headers(tracing_span, headers) -> None:
1515
# Headers are in the following format: b'x-header-1'
1616
for header_key, value in headers.items():
1717
if header_key.lower() == custom_header.lower():
18-
tracing_span.set_tag("http.header.%s" % custom_header, value)
18+
tracing_span.set_attribute("http.header.%s" % custom_header, value)
1919
except Exception:
2020
logger.debug("extract_custom_headers: ", exc_info=True)
2121

0 commit comments

Comments
 (0)