justanalytics-python 0.1.0__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.
- justanalytics/__init__.py +429 -0
- justanalytics/client.py +665 -0
- justanalytics/context.py +143 -0
- justanalytics/integrations/__init__.py +11 -0
- justanalytics/integrations/django.py +157 -0
- justanalytics/integrations/fastapi.py +197 -0
- justanalytics/integrations/flask.py +203 -0
- justanalytics/integrations/logging.py +175 -0
- justanalytics/integrations/requests.py +149 -0
- justanalytics/integrations/urllib3.py +146 -0
- justanalytics/span.py +281 -0
- justanalytics/trace_context.py +124 -0
- justanalytics/transport.py +430 -0
- justanalytics/types.py +214 -0
- justanalytics_python-0.1.0.dist-info/METADATA +173 -0
- justanalytics_python-0.1.0.dist-info/RECORD +17 -0
- justanalytics_python-0.1.0.dist-info/WHEEL +4 -0
justanalytics/context.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context propagation using ``contextvars.ContextVar``.
|
|
3
|
+
|
|
4
|
+
Provides trace context propagation across async/await boundaries using
|
|
5
|
+
Python's built-in ``contextvars`` module (available since Python 3.7).
|
|
6
|
+
This is the Python equivalent of Node.js ``AsyncLocalStorage``.
|
|
7
|
+
|
|
8
|
+
Context is propagated automatically through:
|
|
9
|
+
- asyncio tasks (await, create_task)
|
|
10
|
+
- threading (when using contextvars.copy_context())
|
|
11
|
+
- generator-based coroutines
|
|
12
|
+
|
|
13
|
+
Each ``start_span()`` call sets a new active span in the context.
|
|
14
|
+
Nested spans automatically form parent-child relationships.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import contextvars
|
|
20
|
+
from typing import Any, Dict, Optional, TYPE_CHECKING
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .span import Span
|
|
24
|
+
from .types import UserContext
|
|
25
|
+
|
|
26
|
+
# --- Context Variables ---
|
|
27
|
+
|
|
28
|
+
_active_span_var: contextvars.ContextVar[Optional[Span]] = contextvars.ContextVar(
|
|
29
|
+
"ja_active_span", default=None
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_trace_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
|
33
|
+
"ja_trace_id", default=None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_user_var: contextvars.ContextVar[Optional[UserContext]] = contextvars.ContextVar(
|
|
37
|
+
"ja_user", default=None
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
_tags_var: contextvars.ContextVar[Dict[str, str]] = contextvars.ContextVar(
|
|
41
|
+
"ja_tags", default={}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_active_span() -> Optional[Span]:
|
|
46
|
+
"""Get the currently active span from context.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
The active Span, or None if not in a traced context.
|
|
50
|
+
"""
|
|
51
|
+
return _active_span_var.get(None)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_active_span(span: Optional[Span]) -> contextvars.Token:
|
|
55
|
+
"""Set the active span in context.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
span: The span to make active, or None to clear.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
A token that can be used to restore the previous value.
|
|
62
|
+
"""
|
|
63
|
+
return _active_span_var.set(span)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_trace_id() -> Optional[str]:
|
|
67
|
+
"""Get the current trace ID from context.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The trace ID string, or None if not in a traced context.
|
|
71
|
+
"""
|
|
72
|
+
return _trace_id_var.get(None)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_trace_id(trace_id: Optional[str]) -> contextvars.Token:
|
|
76
|
+
"""Set the trace ID in context.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
trace_id: The trace ID to set, or None to clear.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A token that can be used to restore the previous value.
|
|
83
|
+
"""
|
|
84
|
+
return _trace_id_var.set(trace_id)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_user() -> Optional[UserContext]:
|
|
88
|
+
"""Get the current user context.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The UserContext, or None if not set.
|
|
92
|
+
"""
|
|
93
|
+
return _user_var.get(None)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def set_user(user: Optional[UserContext]) -> contextvars.Token:
|
|
97
|
+
"""Set the user context.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
user: The user context to set, or None to clear.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
A token that can be used to restore the previous value.
|
|
104
|
+
"""
|
|
105
|
+
return _user_var.set(user)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_tags() -> Dict[str, str]:
|
|
109
|
+
"""Get the current context tags.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
A copy of the current tags dict.
|
|
113
|
+
"""
|
|
114
|
+
return dict(_tags_var.get({}))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def set_tag(key: str, value: str) -> contextvars.Token:
|
|
118
|
+
"""Set a single tag in the context (copy-on-write).
|
|
119
|
+
|
|
120
|
+
Tags are attached as attributes on all spans created within this scope.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
key: Tag key.
|
|
124
|
+
value: Tag value.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
A token that can be used to restore the previous value.
|
|
128
|
+
"""
|
|
129
|
+
current = _tags_var.get({})
|
|
130
|
+
new_tags = {**current, key: value}
|
|
131
|
+
return _tags_var.set(new_tags)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def set_tags(tags: Dict[str, str]) -> contextvars.Token:
|
|
135
|
+
"""Replace all tags in the context.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
tags: The new tags dict.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
A token that can be used to restore the previous value.
|
|
142
|
+
"""
|
|
143
|
+
return _tags_var.set(dict(tags))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-instrumentation integrations for the JustAnalytics Python SDK.
|
|
3
|
+
|
|
4
|
+
Available integrations:
|
|
5
|
+
- ``requests``: Monkey-patches ``requests.Session.send()`` for outgoing HTTP spans.
|
|
6
|
+
- ``urllib3``: Patches ``urllib3.HTTPConnectionPool.urlopen()`` for lower-level HTTP tracking.
|
|
7
|
+
- ``django``: ``JustAnalyticsMiddleware`` for incoming request spans, user extraction, error capture.
|
|
8
|
+
- ``flask``: ``JustAnalyticsMiddleware`` for before/after request hooks, error handler.
|
|
9
|
+
- ``fastapi``: ASGI middleware + exception handler for FastAPI/Starlette.
|
|
10
|
+
- ``logging``: ``JustAnalyticsHandler(logging.Handler)`` bridging Python logging to JA log ingestion.
|
|
11
|
+
"""
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django middleware for JustAnalytics.
|
|
3
|
+
|
|
4
|
+
Provides ``JustAnalyticsMiddleware`` that automatically:
|
|
5
|
+
- Creates server spans for incoming HTTP requests
|
|
6
|
+
- Reads ``traceparent`` headers for distributed trace propagation
|
|
7
|
+
- Extracts Django user information
|
|
8
|
+
- Captures unhandled exceptions
|
|
9
|
+
- Records HTTP method, path, status code, and timing
|
|
10
|
+
|
|
11
|
+
Usage (settings.py)::
|
|
12
|
+
|
|
13
|
+
MIDDLEWARE = [
|
|
14
|
+
"justanalytics.integrations.django.JustAnalyticsMiddleware",
|
|
15
|
+
# ... other middleware
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
The middleware expects ``justanalytics.init()`` to have been called before
|
|
19
|
+
the first request is served. If the SDK is not initialized, the middleware
|
|
20
|
+
is a no-op passthrough.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import time
|
|
27
|
+
from typing import Any, Callable, Optional
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger("justanalytics.integrations.django")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class JustAnalyticsMiddleware:
|
|
33
|
+
"""
|
|
34
|
+
Django middleware that creates server spans for incoming HTTP requests.
|
|
35
|
+
|
|
36
|
+
This middleware:
|
|
37
|
+
1. Reads the ``traceparent`` header to continue an existing trace
|
|
38
|
+
2. Creates a ``server`` span for the request
|
|
39
|
+
3. Extracts the authenticated Django user (if available)
|
|
40
|
+
4. Captures exceptions and sets span status to ``error``
|
|
41
|
+
5. Records ``http.method``, ``http.url``, ``http.status_code``
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
get_response: The next middleware or view in the Django chain.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, get_response: Callable[..., Any]) -> None:
|
|
48
|
+
self.get_response = get_response
|
|
49
|
+
|
|
50
|
+
def __call__(self, request: Any) -> Any:
|
|
51
|
+
"""Process the request, wrapping it in a JA span."""
|
|
52
|
+
# Import here to avoid circular imports and allow lazy SDK init
|
|
53
|
+
try:
|
|
54
|
+
from .. import context as ctx
|
|
55
|
+
from ..client import JustAnalyticsClient
|
|
56
|
+
from ..span import Span, _generate_trace_id
|
|
57
|
+
from ..trace_context import parse_traceparent, serialize_traceparent
|
|
58
|
+
from ..types import SpanKind, SpanStatus, UserContext
|
|
59
|
+
except Exception:
|
|
60
|
+
return self.get_response(request)
|
|
61
|
+
|
|
62
|
+
# Access the global client through the module
|
|
63
|
+
try:
|
|
64
|
+
import justanalytics as ja
|
|
65
|
+
client = ja._client
|
|
66
|
+
if not client.is_initialized or not client._enabled:
|
|
67
|
+
return self.get_response(request)
|
|
68
|
+
except Exception:
|
|
69
|
+
return self.get_response(request)
|
|
70
|
+
|
|
71
|
+
method = request.method or "GET"
|
|
72
|
+
path = request.path or "/"
|
|
73
|
+
span_name = f"{method} {path}"
|
|
74
|
+
|
|
75
|
+
# Parse incoming traceparent header
|
|
76
|
+
traceparent_header = request.META.get("HTTP_TRACEPARENT", "")
|
|
77
|
+
parsed = parse_traceparent(traceparent_header) if traceparent_header else None
|
|
78
|
+
|
|
79
|
+
if parsed:
|
|
80
|
+
trace_id = parsed.trace_id
|
|
81
|
+
parent_span_id = parsed.parent_span_id
|
|
82
|
+
else:
|
|
83
|
+
trace_id = _generate_trace_id()
|
|
84
|
+
parent_span_id = None
|
|
85
|
+
|
|
86
|
+
attributes = {
|
|
87
|
+
"http.method": method,
|
|
88
|
+
"http.url": request.build_absolute_uri() if hasattr(request, "build_absolute_uri") else path,
|
|
89
|
+
"http.target": path,
|
|
90
|
+
"http.scheme": request.scheme if hasattr(request, "scheme") else "http",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if client._environment:
|
|
94
|
+
attributes["environment"] = client._environment
|
|
95
|
+
if client._release:
|
|
96
|
+
attributes["release"] = client._release
|
|
97
|
+
|
|
98
|
+
span = Span(
|
|
99
|
+
operation_name=span_name,
|
|
100
|
+
service_name=client._service_name,
|
|
101
|
+
kind=SpanKind.SERVER,
|
|
102
|
+
trace_id=trace_id,
|
|
103
|
+
parent_span_id=parent_span_id,
|
|
104
|
+
attributes=attributes,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
with span:
|
|
108
|
+
# Extract Django user if authenticated
|
|
109
|
+
try:
|
|
110
|
+
if hasattr(request, "user") and request.user.is_authenticated:
|
|
111
|
+
user_id = str(getattr(request.user, "pk", "")) or str(getattr(request.user, "id", ""))
|
|
112
|
+
email = getattr(request.user, "email", None)
|
|
113
|
+
username = getattr(request.user, "username", None)
|
|
114
|
+
ctx.set_user(UserContext(id=user_id, email=email, username=username))
|
|
115
|
+
span.set_attribute("user.id", user_id)
|
|
116
|
+
if email:
|
|
117
|
+
span.set_attribute("user.email", email)
|
|
118
|
+
except Exception:
|
|
119
|
+
pass # User extraction is best-effort
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
response = self.get_response(request)
|
|
123
|
+
status_code = getattr(response, "status_code", 200)
|
|
124
|
+
span.set_attribute("http.status_code", status_code)
|
|
125
|
+
if status_code >= 500:
|
|
126
|
+
span.set_status(SpanStatus.ERROR, f"HTTP {status_code}")
|
|
127
|
+
elif status_code >= 400:
|
|
128
|
+
span.set_status(SpanStatus.ERROR, f"HTTP {status_code}")
|
|
129
|
+
else:
|
|
130
|
+
span.set_status(SpanStatus.OK)
|
|
131
|
+
return response
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
span.set_status(SpanStatus.ERROR, str(exc))
|
|
134
|
+
# Capture exception via SDK
|
|
135
|
+
try:
|
|
136
|
+
client.capture_exception(exc)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
raise
|
|
140
|
+
|
|
141
|
+
# Enqueue the ended span
|
|
142
|
+
if client._transport and span.is_ended:
|
|
143
|
+
client._transport.enqueue_span(span.to_dict())
|
|
144
|
+
|
|
145
|
+
def process_exception(self, request: Any, exception: BaseException) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Called by Django when a view raises an exception.
|
|
148
|
+
|
|
149
|
+
Captures the exception via the SDK for error tracking.
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
import justanalytics as ja
|
|
153
|
+
client = ja._client
|
|
154
|
+
if client.is_initialized and client._enabled:
|
|
155
|
+
client.capture_exception(exception)
|
|
156
|
+
except Exception:
|
|
157
|
+
pass # Never crash from error capture
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI / Starlette ASGI middleware for JustAnalytics.
|
|
3
|
+
|
|
4
|
+
Provides ``JustAnalyticsMiddleware`` as ASGI middleware that automatically:
|
|
5
|
+
- Creates server spans for incoming HTTP requests
|
|
6
|
+
- Reads ``traceparent`` headers for distributed trace propagation
|
|
7
|
+
- Captures unhandled exceptions
|
|
8
|
+
- Records HTTP method, path, status code, and timing
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI
|
|
13
|
+
from justanalytics.integrations.fastapi import JustAnalyticsMiddleware
|
|
14
|
+
|
|
15
|
+
app = FastAPI()
|
|
16
|
+
app.add_middleware(JustAnalyticsMiddleware)
|
|
17
|
+
|
|
18
|
+
Works with any ASGI framework (FastAPI, Starlette, etc.).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger("justanalytics.integrations.fastapi")
|
|
27
|
+
|
|
28
|
+
# Type aliases for ASGI
|
|
29
|
+
Scope = dict
|
|
30
|
+
Receive = Callable[[], Awaitable[dict]]
|
|
31
|
+
Send = Callable[[dict], Awaitable[None]]
|
|
32
|
+
ASGIApp = Callable[[Scope, Receive, Send], Awaitable[None]]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class JustAnalyticsMiddleware:
|
|
36
|
+
"""
|
|
37
|
+
ASGI middleware that creates server spans for incoming HTTP requests.
|
|
38
|
+
|
|
39
|
+
Compatible with FastAPI, Starlette, and any ASGI-compliant framework.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
app: The ASGI application to wrap.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
46
|
+
self.app = app
|
|
47
|
+
|
|
48
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
49
|
+
"""Process an ASGI request."""
|
|
50
|
+
if scope["type"] != "http":
|
|
51
|
+
await self.app(scope, receive, send)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from .. import context as ctx
|
|
56
|
+
from ..span import Span, _generate_trace_id
|
|
57
|
+
from ..trace_context import parse_traceparent
|
|
58
|
+
from ..types import SpanKind, SpanStatus
|
|
59
|
+
except Exception:
|
|
60
|
+
await self.app(scope, receive, send)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
import justanalytics as ja
|
|
65
|
+
client = ja._client
|
|
66
|
+
if not client.is_initialized or not client._enabled:
|
|
67
|
+
await self.app(scope, receive, send)
|
|
68
|
+
return
|
|
69
|
+
except Exception:
|
|
70
|
+
await self.app(scope, receive, send)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Extract request info from ASGI scope
|
|
74
|
+
method = scope.get("method", "GET")
|
|
75
|
+
path = scope.get("path", "/")
|
|
76
|
+
scheme = scope.get("scheme", "http")
|
|
77
|
+
headers = dict(scope.get("headers", []))
|
|
78
|
+
|
|
79
|
+
span_name = f"{method} {path}"
|
|
80
|
+
|
|
81
|
+
# Parse incoming traceparent header
|
|
82
|
+
traceparent_header = ""
|
|
83
|
+
for key, value in headers.items():
|
|
84
|
+
if key == b"traceparent":
|
|
85
|
+
traceparent_header = value.decode("utf-8", errors="replace")
|
|
86
|
+
break
|
|
87
|
+
|
|
88
|
+
parsed = parse_traceparent(traceparent_header) if traceparent_header else None
|
|
89
|
+
|
|
90
|
+
if parsed:
|
|
91
|
+
trace_id = parsed.trace_id
|
|
92
|
+
parent_span_id = parsed.parent_span_id
|
|
93
|
+
else:
|
|
94
|
+
trace_id = _generate_trace_id()
|
|
95
|
+
parent_span_id = None
|
|
96
|
+
|
|
97
|
+
# Build server/host from scope
|
|
98
|
+
server = scope.get("server")
|
|
99
|
+
if server:
|
|
100
|
+
host = f"{server[0]}:{server[1]}"
|
|
101
|
+
else:
|
|
102
|
+
host = "unknown"
|
|
103
|
+
|
|
104
|
+
attributes = {
|
|
105
|
+
"http.method": method,
|
|
106
|
+
"http.url": f"{scheme}://{host}{path}",
|
|
107
|
+
"http.target": path,
|
|
108
|
+
"http.scheme": scheme,
|
|
109
|
+
}
|
|
110
|
+
if client._environment:
|
|
111
|
+
attributes["environment"] = client._environment
|
|
112
|
+
if client._release:
|
|
113
|
+
attributes["release"] = client._release
|
|
114
|
+
|
|
115
|
+
# Query string
|
|
116
|
+
query_string = scope.get("query_string", b"")
|
|
117
|
+
if query_string:
|
|
118
|
+
attributes["http.query_string"] = query_string.decode("utf-8", errors="replace")
|
|
119
|
+
|
|
120
|
+
span = Span(
|
|
121
|
+
operation_name=span_name,
|
|
122
|
+
service_name=client._service_name,
|
|
123
|
+
kind=SpanKind.SERVER,
|
|
124
|
+
trace_id=trace_id,
|
|
125
|
+
parent_span_id=parent_span_id,
|
|
126
|
+
attributes=attributes,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Track response status code
|
|
130
|
+
response_status: Optional[int] = None
|
|
131
|
+
|
|
132
|
+
async def send_wrapper(message: dict) -> None:
|
|
133
|
+
nonlocal response_status
|
|
134
|
+
if message["type"] == "http.response.start":
|
|
135
|
+
response_status = message.get("status", 200)
|
|
136
|
+
await send(message)
|
|
137
|
+
|
|
138
|
+
with span:
|
|
139
|
+
try:
|
|
140
|
+
await self.app(scope, receive, send_wrapper)
|
|
141
|
+
if response_status is not None:
|
|
142
|
+
span.set_attribute("http.status_code", response_status)
|
|
143
|
+
if response_status >= 400:
|
|
144
|
+
span.set_status(SpanStatus.ERROR, f"HTTP {response_status}")
|
|
145
|
+
else:
|
|
146
|
+
span.set_status(SpanStatus.OK)
|
|
147
|
+
else:
|
|
148
|
+
span.set_status(SpanStatus.OK)
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
span.set_status(SpanStatus.ERROR, str(exc))
|
|
151
|
+
# Capture exception
|
|
152
|
+
try:
|
|
153
|
+
client.capture_exception(exc)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
# Enqueue the ended span
|
|
159
|
+
if client._transport and span.is_ended:
|
|
160
|
+
client._transport.enqueue_span(span.to_dict())
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def setup_exception_handler(app: Any) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Register a FastAPI exception handler that captures exceptions via JA.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
app: The FastAPI application instance.
|
|
169
|
+
|
|
170
|
+
Usage::
|
|
171
|
+
|
|
172
|
+
from fastapi import FastAPI
|
|
173
|
+
from justanalytics.integrations.fastapi import setup_exception_handler
|
|
174
|
+
|
|
175
|
+
app = FastAPI()
|
|
176
|
+
setup_exception_handler(app)
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
from fastapi import Request
|
|
180
|
+
from fastapi.responses import JSONResponse
|
|
181
|
+
except ImportError:
|
|
182
|
+
logger.debug("FastAPI not installed, skipping exception handler setup")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
@app.exception_handler(Exception)
|
|
186
|
+
async def ja_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
|
187
|
+
try:
|
|
188
|
+
import justanalytics as ja
|
|
189
|
+
client = ja._client
|
|
190
|
+
if client.is_initialized and client._enabled:
|
|
191
|
+
client.capture_exception(exc)
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
return JSONResponse(
|
|
195
|
+
status_code=500,
|
|
196
|
+
content={"detail": "Internal Server Error"},
|
|
197
|
+
)
|