fetchnode 0.4.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.
- fetchnode/__init__.py +50 -0
- fetchnode/api.py +309 -0
- fetchnode/apps.py +36 -0
- fetchnode/capture.py +202 -0
- fetchnode/celery.py +138 -0
- fetchnode/conf.py +127 -0
- fetchnode/demo.py +3 -0
- fetchnode/excepthook.py +83 -0
- fetchnode/handlers.py +76 -0
- fetchnode/middleware.py +188 -0
- fetchnode/query.py +90 -0
- fetchnode/scope.py +81 -0
- fetchnode/scrub.py +87 -0
- fetchnode/sender.py +241 -0
- fetchnode/utils.py +78 -0
- fetchnode/version.py +3 -0
- fetchnode-0.4.0.dist-info/METADATA +166 -0
- fetchnode-0.4.0.dist-info/RECORD +36 -0
- fetchnode-0.4.0.dist-info/WHEEL +4 -0
- fetchnode-0.4.0.dist-info/licenses/LICENSE +21 -0
- fetchnode_client/__init__.py +4 -0
- fetchnode_client/api.py +1 -0
- fetchnode_client/apps.py +2 -0
- fetchnode_client/capture.py +1 -0
- fetchnode_client/celery.py +1 -0
- fetchnode_client/conf.py +1 -0
- fetchnode_client/demo.py +1 -0
- fetchnode_client/excepthook.py +1 -0
- fetchnode_client/handlers.py +1 -0
- fetchnode_client/middleware.py +1 -0
- fetchnode_client/query.py +1 -0
- fetchnode_client/scope.py +1 -0
- fetchnode_client/scrub.py +1 -0
- fetchnode_client/sender.py +1 -0
- fetchnode_client/utils.py +1 -0
- fetchnode_client/version.py +1 -0
fetchnode/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
default_app_config = "fetchnode.apps.FetchNodeConfig"
|
|
2
|
+
|
|
3
|
+
from fetchnode.version import SDK_VERSION
|
|
4
|
+
|
|
5
|
+
__version__ = SDK_VERSION
|
|
6
|
+
|
|
7
|
+
from fetchnode.api import capture_exception, capture_message, track, track_event
|
|
8
|
+
from fetchnode.capture import (
|
|
9
|
+
capture_context,
|
|
10
|
+
capture_django_task,
|
|
11
|
+
capture_exceptions,
|
|
12
|
+
capture_job,
|
|
13
|
+
capture_job_context,
|
|
14
|
+
capture_job_exception,
|
|
15
|
+
capture_management_command,
|
|
16
|
+
track_job_execution,
|
|
17
|
+
)
|
|
18
|
+
from fetchnode.excepthook import (
|
|
19
|
+
install_excepthook_instrumentation,
|
|
20
|
+
restore_excepthook,
|
|
21
|
+
uninstall_excepthook_instrumentation,
|
|
22
|
+
)
|
|
23
|
+
from fetchnode.scope import add_breadcrumb, clear_scope, set_context, set_tag, set_user
|
|
24
|
+
from fetchnode.sender import flush, transport_stats
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"__version__",
|
|
28
|
+
"capture_exception",
|
|
29
|
+
"capture_message",
|
|
30
|
+
"track",
|
|
31
|
+
"track_event",
|
|
32
|
+
"capture_exceptions",
|
|
33
|
+
"capture_context",
|
|
34
|
+
"capture_job_exception",
|
|
35
|
+
"capture_job_context",
|
|
36
|
+
"capture_job",
|
|
37
|
+
"capture_django_task",
|
|
38
|
+
"track_job_execution",
|
|
39
|
+
"capture_management_command",
|
|
40
|
+
"install_excepthook_instrumentation",
|
|
41
|
+
"uninstall_excepthook_instrumentation",
|
|
42
|
+
"restore_excepthook",
|
|
43
|
+
"add_breadcrumb",
|
|
44
|
+
"clear_scope",
|
|
45
|
+
"set_context",
|
|
46
|
+
"set_tag",
|
|
47
|
+
"set_user",
|
|
48
|
+
"flush",
|
|
49
|
+
"transport_stats",
|
|
50
|
+
]
|
fetchnode/api.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import uuid
|
|
3
|
+
|
|
4
|
+
from fetchnode.conf import get_client_settings
|
|
5
|
+
from fetchnode.scope import get_scope_data
|
|
6
|
+
from fetchnode.scrub import extract_headers, extract_request_data, scrub_mapping, scrub_query_string, scrub_value
|
|
7
|
+
from fetchnode.sender import send_payload
|
|
8
|
+
from fetchnode.utils import build_exception_values, get_server_name, runtime_context, utc_now_iso
|
|
9
|
+
from fetchnode.version import SDK_VERSION
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
SDK_CONTEXT = {"name": "fetchnode.python", "version": SDK_VERSION}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _base_payload(*, level: str, logger_name: str = "", tags=None, contexts=None, breadcrumbs=None) -> dict:
|
|
16
|
+
config = get_client_settings()
|
|
17
|
+
scope = get_scope_data()
|
|
18
|
+
merged_tags = {**scope["tags"], **(tags or {})}
|
|
19
|
+
merged_contexts = {**runtime_context(), **scope["contexts"], **(contexts or {})}
|
|
20
|
+
return {
|
|
21
|
+
"event_id": uuid.uuid4().hex,
|
|
22
|
+
"timestamp": utc_now_iso(),
|
|
23
|
+
"level": level,
|
|
24
|
+
"logger": logger_name,
|
|
25
|
+
"environment": config["ENVIRONMENT"],
|
|
26
|
+
"release": config["RELEASE"],
|
|
27
|
+
"server_name": get_server_name(),
|
|
28
|
+
"sdk": SDK_CONTEXT,
|
|
29
|
+
"tags": scrub_mapping(merged_tags, sensitive_keys=config["SENSITIVE_KEYS"]),
|
|
30
|
+
"contexts": scrub_value(merged_contexts, sensitive_keys=config["SENSITIVE_KEYS"]),
|
|
31
|
+
"breadcrumbs": scrub_value([*scope["breadcrumbs"], *(breadcrumbs or [])][-100:], sensitive_keys=config["SENSITIVE_KEYS"]),
|
|
32
|
+
"request": {},
|
|
33
|
+
"user": scrub_mapping(scope["user"], sensitive_keys=config["SENSITIVE_KEYS"]),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _request_context(request, config: dict) -> dict:
|
|
38
|
+
resolver_match = getattr(request, "resolver_match", None)
|
|
39
|
+
safe_query = ""
|
|
40
|
+
if config["CAPTURE_QUERY_STRING"]:
|
|
41
|
+
safe_query = scrub_query_string(
|
|
42
|
+
request.META.get("QUERY_STRING", ""),
|
|
43
|
+
sensitive_keys=config["SENSITIVE_KEYS"],
|
|
44
|
+
)
|
|
45
|
+
try:
|
|
46
|
+
url = request.build_absolute_uri(request.path)
|
|
47
|
+
except Exception:
|
|
48
|
+
url = request.path
|
|
49
|
+
if safe_query:
|
|
50
|
+
url = f"{url}?{safe_query}"
|
|
51
|
+
return {
|
|
52
|
+
"method": request.method,
|
|
53
|
+
"path": request.path,
|
|
54
|
+
"url": url,
|
|
55
|
+
"route": getattr(resolver_match, "route", "") or "",
|
|
56
|
+
"view_name": getattr(resolver_match, "view_name", "") or "",
|
|
57
|
+
"query_string": safe_query,
|
|
58
|
+
"ip_address": (
|
|
59
|
+
request.META.get("HTTP_X_FORWARDED_FOR", "").split(",", 1)[0].strip()
|
|
60
|
+
or request.META.get("REMOTE_ADDR", "")
|
|
61
|
+
) if config["CAPTURE_IP_ADDRESS"] else "",
|
|
62
|
+
"headers": extract_headers(
|
|
63
|
+
request,
|
|
64
|
+
capture_headers=config["CAPTURE_HEADERS"],
|
|
65
|
+
sensitive_headers=config["SENSITIVE_HEADERS"],
|
|
66
|
+
),
|
|
67
|
+
"data": extract_request_data(
|
|
68
|
+
request,
|
|
69
|
+
capture_body=config["CAPTURE_BODY"],
|
|
70
|
+
sensitive_keys=config["SENSITIVE_KEYS"],
|
|
71
|
+
max_body_length=config["MAX_BODY_LENGTH"],
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _request_user(request, config: dict) -> dict:
|
|
77
|
+
auth_user = getattr(request, "user", None)
|
|
78
|
+
if auth_user is None or not getattr(auth_user, "is_authenticated", False):
|
|
79
|
+
return {}
|
|
80
|
+
user = {}
|
|
81
|
+
if config["CAPTURE_USER_ID"]:
|
|
82
|
+
user["id"] = getattr(auth_user, "pk", "")
|
|
83
|
+
if config["CAPTURE_USERNAME"]:
|
|
84
|
+
user["username"] = getattr(auth_user, "get_username", lambda: "")()
|
|
85
|
+
if config["CAPTURE_USER_EMAIL"]:
|
|
86
|
+
user["email"] = getattr(auth_user, "email", "")
|
|
87
|
+
return user
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _attach_request_and_user(payload: dict, *, request=None, user=None) -> None:
|
|
91
|
+
config = get_client_settings()
|
|
92
|
+
if request is not None:
|
|
93
|
+
payload["request"] = _request_context(request, config)
|
|
94
|
+
request_id = getattr(request, "_fetchnode_request_id", "") or request.headers.get("X-Request-ID", "")
|
|
95
|
+
traceparent = request.headers.get("Traceparent", "")
|
|
96
|
+
trace_id = getattr(request, "_fetchnode_trace_id", "")
|
|
97
|
+
if request_id:
|
|
98
|
+
payload["tags"]["request_id"] = request_id[:255]
|
|
99
|
+
payload["request_id"] = request_id[:64]
|
|
100
|
+
if trace_id:
|
|
101
|
+
payload["trace_id"] = trace_id[:32]
|
|
102
|
+
if traceparent or trace_id:
|
|
103
|
+
payload["contexts"]["trace"] = {
|
|
104
|
+
"traceparent": traceparent[:255],
|
|
105
|
+
"trace_id": trace_id[:32],
|
|
106
|
+
"transaction_event_id": getattr(request, "_fetchnode_transaction_event_id", "")[:32],
|
|
107
|
+
}
|
|
108
|
+
payload["user"] = scrub_mapping(_request_user(request, config), sensitive_keys=config["SENSITIVE_KEYS"])
|
|
109
|
+
if user:
|
|
110
|
+
payload["user"] = scrub_mapping(user, sensitive_keys=config["SENSITIVE_KEYS"])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_exception_payload(
|
|
114
|
+
exc: BaseException,
|
|
115
|
+
request=None,
|
|
116
|
+
*,
|
|
117
|
+
user=None,
|
|
118
|
+
tags=None,
|
|
119
|
+
contexts=None,
|
|
120
|
+
breadcrumbs=None,
|
|
121
|
+
extra=None,
|
|
122
|
+
queries=None,
|
|
123
|
+
logger_name="",
|
|
124
|
+
mechanism="generic",
|
|
125
|
+
handled=True,
|
|
126
|
+
) -> dict:
|
|
127
|
+
payload = _base_payload(
|
|
128
|
+
level="error",
|
|
129
|
+
logger_name=logger_name,
|
|
130
|
+
tags=tags,
|
|
131
|
+
contexts=contexts,
|
|
132
|
+
breadcrumbs=breadcrumbs,
|
|
133
|
+
)
|
|
134
|
+
values = build_exception_values(exc, mechanism=mechanism, handled=handled)
|
|
135
|
+
primary = values[-1]
|
|
136
|
+
payload["exception"] = {
|
|
137
|
+
"type": primary["type"],
|
|
138
|
+
"message": primary["message"],
|
|
139
|
+
"frames": primary["frames"],
|
|
140
|
+
"values": values,
|
|
141
|
+
}
|
|
142
|
+
if extra:
|
|
143
|
+
payload["extra"] = scrub_value(extra, sensitive_keys=get_client_settings()["SENSITIVE_KEYS"])
|
|
144
|
+
if queries:
|
|
145
|
+
payload["queries"] = scrub_value(list(queries)[:100], sensitive_keys=get_client_settings()["SENSITIVE_KEYS"])
|
|
146
|
+
_attach_request_and_user(payload, request=request, user=user)
|
|
147
|
+
return payload
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def capture_exception(
|
|
151
|
+
exc: BaseException,
|
|
152
|
+
request=None,
|
|
153
|
+
*,
|
|
154
|
+
user=None,
|
|
155
|
+
tags=None,
|
|
156
|
+
contexts=None,
|
|
157
|
+
breadcrumbs=None,
|
|
158
|
+
extra=None,
|
|
159
|
+
queries=None,
|
|
160
|
+
logger_name="",
|
|
161
|
+
mechanism="generic",
|
|
162
|
+
handled=True,
|
|
163
|
+
) -> str:
|
|
164
|
+
payload = build_exception_payload(
|
|
165
|
+
exc,
|
|
166
|
+
request=request,
|
|
167
|
+
user=user,
|
|
168
|
+
tags=tags,
|
|
169
|
+
contexts=contexts,
|
|
170
|
+
breadcrumbs=breadcrumbs,
|
|
171
|
+
extra=extra,
|
|
172
|
+
queries=queries,
|
|
173
|
+
logger_name=logger_name,
|
|
174
|
+
mechanism=mechanism,
|
|
175
|
+
handled=handled,
|
|
176
|
+
)
|
|
177
|
+
send_payload(payload)
|
|
178
|
+
return payload["event_id"]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def capture_message(
|
|
182
|
+
message: str,
|
|
183
|
+
*,
|
|
184
|
+
level: str = "error",
|
|
185
|
+
request=None,
|
|
186
|
+
user=None,
|
|
187
|
+
tags=None,
|
|
188
|
+
contexts=None,
|
|
189
|
+
breadcrumbs=None,
|
|
190
|
+
logger_name="",
|
|
191
|
+
) -> str:
|
|
192
|
+
payload = _base_payload(
|
|
193
|
+
level=level,
|
|
194
|
+
logger_name=logger_name,
|
|
195
|
+
tags=tags,
|
|
196
|
+
contexts=contexts,
|
|
197
|
+
breadcrumbs=breadcrumbs,
|
|
198
|
+
)
|
|
199
|
+
payload["exception"] = {
|
|
200
|
+
"type": "Message",
|
|
201
|
+
"message": str(message),
|
|
202
|
+
"frames": [],
|
|
203
|
+
"values": [{
|
|
204
|
+
"type": "Message",
|
|
205
|
+
"module": "",
|
|
206
|
+
"message": str(message),
|
|
207
|
+
"frames": [],
|
|
208
|
+
"mechanism": {"type": "message", "handled": True},
|
|
209
|
+
}],
|
|
210
|
+
}
|
|
211
|
+
_attach_request_and_user(payload, request=request, user=user)
|
|
212
|
+
send_payload(payload)
|
|
213
|
+
return payload["event_id"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def track_event(
|
|
217
|
+
name: str = "",
|
|
218
|
+
properties: dict | None = None,
|
|
219
|
+
*,
|
|
220
|
+
user_identifier: str = "",
|
|
221
|
+
visitor_id: str = "",
|
|
222
|
+
session_id: str = "",
|
|
223
|
+
path: str = "",
|
|
224
|
+
environment: str = "",
|
|
225
|
+
conversion: dict | bool | str | None = None,
|
|
226
|
+
idempotency_key: str = "",
|
|
227
|
+
request=None,
|
|
228
|
+
event: str = "",
|
|
229
|
+
user_id: str = "",
|
|
230
|
+
distinct_id: str = "",
|
|
231
|
+
payload: dict | None = None,
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Track a server-side product milestone such as signup_completed or lead_submitted.
|
|
234
|
+
|
|
235
|
+
Usage:
|
|
236
|
+
fetchnode.track("lead_submitted", request=request, idempotency_key=str(lead.id))
|
|
237
|
+
fetchnode.track("signup_completed", conversion=True, request=request)
|
|
238
|
+
"""
|
|
239
|
+
event_name = str(name or event or "")
|
|
240
|
+
if not event_name:
|
|
241
|
+
raise ValueError("Event name is required for track_event.")
|
|
242
|
+
|
|
243
|
+
config = get_client_settings()
|
|
244
|
+
stable_key = str(idempotency_key or "")
|
|
245
|
+
if len(stable_key) > 512:
|
|
246
|
+
raise ValueError("idempotency_key must contain at most 512 characters.")
|
|
247
|
+
event_id = (
|
|
248
|
+
hashlib.sha256(f"{event_name}\0{stable_key}".encode("utf-8")).hexdigest()[:32]
|
|
249
|
+
if stable_key
|
|
250
|
+
else uuid.uuid4().hex
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if request is not None:
|
|
254
|
+
path = path or getattr(request, "path", "")
|
|
255
|
+
if not visitor_id:
|
|
256
|
+
cookies = getattr(request, "COOKIES", {}) or {}
|
|
257
|
+
headers = getattr(request, "headers", {}) or {}
|
|
258
|
+
visitor_id = (
|
|
259
|
+
headers.get("X-FetchNode-Visitor-ID", "")
|
|
260
|
+
or cookies.get("_fn_vid", "")
|
|
261
|
+
or str(distinct_id or "")
|
|
262
|
+
)
|
|
263
|
+
if not session_id:
|
|
264
|
+
cookies = getattr(request, "COOKIES", {}) or {}
|
|
265
|
+
headers = getattr(request, "headers", {}) or {}
|
|
266
|
+
session_id = (
|
|
267
|
+
headers.get("X-FetchNode-Session-ID", "")
|
|
268
|
+
or cookies.get("_fn_sid", "")
|
|
269
|
+
)
|
|
270
|
+
if not user_identifier:
|
|
271
|
+
req_user = getattr(request, "user", None)
|
|
272
|
+
if req_user and getattr(req_user, "is_authenticated", False):
|
|
273
|
+
user_identifier = str(getattr(req_user, "pk", ""))
|
|
274
|
+
elif user_id:
|
|
275
|
+
user_identifier = str(user_id)
|
|
276
|
+
|
|
277
|
+
event_properties = scrub_value(properties or payload or {}, sensitive_keys=config["SENSITIVE_KEYS"])
|
|
278
|
+
if conversion is not None:
|
|
279
|
+
if conversion is True:
|
|
280
|
+
conv_obj = {"name": event_name}
|
|
281
|
+
elif isinstance(conversion, str):
|
|
282
|
+
conv_obj = {"name": conversion}
|
|
283
|
+
elif isinstance(conversion, dict):
|
|
284
|
+
conv_obj = conversion
|
|
285
|
+
else:
|
|
286
|
+
conv_obj = {"name": event_name}
|
|
287
|
+
event_properties["_fetchnode_conversion"] = scrub_value(
|
|
288
|
+
conv_obj,
|
|
289
|
+
sensitive_keys=config["SENSITIVE_KEYS"],
|
|
290
|
+
)
|
|
291
|
+
send_payload({
|
|
292
|
+
"event_id": event_id,
|
|
293
|
+
"type": "custom",
|
|
294
|
+
"name": str(event_name),
|
|
295
|
+
"payload": event_properties,
|
|
296
|
+
"user_identifier": str(user_identifier),
|
|
297
|
+
"visitor_id": str(visitor_id),
|
|
298
|
+
"session_id": str(session_id),
|
|
299
|
+
"path": str(path),
|
|
300
|
+
"timestamp": utc_now_iso(),
|
|
301
|
+
"environment": environment or config["ENVIRONMENT"],
|
|
302
|
+
"sdk": SDK_CONTEXT,
|
|
303
|
+
})
|
|
304
|
+
return event_id
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# Intuitive shortcut alias:
|
|
308
|
+
track = track_event
|
|
309
|
+
|
fetchnode/apps.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FetchNodeConfig(AppConfig):
|
|
5
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
6
|
+
name = "fetchnode"
|
|
7
|
+
verbose_name = "FetchNode"
|
|
8
|
+
|
|
9
|
+
def ready(self):
|
|
10
|
+
from fetchnode.conf import get_client_settings
|
|
11
|
+
from fetchnode.api import track_event
|
|
12
|
+
|
|
13
|
+
# Ensure settings.fetchnode_track is available if needed by legacy code
|
|
14
|
+
try:
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
setattr(settings, "fetchnode_track", track_event)
|
|
17
|
+
setattr(settings, "FETCHNODE_TRACK", track_event)
|
|
18
|
+
except Exception:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
config = get_client_settings()
|
|
22
|
+
if config["ENABLED"] and config["INSTRUMENT_LOGGING"]:
|
|
23
|
+
from fetchnode.handlers import install_logging_instrumentation
|
|
24
|
+
install_logging_instrumentation()
|
|
25
|
+
|
|
26
|
+
if config["INSTRUMENT_CELERY"]:
|
|
27
|
+
from fetchnode.celery import install_celery_instrumentation
|
|
28
|
+
install_celery_instrumentation()
|
|
29
|
+
|
|
30
|
+
if config["ENABLED"] and config["INSTRUMENT_EXCEPTHOOK"]:
|
|
31
|
+
from fetchnode.excepthook import install_excepthook_instrumentation
|
|
32
|
+
install_excepthook_instrumentation()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Backwards compatibility alias
|
|
36
|
+
FetchNodeClientConfig = FetchNodeConfig
|
fetchnode/capture.py
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from functools import wraps
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
|
|
6
|
+
from fetchnode.api import capture_exception
|
|
7
|
+
from fetchnode.conf import get_client_settings
|
|
8
|
+
from fetchnode.scrub import scrub_value
|
|
9
|
+
from fetchnode.sender import send_payload
|
|
10
|
+
from fetchnode.utils import utc_now_iso
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def track_job_execution(
|
|
14
|
+
*,
|
|
15
|
+
task_name: str,
|
|
16
|
+
status: str,
|
|
17
|
+
duration_ms: float = 0,
|
|
18
|
+
queue_latency_ms: float = 0,
|
|
19
|
+
retry_count: int = 0,
|
|
20
|
+
task_id: str = "",
|
|
21
|
+
queue: str = "",
|
|
22
|
+
runtime: str = "generic",
|
|
23
|
+
trace_id: str = "",
|
|
24
|
+
parent_trace_id: str = "",
|
|
25
|
+
properties=None,
|
|
26
|
+
) -> str:
|
|
27
|
+
config = get_client_settings()
|
|
28
|
+
event_id = uuid.uuid4().hex
|
|
29
|
+
if not config["CAPTURE_JOBS"]:
|
|
30
|
+
return event_id
|
|
31
|
+
send_payload({
|
|
32
|
+
"event_id": event_id,
|
|
33
|
+
"type": "job",
|
|
34
|
+
"timestamp": utc_now_iso(),
|
|
35
|
+
"task_id": str(task_id),
|
|
36
|
+
"task_name": str(task_name),
|
|
37
|
+
"queue": str(queue),
|
|
38
|
+
"runtime": str(runtime),
|
|
39
|
+
"status": str(status),
|
|
40
|
+
"duration_ms": round(max(0, float(duration_ms)), 2),
|
|
41
|
+
"queue_latency_ms": round(max(0, float(queue_latency_ms)), 2),
|
|
42
|
+
"retry_count": max(0, int(retry_count)),
|
|
43
|
+
"trace_id": trace_id or uuid.uuid4().hex,
|
|
44
|
+
"parent_trace_id": parent_trace_id,
|
|
45
|
+
"environment": config["ENVIRONMENT"],
|
|
46
|
+
"release": config["RELEASE"],
|
|
47
|
+
"payload": scrub_value(properties or {}, sensitive_keys=config["SENSITIVE_KEYS"]),
|
|
48
|
+
})
|
|
49
|
+
return event_id
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def capture_job_exception(
|
|
53
|
+
exc: Exception, *, job_name: str, queue: str = "", tags=None, logger_name="", record_execution=True
|
|
54
|
+
) -> None:
|
|
55
|
+
merged_tags = {"capture_mode": "job", "job_name": job_name}
|
|
56
|
+
if queue:
|
|
57
|
+
merged_tags["queue"] = queue
|
|
58
|
+
if tags:
|
|
59
|
+
merged_tags.update(tags)
|
|
60
|
+
capture_exception(exc, tags=merged_tags, logger_name=logger_name or f"jobs.{job_name}")
|
|
61
|
+
if record_execution:
|
|
62
|
+
track_job_execution(task_name=job_name, queue=queue, status="failed")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def capture_exceptions(*, tags=None, logger_name="", reraise=True):
|
|
66
|
+
def decorator(func):
|
|
67
|
+
@wraps(func)
|
|
68
|
+
def wrapper(*args, **kwargs):
|
|
69
|
+
try:
|
|
70
|
+
return func(*args, **kwargs)
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
merged_tags = {"capture_mode": "decorator", "callable": func.__name__}
|
|
73
|
+
if tags:
|
|
74
|
+
merged_tags.update(tags)
|
|
75
|
+
capture_exception(exc, tags=merged_tags, logger_name=logger_name or func.__module__)
|
|
76
|
+
if reraise:
|
|
77
|
+
raise
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
return wrapper
|
|
81
|
+
|
|
82
|
+
return decorator
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def capture_job(*, job_name: str, queue: str = "", tags=None, logger_name="", reraise=True):
|
|
86
|
+
def decorator(func):
|
|
87
|
+
@wraps(func)
|
|
88
|
+
def wrapper(*args, **kwargs):
|
|
89
|
+
started = time.monotonic()
|
|
90
|
+
trace_id = uuid.uuid4().hex
|
|
91
|
+
try:
|
|
92
|
+
result = func(*args, **kwargs)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
merged_tags = {"capture_mode": "job_decorator", "callable": func.__name__}
|
|
95
|
+
if tags:
|
|
96
|
+
merged_tags.update(tags)
|
|
97
|
+
capture_job_exception(
|
|
98
|
+
exc,
|
|
99
|
+
job_name=job_name,
|
|
100
|
+
queue=queue,
|
|
101
|
+
tags=merged_tags,
|
|
102
|
+
logger_name=logger_name or func.__module__,
|
|
103
|
+
record_execution=False,
|
|
104
|
+
)
|
|
105
|
+
track_job_execution(
|
|
106
|
+
task_name=job_name,
|
|
107
|
+
queue=queue,
|
|
108
|
+
status="failed",
|
|
109
|
+
duration_ms=(time.monotonic() - started) * 1000,
|
|
110
|
+
runtime="decorator",
|
|
111
|
+
trace_id=trace_id,
|
|
112
|
+
properties=merged_tags,
|
|
113
|
+
)
|
|
114
|
+
if reraise:
|
|
115
|
+
raise
|
|
116
|
+
return None
|
|
117
|
+
track_job_execution(
|
|
118
|
+
task_name=job_name,
|
|
119
|
+
queue=queue,
|
|
120
|
+
status="succeeded",
|
|
121
|
+
duration_ms=(time.monotonic() - started) * 1000,
|
|
122
|
+
runtime="decorator",
|
|
123
|
+
trace_id=trace_id,
|
|
124
|
+
properties=tags,
|
|
125
|
+
)
|
|
126
|
+
return result
|
|
127
|
+
|
|
128
|
+
return wrapper
|
|
129
|
+
|
|
130
|
+
return decorator
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def capture_management_command(*, command_name: str, tags=None, logger_name="management", reraise=True):
|
|
134
|
+
merged_tags = {"runtime": "management_command", "command": command_name}
|
|
135
|
+
if tags:
|
|
136
|
+
merged_tags.update(tags)
|
|
137
|
+
return capture_context(tags=merged_tags, logger_name=logger_name, reraise=reraise)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def capture_django_task(*, task_name: str, queue: str = "", tags=None, reraise=True):
|
|
141
|
+
"""Instrument a Django 6 Tasks backend callable without depending on a backend."""
|
|
142
|
+
return capture_job(
|
|
143
|
+
job_name=task_name,
|
|
144
|
+
queue=queue,
|
|
145
|
+
tags={"runtime": "django.tasks", **(tags or {})},
|
|
146
|
+
logger_name=f"django.tasks.{task_name}",
|
|
147
|
+
reraise=reraise,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@contextmanager
|
|
152
|
+
def capture_job_context(*, job_name: str, queue: str = "", tags=None, logger_name="", reraise=True):
|
|
153
|
+
started = time.monotonic()
|
|
154
|
+
trace_id = uuid.uuid4().hex
|
|
155
|
+
try:
|
|
156
|
+
yield
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
merged_tags = {"capture_mode": "job_context"}
|
|
159
|
+
if tags:
|
|
160
|
+
merged_tags.update(tags)
|
|
161
|
+
capture_job_exception(
|
|
162
|
+
exc,
|
|
163
|
+
job_name=job_name,
|
|
164
|
+
queue=queue,
|
|
165
|
+
tags=merged_tags,
|
|
166
|
+
logger_name=logger_name or f"jobs.{job_name}",
|
|
167
|
+
record_execution=False,
|
|
168
|
+
)
|
|
169
|
+
track_job_execution(
|
|
170
|
+
task_name=job_name,
|
|
171
|
+
queue=queue,
|
|
172
|
+
status="failed",
|
|
173
|
+
duration_ms=(time.monotonic() - started) * 1000,
|
|
174
|
+
runtime="contextmanager",
|
|
175
|
+
trace_id=trace_id,
|
|
176
|
+
properties=merged_tags,
|
|
177
|
+
)
|
|
178
|
+
if reraise:
|
|
179
|
+
raise
|
|
180
|
+
else:
|
|
181
|
+
track_job_execution(
|
|
182
|
+
task_name=job_name,
|
|
183
|
+
queue=queue,
|
|
184
|
+
status="succeeded",
|
|
185
|
+
duration_ms=(time.monotonic() - started) * 1000,
|
|
186
|
+
runtime="contextmanager",
|
|
187
|
+
trace_id=trace_id,
|
|
188
|
+
properties=tags,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@contextmanager
|
|
193
|
+
def capture_context(*, tags=None, logger_name="", reraise=True):
|
|
194
|
+
try:
|
|
195
|
+
yield
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
merged_tags = {"capture_mode": "contextmanager"}
|
|
198
|
+
if tags:
|
|
199
|
+
merged_tags.update(tags)
|
|
200
|
+
capture_exception(exc, tags=merged_tags, logger_name=logger_name)
|
|
201
|
+
if reraise:
|
|
202
|
+
raise
|