1
- # (c) Copyright IBM Corp. 2021
2
- # (c) Copyright Instana Inc. 2021
1
+ # (c) Copyright IBM Corp. 2024
3
2
4
3
"""
5
4
Instrumentation for Sanic
8
7
try :
9
8
import sanic
10
9
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
67
26
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 :
104
44
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 )
115
61
if hasattr (request , "url" ):
116
- scope . span .set_tag ( "http.url" , request .url )
62
+ span .set_attribute ( SpanAttributes . HTTP_URL , request .url )
117
63
118
64
query = request .query_string
119
65
120
66
if isinstance (query , (str , bytes )) and len (query ):
121
67
if isinstance (query , bytes ):
122
68
query = query .decode ('utf-8' )
123
69
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 )
126
72
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 )
130
75
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 )
140
126
141
127
except ImportError :
142
- pass
143
- except AttributeError :
144
- logger .debug ("Not supported Sanic version" )
128
+ pass
0 commit comments