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.
Files changed (40) hide show
  1. {structguru-1.0.2 → structguru-1.0.4}/Cargo.lock +2 -2
  2. {structguru-1.0.2 → structguru-1.0.4}/Cargo.toml +1 -1
  3. {structguru-1.0.2 → structguru-1.0.4}/PKG-INFO +14 -14
  4. {structguru-1.0.2 → structguru-1.0.4}/README.md +13 -13
  5. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/filter.rs +1 -1
  6. {structguru-1.0.2 → structguru-1.0.4}/pyproject.toml +1 -4
  7. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/__init__.py +10 -12
  8. structguru-1.0.2/src/structguru/_native.py → structguru-1.0.4/src/structguru/_runtime.py +30 -18
  9. structguru-1.0.4/src/structguru/config.py +27 -0
  10. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/core.py +18 -13
  11. structguru-1.0.4/src/structguru/integrations/stdlib.py +178 -0
  12. structguru-1.0.2/src/structguru/config.py +0 -88
  13. {structguru-1.0.2 → structguru-1.0.4}/LICENSE +0 -0
  14. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/Cargo.toml +0 -0
  15. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/clock.rs +0 -0
  16. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/lib.rs +0 -0
  17. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/render.rs +0 -0
  18. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/value.rs +0 -0
  19. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-core/src/worker.rs +0 -0
  20. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-py/Cargo.toml +0 -0
  21. {structguru-1.0.2 → structguru-1.0.4}/crates/structguru-py/src/lib.rs +0 -0
  22. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/_contextvars.py +0 -0
  23. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/_native_dispatch.py +0 -0
  24. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/_native_env.py +0 -0
  25. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/exceptions.py +0 -0
  26. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/__init__.py +0 -0
  27. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/_util.py +0 -0
  28. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/asgi.py +0 -0
  29. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/celery.py +0 -0
  30. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/django.py +0 -0
  31. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/flask.py +0 -0
  32. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/grpc.py +0 -0
  33. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/httpx.py +0 -0
  34. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/requests.py +0 -0
  35. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/sentry.py +0 -0
  36. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/integrations/sqlalchemy.py +0 -0
  37. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/metrics.py +0 -0
  38. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/otel.py +0 -0
  39. {structguru-1.0.2 → structguru-1.0.4}/src/structguru/py.typed +0 -0
  40. {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.2"
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.2"
315
+ version = "1.0.4"
316
316
  dependencies = [
317
317
  "pyo3",
318
318
  "regex",
@@ -3,7 +3,7 @@ members = ["crates/structguru-core", "crates/structguru-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "1.0.2"
6
+ version = "1.0.4"
7
7
  edition = "2024"
8
8
  rust-version = "1.89"
9
9
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: structguru
3
- Version: 1.0.2
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=True)
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 participates in `flush_native()`, reconfiguration, disable,
191
- fork, and shutdown draining.
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=True)
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", json=False)
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
- | `configure_structlog(...)` | Deprecated compatibility wrapper; use `configure()`. Removed in v2.0. |
335
- | `disable_native()` | Stop the writer; logging is disabled until `configure()` is called. |
336
- | `set_native_level(level)` | Adjust the level threshold at runtime. |
337
- | `native_metrics()` | Writer counters (enqueued/written/dropped/depth/...) plus filter counters (`sampled`/`rate_limited`) when active. |
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** (`json=False`): 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`.
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=True)
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 participates in `flush_native()`, reconfiguration, disable,
147
- fork, and shutdown draining.
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=True)
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", json=False)
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
- | `configure_structlog(...)` | Deprecated compatibility wrapper; use `configure()`. Removed in v2.0. |
291
- | `disable_native()` | Stop the writer; logging is disabled until `configure()` is called. |
292
- | `set_native_level(level)` | Adjust the level threshold at runtime. |
293
- | `native_metrics()` | Writer counters (enqueued/written/dropped/depth/...) plus filter counters (`sampled`/`rate_limited`) when active. |
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** (`json=False`): 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`.
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 `_native.py`).
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.2"
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._native import (
3
+ from structguru._runtime import (
4
4
  configure,
5
- disable_native,
6
- native_available,
7
- native_metrics,
8
- set_native_level,
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.2"
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
- "configure_structlog",
25
- "disable_native",
23
+ "is_available",
26
24
  "logger",
27
- "native_available",
28
- "native_metrics",
29
- "set_native_level",
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 native_available() -> bool:
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 (by configure_structlog), rendered lines are
210
- # written to this stream synchronously on the caller's thread IN ADDITION to the
211
- # Rust writer. This preserves the pre-1.0 contract that logger output is available
212
- # on the configured stream immediately after the call (no flush needed).
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 set_native_level(level: str) -> None:
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
- json: bool = True,
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:`native_metrics`). Pass ``maxsize=0`` only to explicitly opt
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:`native_metrics`). ``sample_max_level``
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
- ``json=False`` selects the native console renderer (colored, human-readable
500
- dev output) instead of JSON. ``colors`` defaults to ``sys.stdout.isatty()``
501
- in console mode; set it explicitly to override. The console format is
502
- structguru's own stable dev format, not a structlog ``ConsoleRenderer`` clone.
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 = not json
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 disable_native() -> None:
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 (used by configure_structlog for backward compat).
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 native_metrics() -> dict[str, Any] | None:
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 _native
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 = _native.current_runtime()
323
+ runtime = _runtime.current_runtime()
324
324
  if runtime is None:
325
325
  return
326
326
 
327
- stack_info = bool(kwargs.get("stack_info") or self._opt_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 _native.is_below_level(method, runtime):
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 _native.should_render(method, formatted_msg, runtime):
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 _native.otel_enabled(runtime):
355
+ if _runtime.otel_enabled(runtime):
356
356
  add_otel_context(None, method, fields)
357
357
  if exc_info:
358
- exception = _native.build_exception_field(exc_info, runtime)
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
- stack = _native.format_stack() if stack_info else None
364
- _native.notify_metrics(method, formatted_msg, fields, runtime)
365
- sentry_line = _native.render_and_enqueue(
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
- _native.notify_sentry(
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 = _native.add_callable_sink(native_callback, min_level=native_level)
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
- _native.remove_callable_sink(token)
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