insider-python 0.1.2__py3-none-any.whl → 0.1.4__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.
- insider/__init__.py +7 -0
- insider/_envelope.py +2 -2
- insider/_footprint.py +64 -0
- insider/_version.py +1 -1
- insider/client.py +317 -5
- insider/contrib/django/apps.py +2 -1
- insider/contrib/django/middleware.py +10 -109
- insider/integrations/__init__.py +24 -0
- insider/integrations/django/__init__.py +60 -0
- insider/integrations/django/capture.py +45 -0
- insider/integrations/django/drf.py +40 -0
- insider/integrations/django/handler.py +71 -0
- insider/integrations/django/perf.py +38 -0
- insider/integrations/django/request.py +140 -0
- insider/integrations/django/signals.py +39 -0
- insider/integrations/django/wsgi.py +43 -0
- insider/integrations/logging/__init__.py +66 -0
- insider/integrations/logging/handler.py +74 -0
- insider/integrations/logging/levels.py +29 -0
- insider/scope.py +144 -3
- insider_python-0.1.4.dist-info/METADATA +174 -0
- insider_python-0.1.4.dist-info/RECORD +32 -0
- insider_python-0.1.2.dist-info/METADATA +0 -126
- insider_python-0.1.2.dist-info/RECORD +0 -19
- {insider_python-0.1.2.dist-info → insider_python-0.1.4.dist-info}/WHEEL +0 -0
- {insider_python-0.1.2.dist-info → insider_python-0.1.4.dist-info}/top_level.txt +0 -0
insider/__init__.py
CHANGED
|
@@ -6,6 +6,7 @@ Public API:
|
|
|
6
6
|
insider.init(dsn=..., environment=..., release=..., ...)
|
|
7
7
|
insider.capture_exception(exc, level="error", tags=..., extra=...)
|
|
8
8
|
insider.capture_message("text", level="info", tags=..., extra=...)
|
|
9
|
+
insider.capture_perf("GET /api/users/", duration_ms=45, status_code=200)
|
|
9
10
|
insider.flush(timeout=2.0)
|
|
10
11
|
insider.close(timeout=2.0)
|
|
11
12
|
|
|
@@ -17,12 +18,15 @@ from ._version import __version__
|
|
|
17
18
|
from .client import (
|
|
18
19
|
Client,
|
|
19
20
|
capture_exception,
|
|
21
|
+
capture_log,
|
|
20
22
|
capture_message,
|
|
23
|
+
capture_perf,
|
|
21
24
|
close,
|
|
22
25
|
flush,
|
|
23
26
|
init,
|
|
24
27
|
)
|
|
25
28
|
from .dsn import DSN, InvalidDSNError
|
|
29
|
+
from .integrations.logging import LoggingIntegration
|
|
26
30
|
|
|
27
31
|
__all__ = [
|
|
28
32
|
"Client",
|
|
@@ -30,8 +34,11 @@ __all__ = [
|
|
|
30
34
|
"InvalidDSNError",
|
|
31
35
|
"__version__",
|
|
32
36
|
"capture_exception",
|
|
37
|
+
"capture_log",
|
|
33
38
|
"capture_message",
|
|
39
|
+
"capture_perf",
|
|
34
40
|
"close",
|
|
35
41
|
"flush",
|
|
36
42
|
"init",
|
|
43
|
+
"LoggingIntegration",
|
|
37
44
|
]
|
insider/_envelope.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Footprint envelope construction + size-budget enforcement.
|
|
3
3
|
|
|
4
4
|
`build_envelope` is called from the capture functions in `client.py`. It
|
|
5
5
|
takes the raw bits (kind, level, message, exception payload, scope,
|
|
@@ -58,7 +58,7 @@ def build_envelope(
|
|
|
58
58
|
occurred_at: Optional[str] = None,
|
|
59
59
|
commit_hash: Optional[str] = None,
|
|
60
60
|
) -> Dict[str, Any]:
|
|
61
|
-
"""Assemble the
|
|
61
|
+
"""Assemble the Footprint envelope. Pure: no I/O, no globals."""
|
|
62
62
|
body: Dict[str, Any] = dict(payload or {})
|
|
63
63
|
if tags:
|
|
64
64
|
body["tags"] = tags
|
insider/_footprint.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Build flat footprint payloads for beam ingest."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
from ._version import __version__
|
|
8
|
+
from .stacktrace import runtime_payload
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_footprint_payload(
|
|
12
|
+
*,
|
|
13
|
+
request_id: Optional[str],
|
|
14
|
+
request_path: str,
|
|
15
|
+
request_method: Optional[str],
|
|
16
|
+
request_user: str = "anonymous",
|
|
17
|
+
request_body: Any = None,
|
|
18
|
+
response_body: Any = None,
|
|
19
|
+
response_time: float,
|
|
20
|
+
status_code: int,
|
|
21
|
+
system_logs: Optional[list] = None,
|
|
22
|
+
ip_address: Optional[str] = None,
|
|
23
|
+
user_agent: Optional[str] = None,
|
|
24
|
+
db_query_count: int = 0,
|
|
25
|
+
exception_block: Optional[Dict[str, Any]] = None,
|
|
26
|
+
environment: str = "production",
|
|
27
|
+
release: Optional[str] = None,
|
|
28
|
+
service_name: Optional[str] = None,
|
|
29
|
+
commit_hash: Optional[str] = None,
|
|
30
|
+
) -> Dict[str, Any]:
|
|
31
|
+
runtime = runtime_payload(__version__)
|
|
32
|
+
stack_trace = None
|
|
33
|
+
exception_name = None
|
|
34
|
+
if exception_block:
|
|
35
|
+
exception_name = exception_block.get("type")
|
|
36
|
+
stack_trace = dict(exception_block)
|
|
37
|
+
if commit_hash:
|
|
38
|
+
stack_trace["commit_hash"] = commit_hash
|
|
39
|
+
|
|
40
|
+
body = request_body
|
|
41
|
+
if body is not None and not isinstance(body, (dict, list, str, int, float, bool)):
|
|
42
|
+
body = str(body)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
"request_id": request_id,
|
|
46
|
+
"request_user": request_user,
|
|
47
|
+
"request_path": request_path,
|
|
48
|
+
"request_body": body if body is not None else None,
|
|
49
|
+
"request_method": (request_method or "").lower() or None,
|
|
50
|
+
"response_body": response_body,
|
|
51
|
+
"response_time": float(response_time),
|
|
52
|
+
"status_code": status_code,
|
|
53
|
+
"system_logs": system_logs,
|
|
54
|
+
"ip_address": ip_address,
|
|
55
|
+
"user_agent": user_agent,
|
|
56
|
+
"db_query_count": db_query_count,
|
|
57
|
+
"exception_name": exception_name,
|
|
58
|
+
"stack_trace": stack_trace,
|
|
59
|
+
"service_name": service_name,
|
|
60
|
+
"environment": environment,
|
|
61
|
+
"language": runtime.get("language"),
|
|
62
|
+
"framework": runtime.get("framework"),
|
|
63
|
+
"release": release,
|
|
64
|
+
}
|
insider/_version.py
CHANGED
insider/client.py
CHANGED
|
@@ -18,9 +18,12 @@ import atexit
|
|
|
18
18
|
import os
|
|
19
19
|
import subprocess
|
|
20
20
|
import threading
|
|
21
|
-
from typing import Any, Callable, Dict, Iterable, List, Optional
|
|
21
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Union
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
import json
|
|
24
|
+
|
|
25
|
+
from ._envelope import MAX_ENVELOPE_BYTES, build_envelope, enforce_size_budget
|
|
26
|
+
from ._footprint import build_footprint_payload
|
|
24
27
|
from ._version import __version__
|
|
25
28
|
from .dsn import DSN, InvalidDSNError
|
|
26
29
|
from .safety import debug, safe, set_debug
|
|
@@ -30,10 +33,27 @@ from .stacktrace import caller_source, exception_payload, runtime_payload
|
|
|
30
33
|
from .transport import BackgroundTransport
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
VALID_KINDS = {"error", "perf", "log", "custom"}
|
|
36
|
+
VALID_KINDS = {"error", "perf", "log", "custom", "request"}
|
|
34
37
|
VALID_LEVELS = {"debug", "info", "warning", "error", "fatal"}
|
|
35
38
|
|
|
36
39
|
|
|
40
|
+
def _byte_len_footprint(obj: Dict[str, Any]) -> int:
|
|
41
|
+
try:
|
|
42
|
+
return len(json.dumps(obj, default=str, ensure_ascii=False).encode("utf-8"))
|
|
43
|
+
except Exception:
|
|
44
|
+
return 10**9
|
|
45
|
+
|
|
46
|
+
IntegrationLike = Union[Any, type]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _setup_integrations(integrations: Sequence[IntegrationLike]) -> None:
|
|
50
|
+
for integration in integrations:
|
|
51
|
+
instance = integration() if isinstance(integration, type) else integration
|
|
52
|
+
setup_once = getattr(instance, "setup_once", None)
|
|
53
|
+
if callable(setup_once):
|
|
54
|
+
setup_once()
|
|
55
|
+
|
|
56
|
+
|
|
37
57
|
# ---------------------------------------------------------------------------
|
|
38
58
|
# DSN resolution
|
|
39
59
|
# ---------------------------------------------------------------------------
|
|
@@ -78,9 +98,11 @@ class Client:
|
|
|
78
98
|
transport_flush_timeout: float = 2.0,
|
|
79
99
|
debug: bool = False,
|
|
80
100
|
transport: Optional[BackgroundTransport] = None,
|
|
101
|
+
enable_logs: bool = False,
|
|
81
102
|
) -> None:
|
|
82
103
|
set_debug(debug)
|
|
83
104
|
self.dsn = dsn
|
|
105
|
+
self.enable_logs = bool(enable_logs)
|
|
84
106
|
self.send_default_pii = bool(send_default_pii)
|
|
85
107
|
self.before_send = before_send
|
|
86
108
|
self.scrub_keys: List[str] = list(scrub_keys or [])
|
|
@@ -135,6 +157,10 @@ class Client:
|
|
|
135
157
|
exception_block = exception_payload(
|
|
136
158
|
exc, in_app_include=self.scope.static.in_app_include
|
|
137
159
|
)
|
|
160
|
+
if self.scope.current_request() is not None:
|
|
161
|
+
self.scope.set_pending_exception(exception_block)
|
|
162
|
+
return self.scope.current_trace_id()
|
|
163
|
+
|
|
138
164
|
payload: Dict[str, Any] = {
|
|
139
165
|
"exception": exception_block,
|
|
140
166
|
"runtime": runtime_payload(__version__),
|
|
@@ -142,6 +168,9 @@ class Client:
|
|
|
142
168
|
request_ctx = self.scope.current_request()
|
|
143
169
|
if request_ctx is not None:
|
|
144
170
|
payload["request"] = request_ctx
|
|
171
|
+
breadcrumbs = self.scope.current_breadcrumbs()
|
|
172
|
+
if breadcrumbs:
|
|
173
|
+
payload["breadcrumbs"] = breadcrumbs
|
|
145
174
|
|
|
146
175
|
envelope = build_envelope(
|
|
147
176
|
kind="error",
|
|
@@ -150,7 +179,7 @@ class Client:
|
|
|
150
179
|
source=self._source_from_exception(exception_block),
|
|
151
180
|
environment=self.scope.static.environment,
|
|
152
181
|
release=self.scope.static.release,
|
|
153
|
-
trace_id=trace_id,
|
|
182
|
+
trace_id=trace_id or self.scope.current_trace_id(),
|
|
154
183
|
commit_hash=self.commit_hash,
|
|
155
184
|
payload=payload,
|
|
156
185
|
tags=tags,
|
|
@@ -173,6 +202,19 @@ class Client:
|
|
|
173
202
|
debug(f"capture_message expects str, got {type(message).__name__}")
|
|
174
203
|
return None
|
|
175
204
|
level = level if level in VALID_LEVELS else "info"
|
|
205
|
+
|
|
206
|
+
if self.scope.current_request() is not None:
|
|
207
|
+
from datetime import datetime, timezone
|
|
208
|
+
|
|
209
|
+
self.scope.add_request_log(
|
|
210
|
+
level=level,
|
|
211
|
+
message=message,
|
|
212
|
+
source=source or caller_source(skip=2),
|
|
213
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
214
|
+
)
|
|
215
|
+
return self.scope.current_trace_id()
|
|
216
|
+
|
|
217
|
+
level = level if level in VALID_LEVELS else "info"
|
|
176
218
|
kind = kind if kind in VALID_KINDS else "log"
|
|
177
219
|
|
|
178
220
|
payload: Dict[str, Any] = {"runtime": runtime_payload(__version__)}
|
|
@@ -195,6 +237,146 @@ class Client:
|
|
|
195
237
|
)
|
|
196
238
|
return self._dispatch(envelope)
|
|
197
239
|
|
|
240
|
+
def capture_log(
|
|
241
|
+
self,
|
|
242
|
+
message: str,
|
|
243
|
+
*,
|
|
244
|
+
level: str = "info",
|
|
245
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
246
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
247
|
+
source: Optional[str] = None,
|
|
248
|
+
trace_id: Optional[str] = None,
|
|
249
|
+
) -> Optional[str]:
|
|
250
|
+
"""Record a structured log line (`kind=log`). Alias ergonomics over `capture_message`."""
|
|
251
|
+
return self.capture_message(
|
|
252
|
+
message,
|
|
253
|
+
level=level,
|
|
254
|
+
tags=tags,
|
|
255
|
+
extra=extra,
|
|
256
|
+
source=source,
|
|
257
|
+
trace_id=trace_id,
|
|
258
|
+
kind="log",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def capture_request(
|
|
262
|
+
self,
|
|
263
|
+
*,
|
|
264
|
+
duration_ms: float,
|
|
265
|
+
op: str,
|
|
266
|
+
status_code: Optional[int] = None,
|
|
267
|
+
method: Optional[str] = None,
|
|
268
|
+
trace_id: Optional[str] = None,
|
|
269
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
270
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
271
|
+
) -> Optional[str]:
|
|
272
|
+
"""Emit one HTTP request footprint to the beam endpoint."""
|
|
273
|
+
if not isinstance(duration_ms, (int, float)) or duration_ms < 0:
|
|
274
|
+
debug(f"capture_request expects duration_ms >= 0, got {duration_ms!r}")
|
|
275
|
+
return None
|
|
276
|
+
if not isinstance(op, str) or not op.strip():
|
|
277
|
+
debug("capture_request expects non-empty op str")
|
|
278
|
+
return None
|
|
279
|
+
code = status_code if status_code is not None else 200
|
|
280
|
+
if not isinstance(code, int) or not (100 <= code <= 599):
|
|
281
|
+
debug(f"capture_request invalid status_code: {status_code!r}")
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
exception_block = self.scope.current_pending_exception()
|
|
285
|
+
request_ctx = self.scope.current_request() or {}
|
|
286
|
+
logs = self.scope.current_request_logs()
|
|
287
|
+
headers = request_ctx.get("headers") if isinstance(request_ctx.get("headers"), dict) else {}
|
|
288
|
+
|
|
289
|
+
footprint = build_footprint_payload(
|
|
290
|
+
request_id=trace_id or self.scope.current_trace_id(),
|
|
291
|
+
request_path=op.strip(),
|
|
292
|
+
request_method=method,
|
|
293
|
+
request_user=str(request_ctx.get("user") or "anonymous"),
|
|
294
|
+
request_body=request_ctx.get("body"),
|
|
295
|
+
response_time=float(duration_ms),
|
|
296
|
+
status_code=code,
|
|
297
|
+
system_logs=logs or None,
|
|
298
|
+
ip_address=request_ctx.get("ip") or request_ctx.get("ip_address") or headers.get("x-forwarded-for"),
|
|
299
|
+
user_agent=request_ctx.get("user_agent") or headers.get("user-agent"),
|
|
300
|
+
exception_block=exception_block,
|
|
301
|
+
environment=self.scope.static.environment,
|
|
302
|
+
release=self.scope.static.release,
|
|
303
|
+
commit_hash=self.commit_hash,
|
|
304
|
+
)
|
|
305
|
+
if tags:
|
|
306
|
+
footprint["_tags"] = tags
|
|
307
|
+
if extra:
|
|
308
|
+
footprint["_extra"] = extra
|
|
309
|
+
return self._dispatch_footprint(footprint)
|
|
310
|
+
|
|
311
|
+
def capture_perf(
|
|
312
|
+
self,
|
|
313
|
+
op: str,
|
|
314
|
+
duration_ms: float,
|
|
315
|
+
*,
|
|
316
|
+
status_code: Optional[int] = None,
|
|
317
|
+
method: Optional[str] = None,
|
|
318
|
+
level: str = "info",
|
|
319
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
320
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
321
|
+
source: str = "django.request",
|
|
322
|
+
trace_id: Optional[str] = None,
|
|
323
|
+
) -> Optional[str]:
|
|
324
|
+
"""
|
|
325
|
+
Record a performance timing beacon (`kind=perf`).
|
|
326
|
+
|
|
327
|
+
Used for HTTP request durations, slow queries, and other timing
|
|
328
|
+
signals. Does not create Issues on the server — perf rows live in
|
|
329
|
+
Beacons only. Pair with `trace_id` to link a perf row to an error
|
|
330
|
+
on the same request (see DjangoIntegration).
|
|
331
|
+
"""
|
|
332
|
+
if not isinstance(op, str) or not op.strip():
|
|
333
|
+
debug("capture_perf expects non-empty op str")
|
|
334
|
+
return None
|
|
335
|
+
if not isinstance(duration_ms, (int, float)) or duration_ms < 0:
|
|
336
|
+
debug(f"capture_perf expects duration_ms >= 0, got {duration_ms!r}")
|
|
337
|
+
return None
|
|
338
|
+
if status_code is not None:
|
|
339
|
+
if not isinstance(status_code, int) or not (100 <= status_code <= 599):
|
|
340
|
+
debug(f"capture_perf invalid status_code: {status_code!r}")
|
|
341
|
+
return None
|
|
342
|
+
level = level if level in VALID_LEVELS else "info"
|
|
343
|
+
|
|
344
|
+
payload: Dict[str, Any] = {
|
|
345
|
+
"runtime": runtime_payload(__version__),
|
|
346
|
+
"duration_ms": float(duration_ms),
|
|
347
|
+
"op": op.strip(),
|
|
348
|
+
}
|
|
349
|
+
if status_code is not None:
|
|
350
|
+
payload["status_code"] = status_code
|
|
351
|
+
if method is not None:
|
|
352
|
+
payload["method"] = method
|
|
353
|
+
|
|
354
|
+
request_ctx = self.scope.current_request()
|
|
355
|
+
if request_ctx is not None:
|
|
356
|
+
payload["request"] = request_ctx
|
|
357
|
+
|
|
358
|
+
message = self._perf_summary_message(
|
|
359
|
+
method=method,
|
|
360
|
+
op=op.strip(),
|
|
361
|
+
status_code=status_code,
|
|
362
|
+
duration_ms=float(duration_ms),
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
envelope = build_envelope(
|
|
366
|
+
kind="perf",
|
|
367
|
+
level=level,
|
|
368
|
+
message=message,
|
|
369
|
+
source=source,
|
|
370
|
+
environment=self.scope.static.environment,
|
|
371
|
+
release=self.scope.static.release,
|
|
372
|
+
trace_id=trace_id or self.scope.current_trace_id(),
|
|
373
|
+
commit_hash=self.commit_hash,
|
|
374
|
+
payload=payload,
|
|
375
|
+
tags=tags,
|
|
376
|
+
extra=extra,
|
|
377
|
+
)
|
|
378
|
+
return self._dispatch(envelope)
|
|
379
|
+
|
|
198
380
|
# ------------------------------------------------------------------
|
|
199
381
|
# Lifecycle
|
|
200
382
|
# ------------------------------------------------------------------
|
|
@@ -209,6 +391,32 @@ class Client:
|
|
|
209
391
|
# Internal helpers
|
|
210
392
|
# ------------------------------------------------------------------
|
|
211
393
|
|
|
394
|
+
def _dispatch_footprint(self, footprint: Dict[str, Any]) -> Optional[str]:
|
|
395
|
+
"""Scrub → before_send → size budget → transport submit."""
|
|
396
|
+
scrubbed = dict(footprint)
|
|
397
|
+
scrubbed.pop("_tags", None)
|
|
398
|
+
scrubbed.pop("_extra", None)
|
|
399
|
+
if self.before_send is not None:
|
|
400
|
+
try:
|
|
401
|
+
wrapped = {"payload": scrubbed, **scrubbed}
|
|
402
|
+
result = self.before_send(wrapped) # type: ignore[assignment]
|
|
403
|
+
except Exception as exc:
|
|
404
|
+
debug(f"before_send raised {type(exc).__name__}: {exc}; dropping footprint")
|
|
405
|
+
return None
|
|
406
|
+
if result is None:
|
|
407
|
+
return None
|
|
408
|
+
if isinstance(result, dict) and "payload" in result:
|
|
409
|
+
scrubbed = result["payload"]
|
|
410
|
+
elif isinstance(result, dict):
|
|
411
|
+
scrubbed = result
|
|
412
|
+
|
|
413
|
+
if _byte_len_footprint(scrubbed) > MAX_ENVELOPE_BYTES:
|
|
414
|
+
debug("footprint exceeds size budget; dropping")
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
accepted = self.transport.submit(scrubbed)
|
|
418
|
+
return scrubbed.get("request_id") if accepted else None
|
|
419
|
+
|
|
212
420
|
def _dispatch(self, envelope: Dict[str, Any]) -> Optional[str]:
|
|
213
421
|
"""Scrub → before_send → size budget → transport submit."""
|
|
214
422
|
envelope["payload"] = scrub(envelope.get("payload"), extra_keys=self.scrub_keys)
|
|
@@ -237,6 +445,24 @@ class Client:
|
|
|
237
445
|
return tail.get("module") or tail.get("function")
|
|
238
446
|
return None
|
|
239
447
|
|
|
448
|
+
@staticmethod
|
|
449
|
+
def _perf_summary_message(
|
|
450
|
+
*,
|
|
451
|
+
method: Optional[str],
|
|
452
|
+
op: str,
|
|
453
|
+
status_code: Optional[int],
|
|
454
|
+
duration_ms: float,
|
|
455
|
+
) -> str:
|
|
456
|
+
"""Short list-view label, e.g. `GET /api/users/ 200 45ms`."""
|
|
457
|
+
parts: List[str] = []
|
|
458
|
+
if method:
|
|
459
|
+
parts.append(method)
|
|
460
|
+
parts.append(op)
|
|
461
|
+
if status_code is not None:
|
|
462
|
+
parts.append(str(status_code))
|
|
463
|
+
parts.append(f"{duration_ms:.0f}ms")
|
|
464
|
+
return " ".join(parts)
|
|
465
|
+
|
|
240
466
|
|
|
241
467
|
# ---------------------------------------------------------------------------
|
|
242
468
|
# Module-level facade
|
|
@@ -259,6 +485,8 @@ def _set_active(client: Optional[Client]) -> None:
|
|
|
259
485
|
@safe
|
|
260
486
|
def init(
|
|
261
487
|
dsn: Optional[str] = None,
|
|
488
|
+
*,
|
|
489
|
+
integrations: Optional[Sequence[IntegrationLike]] = None,
|
|
262
490
|
**kwargs: Any,
|
|
263
491
|
) -> Optional[Client]:
|
|
264
492
|
"""
|
|
@@ -268,6 +496,9 @@ def init(
|
|
|
268
496
|
Calling `init` a second time is allowed but logs a warning and
|
|
269
497
|
closes the previous client first. The new client becomes the
|
|
270
498
|
process-global one.
|
|
499
|
+
|
|
500
|
+
Pass framework integrations via `integrations=[...]`. Each integration's
|
|
501
|
+
`setup_once()` runs after the client is active.
|
|
271
502
|
"""
|
|
272
503
|
global _active_client
|
|
273
504
|
raw = _resolve_dsn_string(dsn)
|
|
@@ -280,6 +511,8 @@ def init(
|
|
|
280
511
|
debug(f"invalid DSN: {exc}; entering disabled mode")
|
|
281
512
|
return None
|
|
282
513
|
|
|
514
|
+
integration_list = list(integrations or [])
|
|
515
|
+
|
|
283
516
|
with _init_lock:
|
|
284
517
|
if _active_client is not None:
|
|
285
518
|
debug("re-initializing; closing previous client")
|
|
@@ -287,9 +520,11 @@ def init(
|
|
|
287
520
|
_active_client.close()
|
|
288
521
|
except Exception as exc:
|
|
289
522
|
debug(f"previous client close failed: {exc}")
|
|
290
|
-
|
|
523
|
+
enable_logs = bool(kwargs.pop("enable_logs", False))
|
|
524
|
+
client = Client(parsed, enable_logs=enable_logs, **kwargs)
|
|
291
525
|
_set_active(client)
|
|
292
526
|
|
|
527
|
+
_setup_integrations(integration_list)
|
|
293
528
|
atexit.register(_atexit_close)
|
|
294
529
|
return client
|
|
295
530
|
|
|
@@ -347,6 +582,83 @@ def capture_message(
|
|
|
347
582
|
)
|
|
348
583
|
|
|
349
584
|
|
|
585
|
+
@safe
|
|
586
|
+
def capture_log(
|
|
587
|
+
message: str,
|
|
588
|
+
*,
|
|
589
|
+
level: str = "info",
|
|
590
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
591
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
592
|
+
source: Optional[str] = None,
|
|
593
|
+
trace_id: Optional[str] = None,
|
|
594
|
+
) -> Optional[str]:
|
|
595
|
+
client = _client()
|
|
596
|
+
if client is None:
|
|
597
|
+
return None
|
|
598
|
+
return client.capture_log(
|
|
599
|
+
message,
|
|
600
|
+
level=level,
|
|
601
|
+
tags=tags,
|
|
602
|
+
extra=extra,
|
|
603
|
+
source=source,
|
|
604
|
+
trace_id=trace_id,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@safe
|
|
609
|
+
def capture_request(
|
|
610
|
+
*,
|
|
611
|
+
duration_ms: float,
|
|
612
|
+
op: str,
|
|
613
|
+
status_code: Optional[int] = None,
|
|
614
|
+
method: Optional[str] = None,
|
|
615
|
+
trace_id: Optional[str] = None,
|
|
616
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
617
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
618
|
+
) -> Optional[str]:
|
|
619
|
+
client = _client()
|
|
620
|
+
if client is None:
|
|
621
|
+
return None
|
|
622
|
+
return client.capture_request(
|
|
623
|
+
duration_ms=duration_ms,
|
|
624
|
+
op=op,
|
|
625
|
+
status_code=status_code,
|
|
626
|
+
method=method,
|
|
627
|
+
trace_id=trace_id,
|
|
628
|
+
tags=tags,
|
|
629
|
+
extra=extra,
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@safe
|
|
634
|
+
def capture_perf(
|
|
635
|
+
op: str,
|
|
636
|
+
duration_ms: float,
|
|
637
|
+
*,
|
|
638
|
+
status_code: Optional[int] = None,
|
|
639
|
+
method: Optional[str] = None,
|
|
640
|
+
level: str = "info",
|
|
641
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
642
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
643
|
+
source: str = "django.request",
|
|
644
|
+
trace_id: Optional[str] = None,
|
|
645
|
+
) -> Optional[str]:
|
|
646
|
+
client = _client()
|
|
647
|
+
if client is None:
|
|
648
|
+
return None
|
|
649
|
+
return client.capture_perf(
|
|
650
|
+
op,
|
|
651
|
+
duration_ms,
|
|
652
|
+
status_code=status_code,
|
|
653
|
+
method=method,
|
|
654
|
+
level=level,
|
|
655
|
+
tags=tags,
|
|
656
|
+
extra=extra,
|
|
657
|
+
source=source,
|
|
658
|
+
trace_id=trace_id,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
|
|
350
662
|
@safe
|
|
351
663
|
def flush(timeout: Optional[float] = None) -> bool:
|
|
352
664
|
client = _client()
|
insider/contrib/django/apps.py
CHANGED
|
@@ -13,6 +13,7 @@ from typing import Any, Dict
|
|
|
13
13
|
from django.apps import AppConfig
|
|
14
14
|
|
|
15
15
|
from ... import init
|
|
16
|
+
from ...integrations.django import DjangoIntegration
|
|
16
17
|
from ...safety import debug
|
|
17
18
|
|
|
18
19
|
|
|
@@ -57,4 +58,4 @@ class InsiderConfig(AppConfig):
|
|
|
57
58
|
# env-var fallback in `init` still applies if Django's settings
|
|
58
59
|
# didn't define it.
|
|
59
60
|
dsn = kwargs.pop("dsn", None)
|
|
60
|
-
init(dsn, **kwargs)
|
|
61
|
+
init(dsn, integrations=[DjangoIntegration()], **kwargs)
|