devlite 0.1.0__tar.gz → 0.1.2__tar.gz
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.
- {devlite-0.1.0/devlite.egg-info → devlite-0.1.2}/PKG-INFO +57 -3
- devlite-0.1.0/PKG-INFO → devlite-0.1.2/README.md +176 -154
- {devlite-0.1.0 → devlite-0.1.2}/devlite/__init__.py +14 -2
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_client.py +16 -6
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_context.py +31 -1
- devlite-0.1.2/devlite/_django.py +158 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_errors.py +2 -1
- devlite-0.1.2/devlite/_fastapi.py +139 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_flask.py +12 -5
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_transport.py +1 -1
- devlite-0.1.0/README.md → devlite-0.1.2/devlite.egg-info/PKG-INFO +208 -130
- {devlite-0.1.0 → devlite-0.1.2}/devlite.egg-info/SOURCES.txt +5 -0
- devlite-0.1.2/devlite.egg-info/requires.txt +16 -0
- {devlite-0.1.0 → devlite-0.1.2}/pyproject.toml +4 -2
- devlite-0.1.2/tests/test_context.py +76 -0
- devlite-0.1.2/tests/test_django.py +121 -0
- devlite-0.1.2/tests/test_fastapi.py +93 -0
- devlite-0.1.0/devlite.egg-info/requires.txt +0 -6
- {devlite-0.1.0 → devlite-0.1.2}/LICENSE +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_config.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_metrics.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_queue.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite/_scrub.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite.egg-info/dependency_links.txt +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/devlite.egg-info/top_level.txt +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/setup.cfg +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/tests/test_errors.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/tests/test_queue.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/tests/test_scrub.py +0 -0
- {devlite-0.1.0 → devlite-0.1.2}/tests/test_transport.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devlite
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: DevLite Python SDK — AI-powered observability with a 2-line integration. Automatic error grouping, source context, user impact tracking, and sensitive-data scrubbing built in.
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://devlite.io
|
|
@@ -18,8 +18,16 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
Provides-Extra: flask
|
|
20
20
|
Requires-Dist: flask>=2.0; extra == "flask"
|
|
21
|
+
Provides-Extra: fastapi
|
|
22
|
+
Requires-Dist: fastapi>=0.100; extra == "fastapi"
|
|
23
|
+
Provides-Extra: django
|
|
24
|
+
Requires-Dist: django>=4.0; extra == "django"
|
|
21
25
|
Provides-Extra: test
|
|
22
26
|
Requires-Dist: pytest; extra == "test"
|
|
27
|
+
Requires-Dist: flask>=2.0; extra == "test"
|
|
28
|
+
Requires-Dist: fastapi>=0.100; extra == "test"
|
|
29
|
+
Requires-Dist: django>=4.0; extra == "test"
|
|
30
|
+
Requires-Dist: httpx>=0.27; extra == "test"
|
|
23
31
|
Dynamic: license-file
|
|
24
32
|
|
|
25
33
|
# devlite — Python SDK
|
|
@@ -31,8 +39,10 @@ This is the Python counterpart of [`@devlite/nodejs`](https://www.npmjs.com/pack
|
|
|
31
39
|
## Install
|
|
32
40
|
|
|
33
41
|
```bash
|
|
34
|
-
pip install devlite # core (Flask optional)
|
|
42
|
+
pip install devlite # core (Flask/FastAPI/Django optional)
|
|
35
43
|
pip install "devlite[flask]" # + Flask instrumentation
|
|
44
|
+
pip install "devlite[fastapi]" # + FastAPI instrumentation
|
|
45
|
+
pip install "devlite[django]" # + Django instrumentation
|
|
36
46
|
```
|
|
37
47
|
|
|
38
48
|
## 60-second quickstart (Flask)
|
|
@@ -56,6 +66,42 @@ That's it — every HTTP request, slow endpoint, and unhandled exception is now
|
|
|
56
66
|
|
|
57
67
|
Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
|
|
58
68
|
|
|
69
|
+
## Auto-instrumentation for every framework
|
|
70
|
+
|
|
71
|
+
One call per framework, with the same automatic request/slow-request/error/user tracking:
|
|
72
|
+
|
|
73
|
+
**FastAPI** (any Starlette app) — call right after creating the app:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fastapi import FastAPI
|
|
77
|
+
import devlite
|
|
78
|
+
|
|
79
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
80
|
+
|
|
81
|
+
app = FastAPI()
|
|
82
|
+
devlite.instrument_fastapi(app) # before the server starts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Django** — `instrument_django()` inserts `devlite._django.InstrumentDjangoMiddleware` at the top of your `MIDDLEWARE` setting:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import devlite
|
|
89
|
+
|
|
90
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
91
|
+
devlite.instrument_django()
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Or add the middleware manually (must be near the top, index 0 ideally):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
MIDDLEWARE = [
|
|
98
|
+
"devlite._django.InstrumentDjangoMiddleware",
|
|
99
|
+
...
|
|
100
|
+
]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Any other framework (or plain WSGI/ASGI) still works through the manual API below.
|
|
104
|
+
|
|
59
105
|
## Manual capture API
|
|
60
106
|
|
|
61
107
|
```python
|
|
@@ -91,11 +137,19 @@ devlite.set_tag("region", "lagos")
|
|
|
91
137
|
`set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
|
|
92
138
|
|
|
93
139
|
```python
|
|
140
|
+
# Flask
|
|
94
141
|
from flask import request
|
|
95
142
|
|
|
96
143
|
@app.before_request
|
|
97
144
|
def identify():
|
|
98
145
|
devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
|
|
146
|
+
|
|
147
|
+
# FastAPI — inside any endpoint
|
|
148
|
+
devlite.set_user({"id": "u-123", "email": "a@example.com"})
|
|
149
|
+
|
|
150
|
+
# Django — inside any view
|
|
151
|
+
def my_view(request):
|
|
152
|
+
devlite.set_user({"id": request.user.id, "email": request.user.email})
|
|
99
153
|
```
|
|
100
154
|
|
|
101
155
|
## Configuration options
|
|
@@ -145,7 +199,7 @@ devlite.close()
|
|
|
145
199
|
## Development
|
|
146
200
|
|
|
147
201
|
```bash
|
|
148
|
-
pip install -e ".[flask,test]"
|
|
202
|
+
pip install -e ".[flask,fastapi,django,test]"
|
|
149
203
|
python -m pytest
|
|
150
204
|
```
|
|
151
205
|
|
|
@@ -1,154 +1,176 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
devlite.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
```python
|
|
126
|
-
devlite.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
# devlite — Python SDK
|
|
2
|
+
|
|
3
|
+
AI-powered observability for Python. Add two lines, get automatic request tracking, error capture, error grouping, source context, user tracking, and slow-endpoint detection — no config files, no manual instrumentation, **zero runtime dependencies**.
|
|
4
|
+
|
|
5
|
+
This is the Python counterpart of [`@devlite/nodejs`](https://www.npmjs.com/package/@devlite/nodejs) — both SDKs speak the same batch protocol to the [DevLite ingest API](https://github.com/Ishimwe-Kevin/devlite-app).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install devlite # core (Flask/FastAPI/Django optional)
|
|
11
|
+
pip install "devlite[flask]" # + Flask instrumentation
|
|
12
|
+
pip install "devlite[fastapi]" # + FastAPI instrumentation
|
|
13
|
+
pip install "devlite[django]" # + Django instrumentation
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 60-second quickstart (Flask)
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from flask import Flask
|
|
20
|
+
import devlite
|
|
21
|
+
|
|
22
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
23
|
+
|
|
24
|
+
app = Flask(__name__)
|
|
25
|
+
devlite.instrument_flask(app)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That's it — every HTTP request, slow endpoint, and unhandled exception is now captured automatically, with:
|
|
29
|
+
|
|
30
|
+
- **Grouped errors** — the same bug occurring 1,000 times shows up as one issue, not 1,000
|
|
31
|
+
- **Real source code context** — the actual lines around the crash, read live from disk
|
|
32
|
+
- **Automatic sensitive-data scrubbing** — emails, tokens, passwords redacted before anything leaves your process
|
|
33
|
+
- **User impact tracking** — know exactly which users hit which bugs
|
|
34
|
+
|
|
35
|
+
Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
|
|
36
|
+
|
|
37
|
+
## Auto-instrumentation for every framework
|
|
38
|
+
|
|
39
|
+
One call per framework, with the same automatic request/slow-request/error/user tracking:
|
|
40
|
+
|
|
41
|
+
**FastAPI** (any Starlette app) — call right after creating the app:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from fastapi import FastAPI
|
|
45
|
+
import devlite
|
|
46
|
+
|
|
47
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
48
|
+
|
|
49
|
+
app = FastAPI()
|
|
50
|
+
devlite.instrument_fastapi(app) # before the server starts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Django** — `instrument_django()` inserts `devlite._django.InstrumentDjangoMiddleware` at the top of your `MIDDLEWARE` setting:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import devlite
|
|
57
|
+
|
|
58
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
59
|
+
devlite.instrument_django()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or add the middleware manually (must be near the top, index 0 ideally):
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
MIDDLEWARE = [
|
|
66
|
+
"devlite._django.InstrumentDjangoMiddleware",
|
|
67
|
+
...
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Any other framework (or plain WSGI/ASGI) still works through the manual API below.
|
|
72
|
+
|
|
73
|
+
## Manual capture API
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
# Capture a handled error with extra context
|
|
77
|
+
devlite.capture_error(err, {"userId": "123", "action": "checkout"})
|
|
78
|
+
devlite.capture_error(err, {"fingerprint": "payment-timeout"}) # force grouping key
|
|
79
|
+
|
|
80
|
+
# Non-error event
|
|
81
|
+
devlite.capture_message("Payment retried after timeout", "warning")
|
|
82
|
+
|
|
83
|
+
# Custom metric — feeds forecasting/anomaly detection
|
|
84
|
+
devlite.report_metric("order.total", 42.5, unit="USD", tags={"region": "lagos"})
|
|
85
|
+
|
|
86
|
+
# Structured log line
|
|
87
|
+
devlite.capture_log("checkout completed", "info", {"orderId": "ord_123", "durationMs": 250})
|
|
88
|
+
|
|
89
|
+
# Trace a unit of work
|
|
90
|
+
span = devlite.start_span("checkout.process", {"trace_id": "abc"})
|
|
91
|
+
span.end("ok") # "ok" | "error"
|
|
92
|
+
|
|
93
|
+
# Tell DevLite about a deployment (powers before/after performance views)
|
|
94
|
+
devlite.report_deployment(version="v2.1.3", commit_sha="a1b2c3d")
|
|
95
|
+
|
|
96
|
+
# Breadcrumbs attach to the next captured error, improving AI root-cause
|
|
97
|
+
devlite.add_breadcrumb({"type": "business_event", "note": "user started checkout"})
|
|
98
|
+
|
|
99
|
+
# Tag all subsequent events
|
|
100
|
+
devlite.set_tag("region", "lagos")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## User impact tracking
|
|
104
|
+
|
|
105
|
+
`set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
# Flask
|
|
109
|
+
from flask import request
|
|
110
|
+
|
|
111
|
+
@app.before_request
|
|
112
|
+
def identify():
|
|
113
|
+
devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
|
|
114
|
+
|
|
115
|
+
# FastAPI — inside any endpoint
|
|
116
|
+
devlite.set_user({"id": "u-123", "email": "a@example.com"})
|
|
117
|
+
|
|
118
|
+
# Django — inside any view
|
|
119
|
+
def my_view(request):
|
|
120
|
+
devlite.set_user({"id": request.user.id, "email": request.user.email})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Configuration options
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
devlite.init({
|
|
127
|
+
"api_key": "dl_live_xxxxx", # required
|
|
128
|
+
"environment": "production", # default: $DEVLITE_ENVIRONMENT or "development"
|
|
129
|
+
"service_name": "payments-api", # default: $DEVLITE_SERVICE_NAME or current folder
|
|
130
|
+
"release": "v2.1.3", # e.g. git sha, shown in deployment views
|
|
131
|
+
"sample_rate": 1.0, # 0.0–1.0. Sampling is COHERENT: a sampled-out
|
|
132
|
+
# request drops its errors/spans/logs/metrics together.
|
|
133
|
+
"capture_body": False, # capture (redacted) request headers — off by default
|
|
134
|
+
"flush_interval_ms": 5000, # how often batched events are sent
|
|
135
|
+
"gzip": True, # compress request bodies (Content-Encoding: gzip)
|
|
136
|
+
"debug": False, # log SDK internals
|
|
137
|
+
"on_error": lambda err: print(err), # SDK-internal send failures
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
All of `api_key`, `service_name`, `release`, and `endpoint` can also be set via environment variables (`DEVLITE_API_KEY`, `DEVLITE_SERVICE_NAME`, `DEVLITE_RELEASE`, `DEVLITE_ENDPOINT`).
|
|
142
|
+
|
|
143
|
+
## Serverless / short-lived processes
|
|
144
|
+
|
|
145
|
+
Events are batched, so call `flush()` before your function returns:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
devlite.flush()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Graceful shutdown
|
|
152
|
+
|
|
153
|
+
The SDK flushes remaining events on interpreter exit (`atexit`). If you manage shutdown yourself:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
devlite.close()
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## What makes this competitive
|
|
160
|
+
|
|
161
|
+
- **Automatic error grouping** — the same underlying bug, however many times it fires, is fingerprinted and grouped into one issue, instead of flooding your dashboard with duplicates.
|
|
162
|
+
- **Source code context** — every captured error includes the actual lines of code around the crash, read live from disk, not just a bare stack trace.
|
|
163
|
+
- **Automatic sensitive-data scrubbing** — on by default. Emails, JWTs, bearer tokens, AWS keys, credit card numbers, and any field literally named `password`/`token`/`secret` are redacted before anything leaves your process.
|
|
164
|
+
- **Never blocks your app** — all sends are async (background thread), batched, and retried with backoff. If DevLite's backend is unreachable, your app keeps running.
|
|
165
|
+
- **Zero dependencies** — pure Python standard library.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
pip install -e ".[flask,fastapi,django,test]"
|
|
171
|
+
python -m pytest
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -12,13 +12,13 @@ Public API (all Pythonic snake_case):
|
|
|
12
12
|
init, capture_error, capture_message, report_metric, capture_log,
|
|
13
13
|
start_span, report_span, report_deployment, add_breadcrumb, set_tag,
|
|
14
14
|
set_user, run_with_context, start_host_metrics, instrument_flask,
|
|
15
|
-
flush, close
|
|
15
|
+
instrument_fastapi, instrument_django, flush, close
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
18
|
from ._client import Client, HostMetricsHandle, Span
|
|
19
19
|
from ._context import run_with_context
|
|
20
20
|
|
|
21
|
-
__version__ = "0.1.
|
|
21
|
+
__version__ = "0.1.1"
|
|
22
22
|
|
|
23
23
|
_client = None
|
|
24
24
|
|
|
@@ -115,6 +115,16 @@ def instrument_flask(app):
|
|
|
115
115
|
return _require_client().instrument_flask(app)
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
def instrument_fastapi(app):
|
|
119
|
+
"""Wire automatic request/error/user tracking into a FastAPI app."""
|
|
120
|
+
return _require_client().instrument_fastapi(app)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def instrument_django():
|
|
124
|
+
"""Insert DevLite's middleware at the top of Django's MIDDLEWARE setting."""
|
|
125
|
+
return _require_client().instrument_django()
|
|
126
|
+
|
|
127
|
+
|
|
118
128
|
def flush():
|
|
119
129
|
"""Force-send whatever is currently queued (useful in serverless)."""
|
|
120
130
|
_require_client().flush()
|
|
@@ -142,6 +152,8 @@ __all__ = [
|
|
|
142
152
|
"run_with_context",
|
|
143
153
|
"start_host_metrics",
|
|
144
154
|
"instrument_flask",
|
|
155
|
+
"instrument_fastapi",
|
|
156
|
+
"instrument_django",
|
|
145
157
|
"flush",
|
|
146
158
|
"close",
|
|
147
159
|
]
|
|
@@ -5,7 +5,7 @@ import threading
|
|
|
5
5
|
import uuid
|
|
6
6
|
|
|
7
7
|
from ._config import build_config
|
|
8
|
-
from ._context import Context, get_user, now_ms, run_with_context
|
|
8
|
+
from ._context import Context, get_trace_id, get_user, new_trace_id, now_ms, run_with_context
|
|
9
9
|
from ._errors import build_error_event
|
|
10
10
|
from ._metrics import collect_host_metrics
|
|
11
11
|
from ._scrub import scrub_deep
|
|
@@ -25,7 +25,7 @@ class Span:
|
|
|
25
25
|
self.type = "span"
|
|
26
26
|
self.id = options.get("id") or _new_hex(6)
|
|
27
27
|
self.name = name
|
|
28
|
-
self.trace_id = options.get("trace_id") or
|
|
28
|
+
self.trace_id = options.get("trace_id") or get_trace_id() or new_trace_id()
|
|
29
29
|
self.parent_id = options.get("parent_id")
|
|
30
30
|
self.tags = options.get("tags") or {}
|
|
31
31
|
self.start_time = now_ms()
|
|
@@ -103,7 +103,7 @@ class Client:
|
|
|
103
103
|
self.transport.enqueue(event)
|
|
104
104
|
|
|
105
105
|
def capture_message(self, message, level="info", extra=None):
|
|
106
|
-
raw = {"type": "message", "level": level, "message": message, "extra": extra or {}, "timestamp": now_ms()}
|
|
106
|
+
raw = {"type": "message", "level": level, "message": message, "extra": extra or {}, "traceId": get_trace_id(), "timestamp": now_ms()}
|
|
107
107
|
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
108
108
|
|
|
109
109
|
def report_metric(self, name, value, unit=None, tags=None):
|
|
@@ -111,11 +111,11 @@ class Client:
|
|
|
111
111
|
raise ValueError("[DevLite] report_metric() requires a string `name`.")
|
|
112
112
|
if not isinstance(value, (int, float)):
|
|
113
113
|
raise ValueError("[DevLite] report_metric() requires a numeric `value`.")
|
|
114
|
-
raw = {"type": "metric", "name": name, "value": value, "unit": unit or None, "tags": tags or {}, "timestamp": now_ms()}
|
|
114
|
+
raw = {"type": "metric", "name": name, "value": value, "unit": unit or None, "tags": tags or {}, "traceId": get_trace_id(), "timestamp": now_ms()}
|
|
115
115
|
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
116
116
|
|
|
117
117
|
def capture_log(self, message, level="info", fields=None):
|
|
118
|
-
raw = {"type": "log", "level": level, "message": message, "fields": fields or {}, "timestamp": now_ms()}
|
|
118
|
+
raw = {"type": "log", "level": level, "message": message, "fields": fields or {}, "traceId": get_trace_id(), "timestamp": now_ms()}
|
|
119
119
|
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
120
120
|
|
|
121
121
|
def start_span(self, name, options=None):
|
|
@@ -126,7 +126,7 @@ class Client:
|
|
|
126
126
|
"type": "span",
|
|
127
127
|
"id": _new_hex(6),
|
|
128
128
|
"name": name,
|
|
129
|
-
"traceId": trace_id or
|
|
129
|
+
"traceId": trace_id or get_trace_id() or new_trace_id(),
|
|
130
130
|
"parentId": parent_id,
|
|
131
131
|
"tags": tags or {},
|
|
132
132
|
"startTime": start_time,
|
|
@@ -164,6 +164,16 @@ class Client:
|
|
|
164
164
|
|
|
165
165
|
return instrument_flask(app, self.transport, self.context, self.config)
|
|
166
166
|
|
|
167
|
+
def instrument_fastapi(self, app):
|
|
168
|
+
from ._fastapi import instrument_fastapi
|
|
169
|
+
|
|
170
|
+
return instrument_fastapi(app, self.transport, self.context, self.config)
|
|
171
|
+
|
|
172
|
+
def instrument_django(self):
|
|
173
|
+
from ._django import instrument_django
|
|
174
|
+
|
|
175
|
+
return instrument_django(self)
|
|
176
|
+
|
|
167
177
|
def flush(self):
|
|
168
178
|
self.transport.flush()
|
|
169
179
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Request-scoped context: breadcrumbs, tags,
|
|
1
|
+
"""Request-scoped context: breadcrumbs, tags, the current user, and traceId.
|
|
2
2
|
|
|
3
3
|
Python equivalent of context.js + userContext.js from the Node SDK. Uses
|
|
4
4
|
contextvars (the modern replacement for thread-locals) so user context set
|
|
@@ -6,6 +6,7 @@ inside one request handler never leaks across concurrent requests.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import contextvars
|
|
9
|
+
import secrets
|
|
9
10
|
import time
|
|
10
11
|
|
|
11
12
|
MAX_BREADCRUMBS = 20
|
|
@@ -13,6 +14,11 @@ MAX_BREADCRUMBS = 20
|
|
|
13
14
|
_request_store = contextvars.ContextVar("devlite_request_store", default=None)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def new_trace_id():
|
|
18
|
+
"""A fresh 16-hex-char traceId (8 bytes), matching the Node SDK."""
|
|
19
|
+
return secrets.token_hex(8)
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
def _store():
|
|
17
23
|
store = _request_store.get()
|
|
18
24
|
if store is None:
|
|
@@ -50,6 +56,19 @@ def run_with_context(fn, seed=None):
|
|
|
50
56
|
_request_store.set(previous)
|
|
51
57
|
|
|
52
58
|
|
|
59
|
+
def reset_context():
|
|
60
|
+
"""Start a fresh request context for the current thread/task.
|
|
61
|
+
|
|
62
|
+
Unlike run_with_context this does NOT restore the previous value when
|
|
63
|
+
done — the fresh store stays active for the whole request. Framework
|
|
64
|
+
middleware calls this once per request so that a stale store (left
|
|
65
|
+
behind by a previous request on a reused thread, or inherited from the
|
|
66
|
+
caller's context, e.g. by anyio portal threads) can never leak user
|
|
67
|
+
data or breadcrumbs into the current request.
|
|
68
|
+
"""
|
|
69
|
+
_request_store.set({})
|
|
70
|
+
|
|
71
|
+
|
|
53
72
|
def set_user(user):
|
|
54
73
|
"""Scope a user to the current request context. Mirrors devlite.setUser()."""
|
|
55
74
|
store = _store()
|
|
@@ -69,6 +88,17 @@ def set_sampled(value):
|
|
|
69
88
|
store["sampled"] = value
|
|
70
89
|
|
|
71
90
|
|
|
91
|
+
def set_trace_id(trace_id):
|
|
92
|
+
"""Scope the current request's traceId so captured events link to it."""
|
|
93
|
+
store = _store()
|
|
94
|
+
store["traceId"] = trace_id
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_trace_id():
|
|
98
|
+
store = _request_store.get()
|
|
99
|
+
return (store or {}).get("traceId") or None
|
|
100
|
+
|
|
101
|
+
|
|
72
102
|
def is_sampled_out():
|
|
73
103
|
store = _request_store.get()
|
|
74
104
|
return bool(store) and store.get("sampled") is False
|