structguru 1.0.2__tar.gz → 1.0.4__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.
- {structguru-1.0.2 → structguru-1.0.4}/Cargo.lock +2 -2
- {structguru-1.0.2 → structguru-1.0.4}/Cargo.toml +1 -1
- {structguru-1.0.2 → structguru-1.0.4}/PKG-INFO +14 -14
- {structguru-1.0.2 → structguru-1.0.4}/README.md +13 -13
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/filter.rs +1 -1
- {structguru-1.0.2 → structguru-1.0.4}/pyproject.toml +1 -4
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/__init__.py +10 -12
- structguru-1.0.2/src/structguru/_native.py → structguru-1.0.4/src/structguru/_runtime.py +30 -18
- structguru-1.0.4/src/structguru/config.py +27 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/core.py +18 -13
- structguru-1.0.4/src/structguru/integrations/stdlib.py +178 -0
- structguru-1.0.2/src/structguru/config.py +0 -88
- {structguru-1.0.2 → structguru-1.0.4}/LICENSE +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/Cargo.toml +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/clock.rs +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/lib.rs +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/render.rs +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/value.rs +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/worker.rs +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-py/Cargo.toml +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-py/src/lib.rs +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/_contextvars.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/_native_dispatch.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/_native_env.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/exceptions.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/__init__.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/_util.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/asgi.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/celery.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/django.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/flask.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/grpc.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/httpx.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/requests.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/sentry.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/sqlalchemy.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/metrics.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/otel.py +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/py.typed +0 -0
- {structguru-1.0.2 → structguru-1.0.4}/src/structguru/redaction.py +0 -0
|
@@ -300,7 +300,7 @@ dependencies = [
|
|
|
300
300
|
|
|
301
301
|
[[package]]
|
|
302
302
|
name = "structguru-core"
|
|
303
|
-
version = "1.0.
|
|
303
|
+
version = "1.0.4"
|
|
304
304
|
dependencies = [
|
|
305
305
|
"fancy-regex",
|
|
306
306
|
"rand",
|
|
@@ -312,7 +312,7 @@ dependencies = [
|
|
|
312
312
|
|
|
313
313
|
[[package]]
|
|
314
314
|
name = "structguru-py"
|
|
315
|
-
version = "1.0.
|
|
315
|
+
version = "1.0.4"
|
|
316
316
|
dependencies = [
|
|
317
317
|
"pyo3",
|
|
318
318
|
"regex",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structguru
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.4
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -101,7 +101,7 @@ Available extras: `otel`, `celery`, `flask`, `django`, `sqlalchemy`, `grpc`,
|
|
|
101
101
|
from structguru import configure, logger
|
|
102
102
|
|
|
103
103
|
# Configure once at startup
|
|
104
|
-
configure(service="myapp", level="DEBUG", json
|
|
104
|
+
configure(service="myapp", level="DEBUG", format="json")
|
|
105
105
|
|
|
106
106
|
# Use anywhere
|
|
107
107
|
logger.info("Hello {name}", name="world")
|
|
@@ -187,18 +187,21 @@ file sink are owner-only (`0600`). Existing files retain their permissions.
|
|
|
187
187
|
|
|
188
188
|
All sink forms receive structguru records. They are also registered with the
|
|
189
189
|
stdlib root logger for third-party records. Native delivery uses the bounded
|
|
190
|
-
callable queue and
|
|
191
|
-
|
|
190
|
+
callable queue and is drained on flush, reconfiguration, `shutdown()`, fork, and
|
|
191
|
+
interpreter exit.
|
|
192
192
|
|
|
193
193
|
### Console vs JSON output
|
|
194
194
|
|
|
195
|
+
`format=` selects the renderer: `"json"` (default, production) or `"console"`
|
|
196
|
+
(colored, human-readable development output).
|
|
197
|
+
|
|
195
198
|
```python
|
|
196
199
|
# JSON (production)
|
|
197
|
-
configure(service="myapp", json
|
|
200
|
+
configure(service="myapp", format="json")
|
|
198
201
|
# → {"timestamp": "...", "service": "myapp", "level": "INFO", "message": "..."}
|
|
199
202
|
|
|
200
203
|
# Console (development) — colored, human-readable
|
|
201
|
-
configure(service="myapp",
|
|
204
|
+
configure(service="myapp", format="console")
|
|
202
205
|
# → 2025-01-15 12:00:00 [info ] Hello world
|
|
203
206
|
```
|
|
204
207
|
|
|
@@ -306,8 +309,6 @@ structguru.logger.info("order {id} accepted", id=987)
|
|
|
306
309
|
|
|
307
310
|
No configuration is required for the default JSON-to-stdout behavior. Call
|
|
308
311
|
`configure(...)` to customize the renderer, filtering, or sinks.
|
|
309
|
-
`configure_structlog(...)` remains as a deprecated compatibility wrapper for
|
|
310
|
-
its simpler service/level/format/stream interface and will be removed in v2.0.
|
|
311
312
|
|
|
312
313
|
```python
|
|
313
314
|
import structguru
|
|
@@ -331,11 +332,10 @@ Public API:
|
|
|
331
332
|
| Symbol | Purpose |
|
|
332
333
|
|--------|---------|
|
|
333
334
|
| `configure(...)` | Configure rendering, filtering, redaction, and output sinks. See the API reference for the complete signature. |
|
|
334
|
-
| `
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
338
|
-
| `native_available()` | Whether the compiled extension is importable. |
|
|
335
|
+
| `shutdown()` | Stop the writer; logging is disabled until `configure()` is called. |
|
|
336
|
+
| `set_level(level)` | Adjust the level threshold at runtime. |
|
|
337
|
+
| `writer_metrics()` | Writer counters (enqueued/written/dropped/depth/...) plus filter counters (`sampled`/`rate_limited`) when active. |
|
|
338
|
+
| `is_available()` | Whether the compiled extension is importable. |
|
|
339
339
|
|
|
340
340
|
Behavior notes:
|
|
341
341
|
|
|
@@ -346,7 +346,7 @@ Behavior notes:
|
|
|
346
346
|
- **Fork/shutdown safe** — the writer is flushed on exit and respawned in forked children (gunicorn/celery prefork). Rotating-file writers sharing a path coordinate through an owner-only `.lock` sidecar; distributed hosts should still prefer stdout and an external collector.
|
|
347
347
|
- **Structured exceptions** (`structured_exceptions=True`) render `type`, `message`, `module`, and frames as a dictionary, with optional redacted/truncated locals controlled by the `exception_*` options.
|
|
348
348
|
- **`stack_info` is supported natively**: the stack is captured in Python and rendered in the same position as `StackInfoRenderer` (`stack` between `service` and `message`). Unlike the standard path, the stack ends at the *user's* calling frame (structguru-internal frames are skipped, the way structlog skips its own).
|
|
349
|
-
- **Console mode** (`
|
|
349
|
+
- **Console mode** (`format="console"`): renders colored, human-readable lines instead of JSON — structguru's own stable dev format (`<timestamp> [<LEVEL>] <message> k=v`), with ANSI colors by default on a TTY. Override with `colors=True/False`.
|
|
350
350
|
- **File sinks** (`file_path=...`): write to a rotating file natively. Defaults mirror `RotatingFileHandler` (50 MB, 5 backups); configure via `file_max_bytes`/`file_backup_count`. Set `also_stdout=True` to mirror output to both file and stdout (e.g. container + persistent log).
|
|
351
351
|
- **Callable sinks** (`callable_sinks=[fn, ...]`): use a bounded queue (`callable_queue_maxsize=1024`). `overflow="block"` provides lossless backpressure; `overflow="drop"` reports `callable_dropped` metrics. Flush and lifecycle operations drain queued calls.
|
|
352
352
|
- **Sentry integration** (`sentry_processor=SentryProcessor(...)`): receives the already-redacted event and raw `exc_info` only for exception capture.
|
|
@@ -57,7 +57,7 @@ Available extras: `otel`, `celery`, `flask`, `django`, `sqlalchemy`, `grpc`,
|
|
|
57
57
|
from structguru import configure, logger
|
|
58
58
|
|
|
59
59
|
# Configure once at startup
|
|
60
|
-
configure(service="myapp", level="DEBUG", json
|
|
60
|
+
configure(service="myapp", level="DEBUG", format="json")
|
|
61
61
|
|
|
62
62
|
# Use anywhere
|
|
63
63
|
logger.info("Hello {name}", name="world")
|
|
@@ -143,18 +143,21 @@ file sink are owner-only (`0600`). Existing files retain their permissions.
|
|
|
143
143
|
|
|
144
144
|
All sink forms receive structguru records. They are also registered with the
|
|
145
145
|
stdlib root logger for third-party records. Native delivery uses the bounded
|
|
146
|
-
callable queue and
|
|
147
|
-
|
|
146
|
+
callable queue and is drained on flush, reconfiguration, `shutdown()`, fork, and
|
|
147
|
+
interpreter exit.
|
|
148
148
|
|
|
149
149
|
### Console vs JSON output
|
|
150
150
|
|
|
151
|
+
`format=` selects the renderer: `"json"` (default, production) or `"console"`
|
|
152
|
+
(colored, human-readable development output).
|
|
153
|
+
|
|
151
154
|
```python
|
|
152
155
|
# JSON (production)
|
|
153
|
-
configure(service="myapp", json
|
|
156
|
+
configure(service="myapp", format="json")
|
|
154
157
|
# → {"timestamp": "...", "service": "myapp", "level": "INFO", "message": "..."}
|
|
155
158
|
|
|
156
159
|
# Console (development) — colored, human-readable
|
|
157
|
-
configure(service="myapp",
|
|
160
|
+
configure(service="myapp", format="console")
|
|
158
161
|
# → 2025-01-15 12:00:00 [info ] Hello world
|
|
159
162
|
```
|
|
160
163
|
|
|
@@ -262,8 +265,6 @@ structguru.logger.info("order {id} accepted", id=987)
|
|
|
262
265
|
|
|
263
266
|
No configuration is required for the default JSON-to-stdout behavior. Call
|
|
264
267
|
`configure(...)` to customize the renderer, filtering, or sinks.
|
|
265
|
-
`configure_structlog(...)` remains as a deprecated compatibility wrapper for
|
|
266
|
-
its simpler service/level/format/stream interface and will be removed in v2.0.
|
|
267
268
|
|
|
268
269
|
```python
|
|
269
270
|
import structguru
|
|
@@ -287,11 +288,10 @@ Public API:
|
|
|
287
288
|
| Symbol | Purpose |
|
|
288
289
|
|--------|---------|
|
|
289
290
|
| `configure(...)` | Configure rendering, filtering, redaction, and output sinks. See the API reference for the complete signature. |
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
292
|
-
| `
|
|
293
|
-
| `
|
|
294
|
-
| `native_available()` | Whether the compiled extension is importable. |
|
|
291
|
+
| `shutdown()` | Stop the writer; logging is disabled until `configure()` is called. |
|
|
292
|
+
| `set_level(level)` | Adjust the level threshold at runtime. |
|
|
293
|
+
| `writer_metrics()` | Writer counters (enqueued/written/dropped/depth/...) plus filter counters (`sampled`/`rate_limited`) when active. |
|
|
294
|
+
| `is_available()` | Whether the compiled extension is importable. |
|
|
295
295
|
|
|
296
296
|
Behavior notes:
|
|
297
297
|
|
|
@@ -302,7 +302,7 @@ Behavior notes:
|
|
|
302
302
|
- **Fork/shutdown safe** — the writer is flushed on exit and respawned in forked children (gunicorn/celery prefork). Rotating-file writers sharing a path coordinate through an owner-only `.lock` sidecar; distributed hosts should still prefer stdout and an external collector.
|
|
303
303
|
- **Structured exceptions** (`structured_exceptions=True`) render `type`, `message`, `module`, and frames as a dictionary, with optional redacted/truncated locals controlled by the `exception_*` options.
|
|
304
304
|
- **`stack_info` is supported natively**: the stack is captured in Python and rendered in the same position as `StackInfoRenderer` (`stack` between `service` and `message`). Unlike the standard path, the stack ends at the *user's* calling frame (structguru-internal frames are skipped, the way structlog skips its own).
|
|
305
|
-
- **Console mode** (`
|
|
305
|
+
- **Console mode** (`format="console"`): renders colored, human-readable lines instead of JSON — structguru's own stable dev format (`<timestamp> [<LEVEL>] <message> k=v`), with ANSI colors by default on a TTY. Override with `colors=True/False`.
|
|
306
306
|
- **File sinks** (`file_path=...`): write to a rotating file natively. Defaults mirror `RotatingFileHandler` (50 MB, 5 backups); configure via `file_max_bytes`/`file_backup_count`. Set `also_stdout=True` to mirror output to both file and stdout (e.g. container + persistent log).
|
|
307
307
|
- **Callable sinks** (`callable_sinks=[fn, ...]`): use a bounded queue (`callable_queue_maxsize=1024`). `overflow="block"` provides lossless backpressure; `overflow="drop"` reports `callable_dropped` metrics. Flush and lifecycle operations drain queued calls.
|
|
308
308
|
- **Sentry integration** (`sentry_processor=SentryProcessor(...)`): receives the already-redacted event and raw `exc_info` only for exception capture.
|
|
@@ -24,7 +24,7 @@ pub trait RecordFilter: Send + Sync {
|
|
|
24
24
|
fn allow(&self, key: &str, level: &str) -> Decision;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/// Numeric level for a method name (mirrors `_LEVEL_NUM` in `
|
|
27
|
+
/// Numeric level for a method name (mirrors `_LEVEL_NUM` in `_runtime.py`).
|
|
28
28
|
fn method_level_num(method: &str) -> u8 {
|
|
29
29
|
match method {
|
|
30
30
|
"trace" => 5,
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structguru"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.4"
|
|
8
8
|
description = "Native structured logging with a loguru-style Python API"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -131,6 +131,3 @@ ignore_missing_imports = true
|
|
|
131
131
|
[tool.pytest.ini_options]
|
|
132
132
|
testpaths = ["tests"]
|
|
133
133
|
asyncio_mode = "auto"
|
|
134
|
-
filterwarnings = [
|
|
135
|
-
"ignore:configure_structlog\\(\\) is deprecated; use configure\\(\\) instead\\. It will be removed in v2\\.0\\.:DeprecationWarning",
|
|
136
|
-
]
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
"""structguru — ergonomic structured logging with a native Rust runtime."""
|
|
2
2
|
|
|
3
|
-
from structguru.
|
|
3
|
+
from structguru._runtime import (
|
|
4
4
|
configure,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
is_available,
|
|
6
|
+
set_level,
|
|
7
|
+
shutdown,
|
|
8
|
+
writer_metrics,
|
|
9
9
|
)
|
|
10
|
-
from structguru.config import configure_structlog
|
|
11
10
|
from structguru.core import Logger, logger
|
|
12
11
|
from structguru.metrics import MetricProcessor
|
|
13
12
|
from structguru.otel import add_otel_context
|
|
14
13
|
from structguru.redaction import DEFAULT_SENSITIVE_KEYS
|
|
15
14
|
|
|
16
|
-
__version__ = "1.0.
|
|
15
|
+
__version__ = "1.0.4"
|
|
17
16
|
|
|
18
17
|
__all__ = [
|
|
19
18
|
"DEFAULT_SENSITIVE_KEYS",
|
|
@@ -21,10 +20,9 @@ __all__ = [
|
|
|
21
20
|
"MetricProcessor",
|
|
22
21
|
"add_otel_context",
|
|
23
22
|
"configure",
|
|
24
|
-
"
|
|
25
|
-
"disable_native",
|
|
23
|
+
"is_available",
|
|
26
24
|
"logger",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
25
|
+
"set_level",
|
|
26
|
+
"shutdown",
|
|
27
|
+
"writer_metrics",
|
|
30
28
|
]
|
|
@@ -40,6 +40,10 @@ _LEVEL_NUM: dict[str, int] = {
|
|
|
40
40
|
"fatal": 50,
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
# Supported output renderers. ``format=`` on configure() accepts these names;
|
|
44
|
+
# the choice maps to the ``console`` runtime flag until more than two formats exist.
|
|
45
|
+
_FORMATS: tuple[str, ...] = ("json", "console")
|
|
46
|
+
|
|
43
47
|
|
|
44
48
|
class _NativeWriter(Protocol):
|
|
45
49
|
def try_enqueue(self, message: str) -> bool: ...
|
|
@@ -161,7 +165,7 @@ def _load_rust_module() -> _RustModule | None:
|
|
|
161
165
|
_RUST = _load_rust_module()
|
|
162
166
|
|
|
163
167
|
|
|
164
|
-
def
|
|
168
|
+
def is_available() -> bool:
|
|
165
169
|
"""Return whether the compiled native extension is importable."""
|
|
166
170
|
return _RUST is not None
|
|
167
171
|
|
|
@@ -206,10 +210,10 @@ _lifecycle_generation = 0
|
|
|
206
210
|
|
|
207
211
|
|
|
208
212
|
_callable_dispatcher = CallableSinkDispatcher()
|
|
209
|
-
# Synchronous stream sink: when set (
|
|
210
|
-
# written to this stream synchronously on the caller's thread IN
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
+
# Synchronous stream sink: when set (via configure(stream_sink=...)), rendered
|
|
214
|
+
# lines are written to this stream synchronously on the caller's thread IN
|
|
215
|
+
# ADDITION to the Rust writer, so logger output is available on the configured
|
|
216
|
+
# stream immediately after the call (no flush needed).
|
|
213
217
|
_hooks_registered = False
|
|
214
218
|
_drop_count = 0
|
|
215
219
|
_drop_lock = threading.Lock()
|
|
@@ -231,7 +235,7 @@ def is_below_level(method: str, runtime: _RuntimeState | None = None) -> bool:
|
|
|
231
235
|
return state is None or _LEVEL_NUM.get(method, _LEVEL_NUM["info"]) < state.level_threshold
|
|
232
236
|
|
|
233
237
|
|
|
234
|
-
def
|
|
238
|
+
def set_level(level: str) -> None:
|
|
235
239
|
"""Adjust the native level threshold at runtime (per-process)."""
|
|
236
240
|
global _runtime, _lifecycle_generation
|
|
237
241
|
with _state_lock:
|
|
@@ -436,7 +440,7 @@ def configure(
|
|
|
436
440
|
exception_include_locals: bool = False,
|
|
437
441
|
exception_max_frames: int = 20,
|
|
438
442
|
exception_max_local_repr: int = 200,
|
|
439
|
-
|
|
443
|
+
format: str = "json",
|
|
440
444
|
colors: bool | None = None,
|
|
441
445
|
file_path: str | None = None,
|
|
442
446
|
file_max_bytes: int = 50 * 1024 * 1024,
|
|
@@ -454,7 +458,7 @@ def configure(
|
|
|
454
458
|
The output queue is bounded to 8192 records by default. ``overflow`` governs a
|
|
455
459
|
full queue: ``"block"`` (default, no loss — the caller waits with the GIL
|
|
456
460
|
released) or ``"drop"`` (drop the new record, count it, and emit a rate-limited
|
|
457
|
-
warning; see :func:`
|
|
461
|
+
warning; see :func:`writer_metrics`). Pass ``maxsize=0`` only to explicitly opt
|
|
458
462
|
into an unbounded queue.
|
|
459
463
|
|
|
460
464
|
``sensitive_patterns`` is a list of regex source strings applied (in addition
|
|
@@ -479,7 +483,7 @@ def configure(
|
|
|
479
483
|
``sample_rate`` (0.0–1.0) and ``rate_limit_max``/``rate_limit_period`` add
|
|
480
484
|
pre-render filters: dropped records cost zero rendering. ``sampled`` and
|
|
481
485
|
``rate_limited`` counters are reported separately from the writer's transport
|
|
482
|
-
``dropped`` counter (see :func:`
|
|
486
|
+
``dropped`` counter (see :func:`writer_metrics`). ``sample_max_level``
|
|
483
487
|
restricts sampling to records at or below that level (more severe records
|
|
484
488
|
always pass) — the native analog of level-gated sampling.
|
|
485
489
|
|
|
@@ -496,10 +500,11 @@ def configure(
|
|
|
496
500
|
``sensitive_keys`` reused for locals redaction) instead of the formatted
|
|
497
501
|
traceback string.
|
|
498
502
|
|
|
499
|
-
``
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
+
``format`` selects the renderer: ``"json"`` (default, production-friendly
|
|
504
|
+
compact JSON) or ``"console"`` (colored, human-readable dev output in
|
|
505
|
+
structguru's own stable format — not a structlog ``ConsoleRenderer`` clone).
|
|
506
|
+
``colors`` defaults to ``sys.stdout.isatty()`` in console mode; set it
|
|
507
|
+
explicitly to override (ignored in JSON mode).
|
|
503
508
|
|
|
504
509
|
``file_path`` enables a native rotating-file sink (append mode). Defaults
|
|
505
510
|
mirror :class:`logging.handlers.RotatingFileHandler`: ``file_max_bytes=50MB``,
|
|
@@ -562,6 +567,13 @@ def configure(
|
|
|
562
567
|
msg = f"exception_max_local_repr must be >= 0, got {exception_max_local_repr}"
|
|
563
568
|
raise ValueError(msg)
|
|
564
569
|
|
|
570
|
+
# Resolve the output format. ``format=`` is the canonical selector
|
|
571
|
+
# ("json" | "console"); ``format="json"`` is the default.
|
|
572
|
+
resolved_format = format
|
|
573
|
+
if resolved_format not in _FORMATS:
|
|
574
|
+
msg = f"format must be one of {_FORMATS}, got {resolved_format!r}"
|
|
575
|
+
raise ValueError(msg)
|
|
576
|
+
|
|
565
577
|
# Validate regex patterns against Rust's engine before enabling. Rust's
|
|
566
578
|
# `regex` guarantees linear-time matching and therefore rejects
|
|
567
579
|
# backreferences and look-around; fail loudly at setup time — redaction
|
|
@@ -625,8 +637,8 @@ def configure(
|
|
|
625
637
|
msg = f"callable_sinks[{i}] must be callable, got {type(fn)!r}"
|
|
626
638
|
raise TypeError(msg)
|
|
627
639
|
|
|
628
|
-
# Resolve console/colors.
|
|
629
|
-
new_console =
|
|
640
|
+
# Resolve console/colors from the chosen format.
|
|
641
|
+
new_console = resolved_format == "console"
|
|
630
642
|
new_colors = colors if colors is not None else (sys.stdout.isatty() if new_console else False)
|
|
631
643
|
|
|
632
644
|
# Construct every fallible native resource before touching the active
|
|
@@ -754,7 +766,7 @@ def _after_in_child() -> None:
|
|
|
754
766
|
_callable_dispatcher.after_fork(enabled=True)
|
|
755
767
|
|
|
756
768
|
|
|
757
|
-
def
|
|
769
|
+
def shutdown() -> None:
|
|
758
770
|
"""Turn native mode off and stop the background writer."""
|
|
759
771
|
global _runtime, _lifecycle_generation
|
|
760
772
|
with _state_lock:
|
|
@@ -856,7 +868,7 @@ def render_and_enqueue(
|
|
|
856
868
|
else _render_json(state, fields, logger, level, message, stack)
|
|
857
869
|
)
|
|
858
870
|
line = rendered + "\n"
|
|
859
|
-
# Synchronous stream sink (
|
|
871
|
+
# Synchronous stream sink (configure(stream_sink=...)).
|
|
860
872
|
if state.stream_sink is not None:
|
|
861
873
|
try:
|
|
862
874
|
state.stream_sink.write(line)
|
|
@@ -917,7 +929,7 @@ def drain_messages() -> list[str]:
|
|
|
917
929
|
return state.writer.messages() if state is not None else []
|
|
918
930
|
|
|
919
931
|
|
|
920
|
-
def
|
|
932
|
+
def writer_metrics() -> dict[str, Any] | None:
|
|
921
933
|
"""Return writer + filter metrics.
|
|
922
934
|
|
|
923
935
|
Writer counters: ``enqueued``, ``dropped`` (queue-full), ``written``, ``depth``,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Level-name helpers for structguru.
|
|
2
|
+
|
|
3
|
+
Since v1.0, structguru uses the native Rust renderer as its only logging path.
|
|
4
|
+
Runtime configuration lives in :func:`structguru.configure`; this module keeps
|
|
5
|
+
the level-name conversion helper shared by the integrations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _to_logging_level(level_name: str) -> int:
|
|
14
|
+
"""Convert a human-readable level name to its :mod:`logging` constant."""
|
|
15
|
+
upper_level = level_name.upper()
|
|
16
|
+
if upper_level == "WARN":
|
|
17
|
+
return logging.WARNING
|
|
18
|
+
result: int | None = getattr(logging, upper_level, None)
|
|
19
|
+
if not isinstance(result, int):
|
|
20
|
+
import warnings
|
|
21
|
+
|
|
22
|
+
warnings.warn(
|
|
23
|
+
f"Unknown log level {level_name!r}, falling back to INFO",
|
|
24
|
+
stacklevel=2,
|
|
25
|
+
)
|
|
26
|
+
return logging.INFO
|
|
27
|
+
return result
|
|
@@ -28,7 +28,7 @@ from dataclasses import dataclass, field, replace
|
|
|
28
28
|
from pathlib import Path
|
|
29
29
|
from typing import Any, TypeAlias
|
|
30
30
|
|
|
31
|
-
from structguru import
|
|
31
|
+
from structguru import _runtime
|
|
32
32
|
from structguru._contextvars import bound_contextvars, get_contextvars
|
|
33
33
|
from structguru.config import _to_logging_level
|
|
34
34
|
from structguru.otel import add_otel_context
|
|
@@ -320,14 +320,14 @@ class Logger:
|
|
|
320
320
|
# One coherent snapshot follows the record through every processing stage.
|
|
321
321
|
# Reconfigure/disable may retire its writer, but cannot invalidate this
|
|
322
322
|
# Python object or expose a partially updated configuration.
|
|
323
|
-
runtime =
|
|
323
|
+
runtime = _runtime.current_runtime()
|
|
324
324
|
if runtime is None:
|
|
325
325
|
return
|
|
326
326
|
|
|
327
|
-
stack_info =
|
|
327
|
+
stack_info = kwargs.get("stack_info") or self._opt_stack_info
|
|
328
328
|
|
|
329
329
|
# Cheap disabled path: level-filter before any formatting.
|
|
330
|
-
if
|
|
330
|
+
if _runtime.is_below_level(method, runtime):
|
|
331
331
|
return
|
|
332
332
|
|
|
333
333
|
formatted_msg, consumed_keys = _safe_format(message, args, kwargs)
|
|
@@ -339,7 +339,7 @@ class Logger:
|
|
|
339
339
|
|
|
340
340
|
# Pre-render filter (sampling/rate-limit): decide before building fields.
|
|
341
341
|
# Keys on the formatted message, matching the rate-limiter's grouping.
|
|
342
|
-
if not
|
|
342
|
+
if not _runtime.should_render(method, formatted_msg, runtime):
|
|
343
343
|
return
|
|
344
344
|
|
|
345
345
|
exc_info = kwargs.get("exc_info", self._opt_exc_info)
|
|
@@ -352,17 +352,22 @@ class Logger:
|
|
|
352
352
|
# matching structlog's merge_contextvars setdefault semantics.
|
|
353
353
|
for key, value in get_contextvars().items():
|
|
354
354
|
fields.setdefault(key, value)
|
|
355
|
-
if
|
|
355
|
+
if _runtime.otel_enabled(runtime):
|
|
356
356
|
add_otel_context(None, method, fields)
|
|
357
357
|
if exc_info:
|
|
358
|
-
exception =
|
|
358
|
+
exception = _runtime.build_exception_field(exc_info, runtime)
|
|
359
359
|
if exception:
|
|
360
360
|
fields["exception"] = exception
|
|
361
361
|
# Stack capture is Python-owned (frame walking); rendering places
|
|
362
362
|
# "stack" between "service" and "message" like StackInfoRenderer.
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
363
|
+
if isinstance(stack_info, str):
|
|
364
|
+
stack = stack_info
|
|
365
|
+
elif stack_info:
|
|
366
|
+
stack = _runtime.format_stack()
|
|
367
|
+
else:
|
|
368
|
+
stack = None
|
|
369
|
+
_runtime.notify_metrics(method, formatted_msg, fields, runtime)
|
|
370
|
+
sentry_line = _runtime.render_and_enqueue(
|
|
366
371
|
fields,
|
|
367
372
|
name,
|
|
368
373
|
method,
|
|
@@ -371,7 +376,7 @@ class Logger:
|
|
|
371
376
|
runtime=runtime,
|
|
372
377
|
)
|
|
373
378
|
if sentry_line is not None:
|
|
374
|
-
|
|
379
|
+
_runtime.notify_sentry(
|
|
375
380
|
method,
|
|
376
381
|
sentry_line,
|
|
377
382
|
tuple(fields),
|
|
@@ -415,7 +420,7 @@ class Logger:
|
|
|
415
420
|
if callable(sink) and not isinstance(sink, logging.Handler)
|
|
416
421
|
else functools.partial(_emit_rendered, handler)
|
|
417
422
|
)
|
|
418
|
-
native_token =
|
|
423
|
+
native_token = _runtime.add_callable_sink(native_callback, min_level=native_level)
|
|
419
424
|
|
|
420
425
|
root = logging.getLogger()
|
|
421
426
|
root.addHandler(handler)
|
|
@@ -451,7 +456,7 @@ class Logger:
|
|
|
451
456
|
]
|
|
452
457
|
for handler, token in registrations:
|
|
453
458
|
if token is not None:
|
|
454
|
-
|
|
459
|
+
_runtime.remove_callable_sink(token)
|
|
455
460
|
if handler is not None:
|
|
456
461
|
root.removeHandler(handler)
|
|
457
462
|
handler.close()
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Route standard-library logging into structguru's native pipeline.
|
|
2
|
+
|
|
3
|
+
Third-party libraries log through the stdlib :mod:`logging` module. Installing
|
|
4
|
+
the bridge attaches a handler to the root logger that re-emits every such record
|
|
5
|
+
through structguru's native renderer, so foreign logs share the same JSON /
|
|
6
|
+
console formatting, redaction, level filtering, and output stream as
|
|
7
|
+
``structguru.logger``::
|
|
8
|
+
|
|
9
|
+
from structguru.integrations.stdlib import install_stdlib_bridge
|
|
10
|
+
|
|
11
|
+
install_stdlib_bridge(level="INFO", suppress_loggers=("urllib3", "botocore"))
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
logging.getLogger("sqlalchemy.engine").info("SELECT 1")
|
|
15
|
+
# -> {"logger":"sqlalchemy.engine","level":"INFO",...,"message":"SELECT 1"}
|
|
16
|
+
|
|
17
|
+
For a logger that sets ``propagate=False`` (so its records never reach the root
|
|
18
|
+
logger), attach :class:`StructguruHandler` to it directly.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import functools
|
|
24
|
+
import logging
|
|
25
|
+
from collections.abc import Iterable
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from structguru.config import _to_logging_level
|
|
29
|
+
from structguru.core import Logger
|
|
30
|
+
|
|
31
|
+
# Attributes present on a vanilla LogRecord (plus the two the Formatter adds).
|
|
32
|
+
# Anything else in ``record.__dict__`` came from a user ``extra=`` mapping and is
|
|
33
|
+
# forwarded as a structured field.
|
|
34
|
+
_STANDARD_RECORD_ATTRS = frozenset(
|
|
35
|
+
{
|
|
36
|
+
"name",
|
|
37
|
+
"msg",
|
|
38
|
+
"args",
|
|
39
|
+
"levelname",
|
|
40
|
+
"levelno",
|
|
41
|
+
"pathname",
|
|
42
|
+
"filename",
|
|
43
|
+
"module",
|
|
44
|
+
"exc_info",
|
|
45
|
+
"exc_text",
|
|
46
|
+
"stack_info",
|
|
47
|
+
"lineno",
|
|
48
|
+
"funcName",
|
|
49
|
+
"created",
|
|
50
|
+
"msecs",
|
|
51
|
+
"relativeCreated",
|
|
52
|
+
"thread",
|
|
53
|
+
"threadName",
|
|
54
|
+
"processName",
|
|
55
|
+
"process",
|
|
56
|
+
"taskName",
|
|
57
|
+
"message",
|
|
58
|
+
"asctime",
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _method_for_level(levelno: int) -> str:
|
|
64
|
+
"""Map a stdlib numeric level to the structguru method that emits it."""
|
|
65
|
+
if levelno >= logging.CRITICAL:
|
|
66
|
+
return "critical"
|
|
67
|
+
if levelno >= logging.ERROR:
|
|
68
|
+
return "error"
|
|
69
|
+
if levelno >= logging.WARNING:
|
|
70
|
+
return "warning"
|
|
71
|
+
if levelno >= logging.INFO:
|
|
72
|
+
return "info"
|
|
73
|
+
return "debug"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@functools.lru_cache(maxsize=1024)
|
|
77
|
+
def _logger_for(name: str) -> Logger:
|
|
78
|
+
"""Return a cached structguru :class:`Logger` bound to *name*.
|
|
79
|
+
|
|
80
|
+
The base logger is immutable in practice — ``bind()``/``opt()`` return fresh
|
|
81
|
+
copies — so a shared per-name instance is safe to reuse across threads.
|
|
82
|
+
"""
|
|
83
|
+
return Logger(name=name)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _record_extras(record: logging.LogRecord) -> dict[str, Any]:
|
|
87
|
+
"""Extract user-supplied ``extra=`` fields from a record."""
|
|
88
|
+
return {
|
|
89
|
+
key: value for key, value in record.__dict__.items() if key not in _STANDARD_RECORD_ATTRS
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StructguruHandler(logging.Handler):
|
|
94
|
+
"""A :class:`logging.Handler` that re-emits stdlib records via structguru.
|
|
95
|
+
|
|
96
|
+
The record's logger name becomes the ``logger`` field, its level selects the
|
|
97
|
+
structguru method, ``extra=`` fields are forwarded as structured fields, and
|
|
98
|
+
``exc_info`` and ``stack_info`` are carried through. The already-formatted
|
|
99
|
+
message is passed verbatim (no brace re-formatting), so literal ``{...}`` in
|
|
100
|
+
a message is never misinterpreted.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
104
|
+
# Never intercept structguru's own records: it keeps the bridge from
|
|
105
|
+
# looping and stops native output being wrapped a second time.
|
|
106
|
+
if record.name == "structguru" or record.name.startswith("structguru."):
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
target = _logger_for(record.name)
|
|
110
|
+
extras = _record_extras(record)
|
|
111
|
+
if extras:
|
|
112
|
+
target = target.bind(**extras)
|
|
113
|
+
if record.exc_info:
|
|
114
|
+
target = target.opt(exception=record.exc_info)
|
|
115
|
+
emit_method = getattr(target, _method_for_level(record.levelno))
|
|
116
|
+
if record.stack_info:
|
|
117
|
+
emit_method(record.getMessage(), stack_info=record.stack_info)
|
|
118
|
+
else:
|
|
119
|
+
emit_method(record.getMessage())
|
|
120
|
+
except Exception: # noqa: BLE001 - a logging handler must never raise
|
|
121
|
+
self.handleError(record)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _apply_suppression(names: Iterable[str], level: str) -> None:
|
|
125
|
+
threshold = _to_logging_level(level)
|
|
126
|
+
for name in names:
|
|
127
|
+
logging.getLogger(name).setLevel(threshold)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def suppress_loggers(*names: str, level: str = "WARNING") -> None:
|
|
131
|
+
"""Raise the level threshold of the named loggers.
|
|
132
|
+
|
|
133
|
+
Use this to quiet noisy third-party loggers (``"urllib3"``,
|
|
134
|
+
``"botocore"``, ``"sqlalchemy.engine"``, …) regardless of whether they reach
|
|
135
|
+
structguru through the bridge or their own handlers.
|
|
136
|
+
"""
|
|
137
|
+
_apply_suppression(names, level)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def install_stdlib_bridge(
|
|
141
|
+
*,
|
|
142
|
+
level: str = "INFO",
|
|
143
|
+
suppress_loggers: Iterable[str] = (),
|
|
144
|
+
suppress_level: str = "WARNING",
|
|
145
|
+
clear_handlers: bool = True,
|
|
146
|
+
) -> StructguruHandler:
|
|
147
|
+
"""Route root stdlib logging through structguru and quiet noisy loggers.
|
|
148
|
+
|
|
149
|
+
Installs a :class:`StructguruHandler` on the root logger so any library
|
|
150
|
+
logging through :mod:`logging` renders through structguru's native path. The
|
|
151
|
+
returned handler can be passed to ``logging.getLogger().removeHandler(...)``
|
|
152
|
+
to uninstall.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
level:
|
|
157
|
+
Level set on the root logger — the floor for which stdlib records
|
|
158
|
+
propagate to the bridge. structguru's own native level (set via
|
|
159
|
+
:func:`structguru.configure`) still applies on top.
|
|
160
|
+
suppress_loggers:
|
|
161
|
+
Logger names to raise to ``suppress_level`` (see :func:`suppress_loggers`).
|
|
162
|
+
suppress_level:
|
|
163
|
+
Level applied to ``suppress_loggers`` (default ``"WARNING"``).
|
|
164
|
+
clear_handlers:
|
|
165
|
+
Remove existing root handlers first (default ``True``), so records are
|
|
166
|
+
not also emitted by a previously configured stdlib handler.
|
|
167
|
+
"""
|
|
168
|
+
root = logging.getLogger()
|
|
169
|
+
if clear_handlers:
|
|
170
|
+
for handler in list(root.handlers):
|
|
171
|
+
root.removeHandler(handler)
|
|
172
|
+
threshold = _to_logging_level(level)
|
|
173
|
+
bridge = StructguruHandler()
|
|
174
|
+
bridge.setLevel(threshold)
|
|
175
|
+
root.addHandler(bridge)
|
|
176
|
+
root.setLevel(threshold)
|
|
177
|
+
_apply_suppression(suppress_loggers, suppress_level)
|
|
178
|
+
return bridge
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
"""Logging configuration for structguru.
|
|
2
|
-
|
|
3
|
-
Since v1.0, structguru uses the native Rust renderer as its only logging path.
|
|
4
|
-
The remaining compatibility shim configures the native renderer to match the
|
|
5
|
-
pre-1.0 observable stream contract.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import logging
|
|
11
|
-
import sys
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
from structguru._native import configure as _configure
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _to_logging_level(level_name: str) -> int:
|
|
18
|
-
"""Convert a human-readable level name to its :mod:`logging` constant."""
|
|
19
|
-
upper_level = level_name.upper()
|
|
20
|
-
if upper_level == "WARN":
|
|
21
|
-
return logging.WARNING
|
|
22
|
-
result: int | None = getattr(logging, upper_level, None)
|
|
23
|
-
if not isinstance(result, int):
|
|
24
|
-
import warnings
|
|
25
|
-
|
|
26
|
-
warnings.warn(
|
|
27
|
-
f"Unknown log level {level_name!r}, falling back to INFO",
|
|
28
|
-
stacklevel=2,
|
|
29
|
-
)
|
|
30
|
-
return logging.INFO
|
|
31
|
-
return result
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def configure_structlog(
|
|
35
|
-
*,
|
|
36
|
-
service: str = "app",
|
|
37
|
-
level: str = "INFO",
|
|
38
|
-
json_logs: bool = True,
|
|
39
|
-
stream: Any = None,
|
|
40
|
-
clear_handlers: bool = True,
|
|
41
|
-
) -> None:
|
|
42
|
-
"""Configure the native logger.
|
|
43
|
-
|
|
44
|
-
.. deprecated:: 1.0
|
|
45
|
-
Use :func:`structguru.configure` instead. This compatibility wrapper
|
|
46
|
-
will be removed in v2.0.
|
|
47
|
-
|
|
48
|
-
Since v1.0, this is a compatibility shim that wires the native Rust renderer
|
|
49
|
-
synchronously to the given *stream*, preserving the pre-1.0 contract
|
|
50
|
-
that ``logger`` output lands on the configured stream. The native renderer
|
|
51
|
-
handles all JSON/console formatting, redaction, and level filtering.
|
|
52
|
-
|
|
53
|
-
Parameters
|
|
54
|
-
----------
|
|
55
|
-
service:
|
|
56
|
-
Application/service name added to every log record.
|
|
57
|
-
level:
|
|
58
|
-
Minimum log level (e.g. ``"DEBUG"``, ``"INFO"``).
|
|
59
|
-
json_logs:
|
|
60
|
-
``True`` for JSON output, ``False`` for colored console output.
|
|
61
|
-
stream:
|
|
62
|
-
Output stream. Defaults to ``sys.stdout`` and is the sole output target.
|
|
63
|
-
clear_handlers:
|
|
64
|
-
Kept for backward compatibility; the native path does not use root
|
|
65
|
-
logger handlers.
|
|
66
|
-
"""
|
|
67
|
-
import warnings
|
|
68
|
-
|
|
69
|
-
warnings.warn(
|
|
70
|
-
"configure_structlog() is deprecated; use configure() instead. "
|
|
71
|
-
"It will be removed in v2.0.",
|
|
72
|
-
DeprecationWarning,
|
|
73
|
-
stacklevel=2,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
if stream is None:
|
|
77
|
-
stream = sys.stdout
|
|
78
|
-
|
|
79
|
-
# Wire the stream as a synchronous sink so logger output lands on it
|
|
80
|
-
# immediately (preserving the pre-1.0 contract that output is available
|
|
81
|
-
# right after the logger call, no flush needed).
|
|
82
|
-
_configure(
|
|
83
|
-
service=service,
|
|
84
|
-
level=level,
|
|
85
|
-
json=json_logs,
|
|
86
|
-
target="null",
|
|
87
|
-
stream_sink=stream,
|
|
88
|
-
)
|
|
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
|
|
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
|