slowpoke-python 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
slowpoke/_tracer.py ADDED
@@ -0,0 +1,235 @@
1
+ import contextvars
2
+ import json
3
+ import math
4
+ import os
5
+ import re
6
+ import sys
7
+ import time
8
+
9
+ VERSION = "0.1.3"
10
+
11
+ SERVER = 2
12
+ CLIENT = 3
13
+ CONSUMER = 5
14
+
15
+ # The trace of the request or job running in this thread or asyncio task. Worker threads and
16
+ # sync_to_async/run_in_threadpool copy the context, so their queries land in the same trace object.
17
+ _current = contextvars.ContextVar("slowpoke_trace", default=None)
18
+
19
+ _STATEMENT = re.compile(r"[\s(]*([A-Za-z]+)")
20
+ _DB_SYSTEMS = {"postgres": "postgresql", "mssql": "microsoft.sql_server", "sqlserver": "microsoft.sql_server"}
21
+
22
+
23
+ def current():
24
+ return _current.get()
25
+
26
+
27
+ def _random_id(size):
28
+ return os.urandom(size).hex()
29
+
30
+
31
+ class Trace:
32
+ __slots__ = ("kind", "name", "start", "end", "error", "trace_id", "span_id", "attributes", "queries", "dropped", "task")
33
+
34
+ def __init__(self, kind, name, start, trace_id, span_id):
35
+ self.kind = kind
36
+ self.name = name
37
+ self.start = start
38
+ self.end = None
39
+ self.error = False
40
+ self.trace_id = trace_id
41
+ self.span_id = span_id
42
+ self.attributes = []
43
+ self.queries = []
44
+ self.dropped = 0
45
+ self.task = _running_task()
46
+
47
+
48
+ class Tracer:
49
+ """Collects one trace per HTTP request or job and turns it into OTLP/JSON for the agent: a SERVER
50
+ (or CONSUMER) span and one CLIENT span per query, with the SQL as the driver received it
51
+ (placeholders, never parameter values) and the application line that ran it.
52
+
53
+ Every public method swallows its own errors: observability must never break the app."""
54
+
55
+ def __init__(self, origin, submit, service="app", version=VERSION, max_queries=500, max_sql_length=10000,
56
+ clock=time.time, ids=_random_id):
57
+ self.origin = origin
58
+ self.submit = submit
59
+ self.service = service
60
+ self.version = version
61
+ self.max_queries = max(0, int(max_queries))
62
+ self.max_sql_length = max(1, int(max_sql_length))
63
+ self.clock = clock
64
+ self.ids = ids
65
+
66
+ # ------------------------------------------------------------ requests
67
+
68
+ def start_request(self, method):
69
+ try:
70
+ method = str(method).upper()
71
+ trace = Trace(SERVER, method, self.clock(), self.ids(16), self.ids(8))
72
+ trace.attributes.append(_kv("http.request.method", method))
73
+ _current.set(trace)
74
+ return trace
75
+ except Exception:
76
+ return None
77
+
78
+ def finish_request(self, trace, route, path, status):
79
+ if trace is None or trace.end is not None:
80
+ return
81
+ try:
82
+ trace.end = self.clock()
83
+ if route:
84
+ route = "/" + str(route).lstrip("/")
85
+ trace.name = trace.name + " " + route
86
+ trace.attributes.append(_kv("http.route", route))
87
+ else:
88
+ trace.attributes.append(_kv("url.path", "/" + str(path or "").split("?", 1)[0].lstrip("/")))
89
+ status = int(status)
90
+ trace.attributes.append(_kv("http.response.status_code", status))
91
+ trace.error = status >= 500
92
+ except Exception:
93
+ trace.end = trace.end or self.clock()
94
+ self._done(trace)
95
+
96
+ # ------------------------------------------------------------ jobs
97
+
98
+ def start_job(self, name, queue=None):
99
+ return self._start_background(name, "job", queue)
100
+
101
+ def start_command(self, name):
102
+ """A command run by cron: nobody waits for it, which is why nobody notices when it doubles."""
103
+ return self._start_background(name, "command", None)
104
+
105
+ def _start_background(self, name, kind, queue):
106
+ running = _current.get()
107
+ if running is not None and running.end is None:
108
+ return None # work run inside a request or another job belongs to it
109
+ try:
110
+ trace = Trace(CONSUMER, str(name), self.clock(), self.ids(16), self.ids(8))
111
+ # The agent files it under Jobs by this attribute, instead of among the endpoints.
112
+ trace.attributes.append(_kv("slowpoke.kind", kind))
113
+ if queue:
114
+ trace.attributes.append(_kv("messaging.destination.name", str(queue)))
115
+ _current.set(trace)
116
+ return trace
117
+ except Exception:
118
+ return None
119
+
120
+ def finish_job(self, trace, failed=False):
121
+ if trace is None or trace.end is not None:
122
+ return
123
+ trace.end = self.clock()
124
+ trace.error = bool(failed)
125
+ self._done(trace)
126
+
127
+ # ------------------------------------------------------------ queries
128
+
129
+ def record_query(self, sql, seconds, system):
130
+ """Called right after a statement ran, from the thread that ran it."""
131
+ trace = _current.get()
132
+ if trace is None or trace.end is not None:
133
+ return # outside requests and jobs, or after the response
134
+ try:
135
+ if len(trace.queries) >= self.max_queries:
136
+ trace.dropped += 1
137
+ return
138
+ end = self.clock()
139
+ sql = sql if isinstance(sql, str) else str(sql)
140
+ trace.queries.append((
141
+ sql[:self.max_sql_length],
142
+ system,
143
+ max(trace.start, end - max(0.0, seconds)),
144
+ end,
145
+ self.origin.find(trace.task),
146
+ ))
147
+ except Exception:
148
+ pass # never let observability break the query that was just run
149
+
150
+ # ------------------------------------------------------------ sending
151
+
152
+ def _done(self, trace):
153
+ trace.task = None # the queued trace must not keep the request's coroutine alive
154
+ try:
155
+ if _current.get() is trace:
156
+ _current.set(None)
157
+ except Exception:
158
+ pass
159
+ try:
160
+ self.submit(trace)
161
+ except Exception:
162
+ pass # the agent is optional: a missing or broken one costs a trace, nothing else
163
+
164
+ def encode(self, t):
165
+ """The OTLP/JSON payload of a finished trace. Runs on the background thread."""
166
+ root = {
167
+ "traceId": t.trace_id,
168
+ "spanId": t.span_id,
169
+ "name": t.name,
170
+ "kind": t.kind,
171
+ "startTimeUnixNano": _nanos(t.start),
172
+ "endTimeUnixNano": _nanos(t.end),
173
+ "attributes": list(t.attributes),
174
+ }
175
+ if t.dropped > 0:
176
+ root["attributes"].append(_kv("slowpoke.dropped_queries", t.dropped))
177
+ if t.error:
178
+ root["status"] = {"code": 2}
179
+ spans = [root]
180
+ for sql, system, start, end, origin in list(t.queries):
181
+ attributes = [_kv("db.system.name", _DB_SYSTEMS.get(system, system)), _kv("db.query.text", sql)]
182
+ if origin is not None:
183
+ attributes.append(_kv("code.file.path", origin[0]))
184
+ if origin[1] is not None:
185
+ attributes.append(_kv("code.line.number", origin[1]))
186
+ m = _STATEMENT.match(sql)
187
+ spans.append({
188
+ "traceId": t.trace_id,
189
+ "spanId": self.ids(8),
190
+ "parentSpanId": t.span_id,
191
+ "name": m.group(1).upper() if m else "QUERY",
192
+ "kind": CLIENT,
193
+ "startTimeUnixNano": _nanos(start),
194
+ "endTimeUnixNano": _nanos(end),
195
+ "attributes": attributes,
196
+ })
197
+ return {"resourceSpans": [{
198
+ "resource": {"attributes": [
199
+ _kv("service.name", self.service),
200
+ _kv("telemetry.sdk.name", "slowpoke-python"),
201
+ _kv("telemetry.sdk.language", "python"),
202
+ _kv("telemetry.sdk.version", self.version),
203
+ ]},
204
+ "scopeSpans": [{
205
+ "scope": {"name": "slowpoke", "version": self.version},
206
+ "spans": spans,
207
+ }],
208
+ }]}
209
+
210
+ def encode_json(self, trace):
211
+ text = json.dumps(self.encode(trace), ensure_ascii=False, separators=(",", ":"))
212
+ return text.encode("utf-8", "replace")
213
+
214
+
215
+ def _running_task():
216
+ asyncio = sys.modules.get("asyncio")
217
+ if asyncio is None:
218
+ return None
219
+ try:
220
+ return asyncio.current_task() if asyncio._get_running_loop() is not None else None
221
+ except Exception:
222
+ return None
223
+
224
+
225
+ def _nanos(seconds):
226
+ # 64-bit integers travel as strings. Microseconds, as the PHP packages, and rounded the same
227
+ # way: Python's round() goes to the even digit (round(0.5) is 0), math.floor(x + 0.5) does not,
228
+ # and the same measure must produce the same trace in every language.
229
+ return "%d000" % math.floor(seconds * 1e6 + 0.5)
230
+
231
+
232
+ def _kv(key, value):
233
+ if isinstance(value, int) and not isinstance(value, bool):
234
+ return {"key": key, "value": {"intValue": str(value)}}
235
+ return {"key": key, "value": {"stringValue": str(value)}}
slowpoke/asgi.py ADDED
@@ -0,0 +1,77 @@
1
+ """ASGI integration for FastAPI and Starlette:
2
+
3
+ from slowpoke.asgi import SlowpokeMiddleware
4
+ app.add_middleware(SlowpokeMiddleware) # add it last: it becomes the outermost middleware
5
+
6
+ Plain ASGI, not BaseHTTPMiddleware: the endpoint runs in the same task and context as the trace.
7
+ """
8
+
9
+ import slowpoke
10
+
11
+
12
+ class SlowpokeMiddleware:
13
+ def __init__(self, app):
14
+ self.app = app
15
+
16
+ async def __call__(self, scope, receive, send):
17
+ tracer = slowpoke.get_tracer() if scope.get("type") == "http" else None
18
+ trace = tracer.start_request(scope.get("method", "GET")) if tracer is not None else None
19
+ if trace is None:
20
+ return await self.app(scope, receive, send)
21
+
22
+ path = scope.get("path", "/")
23
+ root_path = scope.get("root_path", "")
24
+ status = []
25
+
26
+ async def send_with_status(message):
27
+ if message.get("type") == "http.response.start" and not status:
28
+ status.append(message.get("status", 200))
29
+ await send(message)
30
+
31
+ try:
32
+ await self.app(scope, receive, send_with_status)
33
+ except BaseException:
34
+ _finish(tracer, trace, scope, root_path, path, 500)
35
+ raise
36
+ _finish(tracer, trace, scope, root_path, path, status[0] if status else 500)
37
+
38
+
39
+ def _finish(tracer, trace, scope, root_path, path, status):
40
+ try:
41
+ route = route_template(scope, root_path)
42
+ except Exception:
43
+ route = None
44
+ tracer.finish_request(trace, route, path, status)
45
+
46
+
47
+ def route_template(scope, initial_root_path=""):
48
+ """The template of the route that handled the request, read after routing: FastAPI leaves the route
49
+ in scope["route"]; plain Starlette only the endpoint, looked up in the app's routes."""
50
+ route = scope.get("route")
51
+ template = getattr(route, "path", None)
52
+ if isinstance(template, str):
53
+ # A mounted sub-application knows its routes without the mount prefix.
54
+ final_root = scope.get("root_path", "") or ""
55
+ prefix = final_root[len(initial_root_path):] if final_root.startswith(initial_root_path) else ""
56
+ if prefix and not template.startswith(prefix + "/"):
57
+ template = prefix.rstrip("/") + template
58
+ return template
59
+ endpoint = scope.get("endpoint")
60
+ app = scope.get("app")
61
+ if endpoint is None or app is None:
62
+ return None
63
+ return _find_endpoint(getattr(app, "routes", None) or [], endpoint, "", 0)
64
+
65
+
66
+ def _find_endpoint(routes, endpoint, prefix, depth):
67
+ if depth > 8:
68
+ return None
69
+ for r in routes:
70
+ if getattr(r, "endpoint", None) is endpoint and isinstance(getattr(r, "path", None), str):
71
+ return prefix + r.path
72
+ children = getattr(r, "routes", None)
73
+ if children and isinstance(getattr(r, "path", None), str):
74
+ found = _find_endpoint(children, endpoint, prefix + r.path, depth + 1)
75
+ if found is not None:
76
+ return found
77
+ return None
slowpoke/celery.py ADDED
@@ -0,0 +1,67 @@
1
+ """Celery integration: one trace per task a worker runs.
2
+
3
+ # wherever the Celery app is created
4
+ import slowpoke.celery
5
+
6
+ slowpoke.celery.install()
7
+
8
+ A task dispatched from a request is not traced there: it is traced where it runs, in the worker.
9
+ """
10
+
11
+ import slowpoke
12
+
13
+ # Bounded on purpose: a worker that somehow never sees the end of its tasks must not grow a map
14
+ # forever. Past this many open tasks the next ones are simply not traced.
15
+ MAX_OPEN = 512
16
+
17
+ _open = {}
18
+ _state = {"installed": False}
19
+
20
+
21
+ def install():
22
+ """Connects the worker signals. Idempotent, and never raises: a monitoring package must not be
23
+ able to stop a worker from starting."""
24
+ try:
25
+ if _state["installed"]:
26
+ return True
27
+ from celery import signals
28
+
29
+ signals.task_prerun.connect(_prerun, dispatch_uid="slowpoke")
30
+ signals.task_postrun.connect(_postrun, dispatch_uid="slowpoke")
31
+ _state["installed"] = True
32
+ return True
33
+ except Exception:
34
+ return False
35
+
36
+
37
+ def _prerun(task_id=None, task=None, **kwargs):
38
+ try:
39
+ tracer = slowpoke.get_tracer()
40
+ if tracer is None or task_id is None or len(_open) >= MAX_OPEN:
41
+ return
42
+ trace = tracer.start_job(getattr(task, "name", None) or "task", _queue(task))
43
+ if trace is not None:
44
+ _open[task_id] = trace
45
+ except Exception:
46
+ pass # never let observability break the worker
47
+
48
+
49
+ def _postrun(task_id=None, state=None, **kwargs):
50
+ try:
51
+ trace = _open.pop(task_id, None)
52
+ if trace is None:
53
+ return
54
+ tracer = slowpoke.get_tracer()
55
+ if tracer is not None:
56
+ # RETRY is this attempt giving up: to whoever is waiting for the work it is a failure.
57
+ tracer.finish_job(trace, failed=str(state) in ("FAILURE", "RETRY"))
58
+ except Exception:
59
+ pass
60
+
61
+
62
+ def _queue(task):
63
+ try:
64
+ info = getattr(getattr(task, "request", None), "delivery_info", None) or {}
65
+ return info.get("routing_key") or None
66
+ except Exception:
67
+ return None
@@ -0,0 +1,226 @@
1
+ """Django integration: add "slowpoke.django" to INSTALLED_APPS and
2
+ "slowpoke.django.SlowpokeMiddleware" at the top of MIDDLEWARE."""
3
+
4
+ import functools
5
+ import os
6
+ import time
7
+
8
+ from asgiref.sync import iscoroutinefunction, markcoroutinefunction
9
+ from django.db import connections
10
+ from django.db.backends.signals import connection_created
11
+
12
+ import slowpoke
13
+ from slowpoke._tracer import current
14
+
15
+ _state = {"signal": False, "commands": False}
16
+
17
+ # Commands that never end: traced as one run they would hold a trace open for as long as the
18
+ # process lives, and the requests or tasks inside them would be lost. Names from Django itself and
19
+ # from the workers people usually run with manage.py.
20
+ LONG_RUNNING = (
21
+ "runserver", "runserver_plus", "testserver", "runworker", "shell", "dbshell", "test",
22
+ "qcluster", "rqworker", "rqscheduler", "process_tasks", "run_huey", "celery", "mail_queue",
23
+ )
24
+
25
+
26
+ def install():
27
+ """Hooks every database connection, the ones already open and the ones opened later in other threads
28
+ or async contexts. Idempotent: a statement is never recorded twice."""
29
+ try:
30
+ from django.conf import settings
31
+
32
+ base = getattr(settings, "BASE_DIR", None)
33
+ slowpoke.set_defaults(code_root=str(base) if base else None)
34
+ if not _state["signal"]:
35
+ connection_created.connect(_on_connection_created, dispatch_uid="slowpoke")
36
+ _state["signal"] = True
37
+ _wrap_open_connections()
38
+ _trace_commands()
39
+ except Exception:
40
+ pass
41
+
42
+
43
+ def _trace_commands():
44
+ """One trace per management command, which on a server means cron. SLOWPOKE_COMMANDS=false
45
+ turns it off; the commands that never end are left alone whatever it says."""
46
+ if _state["commands"] or os.environ.get("SLOWPOKE_COMMANDS", "").strip().lower() in ("0", "false", "no", "off"):
47
+ return
48
+ try:
49
+ from django.core.management.base import BaseCommand
50
+ except Exception:
51
+ return
52
+ if getattr(BaseCommand.execute, "_slowpoke", False):
53
+ _state["commands"] = True
54
+ return
55
+ original = BaseCommand.execute
56
+
57
+ @functools.wraps(original)
58
+ def execute(self, *args, **options):
59
+ name = command_name(self)
60
+ if name in LONG_RUNNING:
61
+ return original(self, *args, **options)
62
+ with slowpoke.command(name):
63
+ return original(self, *args, **options)
64
+
65
+ execute._slowpoke = True
66
+ BaseCommand.execute = execute
67
+ _state["commands"] = True
68
+
69
+
70
+ def command_name(cmd):
71
+ """The name a person would type: "close_orders", from myapp.management.commands.close_orders."""
72
+ module = getattr(type(cmd), "__module__", "") or ""
73
+ return module.rsplit(".", 1)[-1] or type(cmd).__name__
74
+
75
+
76
+ def _wrap_open_connections():
77
+ try:
78
+ for conn in connections.all(initialized_only=True):
79
+ _wrap(conn)
80
+ except TypeError: # before Django 4.1
81
+ for conn in connections.all():
82
+ _wrap(conn)
83
+
84
+
85
+ def _on_connection_created(sender, connection, **kwargs):
86
+ _wrap(connection)
87
+
88
+
89
+ def _wrap(conn):
90
+ wrappers = conn.execute_wrappers
91
+ if _execute_wrapper not in wrappers:
92
+ # First, not last: connection.execute_wrapper() pops the last one when its block ends.
93
+ wrappers.insert(0, _execute_wrapper)
94
+
95
+
96
+ def _execute_wrapper(execute, sql, params, many, context):
97
+ if current() is None:
98
+ return execute(sql, params, many, context)
99
+ started = time.perf_counter()
100
+ try:
101
+ return execute(sql, params, many, context)
102
+ finally:
103
+ seconds = time.perf_counter() - started
104
+ tracer = slowpoke.get_tracer()
105
+ if tracer is not None:
106
+ try:
107
+ vendor = context["connection"].vendor
108
+ except Exception:
109
+ vendor = "unknown"
110
+ # sql only, never params: Django passes %s placeholders to the driver
111
+ tracer.record_query(sql, seconds, vendor)
112
+
113
+
114
+ class SlowpokeMiddleware:
115
+ """Opens one trace per request. Put it first in MIDDLEWARE so the timing covers the others."""
116
+
117
+ sync_capable = True
118
+ async_capable = True
119
+
120
+ def __init__(self, get_response):
121
+ self.get_response = get_response
122
+ self.is_async = iscoroutinefunction(get_response)
123
+ if self.is_async:
124
+ markcoroutinefunction(self)
125
+ install()
126
+
127
+ def __call__(self, request):
128
+ if self.is_async:
129
+ return self.__acall__(request)
130
+ tracer, trace = _start(request)
131
+ try:
132
+ response = self.get_response(request)
133
+ except BaseException:
134
+ _finish(tracer, trace, request, 500)
135
+ raise
136
+ _finish(tracer, trace, request, getattr(response, "status_code", 200))
137
+ return response
138
+
139
+ async def __acall__(self, request):
140
+ tracer, trace = _start(request)
141
+ try:
142
+ response = await self.get_response(request)
143
+ except BaseException:
144
+ _finish(tracer, trace, request, 500)
145
+ raise
146
+ _finish(tracer, trace, request, getattr(response, "status_code", 200))
147
+ return response
148
+
149
+
150
+ def _start(request):
151
+ tracer = slowpoke.get_tracer()
152
+ if tracer is None:
153
+ return None, None
154
+ try:
155
+ _wrap_open_connections()
156
+ except Exception:
157
+ pass
158
+ return tracer, tracer.start_request(request.method)
159
+
160
+
161
+ def _finish(tracer, trace, request, status):
162
+ if trace is None:
163
+ return
164
+ try:
165
+ match = getattr(request, "resolver_match", None)
166
+ route = route_template(match.route) if match is not None and getattr(match, "route", None) else None
167
+ path = request.path
168
+ except Exception:
169
+ route, path = None, "/"
170
+ tracer.finish_request(trace, route, path, status)
171
+
172
+
173
+ def route_template(route):
174
+ """path() routes already read like templates ("orders/<int:pk>/", the agent normalizes them);
175
+ re_path() gives a regular expression, turned here into "legacy/{slug}/"."""
176
+ if not route or ("(" not in route and not route.startswith("^") and not route.endswith("$")):
177
+ return route or ""
178
+ if route.startswith("^"):
179
+ route = route[1:]
180
+ for end in ("\\Z", "$"):
181
+ if route.endswith(end) and not route.endswith("\\" + end):
182
+ route = route[: -len(end)]
183
+ break
184
+ out = []
185
+ i, n = 0, len(route)
186
+ while i < n:
187
+ c = route[i]
188
+ if c == "\\" and i + 1 < n:
189
+ out.append(route[i + 1])
190
+ i += 2
191
+ elif c == "(":
192
+ name = "param"
193
+ if route.startswith("(?P<", i):
194
+ close = route.find(">", i)
195
+ if close > 0:
196
+ name = route[i + 4:close]
197
+ i = _group_end(route, i)
198
+ while i < n and route[i] in "?*+":
199
+ i += 1
200
+ out.append("{%s}" % name)
201
+ else:
202
+ out.append(c)
203
+ i += 1
204
+ return "".join(out)
205
+
206
+
207
+ def _group_end(pattern, i):
208
+ """Index after the group that opens at pattern[i], skipping escapes and character classes."""
209
+ depth, in_class, n = 0, False, len(pattern)
210
+ while i < n:
211
+ c = pattern[i]
212
+ if c == "\\":
213
+ i += 2
214
+ continue
215
+ if in_class:
216
+ in_class = c != "]"
217
+ elif c == "[":
218
+ in_class = True
219
+ elif c == "(":
220
+ depth += 1
221
+ elif c == ")":
222
+ depth -= 1
223
+ if depth == 0:
224
+ return i + 1
225
+ i += 1
226
+ return n
@@ -0,0 +1,12 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class SlowpokeConfig(AppConfig):
5
+ name = "slowpoke.django"
6
+ label = "slowpoke"
7
+ verbose_name = "Slowpoke"
8
+
9
+ def ready(self):
10
+ from . import install
11
+
12
+ install()
slowpoke/flask.py ADDED
@@ -0,0 +1,55 @@
1
+ """Flask integration (2.x and 3.x):
2
+
3
+ import slowpoke.flask, slowpoke.sqlalchemy
4
+ slowpoke.flask.init_app(app)
5
+ slowpoke.sqlalchemy.instrument(db.engine) # or instrument() for every engine
6
+ """
7
+
8
+ from flask import request
9
+
10
+ import slowpoke
11
+
12
+ _KEY = "slowpoke.trace"
13
+ _STATUS = "slowpoke.status"
14
+
15
+
16
+ def init_app(app):
17
+ """Idempotent. The trace opens before every other before_request hook, so a hook that answers
18
+ early (authentication, rate limits) is traced too."""
19
+ before = app.before_request_funcs.setdefault(None, [])
20
+ if _before not in before:
21
+ before.insert(0, _before)
22
+ if _after not in app.after_request_funcs.setdefault(None, []):
23
+ app.after_request(_after)
24
+ if _teardown not in app.teardown_request_funcs.setdefault(None, []):
25
+ app.teardown_request(_teardown)
26
+ return app
27
+
28
+
29
+ def _before():
30
+ try:
31
+ tracer = slowpoke.get_tracer()
32
+ if tracer is not None:
33
+ request.environ[_KEY] = (tracer, tracer.start_request(request.method))
34
+ except Exception:
35
+ pass
36
+
37
+
38
+ def _after(response):
39
+ try:
40
+ request.environ[_STATUS] = response.status_code
41
+ except Exception:
42
+ pass
43
+ return response
44
+
45
+
46
+ def _teardown(exc):
47
+ try:
48
+ tracer, trace = request.environ.pop(_KEY, (None, None))
49
+ if trace is None:
50
+ return
51
+ status = request.environ.get(_STATUS) or (500 if exc is not None else 200)
52
+ rule = request.url_rule
53
+ tracer.finish_request(trace, rule.rule if rule is not None else None, request.path, status)
54
+ except Exception:
55
+ pass