structguru 1.2.2__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {structguru-1.2.2 → structguru-1.3.0}/Cargo.lock +9 -9
  2. {structguru-1.2.2 → structguru-1.3.0}/Cargo.toml +1 -1
  3. {structguru-1.2.2 → structguru-1.3.0}/PKG-INFO +143 -27
  4. {structguru-1.2.2 → structguru-1.3.0}/README.md +142 -26
  5. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/src/filter.rs +103 -30
  6. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/src/lib.rs +54 -2
  7. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/src/render.rs +101 -25
  8. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/src/value.rs +3 -3
  9. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/src/worker.rs +438 -35
  10. structguru-1.3.0/crates/structguru-py/src/dispatch.rs +1067 -0
  11. structguru-1.3.0/crates/structguru-py/src/lib.rs +1457 -0
  12. {structguru-1.2.2 → structguru-1.3.0}/pyproject.toml +7 -4
  13. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/__init__.py +9 -1
  14. structguru-1.3.0/src/structguru/_levels.py +60 -0
  15. structguru-1.3.0/src/structguru/_native_dispatch.py +73 -0
  16. structguru-1.3.0/src/structguru/_native_env.py +62 -0
  17. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/_runtime.py +481 -301
  18. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/config.py +11 -6
  19. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/core.py +255 -74
  20. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/exceptions.py +64 -19
  21. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/_util.py +19 -9
  22. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/asgi.py +23 -11
  23. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/celery.py +31 -0
  24. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/django.py +37 -28
  25. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/sentry.py +17 -3
  26. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/stdlib.py +37 -15
  27. structguru-1.3.0/src/structguru/settings.py +232 -0
  28. structguru-1.2.2/crates/structguru-py/src/lib.rs +0 -769
  29. structguru-1.2.2/src/structguru/_native_dispatch.py +0 -264
  30. structguru-1.2.2/src/structguru/_native_env.py +0 -26
  31. {structguru-1.2.2 → structguru-1.3.0}/LICENSE +0 -0
  32. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/Cargo.toml +0 -0
  33. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-core/src/clock.rs +0 -0
  34. {structguru-1.2.2 → structguru-1.3.0}/crates/structguru-py/Cargo.toml +0 -0
  35. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/_contextvars.py +0 -0
  36. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/__init__.py +0 -0
  37. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/_stdlib_env.py +0 -0
  38. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/flask.py +0 -0
  39. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/grpc.py +0 -0
  40. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/httpx.py +0 -0
  41. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/requests.py +0 -0
  42. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/integrations/sqlalchemy.py +0 -0
  43. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/metrics.py +0 -0
  44. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/otel.py +0 -0
  45. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/py.typed +0 -0
  46. {structguru-1.2.2 → structguru-1.3.0}/src/structguru/redaction.py +0 -0
@@ -34,9 +34,9 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
34
34
 
35
35
  [[package]]
36
36
  name = "fancy-regex"
37
- version = "0.19.0"
37
+ version = "0.19.2"
38
38
  source = "registry+https://github.com/rust-lang/crates.io-index"
39
- checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6"
39
+ checksum = "d301f5bf187b3c295fce6468d3875037a0bccc5f6b151c63cac2f85babf21912"
40
40
  dependencies = [
41
41
  "bit-set",
42
42
  "regex-automata",
@@ -227,7 +227,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
227
227
  dependencies = [
228
228
  "proc-macro2",
229
229
  "quote",
230
- "syn 3.0.4",
230
+ "syn 3.0.6",
231
231
  ]
232
232
 
233
233
  [[package]]
@@ -245,7 +245,7 @@ dependencies = [
245
245
 
246
246
  [[package]]
247
247
  name = "structguru-core"
248
- version = "1.2.2"
248
+ version = "1.3.0"
249
249
  dependencies = [
250
250
  "fancy-regex",
251
251
  "fastrand",
@@ -257,7 +257,7 @@ dependencies = [
257
257
 
258
258
  [[package]]
259
259
  name = "structguru-py"
260
- version = "1.2.2"
260
+ version = "1.3.0"
261
261
  dependencies = [
262
262
  "pyo3",
263
263
  "regex",
@@ -277,9 +277,9 @@ dependencies = [
277
277
 
278
278
  [[package]]
279
279
  name = "syn"
280
- version = "3.0.4"
280
+ version = "3.0.6"
281
281
  source = "registry+https://github.com/rust-lang/crates.io-index"
282
- checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
282
+ checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
283
283
  dependencies = [
284
284
  "proc-macro2",
285
285
  "quote",
@@ -324,9 +324,9 @@ dependencies = [
324
324
 
325
325
  [[package]]
326
326
  name = "unicode-ident"
327
- version = "1.0.24"
327
+ version = "1.0.26"
328
328
  source = "registry+https://github.com/rust-lang/crates.io-index"
329
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
329
+ checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
330
330
 
331
331
  [[package]]
332
332
  name = "zmij"
@@ -3,7 +3,7 @@ members = ["crates/structguru-core", "crates/structguru-py"]
3
3
  resolver = "2"
4
4
 
5
5
  [workspace.package]
6
- version = "1.2.2"
6
+ version = "1.3.0"
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.2.2
3
+ Version: 1.3.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -98,7 +98,7 @@ Available extras: `otel`, `celery`, `flask`, `django`, `sqlalchemy`, `grpc`,
98
98
  ## Quick start
99
99
 
100
100
  ```python
101
- from structguru import configure, logger
101
+ from structguru import Logger, configure, logger
102
102
 
103
103
  # Configure once at startup
104
104
  configure(service="myapp", level="DEBUG", format="json")
@@ -106,8 +106,66 @@ configure(service="myapp", level="DEBUG", format="json")
106
106
  # Use anywhere
107
107
  logger.info("Hello {name}", name="world")
108
108
  # → {"logger":"...","level":"INFO","severity":6,"timestamp":"...","service":"myapp","message":"Hello world"}
109
+
110
+ # Or choose an explicit module name
111
+ log = Logger(name=__name__)
109
112
  ```
110
113
 
114
+ ## Configuration
115
+
116
+ Call `configure()` once at startup. Each call replaces the previous configuration:
117
+ explicit keywords override environment values, which override built-in defaults.
118
+ Use `update()` to retain existing options while changing selected ones:
119
+
120
+ ```python
121
+ from structguru import Settings, configure, get_config, update
122
+
123
+ configure(service="checkout", sensitive_patterns=[r"token=\w+"])
124
+ update(otel=True, structured_exceptions=True) # retains service and redaction
125
+ current = get_config() # Settings, or None after shutdown()
126
+
127
+ # Applications can load a mapping themselves; no files are read automatically.
128
+ settings = Settings.from_mapping({"service": "checkout", "level": "DEBUG"})
129
+ configure(settings) # uses this object instead of environment values
130
+ ```
131
+
132
+ `Settings.from_env()` resolves environment values without applying them; pass a mapping
133
+ instead of using the process environment when testing. `Settings` validates Python
134
+ values and freezes collections. Native regex compilation and file access are checked
135
+ when configuring; failure leaves the previous runtime active. Explicit keywords,
136
+ including `None` and built-in default values, win over the selected base.
137
+
138
+ `update()` never rereads the environment and requires an active runtime. An empty update
139
+ does nothing. Level-only updates and `set_level()` preserve queues and rate-limit state;
140
+ other updates rebuild writers and reset filter state. Snapshots describe configured
141
+ options, not buffered records, counters, stdlib bridge state, or `logger.add()` sinks.
142
+ Those registered sinks survive reconfiguration. Streams and callbacks retain their identity.
143
+
144
+ Levels accept case-insensitive names (including `NOTSET`) or non-negative integer
145
+ thresholds. Unknown names, booleans and negative integers raise `ValueError`.
146
+
147
+ ### Environment configuration
148
+
149
+ | Variable | Default | Compatibility fallback |
150
+ |---|---|---|
151
+ | `STRUCTGURU_SERVICE` | `app` | — |
152
+ | `STRUCTGURU_LEVEL` | `INFO` | `LOG_LEVEL` |
153
+ | `STRUCTGURU_TARGET` | `stdout` | `STRUCTGURU_NATIVE_TARGET` |
154
+ | `STRUCTGURU_FORMAT` | `json` | — |
155
+ | `STRUCTGURU_SAMPLE_RATE` | `1.0` | `STRUCTGURU_NATIVE_SAMPLE_RATE` |
156
+ | `STRUCTGURU_RATE_LIMIT` | disabled; period defaults to 60 seconds | `STRUCTGURU_NATIVE_RATE_LIMIT` |
157
+ | `STRUCTGURU_AUTOCONFIGURE` | enabled | inverse of `STRUCTGURU_LEGACY` |
158
+
159
+ New names win over their fallbacks; old names remain supported without warnings.
160
+ Rate limits use `MAX` or `MAX/PERIOD`, with integer counts and seconds for the period.
161
+ Autoconfiguration accepts `1/0`, `true/false`, `yes/no`, or `on/off` and controls import
162
+ only. Set it to `0` before import to configure explicitly later. Invalid selected values
163
+ fail validation; an invalid import-time value must be corrected or autoconfiguration
164
+ disabled before the application can call `configure()`.
165
+
166
+ File output, redaction, exceptions and other settings currently use Python configuration.
167
+ The stdlib bridge retains its separate `STRUCTGURU_STDLIB_*` options and explicit installer.
168
+
111
169
  ## Usage
112
170
 
113
171
  ### Log levels
@@ -191,6 +249,16 @@ that path. Install the [stdlib bridge](#stdlib-bridge) to receive them rendered
191
249
  and redacted instead — while it is installed the raw delivery is suspended, so a
192
250
  sink never sees the same record twice.
193
251
 
252
+ Logs emitted inside a sink callback, whether it received a native record or a
253
+ raw stdlib one, reach the native writer but skip callable and `logger.add()`
254
+ sinks, preventing recursive delivery and worker deadlocks. Outside callbacks,
255
+ `logger.remove()` waits for producers that already selected the removed sink
256
+ and for raw stdlib deliveries in progress. Lifecycle calls inside a callback,
257
+ including a stdlib handler's `emit()` on the raw path, cannot wait for the
258
+ worker; previously selected deliveries finish as that worker drains.
259
+ For native file/stdout mirroring, `writer_metrics()["sink_errors"]` includes
260
+ failed destinations even when another destination successfully writes the record.
261
+
194
262
  Native delivery uses the bounded callable queue and is drained on
195
263
  reconfiguration, `shutdown()`, fork, and interpreter exit. Call
196
264
  `structguru.flush()` when you need to block until buffered records have actually
@@ -210,11 +278,11 @@ structguru.flush() # returns once the line has reached its sink
210
278
 
211
279
  ```python
212
280
  # JSON (production)
213
- configure(service="myapp", format="json")
281
+ update(service="myapp", format="json")
214
282
  # → {"logger":"...","level":"INFO","severity":6,"timestamp":"...","service":"myapp","message":"..."}
215
283
 
216
284
  # Console (development) — colored, human-readable
217
- configure(service="myapp", format="console")
285
+ update(service="myapp", format="console")
218
286
  # → 2026-01-15T12:00:00.123456Z [INFO ] Hello world
219
287
  ```
220
288
 
@@ -225,9 +293,9 @@ configure(service="myapp", format="console")
225
293
  Mask sensitive fields automatically:
226
294
 
227
295
  ```python
228
- from structguru import configure
296
+ from structguru import update
229
297
 
230
- configure(
298
+ update(
231
299
  sensitive_keys=["password", "token", "ssn"],
232
300
  sensitive_patterns=[r"\b\d{3}-\d{2}-\d{4}\b"],
233
301
  pattern_replacement="***",
@@ -235,7 +303,7 @@ configure(
235
303
  ```
236
304
 
237
305
  Patterns run on Rust's linear-time regex engine (no ReDoS), which rejects
238
- look-around and backreferences at `configure()` time. Most look-behinds rewrite
306
+ look-around and backreferences at `update()` time. Most look-behinds rewrite
239
307
  as capture groups — `(?<=password=)\S+` becomes `(password=)\S+` with
240
308
  `pattern_replacement="$1[REDACTED]"`, so the prefix is re-emitted and the
241
309
  secret is replaced (`password=hunter2` → `password=[REDACTED]`). Put the
@@ -248,7 +316,7 @@ backtrack limit, it is redacted entirely (fail-closed) rather than emitted
248
316
  unchecked.
249
317
 
250
318
  ```python
251
- configure(
319
+ update(
252
320
  sensitive_patterns=[r"(?<=password=)\S+"],
253
321
  allow_backtracking_patterns=True,
254
322
  )
@@ -259,9 +327,9 @@ configure(
259
327
  Suppress noisy logs:
260
328
 
261
329
  ```python
262
- from structguru import configure
330
+ from structguru import update
263
331
 
264
- configure(sample_rate=0.1, rate_limit_max=5, rate_limit_period=60)
332
+ update(sample_rate=0.1, rate_limit_max=5, rate_limit_period=60)
265
333
  ```
266
334
 
267
335
  ### Metric extraction
@@ -269,13 +337,13 @@ configure(sample_rate=0.1, rate_limit_max=5, rate_limit_period=60)
269
337
  Derive metrics from log events:
270
338
 
271
339
  ```python
272
- from structguru import MetricProcessor, configure
340
+ from structguru import MetricProcessor, update
273
341
 
274
342
  metrics = MetricProcessor()
275
343
  metrics.counter("user.login", lambda ed: login_counter.inc())
276
344
  metrics.histogram("db.query", "duration_ms", lambda v, ed: query_hist.observe(v))
277
345
 
278
- configure(metric_processor=metrics)
346
+ update(metric_processor=metrics)
279
347
  ```
280
348
 
281
349
  ### Exception formatting
@@ -283,38 +351,67 @@ configure(metric_processor=metrics)
283
351
  Render exceptions as JSON-serializable dictionaries:
284
352
 
285
353
  ```python
286
- from structguru import configure
354
+ from structguru import update
287
355
 
288
- configure(structured_exceptions=True, exception_max_frames=20)
356
+ update(structured_exceptions=True, exception_max_frames=20)
289
357
  ```
290
358
 
291
359
  `exception_max_frames=0` omits traceback frames entirely. Negative frame and
292
360
  local-representation limits are rejected during configuration.
293
361
 
362
+ Formatted tracebacks (the default) carry CPython's per-frame position markers,
363
+ the `~~~^^^` lines, and on Python 3.11+ computing them is most of the cost of
364
+ `logger.exception()`. `exception_carets=False` omits them, which formats a
365
+ traceback about five times faster and matches what CPython prints under
366
+ `PYTHONNODEBUGRANGES=1`:
367
+
368
+ ```python
369
+ update(exception_carets=False)
370
+ ```
371
+
294
372
  ### OpenTelemetry correlation
295
373
 
296
374
  Inject trace context into every log event:
297
375
 
298
376
  ```python
299
- from structguru import configure
377
+ from structguru import update
300
378
 
301
- configure(otel=True) # no-op injection when opentelemetry-api is absent
379
+ update(otel=True) # no-op injection when opentelemetry-api is absent
302
380
  ```
303
381
 
304
382
  ### Non-blocking logging
305
383
 
306
384
  Since v1.0, log I/O is offloaded to a background thread by default. The native
307
- Rust writer uses a bounded 8192-record queue with lossless backpressure. Set
385
+ Rust writer uses a bounded 8192-record queue and waits for space while it is open. Set
308
386
  `overflow="drop"` to favor caller latency, or explicitly pass `maxsize=0` only
309
387
  when an unbounded queue is acceptable.
310
388
 
389
+
311
390
  ## Native runtime
312
391
 
313
392
  structguru ships a required Rust extension that renders and enqueues logging
314
- natively, off-thread. It is auto-enabled at import time. The runtime does not depend on `orjson`;
393
+ natively, off-thread. It is auto-enabled at import time, and importing the package
394
+ without the extension raises `RuntimeError`. The runtime does not depend on `orjson`;
315
395
  exotic values (`datetime`, `UUID`, `Enum`, dataclasses) are converted natively
316
396
  in Rust.
317
397
 
398
+ A field value the renderer cannot represent never fails the record. It is
399
+ replaced by a marker so the message, level, remaining fields, and exception
400
+ traceback still ship:
401
+
402
+ - `<unsupported: WSGIRequest>` for any other object (`Decimal`, `bytes`, `set`,
403
+ `Path`, request objects, ...). The marker names the type only: structguru
404
+ never calls `str()`/`repr()` on an arbitrary object or reads its attributes,
405
+ so nothing it holds can leak into a log line.
406
+ - `<cycle: dict>` for a container that refers back to itself.
407
+ - `<max depth exceeded>` beyond 64 levels of nesting.
408
+
409
+ Integers outside the 64-bit range are written as JSON numbers, non-string
410
+ mapping keys are rendered as strings (`{200: 3}` becomes `{"200": 3}`), and
411
+ unpaired surrogates in text become U+FFFD. Markers are redacted like any other
412
+ string. The policy applies to native `logger` fields and to `extra=` fields
413
+ bridged from the standard library alike.
414
+
318
415
  ```python
319
416
  import structguru
320
417
 
@@ -334,10 +431,10 @@ structguru.logger.bind(request_id="abc").info("order {id} accepted", id=987)
334
431
  # → JSON line written to /var/log/app.log by a background writer thread
335
432
  ```
336
433
 
337
- The default import-time configuration also honors environment variables:
434
+ Import-time configuration and `configure()` without a Settings object honor environment variables:
338
435
 
339
436
  ```bash
340
- LOG_LEVEL=INFO STRUCTGURU_SERVICE=myapp python -m myapp
437
+ STRUCTGURU_LEVEL=INFO STRUCTGURU_SERVICE=myapp python -m myapp
341
438
  ```
342
439
 
343
440
  Invalid native environment values fail import with an actionable exception. This
@@ -347,20 +444,24 @@ Public API:
347
444
 
348
445
  | Symbol | Purpose |
349
446
  |--------|---------|
350
- | `configure(...)` | Configure rendering, filtering, redaction, and output sinks. See the API reference for the complete signature. |
447
+ | `configure(...)` | Replace rendering, filtering, redaction, and output settings. |
448
+ | `Settings` | Validate reusable options; construct from Python values, a mapping, or the environment. |
449
+ | `get_config()` | Return configured options, or `None` when shut down. |
450
+ | `update(...)` | Change selected active options without rereading the environment. |
351
451
  | `shutdown()` | Stop the writer; logging is disabled until `configure()` is called. |
352
452
  | `set_level(level)` | Adjust the level threshold at runtime. |
353
- | `writer_metrics()` | Writer counters (enqueued/written/dropped/depth/...) plus filter counters (`sampled`/`rate_limited`) when active. |
354
- | `is_available()` | Whether the compiled extension is importable. |
453
+ | `writer_metrics()` | Current writer counters (enqueued/written/dropped/depth/...) plus filter counters when active; `None` after shutdown. |
454
+ | `lifecycle_metrics()` | Cumulative native deliveries rejected by closed writers; available after shutdown and across reconfiguration. |
455
+ | `is_available()` | Compatibility helper; always `True` once the package has imported, because a missing extension fails import. |
355
456
 
356
457
  Behavior notes:
357
458
 
358
- - **Overflow**: the default `maxsize=8192` uses `overflow="block"` for bounded, lossless backpressure. Use `overflow="drop"` for drop-newest behavior with metrics and rate-limited warnings. `maxsize=0` explicitly opts into an unbounded queue.
459
+ - **Overflow**: the default `maxsize=8192` uses `overflow="block"` to wait for queue space. Use `overflow="drop"` for drop-newest behavior with metrics and rate-limited warnings. `maxsize=0` explicitly opts into an unbounded queue.
359
460
  - **Redaction, level filtering, exceptions, and OpenTelemetry** injection are supported natively; redaction covers the message and all structured string values before rendering or Sentry export. `sensitive_keys` overrides the default redaction keys. Rust's linear-time regex engine rejects backreferences and look-around with `ValueError` at configuration time.
360
461
  - **Sampling & rate limiting** (`sample_rate`, `rate_limit_max`, `rate_limit_period`) are applied as native pre-render filters — dropped records cost zero rendering. `sampled` and `rate_limited` counters are distinct from the transport `dropped` counter. `sample_max_level` restricts sampling to records at or below that level; more severe records always pass.
361
462
  - **Metric hooks** (`metric_processor=...`) invoke a structlog-style processor (e.g. `MetricProcessor`) for every *kept* record on the caller's thread, with `(None, method, {"event": message, **fields})`. Dropped records (level/sampling/rate-limit) never reach it; hook errors are swallowed.
362
463
  - **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.
363
- - **Structured exceptions** (`structured_exceptions=True`) render `type`, `message`, `module`, and frames as a dictionary, with optional redacted/truncated locals controlled by the `exception_*` options.
464
+ - **Structured exceptions** (`structured_exceptions=True`) render `type`, `message`, `module`, and frames as a dictionary, with optional redacted/truncated locals controlled by the `exception_*` options. Exception groups include nested members under `exceptions`, with the same frame limits and redaction. Traversal stops at ten nesting levels or 100 exception nodes; `exceptions_truncated` counts omitted direct children. Failed message conversions produce a marker instead of interrupting logging.
364
465
  - **`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).
365
466
  - **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`.
366
467
  - **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).
@@ -368,6 +469,14 @@ Behavior notes:
368
469
  - **Sentry integration** (`sentry_processor=SentryProcessor(...)`): receives the already-redacted event and raw `exc_info` only for exception capture.
369
470
  - **Scope**: the native renderer covers JSON and console rendering, file/stdout/callable sinks, redaction, sampling/rate limiting, metrics, exceptions, and stack information. `logger.add()` sinks receive native and stdlib records.
370
471
 
472
+ Shutdown and reconfiguration drain records already accepted by the native queue.
473
+ Calls still formatting or waiting for space may be rejected when their writer closes,
474
+ even in block mode. `lifecycle_metrics()["rejected"]` counts these native deliveries
475
+ without queue-full warnings and survives shutdown and configuration changes; forked
476
+ children start at zero. Another destination, such as a synchronous stream, may already
477
+ have received the event. Calls begun while logging is disabled are no-ops and are not
478
+ counted. See the [lifecycle contract](docs/api/config.md#runtime-control) for details.
479
+
371
480
  ## Framework integrations
372
481
 
373
482
  ### ASGI (FastAPI / Starlette)
@@ -379,6 +488,13 @@ app = FastAPI()
379
488
  app.add_middleware(StructguruMiddleware, request_id_header="X-Request-ID")
380
489
  ```
381
490
 
491
+ ### Granian
492
+
493
+ Route Granian's server logs through `StructguruHandler` using `--log-config`,
494
+ and use `StructguruMiddleware` for ASGI request IDs and structured summaries.
495
+ The [Granian integration guide](docs/integrations.md#granian) includes a complete
496
+ logging dictionary, application and launcher examples, and access-log options.
497
+
382
498
  ### Celery
383
499
 
384
500
  ```python
@@ -431,11 +547,11 @@ server = grpc.server(
431
547
  ```python
432
548
  import logging
433
549
 
434
- from structguru import configure
550
+ from structguru import update
435
551
  from structguru.integrations.sentry import SentryProcessor
436
552
 
437
553
  sentry = SentryProcessor(event_level=logging.ERROR, tag_keys=frozenset({"service"}))
438
- configure(sentry_processor=sentry)
554
+ update(sentry_processor=sentry)
439
555
  ```
440
556
 
441
557
  ### Stdlib bridge