insider-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.
- insider/__init__.py +37 -0
- insider/_envelope.py +189 -0
- insider/_version.py +8 -0
- insider/client.py +344 -0
- insider/contrib/__init__.py +0 -0
- insider/contrib/django/__init__.py +27 -0
- insider/contrib/django/apps.py +60 -0
- insider/contrib/django/middleware.py +164 -0
- insider/dsn.py +92 -0
- insider/py.typed +0 -0
- insider/safety.py +61 -0
- insider/scope.py +54 -0
- insider/scrubbing.py +91 -0
- insider/stacktrace.py +153 -0
- insider/transport.py +213 -0
- insider_python-0.1.0.dist-info/METADATA +125 -0
- insider_python-0.1.0.dist-info/RECORD +19 -0
- insider_python-0.1.0.dist-info/WHEEL +5 -0
- insider_python-0.1.0.dist-info/top_level.txt +1 -0
insider/transport.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Background HTTP transport.
|
|
3
|
+
|
|
4
|
+
The capture path is on the customer's request thread. Sending the beacon
|
|
5
|
+
on that thread would add a network round-trip to every error response —
|
|
6
|
+
that is exactly what we will not do. Instead:
|
|
7
|
+
|
|
8
|
+
1. `submit(beacon)` puts the beacon on a bounded in-memory queue
|
|
9
|
+
(`queue.Queue.put_nowait`).
|
|
10
|
+
2. If the queue is full we drop the beacon and increment a counter.
|
|
11
|
+
The customer's thread never blocks waiting on us.
|
|
12
|
+
3. A daemon worker thread loops on `queue.get()` and POSTs each beacon
|
|
13
|
+
to the beam endpoint. Any error during the POST is caught, logged,
|
|
14
|
+
and the beacon discarded. No retries in v1 (see docs/python-sdk-plan
|
|
15
|
+
-> Non-goals).
|
|
16
|
+
4. `atexit` registers `close()` so short-lived scripts and management
|
|
17
|
+
commands drain before exit (bounded by `flush_timeout`).
|
|
18
|
+
|
|
19
|
+
The transport is the *only* place inside the SDK that talks to the
|
|
20
|
+
network. Everything else is in-process work.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import queue
|
|
27
|
+
import threading
|
|
28
|
+
import time
|
|
29
|
+
from typing import Any, Dict, Optional
|
|
30
|
+
|
|
31
|
+
import urllib3
|
|
32
|
+
|
|
33
|
+
from ._version import __version__
|
|
34
|
+
from .dsn import DSN
|
|
35
|
+
from .safety import debug
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_SENTINEL = object()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BackgroundTransport:
|
|
42
|
+
"""
|
|
43
|
+
Bounded queue + daemon worker thread + urllib3 connection pool.
|
|
44
|
+
|
|
45
|
+
Public methods are thread-safe. `submit` is the only hot-path call.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
dsn: DSN,
|
|
51
|
+
*,
|
|
52
|
+
queue_size: int = 1000,
|
|
53
|
+
flush_timeout: float = 2.0,
|
|
54
|
+
connect_timeout: float = 2.0,
|
|
55
|
+
read_timeout: float = 5.0,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._dsn = dsn
|
|
58
|
+
self._flush_timeout = flush_timeout
|
|
59
|
+
self._queue: "queue.Queue[Any]" = queue.Queue(maxsize=queue_size)
|
|
60
|
+
self._pool: Optional[urllib3.PoolManager] = urllib3.PoolManager(
|
|
61
|
+
timeout=urllib3.Timeout(connect=connect_timeout, read=read_timeout),
|
|
62
|
+
retries=False,
|
|
63
|
+
maxsize=4,
|
|
64
|
+
block=False,
|
|
65
|
+
)
|
|
66
|
+
self._closed = threading.Event()
|
|
67
|
+
|
|
68
|
+
# Bookkeeping the customer can read for telemetry-on-telemetry.
|
|
69
|
+
self.submitted_total: int = 0
|
|
70
|
+
self.sent_total: int = 0
|
|
71
|
+
self.dropped_full: int = 0
|
|
72
|
+
self.dropped_error: int = 0
|
|
73
|
+
|
|
74
|
+
self._worker = threading.Thread(
|
|
75
|
+
target=self._run,
|
|
76
|
+
name="insider-transport",
|
|
77
|
+
daemon=True,
|
|
78
|
+
)
|
|
79
|
+
self._worker.start()
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------------
|
|
82
|
+
# Public API
|
|
83
|
+
# ------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
def submit(self, beacon: Dict[str, Any]) -> bool:
|
|
86
|
+
"""
|
|
87
|
+
Enqueue a beacon. Returns True if accepted, False if dropped.
|
|
88
|
+
Never blocks the caller. Never raises.
|
|
89
|
+
"""
|
|
90
|
+
if self._closed.is_set():
|
|
91
|
+
self.dropped_full += 1
|
|
92
|
+
return False
|
|
93
|
+
try:
|
|
94
|
+
self._queue.put_nowait(beacon)
|
|
95
|
+
except queue.Full:
|
|
96
|
+
self.dropped_full += 1
|
|
97
|
+
debug("queue full; dropping beacon")
|
|
98
|
+
return False
|
|
99
|
+
self.submitted_total += 1
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def flush(self, timeout: Optional[float] = None) -> bool:
|
|
103
|
+
"""
|
|
104
|
+
Block until the queue drains or `timeout` elapses. Returns True
|
|
105
|
+
if drained, False otherwise. `timeout=None` uses `flush_timeout`.
|
|
106
|
+
"""
|
|
107
|
+
deadline = time.monotonic() + (
|
|
108
|
+
timeout if timeout is not None else self._flush_timeout
|
|
109
|
+
)
|
|
110
|
+
while time.monotonic() < deadline:
|
|
111
|
+
if self._queue.unfinished_tasks == 0:
|
|
112
|
+
return True
|
|
113
|
+
time.sleep(0.01)
|
|
114
|
+
return self._queue.unfinished_tasks == 0
|
|
115
|
+
|
|
116
|
+
def close(self, timeout: Optional[float] = None) -> None:
|
|
117
|
+
"""
|
|
118
|
+
Stop accepting new beacons, drain the queue, join the worker.
|
|
119
|
+
Idempotent. Bounded by `timeout` (defaults to `flush_timeout`).
|
|
120
|
+
"""
|
|
121
|
+
if self._closed.is_set():
|
|
122
|
+
return
|
|
123
|
+
self._closed.set()
|
|
124
|
+
# Push sentinel even if queue is full — use put with timeout so we
|
|
125
|
+
# don't hang shutdown forever if a runaway producer is filling it.
|
|
126
|
+
try:
|
|
127
|
+
self._queue.put(_SENTINEL, timeout=1.0)
|
|
128
|
+
except queue.Full:
|
|
129
|
+
debug("close: queue full, sentinel may be late")
|
|
130
|
+
wait = timeout if timeout is not None else self._flush_timeout
|
|
131
|
+
self._worker.join(timeout=wait)
|
|
132
|
+
if self._pool is not None:
|
|
133
|
+
try:
|
|
134
|
+
self._pool.clear()
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
debug(f"pool clear failed: {exc}")
|
|
137
|
+
self._pool = None
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------
|
|
140
|
+
# Worker loop
|
|
141
|
+
# ------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _run(self) -> None:
|
|
144
|
+
# Outer try guarantees the worker thread never dies on an unexpected
|
|
145
|
+
# exception. Losing the worker would silently leak beacons forever.
|
|
146
|
+
while True:
|
|
147
|
+
try:
|
|
148
|
+
item = self._queue.get()
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
debug(f"queue.get failed: {exc}")
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
if item is _SENTINEL:
|
|
155
|
+
return
|
|
156
|
+
self._send_one(item)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
self.dropped_error += 1
|
|
159
|
+
debug(f"send loop swallowed {type(exc).__name__}: {exc}")
|
|
160
|
+
finally:
|
|
161
|
+
try:
|
|
162
|
+
self._queue.task_done()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
def _send_one(self, beacon: Dict[str, Any]) -> None:
|
|
167
|
+
if self._pool is None:
|
|
168
|
+
self.dropped_error += 1
|
|
169
|
+
return
|
|
170
|
+
try:
|
|
171
|
+
body = json.dumps(beacon, default=str, ensure_ascii=False).encode("utf-8")
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
# If we can't even serialize, ship a minimal fallback so the
|
|
174
|
+
# event isn't lost entirely.
|
|
175
|
+
self.dropped_error += 1
|
|
176
|
+
debug(f"json encode failed: {exc}; shipping minimal envelope")
|
|
177
|
+
body = json.dumps(
|
|
178
|
+
{
|
|
179
|
+
"kind": "error",
|
|
180
|
+
"level": "error",
|
|
181
|
+
"occurred_at": beacon.get("occurred_at"),
|
|
182
|
+
"message": "<beacon could not be serialized>",
|
|
183
|
+
"payload": {"truncated": True, "encode_error": str(exc)},
|
|
184
|
+
},
|
|
185
|
+
default=str,
|
|
186
|
+
).encode("utf-8")
|
|
187
|
+
|
|
188
|
+
headers = {
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
"Authorization": f"Bearer {self._dsn.token}",
|
|
191
|
+
"User-Agent": f"insider-python/{__version__}",
|
|
192
|
+
"X-Insider-SDK": f"python/{__version__}",
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
resp = self._pool.urlopen(
|
|
197
|
+
"POST",
|
|
198
|
+
self._dsn.beam_url,
|
|
199
|
+
body=body,
|
|
200
|
+
headers=headers,
|
|
201
|
+
retries=False,
|
|
202
|
+
preload_content=True,
|
|
203
|
+
)
|
|
204
|
+
except Exception as exc:
|
|
205
|
+
self.dropped_error += 1
|
|
206
|
+
debug(f"POST failed: {type(exc).__name__}: {exc}")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if resp.status == 202:
|
|
210
|
+
self.sent_total += 1
|
|
211
|
+
else:
|
|
212
|
+
self.dropped_error += 1
|
|
213
|
+
debug(f"server returned {resp.status}; dropping beacon")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: insider-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Insider — ship Beacons to your Insider server.
|
|
5
|
+
Author: Insider
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Insider-Inc/insider-python
|
|
8
|
+
Keywords: insider,telemetry,errors,monitoring,sdk
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: urllib3>=1.26
|
|
20
|
+
Provides-Extra: django
|
|
21
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-django>=4.8; extra == "dev"
|
|
25
|
+
Requires-Dist: django>=4.2; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# insider-python
|
|
28
|
+
|
|
29
|
+
The Python SDK for [Insider](https://insider.moraks.cloud/).
|
|
30
|
+
|
|
31
|
+
Beam Beacons from your Python service to your Insider server with a
|
|
32
|
+
one-line setup. No runtime overhead on your request path. Never raises
|
|
33
|
+
into your code, no matter what.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install insider-python
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For the Django integration:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "insider-python[django]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
### Plain Python
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import insider
|
|
53
|
+
|
|
54
|
+
insider.init(
|
|
55
|
+
dsn="https://<beacon_token>@insider.example.com/<project_uuid>",
|
|
56
|
+
environment="production",
|
|
57
|
+
release="1.2.3",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
risky()
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
insider.capture_exception(exc)
|
|
64
|
+
|
|
65
|
+
insider.capture_message("cache miss spiked", level="warning")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Django
|
|
69
|
+
|
|
70
|
+
Add the integration to `INSTALLED_APPS` and configure via settings:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
INSTALLED_APPS = [
|
|
74
|
+
# ...
|
|
75
|
+
"insider.contrib.django",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
MIDDLEWARE = [
|
|
79
|
+
# ...
|
|
80
|
+
"insider.contrib.django.middleware.InsiderMiddleware",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
INSIDER_DSN = "https://<beacon_token>@insider.example.com/<project_uuid>"
|
|
84
|
+
INSIDER_ENVIRONMENT = "production"
|
|
85
|
+
INSIDER_RELEASE = "1.2.3"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That's the whole setup. Every unhandled exception in a view is now a
|
|
89
|
+
Beacon in your dashboard.
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Order of precedence (first wins):
|
|
94
|
+
|
|
95
|
+
1. Keyword args to `insider.init(...)`.
|
|
96
|
+
2. `INSIDER_*` environment variables.
|
|
97
|
+
3. Django settings (when the Django integration is active).
|
|
98
|
+
4. Hard-coded defaults.
|
|
99
|
+
|
|
100
|
+
If no DSN is found anywhere, the SDK enters **disabled mode**: every
|
|
101
|
+
public call is a no-op, no thread starts, no socket opens.
|
|
102
|
+
|
|
103
|
+
| Option | Default | Notes |
|
|
104
|
+
|--------|---------|-------|
|
|
105
|
+
| `dsn` | env `INSIDER_DSN` | If absent, SDK is disabled |
|
|
106
|
+
| `environment` | `"production"` | Top-level Beacon field |
|
|
107
|
+
| `release` | `None` | Top-level Beacon field |
|
|
108
|
+
| `send_default_pii` | `False` | Required to capture `user.id`, request bodies |
|
|
109
|
+
| `before_send` | `None` | `(beacon) -> beacon | None` hook |
|
|
110
|
+
| `scrub_keys` | `None` | Extra keys to filter (added to the default deny-list) |
|
|
111
|
+
| `in_app_include` | `None` | Filename prefixes considered "your code" |
|
|
112
|
+
| `transport_queue_size` | `1000` | Bounded; drops on overflow |
|
|
113
|
+
| `transport_flush_timeout` | `2.0` | Seconds. Used by `close()` / `flush()` |
|
|
114
|
+
| `debug` | `False` | Print SDK's own warnings to stderr |
|
|
115
|
+
|
|
116
|
+
## Promise
|
|
117
|
+
|
|
118
|
+
The SDK never raises into your code. Every public function catches
|
|
119
|
+
`Exception` at its boundary; if something goes wrong inside the SDK,
|
|
120
|
+
you get nothing back and a debug log (if enabled). Your app keeps
|
|
121
|
+
running.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
insider/__init__.py,sha256=DgDevX2wcy9lonZiHaq5dJLieD3_fp3p6l3lotuTpGE,781
|
|
2
|
+
insider/_envelope.py,sha256=nxMumrnB_KXF51aOmSoPVlvJ_On7cQx2GS96YM64CB4,6583
|
|
3
|
+
insider/_version.py,sha256=oPP6-Q8wh-D2EeEaYkiVm6AZMC4ipVMVwcqq2RJp7u8,246
|
|
4
|
+
insider/client.py,sha256=NacSGY0FycPogWa9m6sZYMNWhn9BWAdr_sXjbhpmIRc,10797
|
|
5
|
+
insider/dsn.py,sha256=S8BbRhmKKKU1N6W8cpMSQoyDO_EuUJ1PkcjhGrrf5hw,3009
|
|
6
|
+
insider/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
insider/safety.py,sha256=mtzzWLujuvmd24W2VtVcMGO_81GPBbB2tYuVqU2KsuI,1910
|
|
8
|
+
insider/scope.py,sha256=F1eMWt6uu14uBdncc5DC6Lh735EPcUv1yF4prBi--pc,1708
|
|
9
|
+
insider/scrubbing.py,sha256=U8N3MS5VtDUNpIWN4PZ8bvrPqmjRRXq5hWGXxzoF5Gw,2853
|
|
10
|
+
insider/stacktrace.py,sha256=i_Okdtu6ZLRicl7YBFzxe3B1likDIS4tK7GJG2iWKcw,5048
|
|
11
|
+
insider/transport.py,sha256=8ncFb3C0LgF2hMRoj_fhxj49NIUD6hsVAHfPZKtNcn4,7292
|
|
12
|
+
insider/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
insider/contrib/django/__init__.py,sha256=UMGrJgVO-FqszZo9Cviw7oWS8PLdXVHt3hwXZ_YMj8A,680
|
|
14
|
+
insider/contrib/django/apps.py,sha256=WOKn-fNp_SSPNyt8xXz_fXYVhkyepafAi1uxpWC9_Bo,1947
|
|
15
|
+
insider/contrib/django/middleware.py,sha256=JWnP_p4JBGyKQMswevjWxoUBSE5o4xgLKYOm1Zrb30g,5365
|
|
16
|
+
insider_python-0.1.0.dist-info/METADATA,sha256=xNxX_dGung9se-WnsdnbUJprcwYHZFXdB0lqB9YEKWI,3531
|
|
17
|
+
insider_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
insider_python-0.1.0.dist-info/top_level.txt,sha256=g_YKp2jCaaefmasZ2nOa9capm0X8q2sAWI_eEClKIos,8
|
|
19
|
+
insider_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
insider
|