devlite 0.1.1__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.1 → devlite-0.1.2}/PKG-INFO +1 -1
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_client.py +6 -6
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_context.py +18 -1
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_django.py +6 -1
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_errors.py +2 -1
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_fastapi.py +6 -1
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_flask.py +10 -3
- {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/PKG-INFO +1 -1
- {devlite-0.1.1 → devlite-0.1.2}/pyproject.toml +1 -1
- {devlite-0.1.1 → devlite-0.1.2}/tests/test_context.py +25 -0
- {devlite-0.1.1 → devlite-0.1.2}/tests/test_fastapi.py +10 -0
- {devlite-0.1.1 → devlite-0.1.2}/LICENSE +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/README.md +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite/__init__.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_config.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_metrics.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_queue.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_scrub.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite/_transport.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/SOURCES.txt +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/dependency_links.txt +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/requires.txt +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/devlite.egg-info/top_level.txt +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/setup.cfg +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/tests/test_django.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/tests/test_errors.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/tests/test_queue.py +0 -0
- {devlite-0.1.1 → devlite-0.1.2}/tests/test_scrub.py +0 -0
- {devlite-0.1.1 → 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
|
|
@@ -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,
|
|
@@ -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:
|
|
@@ -82,6 +88,17 @@ def set_sampled(value):
|
|
|
82
88
|
store["sampled"] = value
|
|
83
89
|
|
|
84
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
|
+
|
|
85
102
|
def is_sampled_out():
|
|
86
103
|
store = _request_store.get()
|
|
87
104
|
return bool(store) and store.get("sampled") is False
|
|
@@ -69,8 +69,10 @@ class InstrumentDjangoMiddleware:
|
|
|
69
69
|
config = client.config
|
|
70
70
|
|
|
71
71
|
reset_context() # fresh request context (also clears inherited/stale stores)
|
|
72
|
-
from ._context import set_sampled
|
|
72
|
+
from ._context import new_trace_id, set_sampled, set_trace_id
|
|
73
73
|
|
|
74
|
+
trace_id = new_trace_id()
|
|
75
|
+
set_trace_id(trace_id)
|
|
74
76
|
if self.sample_rate is None:
|
|
75
77
|
self._configure_sampling(config)
|
|
76
78
|
set_sampled(self._sampled())
|
|
@@ -94,6 +96,7 @@ class InstrumentDjangoMiddleware:
|
|
|
94
96
|
"path": path,
|
|
95
97
|
"statusCode": status,
|
|
96
98
|
"durationMs": duration_ms,
|
|
99
|
+
"traceId": trace_id,
|
|
97
100
|
}
|
|
98
101
|
)
|
|
99
102
|
|
|
@@ -109,6 +112,7 @@ class InstrumentDjangoMiddleware:
|
|
|
109
112
|
"statusCode": status,
|
|
110
113
|
"durationMs": duration_ms,
|
|
111
114
|
"headers": headers,
|
|
115
|
+
"traceId": trace_id,
|
|
112
116
|
"timestamp": start,
|
|
113
117
|
}
|
|
114
118
|
)
|
|
@@ -119,6 +123,7 @@ class InstrumentDjangoMiddleware:
|
|
|
119
123
|
"method": method,
|
|
120
124
|
"path": path,
|
|
121
125
|
"durationMs": duration_ms,
|
|
126
|
+
"traceId": trace_id,
|
|
122
127
|
"timestamp": start,
|
|
123
128
|
}
|
|
124
129
|
)
|
|
@@ -9,7 +9,7 @@ import os
|
|
|
9
9
|
import re
|
|
10
10
|
import traceback
|
|
11
11
|
|
|
12
|
-
from ._context import now_ms
|
|
12
|
+
from ._context import get_trace_id, now_ms
|
|
13
13
|
from ._scrub import scrub_deep
|
|
14
14
|
|
|
15
15
|
_NUMBER_RE = re.compile(r"\d+")
|
|
@@ -102,6 +102,7 @@ def build_error_event(exc, config, breadcrumbs=None, user=None, extra=None, fata
|
|
|
102
102
|
"sourceContext": source_context,
|
|
103
103
|
"user": user or None,
|
|
104
104
|
"breadcrumbs": breadcrumbs or [],
|
|
105
|
+
"traceId": get_trace_id(),
|
|
105
106
|
"extra": extra,
|
|
106
107
|
"timestamp": now_ms(),
|
|
107
108
|
}
|
|
@@ -54,8 +54,10 @@ class _DevLiteASGIMiddleware:
|
|
|
54
54
|
return await self.app(scope, receive, send)
|
|
55
55
|
|
|
56
56
|
reset_context() # fresh request context (also clears inherited/stale stores)
|
|
57
|
-
from ._context import set_sampled
|
|
57
|
+
from ._context import new_trace_id, set_sampled, set_trace_id
|
|
58
58
|
|
|
59
|
+
trace_id = new_trace_id()
|
|
60
|
+
set_trace_id(trace_id)
|
|
59
61
|
set_sampled(self._sampled())
|
|
60
62
|
|
|
61
63
|
start = now_ms()
|
|
@@ -90,6 +92,7 @@ class _DevLiteASGIMiddleware:
|
|
|
90
92
|
"path": path,
|
|
91
93
|
"statusCode": status[0],
|
|
92
94
|
"durationMs": duration_ms,
|
|
95
|
+
"traceId": trace_id,
|
|
93
96
|
}
|
|
94
97
|
)
|
|
95
98
|
|
|
@@ -108,6 +111,7 @@ class _DevLiteASGIMiddleware:
|
|
|
108
111
|
"statusCode": status[0],
|
|
109
112
|
"durationMs": duration_ms,
|
|
110
113
|
"headers": headers,
|
|
114
|
+
"traceId": trace_id,
|
|
111
115
|
"timestamp": start,
|
|
112
116
|
}
|
|
113
117
|
)
|
|
@@ -118,6 +122,7 @@ class _DevLiteASGIMiddleware:
|
|
|
118
122
|
"method": method,
|
|
119
123
|
"path": path,
|
|
120
124
|
"durationMs": duration_ms,
|
|
125
|
+
"traceId": trace_id,
|
|
121
126
|
"timestamp": start,
|
|
122
127
|
}
|
|
123
128
|
)
|
|
@@ -49,11 +49,12 @@ def instrument_flask(app, transport, context, config):
|
|
|
49
49
|
@app.before_request
|
|
50
50
|
def _start():
|
|
51
51
|
reset_context() # fresh request context
|
|
52
|
+
from ._context import new_trace_id, set_sampled, set_trace_id
|
|
53
|
+
|
|
54
|
+
set_trace_id(new_trace_id())
|
|
52
55
|
_record_start()
|
|
53
56
|
counter[0] += 1
|
|
54
57
|
sampled = sample_rate >= 1.0 or (counter[0] % int(sample_every) == 0)
|
|
55
|
-
from ._context import set_sampled
|
|
56
|
-
|
|
57
58
|
set_sampled(sampled)
|
|
58
59
|
|
|
59
60
|
@app.after_request
|
|
@@ -67,7 +68,11 @@ def instrument_flask(app, transport, context, config):
|
|
|
67
68
|
method = meta.get("method") or (response.request.method if getattr(response, "request", None) else "GET")
|
|
68
69
|
status = response.status_code
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
from ._context import get_trace_id
|
|
72
|
+
|
|
73
|
+
trace_id = get_trace_id()
|
|
74
|
+
|
|
75
|
+
context.add_breadcrumb({"type": "request", "method": method, "path": path, "statusCode": status, "durationMs": duration_ms, "traceId": trace_id})
|
|
71
76
|
|
|
72
77
|
headers = None
|
|
73
78
|
if capture_body and getattr(response, "request", None):
|
|
@@ -80,6 +85,7 @@ def instrument_flask(app, transport, context, config):
|
|
|
80
85
|
"statusCode": status,
|
|
81
86
|
"durationMs": duration_ms,
|
|
82
87
|
"headers": headers,
|
|
88
|
+
"traceId": trace_id,
|
|
83
89
|
"timestamp": int(start),
|
|
84
90
|
})
|
|
85
91
|
if duration_ms > SLOW_REQUEST_THRESHOLD_MS:
|
|
@@ -88,6 +94,7 @@ def instrument_flask(app, transport, context, config):
|
|
|
88
94
|
"method": method,
|
|
89
95
|
"path": path,
|
|
90
96
|
"durationMs": duration_ms,
|
|
97
|
+
"traceId": trace_id,
|
|
91
98
|
"timestamp": int(start),
|
|
92
99
|
})
|
|
93
100
|
return response
|
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devlite"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -4,15 +4,40 @@ import sys
|
|
|
4
4
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
5
5
|
|
|
6
6
|
from devlite._context import ( # noqa: E402
|
|
7
|
+
get_trace_id,
|
|
7
8
|
get_user,
|
|
8
9
|
is_sampled_out,
|
|
10
|
+
new_trace_id,
|
|
9
11
|
reset_context,
|
|
10
12
|
run_with_context,
|
|
11
13
|
set_sampled,
|
|
14
|
+
set_trace_id,
|
|
12
15
|
set_user,
|
|
13
16
|
)
|
|
14
17
|
|
|
15
18
|
|
|
19
|
+
def test_trace_id_scope_and_reset():
|
|
20
|
+
assert get_trace_id() is None
|
|
21
|
+
|
|
22
|
+
tid = new_trace_id()
|
|
23
|
+
assert len(tid) == 16
|
|
24
|
+
|
|
25
|
+
set_trace_id(tid)
|
|
26
|
+
assert get_trace_id() == tid
|
|
27
|
+
|
|
28
|
+
reset_context()
|
|
29
|
+
assert get_trace_id() is None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_run_with_context_restores_trace_id():
|
|
33
|
+
tid = new_trace_id()
|
|
34
|
+
set_trace_id(tid)
|
|
35
|
+
observed = []
|
|
36
|
+
run_with_context(lambda: observed.append(get_trace_id()))
|
|
37
|
+
assert observed == [None]
|
|
38
|
+
assert get_trace_id() == tid
|
|
39
|
+
|
|
40
|
+
|
|
16
41
|
def test_reset_context_clears_stale_store():
|
|
17
42
|
set_user({"id": "stale"})
|
|
18
43
|
set_sampled(False)
|
|
@@ -53,6 +53,7 @@ def test_fastapi_request_and_error_events(ingest):
|
|
|
53
53
|
|
|
54
54
|
devlite.set_user({"id": "u-7"})
|
|
55
55
|
devlite.capture_error(Exception("captured in endpoint"))
|
|
56
|
+
devlite.capture_log("log from endpoint")
|
|
56
57
|
return {"status": "ok"}
|
|
57
58
|
|
|
58
59
|
with TestClient(app, raise_server_exceptions=False) as tc:
|
|
@@ -80,4 +81,13 @@ def test_fastapi_request_and_error_events(ingest):
|
|
|
80
81
|
assert {r["method"] for r in reqs} == {"GET"}
|
|
81
82
|
assert all(r["statusCode"] in (200, 500) for r in reqs)
|
|
82
83
|
|
|
84
|
+
# every request carries a 16-hex-char traceId, and the /capture request's
|
|
85
|
+
# error + log + span all share that request's traceId
|
|
86
|
+
assert all(r["traceId"] and len(r["traceId"]) == 16 for r in reqs)
|
|
87
|
+
capture_req = next(r for r in reqs if r["path"] == "/capture")
|
|
88
|
+
cap_tid = capture_req["traceId"]
|
|
89
|
+
assert captured["traceId"] == cap_tid
|
|
90
|
+
captured_log = next(e for e in events if e["type"] == "log")
|
|
91
|
+
assert captured_log["traceId"] == cap_tid
|
|
92
|
+
|
|
83
93
|
c.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|