devlite 0.1.0__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/LICENSE +21 -0
- devlite-0.1.0/PKG-INFO +154 -0
- devlite-0.1.0/README.md +130 -0
- devlite-0.1.0/devlite/__init__.py +147 -0
- devlite-0.1.0/devlite/_client.py +184 -0
- devlite-0.1.0/devlite/_config.py +62 -0
- devlite-0.1.0/devlite/_context.py +78 -0
- devlite-0.1.0/devlite/_errors.py +111 -0
- devlite-0.1.0/devlite/_flask.py +115 -0
- devlite-0.1.0/devlite/_metrics.py +207 -0
- devlite-0.1.0/devlite/_queue.py +39 -0
- devlite-0.1.0/devlite/_scrub.py +74 -0
- devlite-0.1.0/devlite/_transport.py +144 -0
- devlite-0.1.0/devlite.egg-info/PKG-INFO +154 -0
- devlite-0.1.0/devlite.egg-info/SOURCES.txt +22 -0
- devlite-0.1.0/devlite.egg-info/dependency_links.txt +1 -0
- devlite-0.1.0/devlite.egg-info/requires.txt +6 -0
- devlite-0.1.0/devlite.egg-info/top_level.txt +1 -0
- devlite-0.1.0/pyproject.toml +35 -0
- devlite-0.1.0/setup.cfg +4 -0
- devlite-0.1.0/tests/test_errors.py +67 -0
- devlite-0.1.0/tests/test_queue.py +34 -0
- devlite-0.1.0/tests/test_scrub.py +51 -0
- devlite-0.1.0/tests/test_transport.py +136 -0
devlite-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ishvexa Hub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
devlite-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devlite
|
|
3
|
+
Version: 0.1.0
|
|
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
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://devlite.io
|
|
7
|
+
Project-URL: Repository, https://github.com/Ishimwe-Kevin/devlite-app
|
|
8
|
+
Keywords: observability,monitoring,apm,error-tracking,error-monitoring,logging,ai,devlite
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
15
|
+
Classifier: Topic :: System :: Monitoring
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Provides-Extra: flask
|
|
20
|
+
Requires-Dist: flask>=2.0; extra == "flask"
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest; extra == "test"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# devlite — Python SDK
|
|
26
|
+
|
|
27
|
+
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**.
|
|
28
|
+
|
|
29
|
+
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).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install devlite # core (Flask optional)
|
|
35
|
+
pip install "devlite[flask]" # + Flask instrumentation
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 60-second quickstart (Flask)
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from flask import Flask
|
|
42
|
+
import devlite
|
|
43
|
+
|
|
44
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
45
|
+
|
|
46
|
+
app = Flask(__name__)
|
|
47
|
+
devlite.instrument_flask(app)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
That's it — every HTTP request, slow endpoint, and unhandled exception is now captured automatically, with:
|
|
51
|
+
|
|
52
|
+
- **Grouped errors** — the same bug occurring 1,000 times shows up as one issue, not 1,000
|
|
53
|
+
- **Real source code context** — the actual lines around the crash, read live from disk
|
|
54
|
+
- **Automatic sensitive-data scrubbing** — emails, tokens, passwords redacted before anything leaves your process
|
|
55
|
+
- **User impact tracking** — know exactly which users hit which bugs
|
|
56
|
+
|
|
57
|
+
Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
|
|
58
|
+
|
|
59
|
+
## Manual capture API
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Capture a handled error with extra context
|
|
63
|
+
devlite.capture_error(err, {"userId": "123", "action": "checkout"})
|
|
64
|
+
devlite.capture_error(err, {"fingerprint": "payment-timeout"}) # force grouping key
|
|
65
|
+
|
|
66
|
+
# Non-error event
|
|
67
|
+
devlite.capture_message("Payment retried after timeout", "warning")
|
|
68
|
+
|
|
69
|
+
# Custom metric — feeds forecasting/anomaly detection
|
|
70
|
+
devlite.report_metric("order.total", 42.5, unit="USD", tags={"region": "lagos"})
|
|
71
|
+
|
|
72
|
+
# Structured log line
|
|
73
|
+
devlite.capture_log("checkout completed", "info", {"orderId": "ord_123", "durationMs": 250})
|
|
74
|
+
|
|
75
|
+
# Trace a unit of work
|
|
76
|
+
span = devlite.start_span("checkout.process", {"trace_id": "abc"})
|
|
77
|
+
span.end("ok") # "ok" | "error"
|
|
78
|
+
|
|
79
|
+
# Tell DevLite about a deployment (powers before/after performance views)
|
|
80
|
+
devlite.report_deployment(version="v2.1.3", commit_sha="a1b2c3d")
|
|
81
|
+
|
|
82
|
+
# Breadcrumbs attach to the next captured error, improving AI root-cause
|
|
83
|
+
devlite.add_breadcrumb({"type": "business_event", "note": "user started checkout"})
|
|
84
|
+
|
|
85
|
+
# Tag all subsequent events
|
|
86
|
+
devlite.set_tag("region", "lagos")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## User impact tracking
|
|
90
|
+
|
|
91
|
+
`set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from flask import request
|
|
95
|
+
|
|
96
|
+
@app.before_request
|
|
97
|
+
def identify():
|
|
98
|
+
devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Configuration options
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
devlite.init({
|
|
105
|
+
"api_key": "dl_live_xxxxx", # required
|
|
106
|
+
"environment": "production", # default: $DEVLITE_ENVIRONMENT or "development"
|
|
107
|
+
"service_name": "payments-api", # default: $DEVLITE_SERVICE_NAME or current folder
|
|
108
|
+
"release": "v2.1.3", # e.g. git sha, shown in deployment views
|
|
109
|
+
"sample_rate": 1.0, # 0.0–1.0. Sampling is COHERENT: a sampled-out
|
|
110
|
+
# request drops its errors/spans/logs/metrics together.
|
|
111
|
+
"capture_body": False, # capture (redacted) request headers — off by default
|
|
112
|
+
"flush_interval_ms": 5000, # how often batched events are sent
|
|
113
|
+
"gzip": True, # compress request bodies (Content-Encoding: gzip)
|
|
114
|
+
"debug": False, # log SDK internals
|
|
115
|
+
"on_error": lambda err: print(err), # SDK-internal send failures
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
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`).
|
|
120
|
+
|
|
121
|
+
## Serverless / short-lived processes
|
|
122
|
+
|
|
123
|
+
Events are batched, so call `flush()` before your function returns:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
devlite.flush()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Graceful shutdown
|
|
130
|
+
|
|
131
|
+
The SDK flushes remaining events on interpreter exit (`atexit`). If you manage shutdown yourself:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
devlite.close()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## What makes this competitive
|
|
138
|
+
|
|
139
|
+
- **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.
|
|
140
|
+
- **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.
|
|
141
|
+
- **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.
|
|
142
|
+
- **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.
|
|
143
|
+
- **Zero dependencies** — pure Python standard library.
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install -e ".[flask,test]"
|
|
149
|
+
python -m pytest
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
devlite-0.1.0/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
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 optional)
|
|
11
|
+
pip install "devlite[flask]" # + Flask instrumentation
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## 60-second quickstart (Flask)
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from flask import Flask
|
|
18
|
+
import devlite
|
|
19
|
+
|
|
20
|
+
devlite.init(api_key="dl_live_xxxxx")
|
|
21
|
+
|
|
22
|
+
app = Flask(__name__)
|
|
23
|
+
devlite.instrument_flask(app)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it — every HTTP request, slow endpoint, and unhandled exception is now captured automatically, with:
|
|
27
|
+
|
|
28
|
+
- **Grouped errors** — the same bug occurring 1,000 times shows up as one issue, not 1,000
|
|
29
|
+
- **Real source code context** — the actual lines around the crash, read live from disk
|
|
30
|
+
- **Automatic sensitive-data scrubbing** — emails, tokens, passwords redacted before anything leaves your process
|
|
31
|
+
- **User impact tracking** — know exactly which users hit which bugs
|
|
32
|
+
|
|
33
|
+
Point `api_key`/`endpoint` at your own DevLite ingest instance (Supabase-backed) to start seeing data.
|
|
34
|
+
|
|
35
|
+
## Manual capture API
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
# Capture a handled error with extra context
|
|
39
|
+
devlite.capture_error(err, {"userId": "123", "action": "checkout"})
|
|
40
|
+
devlite.capture_error(err, {"fingerprint": "payment-timeout"}) # force grouping key
|
|
41
|
+
|
|
42
|
+
# Non-error event
|
|
43
|
+
devlite.capture_message("Payment retried after timeout", "warning")
|
|
44
|
+
|
|
45
|
+
# Custom metric — feeds forecasting/anomaly detection
|
|
46
|
+
devlite.report_metric("order.total", 42.5, unit="USD", tags={"region": "lagos"})
|
|
47
|
+
|
|
48
|
+
# Structured log line
|
|
49
|
+
devlite.capture_log("checkout completed", "info", {"orderId": "ord_123", "durationMs": 250})
|
|
50
|
+
|
|
51
|
+
# Trace a unit of work
|
|
52
|
+
span = devlite.start_span("checkout.process", {"trace_id": "abc"})
|
|
53
|
+
span.end("ok") # "ok" | "error"
|
|
54
|
+
|
|
55
|
+
# Tell DevLite about a deployment (powers before/after performance views)
|
|
56
|
+
devlite.report_deployment(version="v2.1.3", commit_sha="a1b2c3d")
|
|
57
|
+
|
|
58
|
+
# Breadcrumbs attach to the next captured error, improving AI root-cause
|
|
59
|
+
devlite.add_breadcrumb({"type": "business_event", "note": "user started checkout"})
|
|
60
|
+
|
|
61
|
+
# Tag all subsequent events
|
|
62
|
+
devlite.set_tag("region", "lagos")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## User impact tracking
|
|
66
|
+
|
|
67
|
+
`set_user()` is scoped to the current request (via `contextvars`, so concurrent requests never leak each other's identity):
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from flask import request
|
|
71
|
+
|
|
72
|
+
@app.before_request
|
|
73
|
+
def identify():
|
|
74
|
+
devlite.set_user({"id": request.remote_addr, "email": request.headers.get("X-User-Email")})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration options
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
devlite.init({
|
|
81
|
+
"api_key": "dl_live_xxxxx", # required
|
|
82
|
+
"environment": "production", # default: $DEVLITE_ENVIRONMENT or "development"
|
|
83
|
+
"service_name": "payments-api", # default: $DEVLITE_SERVICE_NAME or current folder
|
|
84
|
+
"release": "v2.1.3", # e.g. git sha, shown in deployment views
|
|
85
|
+
"sample_rate": 1.0, # 0.0–1.0. Sampling is COHERENT: a sampled-out
|
|
86
|
+
# request drops its errors/spans/logs/metrics together.
|
|
87
|
+
"capture_body": False, # capture (redacted) request headers — off by default
|
|
88
|
+
"flush_interval_ms": 5000, # how often batched events are sent
|
|
89
|
+
"gzip": True, # compress request bodies (Content-Encoding: gzip)
|
|
90
|
+
"debug": False, # log SDK internals
|
|
91
|
+
"on_error": lambda err: print(err), # SDK-internal send failures
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
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`).
|
|
96
|
+
|
|
97
|
+
## Serverless / short-lived processes
|
|
98
|
+
|
|
99
|
+
Events are batched, so call `flush()` before your function returns:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
devlite.flush()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Graceful shutdown
|
|
106
|
+
|
|
107
|
+
The SDK flushes remaining events on interpreter exit (`atexit`). If you manage shutdown yourself:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
devlite.close()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## What makes this competitive
|
|
114
|
+
|
|
115
|
+
- **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.
|
|
116
|
+
- **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.
|
|
117
|
+
- **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.
|
|
118
|
+
- **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.
|
|
119
|
+
- **Zero dependencies** — pure Python standard library.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pip install -e ".[flask,test]"
|
|
125
|
+
python -m pytest
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""DevLite Python SDK — AI-powered observability with a 2-line integration.
|
|
2
|
+
|
|
3
|
+
import devlite
|
|
4
|
+
|
|
5
|
+
devlite.init({api_key: "dl_live_xxxxx"})
|
|
6
|
+
|
|
7
|
+
Automatic request tracking, error grouping, source context, user impact
|
|
8
|
+
tracking, and sensitive-data scrubbing — mirroring the @devlite/nodejs npm
|
|
9
|
+
SDK, with zero runtime dependencies.
|
|
10
|
+
|
|
11
|
+
Public API (all Pythonic snake_case):
|
|
12
|
+
init, capture_error, capture_message, report_metric, capture_log,
|
|
13
|
+
start_span, report_span, report_deployment, add_breadcrumb, set_tag,
|
|
14
|
+
set_user, run_with_context, start_host_metrics, instrument_flask,
|
|
15
|
+
flush, close
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from ._client import Client, HostMetricsHandle, Span
|
|
19
|
+
from ._context import run_with_context
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
_client = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def init(options=None):
|
|
27
|
+
"""Initialize DevLite. The only required call.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
options: dict with keys like api_key, environment, service_name,
|
|
31
|
+
release, sample_rate, gzip, debug, endpoint, on_error, ...
|
|
32
|
+
`api_key` is required (or set DEVLITE_API_KEY env var).
|
|
33
|
+
"""
|
|
34
|
+
global _client
|
|
35
|
+
if _client is not None:
|
|
36
|
+
import warnings
|
|
37
|
+
|
|
38
|
+
warnings.warn("[DevLite] init() called more than once — ignoring subsequent call.")
|
|
39
|
+
return
|
|
40
|
+
_client = Client(options or {})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _require_client():
|
|
44
|
+
if _client is None:
|
|
45
|
+
raise RuntimeError("[DevLite] SDK not initialized. Call devlite.init(api_key=...) first.")
|
|
46
|
+
return _client
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def capture_error(error, extra=None):
|
|
50
|
+
"""Capture a handled error with optional extra context."""
|
|
51
|
+
_require_client().capture_error(error, extra=extra)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def capture_message(message, level="info", extra=None):
|
|
55
|
+
"""Capture a non-error event, e.g. a business-logic message."""
|
|
56
|
+
_require_client().capture_message(message, level=level, extra=extra)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def report_metric(name, value, unit=None, tags=None):
|
|
60
|
+
"""Report a metric data point (feeds forecasting/anomaly detection)."""
|
|
61
|
+
_require_client().report_metric(name, value, unit=unit, tags=tags)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def capture_log(message, level="info", fields=None):
|
|
65
|
+
"""Capture a structured log line with arbitrary fields."""
|
|
66
|
+
_require_client().capture_log(message, level=level, fields=fields)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def start_span(name, options=None):
|
|
70
|
+
"""Start a trace span; returns an object with id/trace_id and end(status)."""
|
|
71
|
+
return _require_client().start_span(name, options=options)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def report_span(name, trace_id=None, parent_id=None, tags=None, start_time=None, end_time=None, duration_ms=None, status="ok"):
|
|
75
|
+
"""Report a finished span if you track timing yourself."""
|
|
76
|
+
_require_client().report_span(
|
|
77
|
+
name,
|
|
78
|
+
trace_id=trace_id,
|
|
79
|
+
parent_id=parent_id,
|
|
80
|
+
tags=tags,
|
|
81
|
+
start_time=start_time,
|
|
82
|
+
end_time=end_time,
|
|
83
|
+
duration_ms=duration_ms,
|
|
84
|
+
status=status,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def report_deployment(version=None, commit_sha=None, notes=None):
|
|
89
|
+
"""Report a deployment — powers performance before/after deployment views."""
|
|
90
|
+
_require_client().report_deployment(version=version, commit_sha=commit_sha, notes=notes)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def add_breadcrumb(entry):
|
|
94
|
+
"""Add a manual breadcrumb (e.g. 'user started checkout') for richer AI context."""
|
|
95
|
+
_require_client().add_breadcrumb(entry)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def set_tag(key, value):
|
|
99
|
+
"""Tag all subsequent events, e.g. set_tag('region', 'lagos')."""
|
|
100
|
+
_require_client().set_tag(key, value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def set_user(user):
|
|
104
|
+
"""Identify the user for the CURRENT request (scoped to the request)."""
|
|
105
|
+
_require_client().set_user(user)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def start_host_metrics(interval_ms=15000):
|
|
109
|
+
"""Start sampling host CPU/memory (plus network/disk on Linux) as metrics."""
|
|
110
|
+
return _require_client().start_host_metrics(interval_ms=interval_ms)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def instrument_flask(app):
|
|
114
|
+
"""Wire automatic request/error/user tracking into a Flask app."""
|
|
115
|
+
return _require_client().instrument_flask(app)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def flush():
|
|
119
|
+
"""Force-send whatever is currently queued (useful in serverless)."""
|
|
120
|
+
_require_client().flush()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def close():
|
|
124
|
+
"""Cleanly stop the SDK — flushes remaining events."""
|
|
125
|
+
if _client is not None:
|
|
126
|
+
_client.close()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
__all__ = [
|
|
130
|
+
"__version__",
|
|
131
|
+
"init",
|
|
132
|
+
"capture_error",
|
|
133
|
+
"capture_message",
|
|
134
|
+
"report_metric",
|
|
135
|
+
"capture_log",
|
|
136
|
+
"start_span",
|
|
137
|
+
"report_span",
|
|
138
|
+
"report_deployment",
|
|
139
|
+
"add_breadcrumb",
|
|
140
|
+
"set_tag",
|
|
141
|
+
"set_user",
|
|
142
|
+
"run_with_context",
|
|
143
|
+
"start_host_metrics",
|
|
144
|
+
"instrument_flask",
|
|
145
|
+
"flush",
|
|
146
|
+
"close",
|
|
147
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""DevLite client — the singleton that owns config, transport, and context."""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import threading
|
|
5
|
+
import uuid
|
|
6
|
+
|
|
7
|
+
from ._config import build_config
|
|
8
|
+
from ._context import Context, get_user, now_ms, run_with_context
|
|
9
|
+
from ._errors import build_error_event
|
|
10
|
+
from ._metrics import collect_host_metrics
|
|
11
|
+
from ._scrub import scrub_deep
|
|
12
|
+
from ._transport import Transport
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _new_hex(nbytes):
|
|
16
|
+
return uuid.uuid4().hex[: nbytes * 2]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Span:
|
|
20
|
+
"""A trace span you can end() once the work completes."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, client, name, options=None):
|
|
23
|
+
options = options or {}
|
|
24
|
+
self.client = client
|
|
25
|
+
self.type = "span"
|
|
26
|
+
self.id = options.get("id") or _new_hex(6)
|
|
27
|
+
self.name = name
|
|
28
|
+
self.trace_id = options.get("trace_id") or _new_hex(8)
|
|
29
|
+
self.parent_id = options.get("parent_id")
|
|
30
|
+
self.tags = options.get("tags") or {}
|
|
31
|
+
self.start_time = now_ms()
|
|
32
|
+
self.end_time = None
|
|
33
|
+
self.duration_ms = None
|
|
34
|
+
self.status = None
|
|
35
|
+
|
|
36
|
+
def end(self, status="ok"):
|
|
37
|
+
self.end_time = now_ms()
|
|
38
|
+
self.duration_ms = self.end_time - self.start_time
|
|
39
|
+
self.status = status
|
|
40
|
+
event = {
|
|
41
|
+
"type": "span",
|
|
42
|
+
"id": self.id,
|
|
43
|
+
"name": self.name,
|
|
44
|
+
"traceId": self.trace_id,
|
|
45
|
+
"parentId": self.parent_id,
|
|
46
|
+
"tags": self.tags,
|
|
47
|
+
"startTime": self.start_time,
|
|
48
|
+
"endTime": self.end_time,
|
|
49
|
+
"durationMs": self.duration_ms,
|
|
50
|
+
"status": status,
|
|
51
|
+
}
|
|
52
|
+
self.client._enqueue_scubbed(event)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class HostMetricsHandle:
|
|
56
|
+
def __init__(self, client, interval_ms):
|
|
57
|
+
self.client = client
|
|
58
|
+
self.interval_ms = max(1000, interval_ms)
|
|
59
|
+
self._stop = threading.Event()
|
|
60
|
+
self._thread = threading.Thread(target=self._run, daemon=True, name="devlite-host-metrics")
|
|
61
|
+
self._thread.start()
|
|
62
|
+
|
|
63
|
+
def _run(self):
|
|
64
|
+
self._sample()
|
|
65
|
+
while not self._stop.wait(self.interval_ms / 1000.0):
|
|
66
|
+
self._sample()
|
|
67
|
+
|
|
68
|
+
def _sample(self):
|
|
69
|
+
try:
|
|
70
|
+
for metric in collect_host_metrics():
|
|
71
|
+
self.client._transport.enqueue(metric)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
def stop(self):
|
|
76
|
+
self._stop.set()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class _NoopHandle:
|
|
80
|
+
def stop(self):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class Client:
|
|
85
|
+
def __init__(self, options):
|
|
86
|
+
self.config = build_config(options)
|
|
87
|
+
self.context = Context()
|
|
88
|
+
self.transport = Transport(self.config)
|
|
89
|
+
self._host_metrics = None
|
|
90
|
+
self._closed = False
|
|
91
|
+
atexit.register(self.close)
|
|
92
|
+
|
|
93
|
+
# --- public API (also exposed at package top level) -------------------
|
|
94
|
+
|
|
95
|
+
def capture_error(self, error, extra=None):
|
|
96
|
+
event = build_error_event(
|
|
97
|
+
error,
|
|
98
|
+
self.config,
|
|
99
|
+
breadcrumbs=self.context.snapshot().get("breadcrumbs", []),
|
|
100
|
+
user=get_user(),
|
|
101
|
+
extra=extra or {},
|
|
102
|
+
)
|
|
103
|
+
self.transport.enqueue(event)
|
|
104
|
+
|
|
105
|
+
def capture_message(self, message, level="info", extra=None):
|
|
106
|
+
raw = {"type": "message", "level": level, "message": message, "extra": extra or {}, "timestamp": now_ms()}
|
|
107
|
+
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
108
|
+
|
|
109
|
+
def report_metric(self, name, value, unit=None, tags=None):
|
|
110
|
+
if not isinstance(name, str) or not name:
|
|
111
|
+
raise ValueError("[DevLite] report_metric() requires a string `name`.")
|
|
112
|
+
if not isinstance(value, (int, float)):
|
|
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()}
|
|
115
|
+
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
116
|
+
|
|
117
|
+
def capture_log(self, message, level="info", fields=None):
|
|
118
|
+
raw = {"type": "log", "level": level, "message": message, "fields": fields or {}, "timestamp": now_ms()}
|
|
119
|
+
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
120
|
+
|
|
121
|
+
def start_span(self, name, options=None):
|
|
122
|
+
return Span(self, name, options)
|
|
123
|
+
|
|
124
|
+
def report_span(self, name, trace_id=None, parent_id=None, tags=None, start_time=None, end_time=None, duration_ms=None, status="ok"):
|
|
125
|
+
raw = {
|
|
126
|
+
"type": "span",
|
|
127
|
+
"id": _new_hex(6),
|
|
128
|
+
"name": name,
|
|
129
|
+
"traceId": trace_id or _new_hex(8),
|
|
130
|
+
"parentId": parent_id,
|
|
131
|
+
"tags": tags or {},
|
|
132
|
+
"startTime": start_time,
|
|
133
|
+
"endTime": end_time,
|
|
134
|
+
"durationMs": duration_ms,
|
|
135
|
+
"status": status,
|
|
136
|
+
"timestamp": now_ms(),
|
|
137
|
+
}
|
|
138
|
+
self.transport.enqueue(scrub_deep(raw) if self._should_scrub() else raw)
|
|
139
|
+
|
|
140
|
+
def report_deployment(self, version=None, commit_sha=None, notes=None):
|
|
141
|
+
self.transport.enqueue(
|
|
142
|
+
{"type": "deployment", "version": version, "commitSha": commit_sha, "notes": notes, "timestamp": now_ms()}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def add_breadcrumb(self, entry):
|
|
146
|
+
self.context.add_breadcrumb(entry or {})
|
|
147
|
+
|
|
148
|
+
def set_tag(self, key, value):
|
|
149
|
+
self.context.set_tag(key, value)
|
|
150
|
+
|
|
151
|
+
def set_user(self, user):
|
|
152
|
+
from ._context import set_user as _set_user
|
|
153
|
+
|
|
154
|
+
_set_user(user)
|
|
155
|
+
|
|
156
|
+
def start_host_metrics(self, interval_ms=15000):
|
|
157
|
+
if self._host_metrics:
|
|
158
|
+
return _NoopHandle() # no-op if already running
|
|
159
|
+
self._host_metrics = HostMetricsHandle(self, interval_ms)
|
|
160
|
+
return self._host_metrics
|
|
161
|
+
|
|
162
|
+
def instrument_flask(self, app):
|
|
163
|
+
from ._flask import instrument_flask
|
|
164
|
+
|
|
165
|
+
return instrument_flask(app, self.transport, self.context, self.config)
|
|
166
|
+
|
|
167
|
+
def flush(self):
|
|
168
|
+
self.transport.flush()
|
|
169
|
+
|
|
170
|
+
def close(self):
|
|
171
|
+
if self._closed:
|
|
172
|
+
return
|
|
173
|
+
self._closed = True
|
|
174
|
+
if self._host_metrics:
|
|
175
|
+
self._host_metrics.stop()
|
|
176
|
+
self.transport.close()
|
|
177
|
+
|
|
178
|
+
# --- internals --------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def _should_scrub(self):
|
|
181
|
+
return self.config.get("scrub_sensitive_data", True) is not False
|
|
182
|
+
|
|
183
|
+
def _enqueue_scubbed(self, event):
|
|
184
|
+
self.transport.enqueue(scrub_deep(event) if self._should_scrub() else event)
|