log-foundry 0.6.1.dev5__tar.gz → 0.6.1.dev7__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.
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/PKG-INFO +20 -2
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/README.md +19 -1
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/pyproject.toml +1 -1
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/__init__.py +7 -5
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/decorator.py +3 -1
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/worker.py +60 -6
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/LICENSE +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/api.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/config.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/console.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/context.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/model.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/base.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.6.1.dev5 → log_foundry-0.6.1.dev7}/src/log_foundry/sinks/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: log-foundry
|
|
3
|
-
Version: 0.6.1.
|
|
3
|
+
Version: 0.6.1.dev7
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -631,10 +631,28 @@ returns a snapshot of the worker's counters:
|
|
|
631
631
|
|
|
632
632
|
```python
|
|
633
633
|
h = log_foundry.health()
|
|
634
|
-
if h.dropped or h.failed_batches:
|
|
634
|
+
if h.dropped or h.failed_batches or h.stopped_reason:
|
|
635
635
|
... # logs were silently lost — worth an alert
|
|
636
636
|
```
|
|
637
637
|
|
|
638
|
+
The three tell you different things, and they want different responses:
|
|
639
|
+
|
|
640
|
+
| Field | Means | What to do |
|
|
641
|
+
|---|---|---|
|
|
642
|
+
| `dropped` | The queue filled — the destination is not keeping up. Delivery continues. | Tune `batch_size`/`flush_interval`, or scale the sink. |
|
|
643
|
+
| `failed_batches` | A sink stayed broken through the whole retry budget. Delivery continues. | Fix the destination. |
|
|
644
|
+
| `stopped_reason` | The background thread **died** on that exception type. Nothing further will be delivered, ever. | Restart the process; investigate the named exception. |
|
|
645
|
+
|
|
646
|
+
`stopped_reason` is a type name (e.g. `"SystemExit"`), never the exception's message — a sink's
|
|
647
|
+
error text can carry event data. It reads `None` for a healthy worker, for a process that has never
|
|
648
|
+
logged, and after a clean `shutdown()`, so a plain truthiness check is safe. Without it a dead
|
|
649
|
+
thread showed up only indirectly, as `dropped` climbing once the queue filled — the wrong signal,
|
|
650
|
+
pointing at the wrong fix.
|
|
651
|
+
|
|
652
|
+
Read a snapshot by attribute (`h.dropped`), as above. `Health` is a `NamedTuple` and gained a
|
|
653
|
+
fourth field in `v0.7.0`, so unpacking it whole — `queued, dropped, failed = health()` — raises
|
|
654
|
+
`ValueError` from that version on.
|
|
655
|
+
|
|
638
656
|
`dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
|
|
639
657
|
abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
|
|
640
658
|
every thousandth after it, since overflow is a high-rate condition and a line per drop would be its
|
|
@@ -595,10 +595,28 @@ returns a snapshot of the worker's counters:
|
|
|
595
595
|
|
|
596
596
|
```python
|
|
597
597
|
h = log_foundry.health()
|
|
598
|
-
if h.dropped or h.failed_batches:
|
|
598
|
+
if h.dropped or h.failed_batches or h.stopped_reason:
|
|
599
599
|
... # logs were silently lost — worth an alert
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
+
The three tell you different things, and they want different responses:
|
|
603
|
+
|
|
604
|
+
| Field | Means | What to do |
|
|
605
|
+
|---|---|---|
|
|
606
|
+
| `dropped` | The queue filled — the destination is not keeping up. Delivery continues. | Tune `batch_size`/`flush_interval`, or scale the sink. |
|
|
607
|
+
| `failed_batches` | A sink stayed broken through the whole retry budget. Delivery continues. | Fix the destination. |
|
|
608
|
+
| `stopped_reason` | The background thread **died** on that exception type. Nothing further will be delivered, ever. | Restart the process; investigate the named exception. |
|
|
609
|
+
|
|
610
|
+
`stopped_reason` is a type name (e.g. `"SystemExit"`), never the exception's message — a sink's
|
|
611
|
+
error text can carry event data. It reads `None` for a healthy worker, for a process that has never
|
|
612
|
+
logged, and after a clean `shutdown()`, so a plain truthiness check is safe. Without it a dead
|
|
613
|
+
thread showed up only indirectly, as `dropped` climbing once the queue filled — the wrong signal,
|
|
614
|
+
pointing at the wrong fix.
|
|
615
|
+
|
|
616
|
+
Read a snapshot by attribute (`h.dropped`), as above. `Health` is a `NamedTuple` and gained a
|
|
617
|
+
fourth field in `v0.7.0`, so unpacking it whole — `queued, dropped, failed = health()` — raises
|
|
618
|
+
`ValueError` from that version on.
|
|
619
|
+
|
|
602
620
|
`dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
|
|
603
621
|
abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
|
|
604
622
|
every thousandth after it, since overflow is a high-rate condition and a line per drop would be its
|
|
@@ -53,13 +53,15 @@ def flush(timeout: float | None = 5.0) -> bool:
|
|
|
53
53
|
def health() -> Health:
|
|
54
54
|
"""Snapshot the background worker's delivery counters (SPEC-017 FR-005). Never raises.
|
|
55
55
|
|
|
56
|
-
Returns ``queued`` / ``dropped`` / ``failed_batches``. A non-zero
|
|
57
|
-
filled and submissions were discarded to keep your code
|
|
58
|
-
``failed_batches`` means a sink stayed broken through the whole
|
|
59
|
-
losses the library absorbs on purpose, and this is how you notice
|
|
56
|
+
Returns ``queued`` / ``dropped`` / ``failed_batches`` / ``stopped_reason``. A non-zero
|
|
57
|
+
``dropped`` means the queue filled and submissions were discarded to keep your code
|
|
58
|
+
non-blocking; a non-zero ``failed_batches`` means a sink stayed broken through the whole
|
|
59
|
+
retry budget. Both are losses the library absorbs on purpose, and this is how you notice
|
|
60
|
+
them. A non-``None`` ``stopped_reason`` is worse than either: the background thread died
|
|
61
|
+
on that exception type, so nothing further will be delivered at all (SPEC-019)::
|
|
60
62
|
|
|
61
63
|
h = log_foundry.health()
|
|
62
|
-
if h.dropped or h.failed_batches:
|
|
64
|
+
if h.dropped or h.failed_batches or h.stopped_reason:
|
|
63
65
|
... # raise an alert; logs were silently lost
|
|
64
66
|
|
|
65
67
|
A process that has never logged has no worker, and asking after its health does not create
|
|
@@ -240,7 +240,9 @@ def _worker_health() -> Health:
|
|
|
240
240
|
|
|
241
241
|
Backs :func:`log_foundry.health` (SPEC-017 FR-005). Like :func:`_flush_worker` this
|
|
242
242
|
deliberately does *not* call :func:`_get_worker`: starting a thread and registering an
|
|
243
|
-
``atexit`` drain in order to report
|
|
243
|
+
``atexit`` drain in order to report an empty snapshot would be pure cost. That snapshot
|
|
244
|
+
reads ``stopped_reason=None`` — a worker that was never created has not died, which is why
|
|
245
|
+
SPEC-019 reports the terminal failure as a reason rather than an ``alive`` flag.
|
|
244
246
|
"""
|
|
245
247
|
worker = _worker
|
|
246
248
|
if worker is None:
|
|
@@ -48,11 +48,19 @@ class Health(NamedTuple):
|
|
|
48
48
|
alongside real submissions.
|
|
49
49
|
dropped: Submissions discarded because the queue was full (backpressure).
|
|
50
50
|
failed_batches: Batches abandoned after the retry budget was spent.
|
|
51
|
+
stopped_reason: The exception type name that terminated the drain thread, or ``None``
|
|
52
|
+
if it never died — which is also what a live worker and a process that never
|
|
53
|
+
logged report. Non-``None`` is categorically worse than the two counters above:
|
|
54
|
+
they measure loss the worker absorbed and kept running through, this one means
|
|
55
|
+
the worker is gone and nothing further will be delivered (SPEC-019 FR-003).
|
|
51
56
|
"""
|
|
52
57
|
|
|
53
58
|
queued: int
|
|
54
59
|
dropped: int
|
|
55
60
|
failed_batches: int
|
|
61
|
+
# Defaulted so the zeroed snapshot in `decorator._worker_health` — and any third-party
|
|
62
|
+
# construction — keeps working unchanged.
|
|
63
|
+
stopped_reason: str | None = None
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
class _FlushMarker:
|
|
@@ -92,6 +100,7 @@ class Worker:
|
|
|
92
100
|
self.max_retries = max_retries
|
|
93
101
|
self.dropped = 0 # submissions dropped because the queue was full (backpressure)
|
|
94
102
|
self.failed_batches = 0 # batches abandoned after exhausting retries (worker-thread only)
|
|
103
|
+
self.stopped_reason: str | None = None # set once if the drain loop dies (SPEC-019)
|
|
95
104
|
self._queue: queue.Queue[object] = queue.Queue(maxsize=max_queue)
|
|
96
105
|
self._stop = threading.Event()
|
|
97
106
|
self._shutdown_done = False
|
|
@@ -133,15 +142,21 @@ class Worker:
|
|
|
133
142
|
pass
|
|
134
143
|
|
|
135
144
|
def health(self) -> Health:
|
|
136
|
-
"""Snapshot the delivery counters (SPEC-017 FR-005). Never raises.
|
|
145
|
+
"""Snapshot the delivery counters (SPEC-017 FR-005, SPEC-019 FR-003). Never raises.
|
|
137
146
|
|
|
138
147
|
Valid after :meth:`shutdown` — the counters are plain integers that outlive the thread,
|
|
139
148
|
and the final drain consumes the queue, so ``queued`` reads 0 rather than a stale marker.
|
|
149
|
+
The same applies to ``stopped_reason``: a terminal failure stays readable afterwards,
|
|
150
|
+
since a caller finding a dead worker will usually call ``shutdown()`` next.
|
|
140
151
|
"""
|
|
141
152
|
with self._lock:
|
|
142
153
|
dropped, failed_batches = self.dropped, self.failed_batches
|
|
154
|
+
stopped_reason = self.stopped_reason
|
|
143
155
|
return Health(
|
|
144
|
-
queued=self._queue.qsize(),
|
|
156
|
+
queued=self._queue.qsize(),
|
|
157
|
+
dropped=dropped,
|
|
158
|
+
failed_batches=failed_batches,
|
|
159
|
+
stopped_reason=stopped_reason,
|
|
145
160
|
)
|
|
146
161
|
|
|
147
162
|
def flush(self, timeout: float | None = 5.0) -> bool:
|
|
@@ -195,8 +210,47 @@ class Worker:
|
|
|
195
210
|
# -- worker thread ------------------------------------------------------------------
|
|
196
211
|
|
|
197
212
|
def _run(self) -> None:
|
|
198
|
-
"""Drain loop: accumulate event-lists and emit a batch on the count/time trigger.
|
|
213
|
+
"""Drain loop: accumulate event-lists and emit a batch on the count/time trigger.
|
|
214
|
+
|
|
215
|
+
Guarded end to end (SPEC-019 FR-001). ``_emit`` already absorbs an ``Exception`` from
|
|
216
|
+
the sink, so anything reaching this handler has ended the only thread that delivers —
|
|
217
|
+
and CPython's thread bootstrap discards a ``SystemExit`` without even a traceback, which
|
|
218
|
+
is why the catch is ``BaseException`` rather than ``Exception``. It records and exits;
|
|
219
|
+
looping onward past a ``KeyboardInterrupt`` would be a worse failure than the one this
|
|
220
|
+
prevents.
|
|
221
|
+
"""
|
|
199
222
|
pending: list[list[dict[str, object]]] = []
|
|
223
|
+
try:
|
|
224
|
+
self._drain(pending)
|
|
225
|
+
except BaseException as exc: # deliberately broad — see the docstring
|
|
226
|
+
self._terminal_failure(exc, len(pending))
|
|
227
|
+
|
|
228
|
+
def _terminal_failure(self, exc: BaseException, undrained: int) -> None:
|
|
229
|
+
"""Record the drain loop's terminal exit, then announce it (FR-001, FR-002).
|
|
230
|
+
|
|
231
|
+
Recording precedes announcing: stderr may be closed or wedged, and unlike the overflow
|
|
232
|
+
warning this line is written exactly once and cannot be re-emitted later, so the record
|
|
233
|
+
must not be able to ride on it. The exception's *type* is reported and its message is
|
|
234
|
+
not — a sink's exception text can carry event data, and arch §6 keeps caller data out of
|
|
235
|
+
places it was not asked for (the same rule behind ``sanitize``'s type-name placeholder).
|
|
236
|
+
"""
|
|
237
|
+
name = type(exc).__name__
|
|
238
|
+
with self._lock:
|
|
239
|
+
self.stopped_reason = name
|
|
240
|
+
try:
|
|
241
|
+
sys.stderr.write(
|
|
242
|
+
f"log-foundry: worker thread stopped on {name}; {undrained} undrained "
|
|
243
|
+
f"event-list(s), nothing further will be delivered\n"
|
|
244
|
+
)
|
|
245
|
+
except Exception: # best-effort: the record above is what an operator reads.
|
|
246
|
+
pass
|
|
247
|
+
|
|
248
|
+
def _drain(self, pending: list[list[dict[str, object]]]) -> None:
|
|
249
|
+
"""The drain loop proper. ``pending`` is owned by :meth:`_run`, which reports its size.
|
|
250
|
+
|
|
251
|
+
It is mutated in place rather than rebound so the terminal handler sees what was still
|
|
252
|
+
in hand; a local ``pending = []`` here would leave that count reading zero.
|
|
253
|
+
"""
|
|
200
254
|
last_flush = time.monotonic()
|
|
201
255
|
while not self._stop.is_set():
|
|
202
256
|
timeout = max(0.0, self.flush_interval - (time.monotonic() - last_flush))
|
|
@@ -213,7 +267,7 @@ class Worker:
|
|
|
213
267
|
try:
|
|
214
268
|
if pending:
|
|
215
269
|
self._emit(pending)
|
|
216
|
-
pending
|
|
270
|
+
pending.clear()
|
|
217
271
|
finally:
|
|
218
272
|
# Signal even if the emit died, so a waiter is released rather than left to
|
|
219
273
|
# wait out its timeout on a thread that is no longer running.
|
|
@@ -226,7 +280,7 @@ class Worker:
|
|
|
226
280
|
if len(pending) >= self.batch_size or now - last_flush >= self.flush_interval:
|
|
227
281
|
if pending:
|
|
228
282
|
self._emit(pending)
|
|
229
|
-
pending
|
|
283
|
+
pending.clear()
|
|
230
284
|
# Advance the window even when idle (pending empty). Otherwise last_flush never
|
|
231
285
|
# moves while the queue is empty, timeout collapses to 0.0, and get(timeout=0.0)
|
|
232
286
|
# busy-spins a core. Resetting it lets the next get() block a full interval.
|
|
@@ -242,7 +296,7 @@ class Worker:
|
|
|
242
296
|
except queue.Empty:
|
|
243
297
|
break
|
|
244
298
|
if isinstance(item, _FlushMarker):
|
|
245
|
-
# This guard is a second copy of
|
|
299
|
+
# This guard is a second copy of _drain's and needs the same exclusion. Markers
|
|
246
300
|
# are answered *after* the final emit below, so a flush() that raced shutdown()
|
|
247
301
|
# still returns True — and truthfully: its events really did reach the sink.
|
|
248
302
|
markers.append(item)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|