log-foundry 0.10.2.dev18__tar.gz → 0.10.2.dev19__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.10.2.dev18 → log_foundry-0.10.2.dev19}/PKG-INFO +84 -15
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/README.md +83 -14
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/__init__.py +18 -3
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/base.py +6 -4
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/pubsub.py +6 -2
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/worker.py +3 -3
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/LICENSE +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/api.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/config.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/context.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/decorator.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/_socket.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/clickhouse.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/eventhubs.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/filtering.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/firehose.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/http.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/kinesis.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/logstash.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/mongodb.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/multi.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/postgres.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/rabbitmq.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/redis.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/sentry.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/sns.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/sqs.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/syslog.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/transform.py +0 -0
- {log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/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.10.2.
|
|
3
|
+
Version: 0.10.2.dev19
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -438,12 +438,16 @@ A few conventions hold across every sink below:
|
|
|
438
438
|
The tables show the destination-defining arguments only; sinks that retry also take `max_retries`.
|
|
439
439
|
- **Ownership.** A resource the sink opens itself is closed on `shutdown()`; an injected one is left
|
|
440
440
|
open for you to manage.
|
|
441
|
-
- **Never crashes the app.** A
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
441
|
+
- **Never crashes the app.** A broken destination degrades logging and nothing more. A sink that
|
|
442
|
+
delivered *part* of a batch counts what it lost (`.failed`, `.dropped_oversized`,
|
|
443
|
+
`.dropped_unadjudicated`, …) and returns, since retrying would re-deliver what already landed.
|
|
444
|
+
A sink that delivered **none** of it raises instead, so the worker's bounded retry engages and
|
|
445
|
+
`health().failed_batches` records the loss — there is nothing downstream to duplicate. Three
|
|
446
|
+
cases are excepted, each because a retry would be wrong rather than merely futile: an oversized
|
|
447
|
+
event (it can never fit), a response the sink could not adjudicate (it cannot prove nothing
|
|
448
|
+
landed), and an SQS sender fault (a byte-identical re-send can only fail again). Either way the
|
|
449
|
+
exception never reaches your code — inside a span the worker catches it, and on the orphan path
|
|
450
|
+
(`log_foundry.info(...)` outside any span, which emits synchronously) the emitter does.
|
|
447
451
|
|
|
448
452
|
#### Built-in, zero-dependency
|
|
449
453
|
|
|
@@ -466,7 +470,7 @@ to a plain callable.
|
|
|
466
470
|
|
|
467
471
|
| Sink | Import from | Configure |
|
|
468
472
|
|---|---|---|
|
|
469
|
-
| `MultiSink` | `log_foundry.sinks.multi` | `MultiSink(*sinks)` — forward each batch to every child; a failing child is isolated and counted on `.failed
|
|
473
|
+
| `MultiSink` | `log_foundry.sinks.multi` | `MultiSink(*sinks)` — forward each batch to every child; a failing child is isolated and counted on `.failed`, unless *every* child failed, which re-raises |
|
|
470
474
|
| `FilteringSink` | `log_foundry.sinks.filtering` | `FilteringSink(inner, *, predicate=None, min_level=None)` — forward only events passing `predicate` and/or at/above `min_level` |
|
|
471
475
|
| `TransformSink` | `log_foundry.sinks.transform` | `TransformSink(inner, fn)` — map each event through `fn` before forwarding; return `None` to drop one |
|
|
472
476
|
| `CallbackSink` | `log_foundry.sinks.callback` | `CallbackSink(fn, *, on_close=None)` — hand each batch to any callable |
|
|
@@ -659,8 +663,56 @@ Write-only inserts (querying is the downstream tool's job); each needs its own e
|
|
|
659
663
|
`PostgresSink` / `ClickHouseSink` default `create_table=False` (you own the schema and indexes); set
|
|
660
664
|
it `True` for an idempotent `CREATE TABLE IF NOT EXISTS` convenience.
|
|
661
665
|
|
|
662
|
-
Prefer a destination not listed here? Implement the
|
|
663
|
-
|
|
666
|
+
Prefer a destination not listed here? Implement the `Sink` protocol yourself, or wrap any callable
|
|
667
|
+
in `CallbackSink`.
|
|
668
|
+
|
|
669
|
+
#### Writing your own sink
|
|
670
|
+
|
|
671
|
+
`Sink` is two required methods, `emit(batch)` and `close()`, plus two rules about *how* `emit`
|
|
672
|
+
fails. They are not stylistic — the library's whole loss-reporting apparatus is built on them:
|
|
673
|
+
|
|
674
|
+
- **Raise when you delivered none of the batch**, after your own retries are spent. That is the
|
|
675
|
+
signal the worker's bounded retry and `health().failed_batches` depend on, and the one case where
|
|
676
|
+
a retry cannot duplicate anything: nothing landed downstream. Raise `SinkDeliveryError` (from
|
|
677
|
+
`log_foundry.sinks.base`) or any exception of your own — the contract is that *something*
|
|
678
|
+
propagates.
|
|
679
|
+
- **Do not raise when you delivered some of it.** The worker retries whole batches, so raising on a
|
|
680
|
+
partial success re-delivers the records that already arrived, and duplicates downstream are worse
|
|
681
|
+
than a counted loss.
|
|
682
|
+
|
|
683
|
+
A sink that absorbs a total failure and returns normally is a sink the worker believes: the retry
|
|
684
|
+
never engages, `failed_batches` stays at zero, and `flush()` returns `True` while every event is
|
|
685
|
+
lost.
|
|
686
|
+
|
|
687
|
+
Optionally add `losses()` to report what you absorbed. It must never raise and must be safe to call
|
|
688
|
+
while `emit` is running (`health()` is a poll):
|
|
689
|
+
|
|
690
|
+
```python
|
|
691
|
+
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
692
|
+
|
|
693
|
+
class MySink:
|
|
694
|
+
def __init__(self) -> None:
|
|
695
|
+
self._dropped = self._failed = 0
|
|
696
|
+
|
|
697
|
+
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
698
|
+
delivered = 0
|
|
699
|
+
for chunk in self._chunks(batch):
|
|
700
|
+
if self._send(chunk): # your own bounded retry
|
|
701
|
+
delivered += len(chunk)
|
|
702
|
+
else:
|
|
703
|
+
self._failed += len(chunk)
|
|
704
|
+
if batch and not delivered:
|
|
705
|
+
raise SinkDeliveryError(f"MySink delivered none of {len(batch)} event(s)")
|
|
706
|
+
|
|
707
|
+
def losses(self) -> SinkLosses:
|
|
708
|
+
return SinkLosses(dropped=self._dropped, failed=self._failed)
|
|
709
|
+
|
|
710
|
+
def close(self) -> None: ...
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
`losses()` is optional and probed by name, so a sink written before it existed keeps working and
|
|
714
|
+
simply contributes nothing to `health().sink`. `emit([])` must be a no-op: an empty batch has not
|
|
715
|
+
failed to deliver.
|
|
664
716
|
|
|
665
717
|
### Flushing and shutdown
|
|
666
718
|
|
|
@@ -676,17 +728,33 @@ returns a snapshot of the worker's counters:
|
|
|
676
728
|
|
|
677
729
|
```python
|
|
678
730
|
h = log_foundry.health()
|
|
679
|
-
if h.dropped or h.failed_batches or h.stopped_reason:
|
|
731
|
+
if h.dropped or h.failed_batches or h.stopped_reason or (h.sink and (h.sink.dropped or h.sink.failed)):
|
|
680
732
|
... # logs were silently lost — worth an alert
|
|
681
733
|
```
|
|
682
734
|
|
|
683
|
-
|
|
735
|
+
They tell you different things, and they want different responses:
|
|
684
736
|
|
|
685
737
|
| Field | Means | What to do |
|
|
686
738
|
|---|---|---|
|
|
687
739
|
| `dropped` | The queue filled — the destination is not keeping up. Delivery continues. | Tune `batch_size`/`flush_interval`, or scale the sink. |
|
|
688
740
|
| `failed_batches` | A sink stayed broken through the whole retry budget. Delivery continues. | Fix the destination. |
|
|
689
741
|
| `stopped_reason` | The background thread **died** on that exception type. Nothing further will be delivered, ever. | Restart the process; investigate the named exception. |
|
|
742
|
+
| `sink.dropped` | The sink discarded events **before** attempting delivery — an oversized record, or one the client refused outright. | Read the stderr line: it names the cause. An oversized record means shrink what you log; a refused local produce/publish (Kafka, Pub/Sub) points at the client — a saturated buffer, a bad topic, a credential. |
|
|
743
|
+
| `sink.failed` | The sink attempted delivery and could not confirm it — abandoned requests, partially-failed batches, responses it could not adjudicate. | Fix the destination. |
|
|
744
|
+
|
|
745
|
+
`h.sink` is a `SinkLosses(dropped, failed)` or `None` — `None` when no worker exists yet, or when
|
|
746
|
+
the configured sink reports nothing (`losses()` is optional). Note the two `dropped` fields count
|
|
747
|
+
different things: the worker's is backpressure at *its* queue, the sink's is an event that never
|
|
748
|
+
reached the wire. They are separate because the remedies do not overlap — and `sink.dropped` is
|
|
749
|
+
itself two causes, which is why the diagnostic line matters. Most sinks drop only what can never
|
|
750
|
+
fit; `KafkaSink` and `GooglePubSubSink` also count what their client refused outright, which may
|
|
751
|
+
be backpressure one layer further out than the worker's, or may be a misconfiguration. The stderr
|
|
752
|
+
line carries the exception type that distinguishes them.
|
|
753
|
+
|
|
754
|
+
`sink.failed` is an **upper bound** on loss, not a count of it. A sink that raises on total failure
|
|
755
|
+
counts the attempt *and* hands the batch back to the worker, whose retry may then deliver it — so a
|
|
756
|
+
transient outage leaves it non-zero with nothing actually lost. `failed_batches` is the record of a
|
|
757
|
+
batch given up on for good.
|
|
690
758
|
|
|
691
759
|
`stopped_reason` is a type name (e.g. `"SystemExit"`), never the exception's message — a sink's
|
|
692
760
|
error text can carry event data. It reads `None` for a healthy worker, for a process that has never
|
|
@@ -694,9 +762,10 @@ logged, and after a clean `shutdown()`, so a plain truthiness check is safe. Wit
|
|
|
694
762
|
thread showed up only indirectly, as `dropped` climbing once the queue filled — the wrong signal,
|
|
695
763
|
pointing at the wrong fix.
|
|
696
764
|
|
|
697
|
-
Read a snapshot by attribute (`h.dropped`), as above. `Health` is a `NamedTuple` and gained
|
|
698
|
-
fourth
|
|
699
|
-
`
|
|
765
|
+
Read a snapshot by attribute (`h.dropped`), as above. `Health` is a `NamedTuple` and has gained
|
|
766
|
+
fields over time — a fourth (`stopped_reason`) in `v0.7.0` and a fifth (`sink`) not yet in a tagged
|
|
767
|
+
release — so unpacking it whole (`queued, dropped, failed = health()`) raises `ValueError`. Every
|
|
768
|
+
field keeps its position when a new one is appended, so attribute and index access stay stable.
|
|
700
769
|
|
|
701
770
|
`dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
|
|
702
771
|
abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
|
|
@@ -402,12 +402,16 @@ A few conventions hold across every sink below:
|
|
|
402
402
|
The tables show the destination-defining arguments only; sinks that retry also take `max_retries`.
|
|
403
403
|
- **Ownership.** A resource the sink opens itself is closed on `shutdown()`; an injected one is left
|
|
404
404
|
open for you to manage.
|
|
405
|
-
- **Never crashes the app.** A
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
- **Never crashes the app.** A broken destination degrades logging and nothing more. A sink that
|
|
406
|
+
delivered *part* of a batch counts what it lost (`.failed`, `.dropped_oversized`,
|
|
407
|
+
`.dropped_unadjudicated`, …) and returns, since retrying would re-deliver what already landed.
|
|
408
|
+
A sink that delivered **none** of it raises instead, so the worker's bounded retry engages and
|
|
409
|
+
`health().failed_batches` records the loss — there is nothing downstream to duplicate. Three
|
|
410
|
+
cases are excepted, each because a retry would be wrong rather than merely futile: an oversized
|
|
411
|
+
event (it can never fit), a response the sink could not adjudicate (it cannot prove nothing
|
|
412
|
+
landed), and an SQS sender fault (a byte-identical re-send can only fail again). Either way the
|
|
413
|
+
exception never reaches your code — inside a span the worker catches it, and on the orphan path
|
|
414
|
+
(`log_foundry.info(...)` outside any span, which emits synchronously) the emitter does.
|
|
411
415
|
|
|
412
416
|
#### Built-in, zero-dependency
|
|
413
417
|
|
|
@@ -430,7 +434,7 @@ to a plain callable.
|
|
|
430
434
|
|
|
431
435
|
| Sink | Import from | Configure |
|
|
432
436
|
|---|---|---|
|
|
433
|
-
| `MultiSink` | `log_foundry.sinks.multi` | `MultiSink(*sinks)` — forward each batch to every child; a failing child is isolated and counted on `.failed
|
|
437
|
+
| `MultiSink` | `log_foundry.sinks.multi` | `MultiSink(*sinks)` — forward each batch to every child; a failing child is isolated and counted on `.failed`, unless *every* child failed, which re-raises |
|
|
434
438
|
| `FilteringSink` | `log_foundry.sinks.filtering` | `FilteringSink(inner, *, predicate=None, min_level=None)` — forward only events passing `predicate` and/or at/above `min_level` |
|
|
435
439
|
| `TransformSink` | `log_foundry.sinks.transform` | `TransformSink(inner, fn)` — map each event through `fn` before forwarding; return `None` to drop one |
|
|
436
440
|
| `CallbackSink` | `log_foundry.sinks.callback` | `CallbackSink(fn, *, on_close=None)` — hand each batch to any callable |
|
|
@@ -623,8 +627,56 @@ Write-only inserts (querying is the downstream tool's job); each needs its own e
|
|
|
623
627
|
`PostgresSink` / `ClickHouseSink` default `create_table=False` (you own the schema and indexes); set
|
|
624
628
|
it `True` for an idempotent `CREATE TABLE IF NOT EXISTS` convenience.
|
|
625
629
|
|
|
626
|
-
Prefer a destination not listed here? Implement the
|
|
627
|
-
|
|
630
|
+
Prefer a destination not listed here? Implement the `Sink` protocol yourself, or wrap any callable
|
|
631
|
+
in `CallbackSink`.
|
|
632
|
+
|
|
633
|
+
#### Writing your own sink
|
|
634
|
+
|
|
635
|
+
`Sink` is two required methods, `emit(batch)` and `close()`, plus two rules about *how* `emit`
|
|
636
|
+
fails. They are not stylistic — the library's whole loss-reporting apparatus is built on them:
|
|
637
|
+
|
|
638
|
+
- **Raise when you delivered none of the batch**, after your own retries are spent. That is the
|
|
639
|
+
signal the worker's bounded retry and `health().failed_batches` depend on, and the one case where
|
|
640
|
+
a retry cannot duplicate anything: nothing landed downstream. Raise `SinkDeliveryError` (from
|
|
641
|
+
`log_foundry.sinks.base`) or any exception of your own — the contract is that *something*
|
|
642
|
+
propagates.
|
|
643
|
+
- **Do not raise when you delivered some of it.** The worker retries whole batches, so raising on a
|
|
644
|
+
partial success re-delivers the records that already arrived, and duplicates downstream are worse
|
|
645
|
+
than a counted loss.
|
|
646
|
+
|
|
647
|
+
A sink that absorbs a total failure and returns normally is a sink the worker believes: the retry
|
|
648
|
+
never engages, `failed_batches` stays at zero, and `flush()` returns `True` while every event is
|
|
649
|
+
lost.
|
|
650
|
+
|
|
651
|
+
Optionally add `losses()` to report what you absorbed. It must never raise and must be safe to call
|
|
652
|
+
while `emit` is running (`health()` is a poll):
|
|
653
|
+
|
|
654
|
+
```python
|
|
655
|
+
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
656
|
+
|
|
657
|
+
class MySink:
|
|
658
|
+
def __init__(self) -> None:
|
|
659
|
+
self._dropped = self._failed = 0
|
|
660
|
+
|
|
661
|
+
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
662
|
+
delivered = 0
|
|
663
|
+
for chunk in self._chunks(batch):
|
|
664
|
+
if self._send(chunk): # your own bounded retry
|
|
665
|
+
delivered += len(chunk)
|
|
666
|
+
else:
|
|
667
|
+
self._failed += len(chunk)
|
|
668
|
+
if batch and not delivered:
|
|
669
|
+
raise SinkDeliveryError(f"MySink delivered none of {len(batch)} event(s)")
|
|
670
|
+
|
|
671
|
+
def losses(self) -> SinkLosses:
|
|
672
|
+
return SinkLosses(dropped=self._dropped, failed=self._failed)
|
|
673
|
+
|
|
674
|
+
def close(self) -> None: ...
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
`losses()` is optional and probed by name, so a sink written before it existed keeps working and
|
|
678
|
+
simply contributes nothing to `health().sink`. `emit([])` must be a no-op: an empty batch has not
|
|
679
|
+
failed to deliver.
|
|
628
680
|
|
|
629
681
|
### Flushing and shutdown
|
|
630
682
|
|
|
@@ -640,17 +692,33 @@ returns a snapshot of the worker's counters:
|
|
|
640
692
|
|
|
641
693
|
```python
|
|
642
694
|
h = log_foundry.health()
|
|
643
|
-
if h.dropped or h.failed_batches or h.stopped_reason:
|
|
695
|
+
if h.dropped or h.failed_batches or h.stopped_reason or (h.sink and (h.sink.dropped or h.sink.failed)):
|
|
644
696
|
... # logs were silently lost — worth an alert
|
|
645
697
|
```
|
|
646
698
|
|
|
647
|
-
|
|
699
|
+
They tell you different things, and they want different responses:
|
|
648
700
|
|
|
649
701
|
| Field | Means | What to do |
|
|
650
702
|
|---|---|---|
|
|
651
703
|
| `dropped` | The queue filled — the destination is not keeping up. Delivery continues. | Tune `batch_size`/`flush_interval`, or scale the sink. |
|
|
652
704
|
| `failed_batches` | A sink stayed broken through the whole retry budget. Delivery continues. | Fix the destination. |
|
|
653
705
|
| `stopped_reason` | The background thread **died** on that exception type. Nothing further will be delivered, ever. | Restart the process; investigate the named exception. |
|
|
706
|
+
| `sink.dropped` | The sink discarded events **before** attempting delivery — an oversized record, or one the client refused outright. | Read the stderr line: it names the cause. An oversized record means shrink what you log; a refused local produce/publish (Kafka, Pub/Sub) points at the client — a saturated buffer, a bad topic, a credential. |
|
|
707
|
+
| `sink.failed` | The sink attempted delivery and could not confirm it — abandoned requests, partially-failed batches, responses it could not adjudicate. | Fix the destination. |
|
|
708
|
+
|
|
709
|
+
`h.sink` is a `SinkLosses(dropped, failed)` or `None` — `None` when no worker exists yet, or when
|
|
710
|
+
the configured sink reports nothing (`losses()` is optional). Note the two `dropped` fields count
|
|
711
|
+
different things: the worker's is backpressure at *its* queue, the sink's is an event that never
|
|
712
|
+
reached the wire. They are separate because the remedies do not overlap — and `sink.dropped` is
|
|
713
|
+
itself two causes, which is why the diagnostic line matters. Most sinks drop only what can never
|
|
714
|
+
fit; `KafkaSink` and `GooglePubSubSink` also count what their client refused outright, which may
|
|
715
|
+
be backpressure one layer further out than the worker's, or may be a misconfiguration. The stderr
|
|
716
|
+
line carries the exception type that distinguishes them.
|
|
717
|
+
|
|
718
|
+
`sink.failed` is an **upper bound** on loss, not a count of it. A sink that raises on total failure
|
|
719
|
+
counts the attempt *and* hands the batch back to the worker, whose retry may then deliver it — so a
|
|
720
|
+
transient outage leaves it non-zero with nothing actually lost. `failed_batches` is the record of a
|
|
721
|
+
batch given up on for good.
|
|
654
722
|
|
|
655
723
|
`stopped_reason` is a type name (e.g. `"SystemExit"`), never the exception's message — a sink's
|
|
656
724
|
error text can carry event data. It reads `None` for a healthy worker, for a process that has never
|
|
@@ -658,9 +726,10 @@ logged, and after a clean `shutdown()`, so a plain truthiness check is safe. Wit
|
|
|
658
726
|
thread showed up only indirectly, as `dropped` climbing once the queue filled — the wrong signal,
|
|
659
727
|
pointing at the wrong fix.
|
|
660
728
|
|
|
661
|
-
Read a snapshot by attribute (`h.dropped`), as above. `Health` is a `NamedTuple` and gained
|
|
662
|
-
fourth
|
|
663
|
-
`
|
|
729
|
+
Read a snapshot by attribute (`h.dropped`), as above. `Health` is a `NamedTuple` and has gained
|
|
730
|
+
fields over time — a fourth (`stopped_reason`) in `v0.7.0` and a fifth (`sink`) not yet in a tagged
|
|
731
|
+
release — so unpacking it whole (`queued, dropped, failed = health()`) raises `ValueError`. Every
|
|
732
|
+
field keeps its position when a new one is appended, so attribute and index access stay stable.
|
|
664
733
|
|
|
665
734
|
`dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
|
|
666
735
|
abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
|
|
@@ -20,6 +20,7 @@ from log_foundry.context import (
|
|
|
20
20
|
reset_context,
|
|
21
21
|
)
|
|
22
22
|
from log_foundry.decorator import continue_trace, trace
|
|
23
|
+
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
23
24
|
from log_foundry.worker import Health
|
|
24
25
|
|
|
25
26
|
try:
|
|
@@ -59,17 +60,29 @@ def flush(timeout: float | None = 5.0) -> bool:
|
|
|
59
60
|
def health() -> Health:
|
|
60
61
|
"""Snapshot the background worker's delivery counters (SPEC-017 FR-005). Never raises.
|
|
61
62
|
|
|
62
|
-
Returns ``queued`` / ``dropped`` / ``failed_batches`` / ``stopped_reason``. A
|
|
63
|
-
``dropped`` means the queue filled and submissions were discarded to keep your code
|
|
63
|
+
Returns ``queued`` / ``dropped`` / ``failed_batches`` / ``stopped_reason`` / ``sink``. A
|
|
64
|
+
non-zero ``dropped`` means the queue filled and submissions were discarded to keep your code
|
|
64
65
|
non-blocking; a non-zero ``failed_batches`` means a sink stayed broken through the whole
|
|
65
66
|
retry budget. Both are losses the library absorbs on purpose, and this is how you notice
|
|
66
67
|
them. A non-``None`` ``stopped_reason`` is worse than either: the background thread died
|
|
67
68
|
on that exception type, so nothing further will be delivered at all (SPEC-019)::
|
|
68
69
|
|
|
69
70
|
h = log_foundry.health()
|
|
70
|
-
if h.dropped or h.failed_batches or h.stopped_reason
|
|
71
|
+
if h.dropped or h.failed_batches or h.stopped_reason or (
|
|
72
|
+
h.sink and (h.sink.dropped or h.sink.failed)
|
|
73
|
+
):
|
|
71
74
|
... # raise an alert; logs were silently lost
|
|
72
75
|
|
|
76
|
+
``sink`` is the configured sink's own :class:`~log_foundry.sinks.base.SinkLosses` — loss the
|
|
77
|
+
*sink* absorbed rather than the worker, which the worker's counters cannot see (SPEC-026).
|
|
78
|
+
It is ``None`` when no worker exists and when the sink reports nothing, since ``losses()`` is
|
|
79
|
+
optional. Its ``dropped`` is not the worker's: the worker's is backpressure at the queue, the
|
|
80
|
+
sink's is an event that never reached the wire — usually one too large to ever fit, and for the
|
|
81
|
+
sinks whose client owns a local buffer (Kafka, Pub/Sub) also what that buffer refused. The
|
|
82
|
+
stderr line names which. Its ``failed`` is an upper bound on loss, not a count of it: a sink
|
|
83
|
+
that raises on total failure counts the attempt *and* hands the batch back, and the worker's
|
|
84
|
+
retry may then deliver it.
|
|
85
|
+
|
|
73
86
|
A process that has never logged has no worker, and asking after its health does not create
|
|
74
87
|
one — the snapshot is simply zeroed. Valid after :func:`shutdown`, which leaves the final
|
|
75
88
|
counters readable.
|
|
@@ -96,6 +109,8 @@ def shutdown() -> None:
|
|
|
96
109
|
|
|
97
110
|
__all__ = [
|
|
98
111
|
"Health",
|
|
112
|
+
"SinkDeliveryError",
|
|
113
|
+
"SinkLosses",
|
|
99
114
|
"__version__",
|
|
100
115
|
"configure",
|
|
101
116
|
"continue_trace",
|
|
@@ -41,10 +41,12 @@ class SinkDeliveryError(Exception):
|
|
|
41
41
|
class SinkLosses(NamedTuple):
|
|
42
42
|
"""What a sink discarded or could not confirm, cumulative for its lifetime (SPEC-026 FR-002).
|
|
43
43
|
|
|
44
|
-
Two fields rather than one because the remedies differ
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
Two fields rather than one because the remedies differ. ``dropped`` is an event the sink
|
|
45
|
+
discarded *before* attempting delivery — usually one the destination could never have accepted
|
|
46
|
+
as built (an oversized record), so the fix is upstream in what the application logs; for a sink
|
|
47
|
+
whose client owns a local buffer it also covers what that buffer refused, which is
|
|
48
|
+
backpressure. The stderr line names which. ``failed`` means delivery was attempted and the
|
|
49
|
+
destination did not confirm it, so the fix is the destination or the network.
|
|
48
50
|
|
|
49
51
|
``failed`` is an **upper bound** on loss, not a count of it. A sink that also raises on total
|
|
50
52
|
failure counts the attempt here *and* hands the batch back to the worker, whose retry may
|
|
@@ -55,7 +55,9 @@ class GooglePubSubSink:
|
|
|
55
55
|
future = self.client.publish(self.topic, data=json.dumps(event).encode("utf-8"))
|
|
56
56
|
except Exception as err: # isolation boundary: one event must not fail the batch
|
|
57
57
|
self.rejected += 1
|
|
58
|
-
|
|
58
|
+
# "refused" vs "unconfirmed" below: the two counters mean different things and
|
|
59
|
+
# the line is the only place an operator sees which one moved.
|
|
60
|
+
_diag.lost("event", 1, f"GooglePubSubSink refused the publish, {type(err).__name__}")
|
|
59
61
|
continue
|
|
60
62
|
self._futures.append(future)
|
|
61
63
|
published += 1
|
|
@@ -71,5 +73,7 @@ class GooglePubSubSink:
|
|
|
71
73
|
future.result()
|
|
72
74
|
except Exception as err: # isolation boundary: never crash the worker (FR-011)
|
|
73
75
|
self.failed += 1
|
|
74
|
-
_diag.lost(
|
|
76
|
+
_diag.lost(
|
|
77
|
+
"event", 1, f"GooglePubSubSink publish unconfirmed, {type(err).__name__}"
|
|
78
|
+
)
|
|
75
79
|
self._futures.clear()
|
|
@@ -56,9 +56,9 @@ class Health(NamedTuple):
|
|
|
56
56
|
the worker is gone and nothing further will be delivered (SPEC-019 FR-003).
|
|
57
57
|
sink: The configured sink's own loss counters, or ``None`` when there is no worker or
|
|
58
58
|
the sink reports nothing (SPEC-026 FR-003). Nested rather than folded into the two
|
|
59
|
-
integers above because they
|
|
60
|
-
at
|
|
61
|
-
|
|
59
|
+
integers above because they count different things: ``dropped`` here is backpressure
|
|
60
|
+
at *this* queue, ``dropped`` on the sink is an event that never reached the wire,
|
|
61
|
+
and one number would make the remedies indistinguishable.
|
|
62
62
|
"""
|
|
63
63
|
|
|
64
64
|
queued: int
|
|
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
|
{log_foundry-0.10.2.dev18 → log_foundry-0.10.2.dev19}/src/log_foundry/sinks/elasticsearch.py
RENAMED
|
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
|