structguru 1.3.0__tar.gz → 1.4.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.
- {structguru-1.3.0 → structguru-1.4.0}/Cargo.lock +2 -2
- {structguru-1.3.0 → structguru-1.4.0}/Cargo.toml +1 -1
- {structguru-1.3.0 → structguru-1.4.0}/PKG-INFO +45 -16
- {structguru-1.3.0 → structguru-1.4.0}/README.md +44 -15
- structguru-1.4.0/crates/structguru-core/src/clock.rs +113 -0
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-core/src/filter.rs +62 -33
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-core/src/lib.rs +7 -5
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-core/src/render.rs +513 -125
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-core/src/value.rs +90 -1
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-core/src/worker.rs +148 -96
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-py/src/dispatch.rs +451 -107
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-py/src/lib.rs +248 -69
- {structguru-1.3.0 → structguru-1.4.0}/pyproject.toml +1 -1
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/__init__.py +1 -1
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/_contextvars.py +19 -3
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/_native_dispatch.py +17 -3
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/_native_env.py +3 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/_runtime.py +197 -64
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/core.py +393 -97
- structguru-1.4.0/src/structguru/exceptions.py +511 -0
- structguru-1.4.0/src/structguru/integrations/_util.py +345 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/asgi.py +67 -27
- structguru-1.4.0/src/structguru/integrations/celery.py +244 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/django.py +77 -11
- structguru-1.4.0/src/structguru/integrations/flask.py +220 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/grpc.py +7 -4
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/requests.py +30 -3
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/sentry.py +3 -1
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/sqlalchemy.py +4 -2
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/stdlib.py +27 -3
- structguru-1.3.0/crates/structguru-core/src/clock.rs +0 -52
- structguru-1.3.0/src/structguru/exceptions.py +0 -167
- structguru-1.3.0/src/structguru/integrations/_util.py +0 -57
- structguru-1.3.0/src/structguru/integrations/celery.py +0 -120
- structguru-1.3.0/src/structguru/integrations/flask.py +0 -82
- {structguru-1.3.0 → structguru-1.4.0}/LICENSE +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-core/Cargo.toml +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/crates/structguru-py/Cargo.toml +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/_levels.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/config.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/__init__.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/_stdlib_env.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/integrations/httpx.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/metrics.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/otel.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/py.typed +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/redaction.py +0 -0
- {structguru-1.3.0 → structguru-1.4.0}/src/structguru/settings.py +0 -0
|
@@ -245,7 +245,7 @@ dependencies = [
|
|
|
245
245
|
|
|
246
246
|
[[package]]
|
|
247
247
|
name = "structguru-core"
|
|
248
|
-
version = "1.
|
|
248
|
+
version = "1.4.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.
|
|
260
|
+
version = "1.4.0"
|
|
261
261
|
dependencies = [
|
|
262
262
|
"pyo3",
|
|
263
263
|
"regex",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structguru
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -157,6 +157,7 @@ thresholds. Unknown names, booleans and negative integers raise `ValueError`.
|
|
|
157
157
|
| `STRUCTGURU_AUTOCONFIGURE` | enabled | inverse of `STRUCTGURU_LEGACY` |
|
|
158
158
|
|
|
159
159
|
New names win over their fallbacks; old names remain supported without warnings.
|
|
160
|
+
Level variables accept a name or a non-negative integer threshold (`LOG_LEVEL=10`).
|
|
160
161
|
Rate limits use `MAX` or `MAX/PERIOD`, with integer counts and seconds for the period.
|
|
161
162
|
Autoconfiguration accepts `1/0`, `true/false`, `yes/no`, or `on/off` and controls import
|
|
162
163
|
only. Set it to `0` before import to configure explicitly later. Invalid selected values
|
|
@@ -195,6 +196,17 @@ logger.info("User {user_id} logged in", user_id=42, ip="10.0.0.1")
|
|
|
195
196
|
# user_id is consumed by formatting and not duplicated
|
|
196
197
|
```
|
|
197
198
|
|
|
199
|
+
The message is formatted whenever positional or keyword arguments are passed, as
|
|
200
|
+
`str.format` would (`"a }} b"` becomes `"a } b"`); `exc_info` and `stack_info` alone
|
|
201
|
+
do not count, so `logger.exception("{{x}}")` logs `{{x}}` like `logger.error`. Fields
|
|
202
|
+
named in a nested format spec (`"{x:{width}}"`) are consumed too. A placeholder that
|
|
203
|
+
names a sensitive key — as the root (`{token}`) or an attribute or index
|
|
204
|
+
(`{creds[password]}`) — is interpolated as `[REDACTED]`. A value interpolated whole
|
|
205
|
+
(`{creds}`) is covered by `sensitive_patterns` only. The message is positional-only,
|
|
206
|
+
so `message=` and `self=` are ordinary fields. A template that fails to format, or a
|
|
207
|
+
message whose `str()` raises, is logged raw or as a placeholder with a one-shot
|
|
208
|
+
`UserWarning`; logging calls do not raise for their arguments.
|
|
209
|
+
|
|
198
210
|
### Bound context
|
|
199
211
|
|
|
200
212
|
```python
|
|
@@ -240,6 +252,14 @@ logger.remove(handler_id)
|
|
|
240
252
|
logger.remove()
|
|
241
253
|
```
|
|
242
254
|
|
|
255
|
+
Handler ids are process-wide: any `Logger` can remove a sink that another one added.
|
|
256
|
+
Removing an id that names no active sink raises `ValueError` (a non-integer raises
|
|
257
|
+
`TypeError`), as in loguru; `remove()` without an id removes the sinks added through
|
|
258
|
+
that logger and its `bind()`/`opt()` children. `level` accepts a name or a non-negative
|
|
259
|
+
integer and raises `ValueError` for anything else, like `configure(level=...)`.
|
|
260
|
+
A `logging.Handler` passed to `add()` keeps its own level and formatter: its formatter
|
|
261
|
+
wraps the rendered line, only `level` gates what it receives, and `remove()` closes it.
|
|
262
|
+
|
|
243
263
|
On Unix, new files created by either `logger.add(path)` or the native rotating
|
|
244
264
|
file sink are owner-only (`0600`). Existing files retain their permissions.
|
|
245
265
|
|
|
@@ -302,6 +322,11 @@ update(
|
|
|
302
322
|
)
|
|
303
323
|
```
|
|
304
324
|
|
|
325
|
+
Patterns apply to the message, field names, and string values at any depth,
|
|
326
|
+
and to integers by their decimal digits (a matching integer is emitted as the
|
|
327
|
+
redacted string). If redaction makes two keys of one map equal, the record
|
|
328
|
+
keeps one key with the last value.
|
|
329
|
+
|
|
305
330
|
Patterns run on Rust's linear-time regex engine (no ReDoS), which rejects
|
|
306
331
|
look-around and backreferences at `update()` time. Most look-behinds rewrite
|
|
307
332
|
as capture groups — `(?<=password=)\S+` becomes `(password=)\S+` with
|
|
@@ -406,10 +431,13 @@ traceback still ship:
|
|
|
406
431
|
- `<cycle: dict>` for a container that refers back to itself.
|
|
407
432
|
- `<max depth exceeded>` beyond 64 levels of nesting.
|
|
408
433
|
|
|
409
|
-
Integers outside the 64-bit range are written as JSON numbers,
|
|
434
|
+
Integers outside the 64-bit range are written as JSON numbers, `int` and
|
|
435
|
+
`float` subclasses (such as `numpy.float64`) are written by value, non-string
|
|
410
436
|
mapping keys are rendered as strings (`{200: 3}` becomes `{"200": 3}`), and
|
|
411
|
-
unpaired
|
|
412
|
-
|
|
437
|
+
each unpaired surrogate in text becomes one U+FFFD while a split surrogate pair
|
|
438
|
+
decodes to its character. Keys that render to the same text never produce
|
|
439
|
+
duplicate JSON keys: the later value wins at the earlier key's position.
|
|
440
|
+
Markers are redacted like any other string. The policy applies to native `logger` fields and to `extra=` fields
|
|
413
441
|
bridged from the standard library alike.
|
|
414
442
|
|
|
415
443
|
```python
|
|
@@ -457,15 +485,15 @@ Public API:
|
|
|
457
485
|
Behavior notes:
|
|
458
486
|
|
|
459
487
|
- **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.
|
|
460
|
-
- **Redaction, level filtering, exceptions, and OpenTelemetry** injection are supported natively; redaction covers the message
|
|
488
|
+
- **Redaction, level filtering, exceptions, and OpenTelemetry** injection are supported natively; redaction covers the message, field names, string values, and integers at any depth before rendering or Sentry export, and message placeholders that name a sensitive key are interpolated as `[REDACTED]` (see the redaction API reference for the exact coverage). `sensitive_keys` overrides the default redaction keys. Rust's linear-time regex engine rejects backreferences and look-around with `ValueError` at configuration time.
|
|
461
489
|
- **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.
|
|
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})
|
|
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.
|
|
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.
|
|
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
|
|
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`.
|
|
490
|
+
- **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})`; the message stays in `"event"` even when a field has that name. Dropped records (level/sampling/rate-limit) never reach it; hook errors are swallowed.
|
|
491
|
+
- **Fork/shutdown safe** — the writer is flushed on exit and respawned in forked children (gunicorn/celery prefork). The exit drain is registered when structguru is imported, so `atexit` handlers registered afterwards can still log. Rotating-file writers sharing a path coordinate through an owner-only `.lock` sidecar; distributed hosts should still prefer stdout and an external collector.
|
|
492
|
+
- **Structured exceptions** (`structured_exceptions=True`) render `type`, `message`, `module`, and frames as a dictionary, with optional redacted/truncated locals controlled by the `exception_*` options. A local named like a sensitive key is redacted, and so is any value under a sensitive key inside a mapping, list, tuple, named tuple, set, dataclass with its generated `__repr__`, or `SimpleNamespace` (walked with bounded depth and size, cycle-safe); other objects' own `repr()` text is covered by `sensitive_patterns`, which see each local's full rendering before it is truncated to `exception_max_local_repr`. 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.
|
|
493
|
+
- **`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 (frames of the `structguru` package are skipped, the way structlog skips its own).
|
|
494
|
+
- **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`. String values are double-quoted with `"` and `\` escaped, keys containing spaces, `=`, quotes, or backslashes are quoted, and control characters, U+2028/U+2029, and bidirectional controls are escaped everywhere, so a field key or value cannot forge another field or line. Floats use the JSON renderer's text (`1.0`, `1e+300`); the stack and a formatted `exception` follow as indented blocks.
|
|
467
495
|
- **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).
|
|
468
|
-
- **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.
|
|
496
|
+
- **Callable sinks** (`callable_sinks=[fn, ...]`): use a bounded queue (`callable_queue_maxsize=1024`). `overflow="block"` provides lossless backpressure; `overflow="drop"` reports `callable_dropped` metrics. One background thread at a time calls the sinks, in log order, also across reconfiguration; `logger.add()` never waits for the queue. Sink exceptions are counted in `callable_errors` with a rate-limited warning. Flush and lifecycle operations drain queued calls; a sink that makes no progress for 10 seconds is abandoned with a warning instead of blocking them.
|
|
469
497
|
- **Sentry integration** (`sentry_processor=SentryProcessor(...)`): receives the already-redacted event and raw `exc_info` only for exception capture.
|
|
470
498
|
- **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.
|
|
471
499
|
|
|
@@ -473,9 +501,9 @@ Shutdown and reconfiguration drain records already accepted by the native queue.
|
|
|
473
501
|
Calls still formatting or waiting for space may be rejected when their writer closes,
|
|
474
502
|
even in block mode. `lifecycle_metrics()["rejected"]` counts these native deliveries
|
|
475
503
|
without queue-full warnings and survives shutdown and configuration changes; forked
|
|
476
|
-
children start at zero.
|
|
477
|
-
|
|
478
|
-
counted. See the [lifecycle contract](docs/api/config.md#runtime-control) for details.
|
|
504
|
+
children start at zero. A rejected record reaches no other destination either: the
|
|
505
|
+
synchronous stream, callable sinks, and Sentry follow the native writer's outcome.
|
|
506
|
+
Calls begun while logging is disabled are no-ops and are not counted. See the [lifecycle contract](docs/api/config.md#runtime-control) for details.
|
|
479
507
|
|
|
480
508
|
## Framework integrations
|
|
481
509
|
|
|
@@ -501,7 +529,7 @@ logging dictionary, application and launcher examples, and access-log options.
|
|
|
501
529
|
from structguru.integrations.celery import setup_celery_logging
|
|
502
530
|
|
|
503
531
|
setup_celery_logging(propagate_context=True, context_keys=["request_id"])
|
|
504
|
-
# Binds task_id/task_name
|
|
532
|
+
# Binds task_id/task_name; propagates selected keys (redacted, JSON-safe) via headers
|
|
505
533
|
```
|
|
506
534
|
|
|
507
535
|
### Flask
|
|
@@ -622,7 +650,8 @@ bridge = install_stdlib_bridge_from_env()
|
|
|
622
650
|
## Requirements
|
|
623
651
|
|
|
624
652
|
- Python 3.11+
|
|
625
|
-
- The compiled Rust extension (shipped as abi3 wheels for Linux/macOS/Windows
|
|
653
|
+
- The compiled Rust extension (shipped as abi3 wheels for Linux/macOS/Windows, plus
|
|
654
|
+
free-threaded CPython 3.14t wheels for Linux x86_64, macOS arm64, and Windows x64)
|
|
626
655
|
|
|
627
656
|
## Documentation & Examples
|
|
628
657
|
|
|
@@ -113,6 +113,7 @@ thresholds. Unknown names, booleans and negative integers raise `ValueError`.
|
|
|
113
113
|
| `STRUCTGURU_AUTOCONFIGURE` | enabled | inverse of `STRUCTGURU_LEGACY` |
|
|
114
114
|
|
|
115
115
|
New names win over their fallbacks; old names remain supported without warnings.
|
|
116
|
+
Level variables accept a name or a non-negative integer threshold (`LOG_LEVEL=10`).
|
|
116
117
|
Rate limits use `MAX` or `MAX/PERIOD`, with integer counts and seconds for the period.
|
|
117
118
|
Autoconfiguration accepts `1/0`, `true/false`, `yes/no`, or `on/off` and controls import
|
|
118
119
|
only. Set it to `0` before import to configure explicitly later. Invalid selected values
|
|
@@ -151,6 +152,17 @@ logger.info("User {user_id} logged in", user_id=42, ip="10.0.0.1")
|
|
|
151
152
|
# user_id is consumed by formatting and not duplicated
|
|
152
153
|
```
|
|
153
154
|
|
|
155
|
+
The message is formatted whenever positional or keyword arguments are passed, as
|
|
156
|
+
`str.format` would (`"a }} b"` becomes `"a } b"`); `exc_info` and `stack_info` alone
|
|
157
|
+
do not count, so `logger.exception("{{x}}")` logs `{{x}}` like `logger.error`. Fields
|
|
158
|
+
named in a nested format spec (`"{x:{width}}"`) are consumed too. A placeholder that
|
|
159
|
+
names a sensitive key — as the root (`{token}`) or an attribute or index
|
|
160
|
+
(`{creds[password]}`) — is interpolated as `[REDACTED]`. A value interpolated whole
|
|
161
|
+
(`{creds}`) is covered by `sensitive_patterns` only. The message is positional-only,
|
|
162
|
+
so `message=` and `self=` are ordinary fields. A template that fails to format, or a
|
|
163
|
+
message whose `str()` raises, is logged raw or as a placeholder with a one-shot
|
|
164
|
+
`UserWarning`; logging calls do not raise for their arguments.
|
|
165
|
+
|
|
154
166
|
### Bound context
|
|
155
167
|
|
|
156
168
|
```python
|
|
@@ -196,6 +208,14 @@ logger.remove(handler_id)
|
|
|
196
208
|
logger.remove()
|
|
197
209
|
```
|
|
198
210
|
|
|
211
|
+
Handler ids are process-wide: any `Logger` can remove a sink that another one added.
|
|
212
|
+
Removing an id that names no active sink raises `ValueError` (a non-integer raises
|
|
213
|
+
`TypeError`), as in loguru; `remove()` without an id removes the sinks added through
|
|
214
|
+
that logger and its `bind()`/`opt()` children. `level` accepts a name or a non-negative
|
|
215
|
+
integer and raises `ValueError` for anything else, like `configure(level=...)`.
|
|
216
|
+
A `logging.Handler` passed to `add()` keeps its own level and formatter: its formatter
|
|
217
|
+
wraps the rendered line, only `level` gates what it receives, and `remove()` closes it.
|
|
218
|
+
|
|
199
219
|
On Unix, new files created by either `logger.add(path)` or the native rotating
|
|
200
220
|
file sink are owner-only (`0600`). Existing files retain their permissions.
|
|
201
221
|
|
|
@@ -258,6 +278,11 @@ update(
|
|
|
258
278
|
)
|
|
259
279
|
```
|
|
260
280
|
|
|
281
|
+
Patterns apply to the message, field names, and string values at any depth,
|
|
282
|
+
and to integers by their decimal digits (a matching integer is emitted as the
|
|
283
|
+
redacted string). If redaction makes two keys of one map equal, the record
|
|
284
|
+
keeps one key with the last value.
|
|
285
|
+
|
|
261
286
|
Patterns run on Rust's linear-time regex engine (no ReDoS), which rejects
|
|
262
287
|
look-around and backreferences at `update()` time. Most look-behinds rewrite
|
|
263
288
|
as capture groups — `(?<=password=)\S+` becomes `(password=)\S+` with
|
|
@@ -362,10 +387,13 @@ traceback still ship:
|
|
|
362
387
|
- `<cycle: dict>` for a container that refers back to itself.
|
|
363
388
|
- `<max depth exceeded>` beyond 64 levels of nesting.
|
|
364
389
|
|
|
365
|
-
Integers outside the 64-bit range are written as JSON numbers,
|
|
390
|
+
Integers outside the 64-bit range are written as JSON numbers, `int` and
|
|
391
|
+
`float` subclasses (such as `numpy.float64`) are written by value, non-string
|
|
366
392
|
mapping keys are rendered as strings (`{200: 3}` becomes `{"200": 3}`), and
|
|
367
|
-
unpaired
|
|
368
|
-
|
|
393
|
+
each unpaired surrogate in text becomes one U+FFFD while a split surrogate pair
|
|
394
|
+
decodes to its character. Keys that render to the same text never produce
|
|
395
|
+
duplicate JSON keys: the later value wins at the earlier key's position.
|
|
396
|
+
Markers are redacted like any other string. The policy applies to native `logger` fields and to `extra=` fields
|
|
369
397
|
bridged from the standard library alike.
|
|
370
398
|
|
|
371
399
|
```python
|
|
@@ -413,15 +441,15 @@ Public API:
|
|
|
413
441
|
Behavior notes:
|
|
414
442
|
|
|
415
443
|
- **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.
|
|
416
|
-
- **Redaction, level filtering, exceptions, and OpenTelemetry** injection are supported natively; redaction covers the message
|
|
444
|
+
- **Redaction, level filtering, exceptions, and OpenTelemetry** injection are supported natively; redaction covers the message, field names, string values, and integers at any depth before rendering or Sentry export, and message placeholders that name a sensitive key are interpolated as `[REDACTED]` (see the redaction API reference for the exact coverage). `sensitive_keys` overrides the default redaction keys. Rust's linear-time regex engine rejects backreferences and look-around with `ValueError` at configuration time.
|
|
417
445
|
- **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.
|
|
418
|
-
- **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})
|
|
419
|
-
- **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.
|
|
420
|
-
- **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.
|
|
421
|
-
- **`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
|
|
422
|
-
- **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`.
|
|
446
|
+
- **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})`; the message stays in `"event"` even when a field has that name. Dropped records (level/sampling/rate-limit) never reach it; hook errors are swallowed.
|
|
447
|
+
- **Fork/shutdown safe** — the writer is flushed on exit and respawned in forked children (gunicorn/celery prefork). The exit drain is registered when structguru is imported, so `atexit` handlers registered afterwards can still log. Rotating-file writers sharing a path coordinate through an owner-only `.lock` sidecar; distributed hosts should still prefer stdout and an external collector.
|
|
448
|
+
- **Structured exceptions** (`structured_exceptions=True`) render `type`, `message`, `module`, and frames as a dictionary, with optional redacted/truncated locals controlled by the `exception_*` options. A local named like a sensitive key is redacted, and so is any value under a sensitive key inside a mapping, list, tuple, named tuple, set, dataclass with its generated `__repr__`, or `SimpleNamespace` (walked with bounded depth and size, cycle-safe); other objects' own `repr()` text is covered by `sensitive_patterns`, which see each local's full rendering before it is truncated to `exception_max_local_repr`. 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.
|
|
449
|
+
- **`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 (frames of the `structguru` package are skipped, the way structlog skips its own).
|
|
450
|
+
- **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`. String values are double-quoted with `"` and `\` escaped, keys containing spaces, `=`, quotes, or backslashes are quoted, and control characters, U+2028/U+2029, and bidirectional controls are escaped everywhere, so a field key or value cannot forge another field or line. Floats use the JSON renderer's text (`1.0`, `1e+300`); the stack and a formatted `exception` follow as indented blocks.
|
|
423
451
|
- **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).
|
|
424
|
-
- **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.
|
|
452
|
+
- **Callable sinks** (`callable_sinks=[fn, ...]`): use a bounded queue (`callable_queue_maxsize=1024`). `overflow="block"` provides lossless backpressure; `overflow="drop"` reports `callable_dropped` metrics. One background thread at a time calls the sinks, in log order, also across reconfiguration; `logger.add()` never waits for the queue. Sink exceptions are counted in `callable_errors` with a rate-limited warning. Flush and lifecycle operations drain queued calls; a sink that makes no progress for 10 seconds is abandoned with a warning instead of blocking them.
|
|
425
453
|
- **Sentry integration** (`sentry_processor=SentryProcessor(...)`): receives the already-redacted event and raw `exc_info` only for exception capture.
|
|
426
454
|
- **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.
|
|
427
455
|
|
|
@@ -429,9 +457,9 @@ Shutdown and reconfiguration drain records already accepted by the native queue.
|
|
|
429
457
|
Calls still formatting or waiting for space may be rejected when their writer closes,
|
|
430
458
|
even in block mode. `lifecycle_metrics()["rejected"]` counts these native deliveries
|
|
431
459
|
without queue-full warnings and survives shutdown and configuration changes; forked
|
|
432
|
-
children start at zero.
|
|
433
|
-
|
|
434
|
-
counted. See the [lifecycle contract](docs/api/config.md#runtime-control) for details.
|
|
460
|
+
children start at zero. A rejected record reaches no other destination either: the
|
|
461
|
+
synchronous stream, callable sinks, and Sentry follow the native writer's outcome.
|
|
462
|
+
Calls begun while logging is disabled are no-ops and are not counted. See the [lifecycle contract](docs/api/config.md#runtime-control) for details.
|
|
435
463
|
|
|
436
464
|
## Framework integrations
|
|
437
465
|
|
|
@@ -457,7 +485,7 @@ logging dictionary, application and launcher examples, and access-log options.
|
|
|
457
485
|
from structguru.integrations.celery import setup_celery_logging
|
|
458
486
|
|
|
459
487
|
setup_celery_logging(propagate_context=True, context_keys=["request_id"])
|
|
460
|
-
# Binds task_id/task_name
|
|
488
|
+
# Binds task_id/task_name; propagates selected keys (redacted, JSON-safe) via headers
|
|
461
489
|
```
|
|
462
490
|
|
|
463
491
|
### Flask
|
|
@@ -578,7 +606,8 @@ bridge = install_stdlib_bridge_from_env()
|
|
|
578
606
|
## Requirements
|
|
579
607
|
|
|
580
608
|
- Python 3.11+
|
|
581
|
-
- The compiled Rust extension (shipped as abi3 wheels for Linux/macOS/Windows
|
|
609
|
+
- The compiled Rust extension (shipped as abi3 wheels for Linux/macOS/Windows, plus
|
|
610
|
+
free-threaded CPython 3.14t wheels for Linux x86_64, macOS arm64, and Windows x64)
|
|
582
611
|
|
|
583
612
|
## Documentation & Examples
|
|
584
613
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//! Timestamp generation for the native render path.
|
|
2
|
+
//!
|
|
3
|
+
//! Produces an ISO-8601 UTC timestamp matching structlog's
|
|
4
|
+
//! `TimeStamper(fmt="iso", utc=True)` output, e.g. `2026-07-08T16:19:24.616660Z`.
|
|
5
|
+
//!
|
|
6
|
+
//! The whole-second prefix (`YYYY-MM-DDTHH:MM:SS`) is cached per thread and only
|
|
7
|
+
//! re-rendered when the second rolls over; the microsecond suffix is appended
|
|
8
|
+
//! cheaply on every call. This keeps the calendar breakdown off the hot path
|
|
9
|
+
//! (architecture §5/§14).
|
|
10
|
+
|
|
11
|
+
use std::cell::RefCell;
|
|
12
|
+
|
|
13
|
+
use time::OffsetDateTime;
|
|
14
|
+
use time::macros::format_description;
|
|
15
|
+
|
|
16
|
+
thread_local! {
|
|
17
|
+
// (unix_second, "YYYY-MM-DDTHH:MM:SS")
|
|
18
|
+
static PREFIX_CACHE: RefCell<(i64, String)> = const { RefCell::new((i64::MIN, String::new())) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Current time as an ISO-8601 UTC string with microsecond precision + `Z`.
|
|
22
|
+
pub fn now_iso8601() -> String {
|
|
23
|
+
let now = OffsetDateTime::now_utc();
|
|
24
|
+
let secs = now.unix_timestamp();
|
|
25
|
+
let micros = now.microsecond(); // 0..1_000_000
|
|
26
|
+
|
|
27
|
+
PREFIX_CACHE.with(|cell| {
|
|
28
|
+
let mut cache = cell.borrow_mut();
|
|
29
|
+
if cache.0 != secs {
|
|
30
|
+
let fmt = format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
|
|
31
|
+
cache.1 = now.format(&fmt).unwrap_or_default();
|
|
32
|
+
cache.0 = secs;
|
|
33
|
+
}
|
|
34
|
+
format!("{}.{:06}Z", cache.1, micros)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Unix seconds bounding years 1 through 9999, the range of Python's `datetime`.
|
|
39
|
+
const UNIX_SECONDS_RANGE: std::ops::Range<f64> = -62_135_596_800.0..253_402_300_800.0;
|
|
40
|
+
|
|
41
|
+
/// `seconds` since the Unix epoch, as `time.time()` and `LogRecord.created`
|
|
42
|
+
/// give them, in the [`now_iso8601`] format; `None` when the value is not
|
|
43
|
+
/// finite or falls outside years 1 through 9999.
|
|
44
|
+
///
|
|
45
|
+
/// The fraction rounds to the nearest microsecond, ties to even, the way
|
|
46
|
+
/// `datetime.fromtimestamp` rounds it.
|
|
47
|
+
pub fn unix_to_iso8601(seconds: f64) -> Option<String> {
|
|
48
|
+
if !UNIX_SECONDS_RANGE.contains(&seconds) {
|
|
49
|
+
return None;
|
|
50
|
+
}
|
|
51
|
+
let whole = seconds.floor();
|
|
52
|
+
// `seconds - whole` is exact, so this rounds only once, like CPython.
|
|
53
|
+
let micros = ((seconds - whole) * 1_000_000.0).round_ties_even();
|
|
54
|
+
let (whole, micros) = if micros >= 1_000_000.0 {
|
|
55
|
+
(whole + 1.0, 0)
|
|
56
|
+
} else {
|
|
57
|
+
(whole, micros as u32)
|
|
58
|
+
};
|
|
59
|
+
let datetime = OffsetDateTime::from_unix_timestamp(whole as i64).ok()?;
|
|
60
|
+
let fmt = format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]");
|
|
61
|
+
Some(format!("{}.{micros:06}Z", datetime.format(&fmt).ok()?))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[cfg(test)]
|
|
65
|
+
mod tests {
|
|
66
|
+
use super::{now_iso8601, unix_to_iso8601};
|
|
67
|
+
|
|
68
|
+
#[test]
|
|
69
|
+
fn produces_iso8601_microsecond_utc() {
|
|
70
|
+
let ts = now_iso8601();
|
|
71
|
+
// e.g. 2026-07-08T16:19:24.616660Z
|
|
72
|
+
assert_eq!(ts.len(), 27, "unexpected timestamp: {ts}");
|
|
73
|
+
assert!(ts.ends_with('Z'));
|
|
74
|
+
assert_eq!(&ts[4..5], "-");
|
|
75
|
+
assert_eq!(&ts[10..11], "T");
|
|
76
|
+
assert_eq!(&ts[19..20], ".");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[test]
|
|
80
|
+
fn formats_unix_seconds_like_datetime_fromtimestamp() {
|
|
81
|
+
let cases = [
|
|
82
|
+
(0.0, "1970-01-01T00:00:00.000000Z"),
|
|
83
|
+
(1_000_000_000.123456, "2001-09-09T01:46:40.123456Z"),
|
|
84
|
+
// Rounds to the nearest microsecond, carrying into the second.
|
|
85
|
+
(1_700_000_000.999_999_6, "2023-11-14T22:13:21.000000Z"),
|
|
86
|
+
// A tie rounds to even: 1/128 s is 7812.5 us.
|
|
87
|
+
(0.0078125, "1970-01-01T00:00:00.007812Z"),
|
|
88
|
+
(-1.5, "1969-12-31T23:59:58.500000Z"),
|
|
89
|
+
(-62_135_596_800.0, "0001-01-01T00:00:00.000000Z"),
|
|
90
|
+
];
|
|
91
|
+
for (seconds, expected) in cases {
|
|
92
|
+
assert_eq!(
|
|
93
|
+
unix_to_iso8601(seconds).as_deref(),
|
|
94
|
+
Some(expected),
|
|
95
|
+
"{seconds}"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#[test]
|
|
101
|
+
fn rejects_unrepresentable_unix_seconds() {
|
|
102
|
+
for seconds in [
|
|
103
|
+
f64::NAN,
|
|
104
|
+
f64::INFINITY,
|
|
105
|
+
f64::NEG_INFINITY,
|
|
106
|
+
1e20,
|
|
107
|
+
-62_135_596_801.0,
|
|
108
|
+
253_402_300_800.0,
|
|
109
|
+
] {
|
|
110
|
+
assert_eq!(unix_to_iso8601(seconds), None, "{seconds}");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -18,23 +18,26 @@ pub enum Decision {
|
|
|
18
18
|
/// A pre-render filter deciding whether a record should be rendered.
|
|
19
19
|
pub trait RecordFilter: Send + Sync {
|
|
20
20
|
/// Inspect `key` (the rate-limit grouping key, usually the formatted message)
|
|
21
|
-
/// and `level` (the
|
|
22
|
-
|
|
21
|
+
/// and `level` (the record's numeric level, as in `logging`) and return
|
|
22
|
+
/// `Keep` or `Drop`.
|
|
23
|
+
fn allow(&self, key: &str, level: u32) -> Decision;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
/// Numeric level for a method name; unknown names rank as `info`.
|
|
26
|
-
fn
|
|
27
|
-
crate::level_number(method).unwrap_or(20)
|
|
27
|
+
pub fn method_level_number(method: &str) -> u32 {
|
|
28
|
+
u32::from(crate::level_number(method).unwrap_or(20))
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
/// Probabilistic sampler: keeps a record with probability `rate`.
|
|
31
32
|
///
|
|
32
33
|
/// With `max_level_num` set, only records at or below that level are sampled;
|
|
33
34
|
/// more severe records always pass — the native analog of wrapping
|
|
34
|
-
/// `SamplingProcessor` in `ConditionalProcessor(max_level=...)`.
|
|
35
|
+
/// `SamplingProcessor` in `ConditionalProcessor(max_level=...)`. Levels
|
|
36
|
+
/// compare by number, so a custom stdlib level between two named ones ranks
|
|
37
|
+
/// by its own value.
|
|
35
38
|
pub struct Sampler {
|
|
36
39
|
rate: f64,
|
|
37
|
-
max_level_num: Option<
|
|
40
|
+
max_level_num: Option<u32>,
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
impl Sampler {
|
|
@@ -43,15 +46,15 @@ impl Sampler {
|
|
|
43
46
|
pub fn new(rate: f64, max_level: Option<&str>) -> Self {
|
|
44
47
|
Self {
|
|
45
48
|
rate: rate.clamp(0.0, 1.0),
|
|
46
|
-
max_level_num: max_level.map(
|
|
49
|
+
max_level_num: max_level.map(method_level_number),
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
impl RecordFilter for Sampler {
|
|
52
|
-
fn allow(&self, _key: &str, level:
|
|
55
|
+
fn allow(&self, _key: &str, level: u32) -> Decision {
|
|
53
56
|
if let Some(max) = self.max_level_num
|
|
54
|
-
&&
|
|
57
|
+
&& level > max
|
|
55
58
|
{
|
|
56
59
|
return Decision::Keep;
|
|
57
60
|
}
|
|
@@ -201,7 +204,7 @@ fn evict_least_recent(state: &mut RateLimiterState, keep: usize) {
|
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
impl RecordFilter for RateLimiter {
|
|
204
|
-
fn allow(&self, key: &str, _level:
|
|
207
|
+
fn allow(&self, key: &str, _level: u32) -> Decision {
|
|
205
208
|
self.allow_key(key, Instant::now())
|
|
206
209
|
}
|
|
207
210
|
}
|
|
@@ -254,8 +257,9 @@ impl Pipeline {
|
|
|
254
257
|
}
|
|
255
258
|
}
|
|
256
259
|
|
|
257
|
-
/// Returns `false` when any stage drops the record.
|
|
258
|
-
|
|
260
|
+
/// Returns `false` when any stage drops the record. `level` is the
|
|
261
|
+
/// record's numeric level; see [`method_level_number`] for method names.
|
|
262
|
+
pub fn allow(&self, key: &str, level: u32) -> bool {
|
|
259
263
|
for stage in &self.stages {
|
|
260
264
|
let (decision, sampled, rate_limited) = match stage {
|
|
261
265
|
Stage::Sampler(f) => {
|
|
@@ -300,7 +304,7 @@ mod tests {
|
|
|
300
304
|
fn sampler_rate_zero_drops_all() {
|
|
301
305
|
let s = Sampler::new(0.0, None);
|
|
302
306
|
for _ in 0..100 {
|
|
303
|
-
assert_eq!(s.allow("k",
|
|
307
|
+
assert_eq!(s.allow("k", 20), Decision::Drop);
|
|
304
308
|
}
|
|
305
309
|
}
|
|
306
310
|
|
|
@@ -308,7 +312,7 @@ mod tests {
|
|
|
308
312
|
fn sampler_rate_one_keeps_all() {
|
|
309
313
|
let s = Sampler::new(1.0, None);
|
|
310
314
|
for _ in 0..100 {
|
|
311
|
-
assert_eq!(s.allow("k",
|
|
315
|
+
assert_eq!(s.allow("k", 20), Decision::Keep);
|
|
312
316
|
}
|
|
313
317
|
}
|
|
314
318
|
|
|
@@ -318,7 +322,7 @@ mod tests {
|
|
|
318
322
|
let s = Sampler::new(0.5, None);
|
|
319
323
|
let mut kept = 0;
|
|
320
324
|
for _ in 0..1000 {
|
|
321
|
-
if s.allow("k",
|
|
325
|
+
if s.allow("k", 20) == Decision::Keep {
|
|
322
326
|
kept += 1;
|
|
323
327
|
}
|
|
324
328
|
}
|
|
@@ -329,20 +333,45 @@ mod tests {
|
|
|
329
333
|
fn sampler_max_level_gates_sampling_to_low_levels() {
|
|
330
334
|
// rate=0 would drop everything, but only records <= INFO are sampled.
|
|
331
335
|
let s = Sampler::new(0.0, Some("info"));
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
for (method, expected) in [
|
|
337
|
+
("debug", Decision::Drop),
|
|
338
|
+
("info", Decision::Drop),
|
|
339
|
+
("success", Decision::Drop),
|
|
340
|
+
("warning", Decision::Keep),
|
|
341
|
+
("error", Decision::Keep),
|
|
342
|
+
("critical", Decision::Keep),
|
|
343
|
+
] {
|
|
344
|
+
assert_eq!(
|
|
345
|
+
s.allow("k", method_level_number(method)),
|
|
346
|
+
expected,
|
|
347
|
+
"{method}"
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
#[test]
|
|
353
|
+
fn sampler_max_level_ranks_custom_levels_by_number() {
|
|
354
|
+
// A stdlib level between INFO (20) and WARNING (30) is more severe
|
|
355
|
+
// than INFO, so it is never sampled.
|
|
356
|
+
let s = Sampler::new(0.0, Some("info"));
|
|
357
|
+
assert_eq!(s.allow("k", 19), Decision::Drop);
|
|
358
|
+
assert_eq!(s.allow("k", 20), Decision::Drop);
|
|
359
|
+
assert_eq!(s.allow("k", 21), Decision::Keep);
|
|
360
|
+
assert_eq!(s.allow("k", 25), Decision::Keep);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
#[test]
|
|
364
|
+
fn unknown_method_names_rank_as_info() {
|
|
365
|
+
assert_eq!(method_level_number("notice"), 20);
|
|
366
|
+
assert_eq!(method_level_number("WARN"), 30);
|
|
338
367
|
}
|
|
339
368
|
|
|
340
369
|
#[test]
|
|
341
370
|
fn pipeline_level_gated_sampler_counts_only_gated_drops() {
|
|
342
371
|
let p = Pipeline::new(0.0, Some("debug"), None, Duration::from_secs(60));
|
|
343
|
-
assert!(!p.allow("k",
|
|
344
|
-
assert!(p.allow("k",
|
|
345
|
-
assert!(p.allow("k",
|
|
372
|
+
assert!(!p.allow("k", 10));
|
|
373
|
+
assert!(p.allow("k", 20));
|
|
374
|
+
assert!(p.allow("k", 40));
|
|
346
375
|
let stats = p.stats();
|
|
347
376
|
assert_eq!(stats.sampled, 1);
|
|
348
377
|
}
|
|
@@ -411,7 +440,7 @@ mod tests {
|
|
|
411
440
|
let p = Pipeline::new(1.0, None, None, Duration::from_secs(60));
|
|
412
441
|
assert!(p.is_empty());
|
|
413
442
|
for _ in 0..10 {
|
|
414
|
-
assert!(p.allow("k",
|
|
443
|
+
assert!(p.allow("k", 20));
|
|
415
444
|
}
|
|
416
445
|
let stats = p.stats();
|
|
417
446
|
assert_eq!(stats.sampled, 0);
|
|
@@ -422,7 +451,7 @@ mod tests {
|
|
|
422
451
|
fn pipeline_sampler_only_counts_sampled() {
|
|
423
452
|
let p = Pipeline::new(0.0, None, None, Duration::from_secs(60));
|
|
424
453
|
for _ in 0..5 {
|
|
425
|
-
assert!(!p.allow("k",
|
|
454
|
+
assert!(!p.allow("k", 20));
|
|
426
455
|
}
|
|
427
456
|
let stats = p.stats();
|
|
428
457
|
assert_eq!(stats.sampled, 5);
|
|
@@ -432,9 +461,9 @@ mod tests {
|
|
|
432
461
|
#[test]
|
|
433
462
|
fn pipeline_rate_limiter_only_counts_rate_limited() {
|
|
434
463
|
let p = Pipeline::new(1.0, None, Some(2), Duration::from_secs(60));
|
|
435
|
-
assert!(p.allow("k",
|
|
436
|
-
assert!(p.allow("k",
|
|
437
|
-
assert!(!p.allow("k",
|
|
464
|
+
assert!(p.allow("k", 20));
|
|
465
|
+
assert!(p.allow("k", 20));
|
|
466
|
+
assert!(!p.allow("k", 20));
|
|
438
467
|
let stats = p.stats();
|
|
439
468
|
assert_eq!(stats.sampled, 0);
|
|
440
469
|
assert_eq!(stats.rate_limited, 1);
|
|
@@ -445,7 +474,7 @@ mod tests {
|
|
|
445
474
|
// rate=0 drops everything; the rate limiter never runs, so its counter stays 0.
|
|
446
475
|
let p = Pipeline::new(0.0, None, Some(2), Duration::from_secs(60));
|
|
447
476
|
for _ in 0..10 {
|
|
448
|
-
assert!(!p.allow("k",
|
|
477
|
+
assert!(!p.allow("k", 20));
|
|
449
478
|
}
|
|
450
479
|
let stats = p.stats();
|
|
451
480
|
assert_eq!(stats.sampled, 10);
|
|
@@ -456,9 +485,9 @@ mod tests {
|
|
|
456
485
|
fn pipeline_sampler_then_rate_limiter_both_count() {
|
|
457
486
|
// rate=1 so the sampler always keeps; the rate limiter governs drops.
|
|
458
487
|
let p = Pipeline::new(1.0, None, Some(1), Duration::from_secs(60));
|
|
459
|
-
assert!(p.allow("alpha",
|
|
460
|
-
assert!(!p.allow("alpha",
|
|
461
|
-
assert!(p.allow("beta",
|
|
488
|
+
assert!(p.allow("alpha", 20));
|
|
489
|
+
assert!(!p.allow("alpha", 20));
|
|
490
|
+
assert!(p.allow("beta", 20));
|
|
462
491
|
let stats = p.stats();
|
|
463
492
|
assert_eq!(stats.sampled, 0);
|
|
464
493
|
assert_eq!(stats.rate_limited, 1);
|
|
@@ -9,13 +9,15 @@ mod render;
|
|
|
9
9
|
mod value;
|
|
10
10
|
mod worker;
|
|
11
11
|
|
|
12
|
-
pub use clock::now_iso8601;
|
|
13
|
-
pub use filter::{
|
|
12
|
+
pub use clock::{now_iso8601, unix_to_iso8601};
|
|
13
|
+
pub use filter::{
|
|
14
|
+
Decision, FilterStats, Pipeline, RateLimiter, RecordFilter, Sampler, method_level_number,
|
|
15
|
+
};
|
|
14
16
|
pub use render::{DEFAULT_SENSITIVE_KEYS, RedactionPattern, render_line, render_line_console};
|
|
15
|
-
pub use value::{Value, ValueStats};
|
|
17
|
+
pub use value::{Value, ValueStats, collapse_duplicate_keys};
|
|
16
18
|
pub use worker::{
|
|
17
|
-
EnqueueError, MemorySink, MemorySinkHandle, MultiSink, RotatingFileSink, SinkError,
|
|
18
|
-
StringWriter, WorkerMetrics, WriteSink,
|
|
19
|
+
EnqueueError, MemorySink, MemorySinkHandle, MultiSink, RotatingFileSink, SinkError, StdoutSink,
|
|
20
|
+
StringSink, StringWriter, WorkerMetrics, WriteSink,
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
/// Return the Rust core crate version.
|