log-foundry 0.10.2.dev40__tar.gz → 0.10.2.dev42__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.dev40 → log_foundry-0.10.2.dev42}/PKG-INFO +62 -9
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/README.md +61 -8
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/pyproject.toml +1 -1
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/__init__.py +7 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/_lifecycle.py +4 -3
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/api.py +1 -1
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/config.py +9 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/context.py +33 -3
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/decorator.py +1 -1
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_socket.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/base.py +36 -1
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/clickhouse.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/eventhubs.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/filtering.py +4 -4
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/firehose.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/http.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/kinesis.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/logstash.py +5 -5
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/mongodb.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/multi.py +4 -4
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/postgres.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/rabbitmq.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/redis.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sentry.py +10 -10
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sns.py +2 -2
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sqs.py +3 -3
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/syslog.py +4 -4
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/transform.py +4 -4
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/worker.py +1 -1
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/LICENSE +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/_diag.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/console.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/ids.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/model.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/py.typed +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sanitize.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/__init__.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_batch.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_chunk.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_retry.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_time.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/callback.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/datadog.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/elasticsearch.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/file.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/honeycomb.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/kafka.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/logging_sink.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/loki.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/nats.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/newrelic.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/pubsub.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/splunk.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sqlite.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/stdout.py +0 -0
- {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/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.dev42
|
|
4
4
|
Summary: Generate logs for your console and JSON events for downstream consumption.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -396,6 +396,7 @@ def handler(event, context):
|
|
|
396
396
|
| `current_traceparent()` | This span as a W3C `traceparent` string, or `None` if no span is active. |
|
|
397
397
|
| `current_trace_context()` | `(trace_id, span_id)`, for when moving two fields beats moving a string. |
|
|
398
398
|
| `current_baggage_header()` | Current baggage in W3C `baggage` format (`""` when empty). |
|
|
399
|
+
| `get_baggage()` | Current baggage as a `dict`. A shallow copy — rebinding a key does not reach the library, but a nested mutable value is shared. |
|
|
399
400
|
| `reset_context()` | Clear baggage and any adopted context. `@trace` users do not need it. Never raises. |
|
|
400
401
|
|
|
401
402
|
Details worth knowing:
|
|
@@ -465,14 +466,20 @@ undoing the erasure. It never raises.
|
|
|
465
466
|
A **sink** is the swappable output transport — any object satisfying the `Sink` protocol. It
|
|
466
467
|
receives already-built, batched event dicts and knows nothing about spans or context:
|
|
467
468
|
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
def close(self) -> None: ...
|
|
469
|
+
```
|
|
470
|
+
emit(batch: list[dict[str, object]]) -> None
|
|
471
|
+
close() -> None
|
|
472
472
|
```
|
|
473
473
|
|
|
474
|
+
Satisfy it **structurally** — any object with those two methods is a sink, and every sink shipped
|
|
475
|
+
here is one. You do not need to inherit. If you prefer to, `from log_foundry import Sink` gives you
|
|
476
|
+
the protocol to annotate against or subclass; both methods are abstract, so a subclass that
|
|
477
|
+
misspells `emit` fails at construction rather than silently accepting every batch and delivering
|
|
478
|
+
nothing.
|
|
479
|
+
|
|
474
480
|
Wire one up by passing an instance to `configure(sink=...)`; if you never do, the first decorated
|
|
475
|
-
call falls back to `StdoutSink()`.
|
|
481
|
+
call falls back to `StdoutSink()`. The **protocol** is a top-level export, alongside
|
|
482
|
+
`SinkDeliveryError`, `SinkLosses` and `read_losses`; the **concrete sinks** are not, so import each
|
|
476
483
|
from its own module, e.g. `from log_foundry.sinks.sqs import SQSSink`.
|
|
477
484
|
|
|
478
485
|
A few conventions hold across every sink below:
|
|
@@ -728,11 +735,14 @@ loss-reporting apparatus is built on them:
|
|
|
728
735
|
whole operation that assumes exclusivity. If it holds none, you need do nothing. The library
|
|
729
736
|
cannot serialize this for you: it does not own the calling thread.
|
|
730
737
|
|
|
738
|
+
- **The batch is borrowed, not given.** The list and the dicts in it may go to other sinks after
|
|
739
|
+
you — `MultiSink` hands the same objects to every child in turn — so copy before you redact or
|
|
740
|
+
reshape. A child that cleared the list in place left the next child with nothing, and no error
|
|
741
|
+
anywhere.
|
|
731
742
|
- **Raise when you delivered none of the batch**, after your own retries are spent. That is the
|
|
732
743
|
signal the worker's bounded retry and `health().failed_batches` depend on, and the one case where
|
|
733
744
|
a retry cannot duplicate anything: nothing landed downstream. Raise `SinkDeliveryError` (from
|
|
734
|
-
`log_foundry
|
|
735
|
-
propagates.
|
|
745
|
+
`log_foundry`) or any exception of your own — the contract is that *something* propagates.
|
|
736
746
|
- **Do not raise when you delivered some of it.** The worker retries whole batches, so raising on a
|
|
737
747
|
partial success re-delivers the records that already arrived, and duplicates downstream are worse
|
|
738
748
|
than a counted loss.
|
|
@@ -756,7 +766,7 @@ separate from the transport one so a poll never waits on an in-flight send:
|
|
|
756
766
|
|
|
757
767
|
```python
|
|
758
768
|
import threading
|
|
759
|
-
from log_foundry
|
|
769
|
+
from log_foundry import SinkDeliveryError, SinkLosses
|
|
760
770
|
|
|
761
771
|
class MySink:
|
|
762
772
|
def __init__(self) -> None:
|
|
@@ -764,6 +774,7 @@ class MySink:
|
|
|
764
774
|
self._closed = False
|
|
765
775
|
self._lock = threading.Lock() # transport state
|
|
766
776
|
self._counter_lock = threading.Lock() # counters only, never held across I/O
|
|
777
|
+
self.log_foundry_stop_signal: threading.Event | None = None # optional; see below
|
|
767
778
|
|
|
768
779
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
769
780
|
if not batch:
|
|
@@ -799,6 +810,48 @@ class MySink:
|
|
|
799
810
|
simply contributes nothing to `health().sink`. `emit([])` must be a no-op: an empty batch has not
|
|
800
811
|
failed to deliver.
|
|
801
812
|
|
|
813
|
+
`log_foundry_stop_signal` is optional in the same way, and is an attribute rather than a method.
|
|
814
|
+
Declare it as a plain `threading.Event | None` initialised to `None` and the library assigns the
|
|
815
|
+
worker's shutdown event to it; leave it out and you are simply never offered one. **Honour it in
|
|
816
|
+
your retry backoff** — pass it to `Event.wait(timeout)` instead of calling `time.sleep`:
|
|
817
|
+
|
|
818
|
+
```python
|
|
819
|
+
if self.log_foundry_stop_signal is not None:
|
|
820
|
+
self.log_foundry_stop_signal.wait(delay) # returns early when shutdown starts
|
|
821
|
+
else:
|
|
822
|
+
time.sleep(delay)
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
There is one drain thread, so your backoff pauses *all* log delivery, and it is held across
|
|
826
|
+
`shutdown()` — which joins that thread. A sink that sleeps through a 30-second backoff holds
|
|
827
|
+
process exit for 30 seconds. The name is prefixed because the library assigns this attribute onto
|
|
828
|
+
an object it does not own: a bare `stop_signal` would silently overwrite one you already had.
|
|
829
|
+
|
|
830
|
+
If you write a **wrapper** sink, forward it to whatever actually holds the retry loop — a plain
|
|
831
|
+
attribute on the wrapper is assigned, stops there, and the inner sink never sees it. Measured: a
|
|
832
|
+
wrapper built from the leaf template above left an inner sink's 4-second backoff uninterrupted,
|
|
833
|
+
`shutdown(timeout=30)` ran the full 30 seconds, `stopped_reason` read `"ShutdownTimeout"` and the
|
|
834
|
+
sink was left open — against 0.00 s for the same sink configured directly. Use a property:
|
|
835
|
+
|
|
836
|
+
```python
|
|
837
|
+
class MyWrapper:
|
|
838
|
+
def __init__(self, inner) -> None:
|
|
839
|
+
self._inner = inner
|
|
840
|
+
self._stop_signal: threading.Event | None = None
|
|
841
|
+
|
|
842
|
+
@property
|
|
843
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
844
|
+
return self._stop_signal
|
|
845
|
+
|
|
846
|
+
@log_foundry_stop_signal.setter
|
|
847
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
848
|
+
self._stop_signal = signal
|
|
849
|
+
self._inner.log_foundry_stop_signal = signal # the one line that matters
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
Every wrapper shipped here — `MultiSink`, `FilteringSink`, `TransformSink`, `SyslogSink`,
|
|
853
|
+
`LogstashSink`, `SentrySink` — does exactly this.
|
|
854
|
+
|
|
802
855
|
### Flushing and shutdown
|
|
803
856
|
|
|
804
857
|
Delivery is off the hot path. When a span ends, its events are handed to a per-process
|
|
@@ -360,6 +360,7 @@ def handler(event, context):
|
|
|
360
360
|
| `current_traceparent()` | This span as a W3C `traceparent` string, or `None` if no span is active. |
|
|
361
361
|
| `current_trace_context()` | `(trace_id, span_id)`, for when moving two fields beats moving a string. |
|
|
362
362
|
| `current_baggage_header()` | Current baggage in W3C `baggage` format (`""` when empty). |
|
|
363
|
+
| `get_baggage()` | Current baggage as a `dict`. A shallow copy — rebinding a key does not reach the library, but a nested mutable value is shared. |
|
|
363
364
|
| `reset_context()` | Clear baggage and any adopted context. `@trace` users do not need it. Never raises. |
|
|
364
365
|
|
|
365
366
|
Details worth knowing:
|
|
@@ -429,14 +430,20 @@ undoing the erasure. It never raises.
|
|
|
429
430
|
A **sink** is the swappable output transport — any object satisfying the `Sink` protocol. It
|
|
430
431
|
receives already-built, batched event dicts and knows nothing about spans or context:
|
|
431
432
|
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def close(self) -> None: ...
|
|
433
|
+
```
|
|
434
|
+
emit(batch: list[dict[str, object]]) -> None
|
|
435
|
+
close() -> None
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
+
Satisfy it **structurally** — any object with those two methods is a sink, and every sink shipped
|
|
439
|
+
here is one. You do not need to inherit. If you prefer to, `from log_foundry import Sink` gives you
|
|
440
|
+
the protocol to annotate against or subclass; both methods are abstract, so a subclass that
|
|
441
|
+
misspells `emit` fails at construction rather than silently accepting every batch and delivering
|
|
442
|
+
nothing.
|
|
443
|
+
|
|
438
444
|
Wire one up by passing an instance to `configure(sink=...)`; if you never do, the first decorated
|
|
439
|
-
call falls back to `StdoutSink()`.
|
|
445
|
+
call falls back to `StdoutSink()`. The **protocol** is a top-level export, alongside
|
|
446
|
+
`SinkDeliveryError`, `SinkLosses` and `read_losses`; the **concrete sinks** are not, so import each
|
|
440
447
|
from its own module, e.g. `from log_foundry.sinks.sqs import SQSSink`.
|
|
441
448
|
|
|
442
449
|
A few conventions hold across every sink below:
|
|
@@ -692,11 +699,14 @@ loss-reporting apparatus is built on them:
|
|
|
692
699
|
whole operation that assumes exclusivity. If it holds none, you need do nothing. The library
|
|
693
700
|
cannot serialize this for you: it does not own the calling thread.
|
|
694
701
|
|
|
702
|
+
- **The batch is borrowed, not given.** The list and the dicts in it may go to other sinks after
|
|
703
|
+
you — `MultiSink` hands the same objects to every child in turn — so copy before you redact or
|
|
704
|
+
reshape. A child that cleared the list in place left the next child with nothing, and no error
|
|
705
|
+
anywhere.
|
|
695
706
|
- **Raise when you delivered none of the batch**, after your own retries are spent. That is the
|
|
696
707
|
signal the worker's bounded retry and `health().failed_batches` depend on, and the one case where
|
|
697
708
|
a retry cannot duplicate anything: nothing landed downstream. Raise `SinkDeliveryError` (from
|
|
698
|
-
`log_foundry
|
|
699
|
-
propagates.
|
|
709
|
+
`log_foundry`) or any exception of your own — the contract is that *something* propagates.
|
|
700
710
|
- **Do not raise when you delivered some of it.** The worker retries whole batches, so raising on a
|
|
701
711
|
partial success re-delivers the records that already arrived, and duplicates downstream are worse
|
|
702
712
|
than a counted loss.
|
|
@@ -720,7 +730,7 @@ separate from the transport one so a poll never waits on an in-flight send:
|
|
|
720
730
|
|
|
721
731
|
```python
|
|
722
732
|
import threading
|
|
723
|
-
from log_foundry
|
|
733
|
+
from log_foundry import SinkDeliveryError, SinkLosses
|
|
724
734
|
|
|
725
735
|
class MySink:
|
|
726
736
|
def __init__(self) -> None:
|
|
@@ -728,6 +738,7 @@ class MySink:
|
|
|
728
738
|
self._closed = False
|
|
729
739
|
self._lock = threading.Lock() # transport state
|
|
730
740
|
self._counter_lock = threading.Lock() # counters only, never held across I/O
|
|
741
|
+
self.log_foundry_stop_signal: threading.Event | None = None # optional; see below
|
|
731
742
|
|
|
732
743
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
733
744
|
if not batch:
|
|
@@ -763,6 +774,48 @@ class MySink:
|
|
|
763
774
|
simply contributes nothing to `health().sink`. `emit([])` must be a no-op: an empty batch has not
|
|
764
775
|
failed to deliver.
|
|
765
776
|
|
|
777
|
+
`log_foundry_stop_signal` is optional in the same way, and is an attribute rather than a method.
|
|
778
|
+
Declare it as a plain `threading.Event | None` initialised to `None` and the library assigns the
|
|
779
|
+
worker's shutdown event to it; leave it out and you are simply never offered one. **Honour it in
|
|
780
|
+
your retry backoff** — pass it to `Event.wait(timeout)` instead of calling `time.sleep`:
|
|
781
|
+
|
|
782
|
+
```python
|
|
783
|
+
if self.log_foundry_stop_signal is not None:
|
|
784
|
+
self.log_foundry_stop_signal.wait(delay) # returns early when shutdown starts
|
|
785
|
+
else:
|
|
786
|
+
time.sleep(delay)
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
There is one drain thread, so your backoff pauses *all* log delivery, and it is held across
|
|
790
|
+
`shutdown()` — which joins that thread. A sink that sleeps through a 30-second backoff holds
|
|
791
|
+
process exit for 30 seconds. The name is prefixed because the library assigns this attribute onto
|
|
792
|
+
an object it does not own: a bare `stop_signal` would silently overwrite one you already had.
|
|
793
|
+
|
|
794
|
+
If you write a **wrapper** sink, forward it to whatever actually holds the retry loop — a plain
|
|
795
|
+
attribute on the wrapper is assigned, stops there, and the inner sink never sees it. Measured: a
|
|
796
|
+
wrapper built from the leaf template above left an inner sink's 4-second backoff uninterrupted,
|
|
797
|
+
`shutdown(timeout=30)` ran the full 30 seconds, `stopped_reason` read `"ShutdownTimeout"` and the
|
|
798
|
+
sink was left open — against 0.00 s for the same sink configured directly. Use a property:
|
|
799
|
+
|
|
800
|
+
```python
|
|
801
|
+
class MyWrapper:
|
|
802
|
+
def __init__(self, inner) -> None:
|
|
803
|
+
self._inner = inner
|
|
804
|
+
self._stop_signal: threading.Event | None = None
|
|
805
|
+
|
|
806
|
+
@property
|
|
807
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
808
|
+
return self._stop_signal
|
|
809
|
+
|
|
810
|
+
@log_foundry_stop_signal.setter
|
|
811
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
812
|
+
self._stop_signal = signal
|
|
813
|
+
self._inner.log_foundry_stop_signal = signal # the one line that matters
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Every wrapper shipped here — `MultiSink`, `FilteringSink`, `TransformSink`, `SyslogSink`,
|
|
817
|
+
`LogstashSink`, `SentrySink` — does exactly this.
|
|
818
|
+
|
|
766
819
|
### Flushing and shutdown
|
|
767
820
|
|
|
768
821
|
Delivery is off the hot path. When a span ends, its events are handed to a per-process
|
|
@@ -4,15 +4,16 @@ from importlib.metadata import PackageNotFoundError
|
|
|
4
4
|
from importlib.metadata import version as _dist_version
|
|
5
5
|
|
|
6
6
|
from log_foundry.api import critical, debug, error, info, set_baggage, warning
|
|
7
|
-
from log_foundry.config import configure, get_config
|
|
7
|
+
from log_foundry.config import Config, configure, get_config
|
|
8
8
|
from log_foundry.context import (
|
|
9
9
|
current_baggage_header,
|
|
10
10
|
current_trace_context,
|
|
11
11
|
current_traceparent,
|
|
12
|
+
get_baggage,
|
|
12
13
|
reset_context,
|
|
13
14
|
)
|
|
14
15
|
from log_foundry.decorator import continue_trace, trace
|
|
15
|
-
from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
|
|
16
|
+
from log_foundry.sinks.base import Sink, SinkDeliveryError, SinkLosses, read_losses
|
|
16
17
|
from log_foundry.worker import DEFAULT_SHUTDOWN_TIMEOUT, Health
|
|
17
18
|
|
|
18
19
|
try:
|
|
@@ -156,7 +157,9 @@ def shutdown(timeout: float | None = DEFAULT_SHUTDOWN_TIMEOUT) -> None:
|
|
|
156
157
|
|
|
157
158
|
__all__ = [
|
|
158
159
|
"DEFAULT_SHUTDOWN_TIMEOUT",
|
|
160
|
+
"Config",
|
|
159
161
|
"Health",
|
|
162
|
+
"Sink",
|
|
160
163
|
"SinkDeliveryError",
|
|
161
164
|
"SinkLosses",
|
|
162
165
|
"__version__",
|
|
@@ -169,9 +172,11 @@ __all__ = [
|
|
|
169
172
|
"debug",
|
|
170
173
|
"error",
|
|
171
174
|
"flush",
|
|
175
|
+
"get_baggage",
|
|
172
176
|
"get_config",
|
|
173
177
|
"health",
|
|
174
178
|
"info",
|
|
179
|
+
"read_losses",
|
|
175
180
|
"reset_context",
|
|
176
181
|
"set_baggage",
|
|
177
182
|
"shutdown",
|
|
@@ -165,11 +165,12 @@ def offer_stop_signal(sink: Sink, stop: threading.Event) -> None:
|
|
|
165
165
|
None.
|
|
166
166
|
|
|
167
167
|
Raises:
|
|
168
|
-
None. A sink whose ``
|
|
168
|
+
None. A sink whose ``log_foundry_stop_signal`` is a read-only property, or whose
|
|
169
|
+
``__setattr__``
|
|
169
170
|
objects, loses interruptibility rather than preventing the caller from proceeding.
|
|
170
171
|
"""
|
|
171
172
|
try:
|
|
172
|
-
if hasattr(sink, "
|
|
173
|
-
sink.
|
|
173
|
+
if hasattr(sink, "log_foundry_stop_signal"):
|
|
174
|
+
sink.log_foundry_stop_signal = stop
|
|
174
175
|
except Exception as exc:
|
|
175
176
|
_diag.absorbed("handing the sink its stop signal", exc, "its backoff stays uninterruptible")
|
|
@@ -72,7 +72,7 @@ def _log(level: str, message: str, echo: bool, fields: dict[str, object]) -> Non
|
|
|
72
72
|
None. A logging call must never hand the application an exception from a destination
|
|
73
73
|
it never chose to talk to; absorbed faults are reported through ``_diag``.
|
|
74
74
|
"""
|
|
75
|
-
baggage = context.
|
|
75
|
+
baggage = context._live_baggage()
|
|
76
76
|
span = context.current_span()
|
|
77
77
|
event: dict[str, object] | None = None
|
|
78
78
|
if span is not None:
|
|
@@ -181,13 +181,20 @@ def _swap_live_sink(sink: Sink) -> None:
|
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
def get_config() -> Config:
|
|
184
|
-
"""Returns the current global config
|
|
184
|
+
"""Returns the current global config, for reading.
|
|
185
|
+
|
|
186
|
+
**Mutating what this returns is unsupported and will raise from 1.0** (SPEC-034 FR-003).
|
|
187
|
+
Today it hands back the live singleton, so assigning to it retargets what the config
|
|
188
|
+
*reports* while every event continues to the sink the worker already captured, and assigning
|
|
189
|
+
a ceiling bypasses the validation :func:`configure` performs — ``max_value_bytes = 0`` is
|
|
190
|
+
accepted and empties every event it touches. Both measured. :func:`configure` is the only
|
|
191
|
+
supported route to a change.
|
|
185
192
|
|
|
186
193
|
Args:
|
|
187
194
|
None.
|
|
188
195
|
|
|
189
196
|
Returns:
|
|
190
|
-
The process-wide :class:`Config
|
|
197
|
+
The process-wide :class:`Config`, to be treated as read-only.
|
|
191
198
|
|
|
192
199
|
Raises:
|
|
193
200
|
None.
|
|
@@ -96,18 +96,48 @@ def pop_span(token: contextvars.Token[tuple[Span, ...]]) -> None:
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def get_baggage() -> dict[str, object]:
|
|
99
|
-
"""Returns the current trace's baggage
|
|
99
|
+
"""Returns a copy of the current trace's baggage.
|
|
100
100
|
|
|
101
101
|
The trace's own keys are discarded when the enclosing root span closes, restoring the
|
|
102
102
|
baggage in effect before it, so a later trace sees only a process-level default set
|
|
103
103
|
before any span opened. With no span open at all nothing releases them and they
|
|
104
104
|
accumulate for the life of the context; :func:`reset_context` is the release there.
|
|
105
105
|
|
|
106
|
+
It is a **shallow** copy (SPEC-034 FR-005). This became public at 1.0, and a public accessor
|
|
107
|
+
that hands out the live mapping documented as "do not mutate" is FR-003's ``get_config()``
|
|
108
|
+
defect under another name. Shallow rather than deep because :func:`set_baggage` accepts
|
|
109
|
+
arbitrary values: a deep copy would run user objects through ``copy.deepcopy`` inside a
|
|
110
|
+
getter that must never raise, which trades a narrow sharing bound for a wide new failure —
|
|
111
|
+
and the library never mutates a baggage value either, so the sharing is only reachable
|
|
112
|
+
through the caller's own object. The library's own hot path does not pay even for the
|
|
113
|
+
shallow copy: :func:`_live_baggage` is the internal read, for the reason FR-003 AC-6 gives.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
None.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
A shallow copy of the baggage mapping. Rebinding a key does not affect the library; a
|
|
120
|
+
nested mutable value is shared, and mutating one *does* reach every later event.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
None.
|
|
124
|
+
"""
|
|
125
|
+
return dict(_baggage.get())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _live_baggage() -> dict[str, object]:
|
|
129
|
+
"""Returns the baggage mapping itself, for callers inside the package.
|
|
130
|
+
|
|
131
|
+
:func:`api._log` reads baggage once per event, so a copy here would allocate per event —
|
|
132
|
+
the cost FR-003 AC-6 measures and refuses on the config's own hot path. Callers must treat
|
|
133
|
+
the result as read-only; the freeze is a guarantee to the library's *users*, not one the
|
|
134
|
+
library needs against itself.
|
|
135
|
+
|
|
106
136
|
Args:
|
|
107
137
|
None.
|
|
108
138
|
|
|
109
139
|
Returns:
|
|
110
|
-
The baggage mapping
|
|
140
|
+
The live baggage mapping.
|
|
111
141
|
|
|
112
142
|
Raises:
|
|
113
143
|
None.
|
|
@@ -300,7 +330,7 @@ def current_baggage_header() -> str:
|
|
|
300
330
|
Raises:
|
|
301
331
|
None.
|
|
302
332
|
"""
|
|
303
|
-
return format_baggage_header(
|
|
333
|
+
return format_baggage_header(_live_baggage())
|
|
304
334
|
|
|
305
335
|
|
|
306
336
|
def format_baggage_header(baggage: dict[str, object]) -> str:
|
|
@@ -853,7 +853,7 @@ def _close_span(span: Span, status: str, exc: BaseException | None) -> None:
|
|
|
853
853
|
guard.
|
|
854
854
|
"""
|
|
855
855
|
span.events.append(end_event(span, status, exc))
|
|
856
|
-
backfill_baggage(span, context.
|
|
856
|
+
backfill_baggage(span, context._live_baggage())
|
|
857
857
|
_flush(span)
|
|
858
858
|
|
|
859
859
|
|
|
@@ -133,7 +133,7 @@ class SocketTransport:
|
|
|
133
133
|
self._sock: socket.socket | None = None
|
|
134
134
|
self.failed = 0
|
|
135
135
|
self._counter_lock = threading.Lock()
|
|
136
|
-
self.
|
|
136
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
137
137
|
self._lock = threading.Lock()
|
|
138
138
|
self._closed = False
|
|
139
139
|
|
|
@@ -243,7 +243,7 @@ class SocketTransport:
|
|
|
243
243
|
except OSError as err:
|
|
244
244
|
self._reset()
|
|
245
245
|
if attempt < self._max_retries:
|
|
246
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
246
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
247
247
|
continue
|
|
248
248
|
with self._counter_lock:
|
|
249
249
|
self.failed += 1
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from abc import abstractmethod
|
|
5
6
|
from typing import NamedTuple, Protocol, runtime_checkable
|
|
6
7
|
|
|
7
8
|
__all__ = ["Sink", "SinkDeliveryError", "SinkLosses", "read_losses"]
|
|
@@ -58,6 +59,34 @@ class Sink(Protocol):
|
|
|
58
59
|
sink written against the pre-SPEC-026 interface satisfying this one. :func:`read_losses` is
|
|
59
60
|
the probe.
|
|
60
61
|
|
|
62
|
+
:meth:`emit` and :meth:`close` are ``@abstractmethod`` (SPEC-034 FR-005). A ``Protocol`` is
|
|
63
|
+
normally satisfied *structurally* and that is still how every shipped sink satisfies this
|
|
64
|
+
one — but making it a public export invites inheritance, and an incomplete subclass of a
|
|
65
|
+
protocol whose members have empty bodies instantiates happily and returns ``None`` from the
|
|
66
|
+
method it failed to define. Reproduced before the decorators were added: one typo
|
|
67
|
+
(``def emmit``) and three events were gone with ``flush()`` reporting ``True`` and every
|
|
68
|
+
counter at zero — the "sink the worker believes" failure this file exists to prevent.
|
|
69
|
+
``mypy`` already refused it; only the runtime did not. Inheriting is still not required, and
|
|
70
|
+
``isinstance`` remains structural.
|
|
71
|
+
|
|
72
|
+
A fourth is optional in the same way and is an attribute rather than a method:
|
|
73
|
+
``log_foundry_stop_signal``. A sink that defines it — a plain
|
|
74
|
+
``threading.Event | None``, initialised to ``None`` — is handed the worker's shutdown event
|
|
75
|
+
whenever the library takes ownership of it, and a sink that does not is simply never
|
|
76
|
+
offered one. **Honouring it is how a retrying sink stays interruptible** (SPEC-027): the
|
|
77
|
+
worker owns a single drain thread, so a sink's backoff is a global pause on log delivery
|
|
78
|
+
held across ``shutdown()``, which joins that thread. Pass it to ``sinks/_retry.wait`` — or
|
|
79
|
+
wait on it directly — rather than calling ``time.sleep``, or a shutdown cannot cut the
|
|
80
|
+
wait short and a slow destination holds process exit for the length of its own backoff.
|
|
81
|
+
A **wrapper** sink must forward it to whatever actually holds the retry loop; set on a
|
|
82
|
+
wrapper and stopped there, the signal reaches nothing.
|
|
83
|
+
|
|
84
|
+
The name is namespaced deliberately (SPEC-034 FR-006). The library assigns this attribute
|
|
85
|
+
onto an object it does not own, by ``hasattr`` probe, so a bare ``stop_signal`` — which is
|
|
86
|
+
what shipped before 1.0 — silently overwrote any attribute of that name a third-party sink
|
|
87
|
+
already used, with a ``threading.Event``. Reproduced. A prefixed name cannot collide by
|
|
88
|
+
accident, and the cost is one verbose attribute on the sinks that want interruptibility.
|
|
89
|
+
|
|
61
90
|
"Safe to call during an emit" is a concurrency requirement once :meth:`emit` is (SPEC-028
|
|
62
91
|
FR-003). The shipped sinks keep their loss counters under a **dedicated** lock, separate
|
|
63
92
|
from whatever guards their transport: an increment is a read-modify-write that Python does
|
|
@@ -68,6 +97,7 @@ class Sink(Protocol):
|
|
|
68
97
|
both, the order is always transport then counter, never the reverse.
|
|
69
98
|
"""
|
|
70
99
|
|
|
100
|
+
@abstractmethod
|
|
71
101
|
def emit(self, batch: list[dict[str, object]]) -> None:
|
|
72
102
|
"""Ships a batch of serialized event dicts.
|
|
73
103
|
|
|
@@ -111,7 +141,11 @@ class Sink(Protocol):
|
|
|
111
141
|
docstring and a test holds it to it.
|
|
112
142
|
|
|
113
143
|
Args:
|
|
114
|
-
batch: The events to ship.
|
|
144
|
+
batch: The events to ship. **Borrowed, not given** — the list and the dicts inside it
|
|
145
|
+
may be handed to other sinks afterwards, which ``MultiSink`` does to every child in
|
|
146
|
+
turn, so mutating either in place silently changes or empties what a later sink
|
|
147
|
+
receives. Copy before reshaping or redacting. Reproduced: a child that cleared the
|
|
148
|
+
list left the next child with nothing and no error anywhere. ``emit([])`` is a no-op and never raises, since an empty
|
|
115
149
|
batch has not failed to deliver — closed or not.
|
|
116
150
|
|
|
117
151
|
Returns:
|
|
@@ -122,6 +156,7 @@ class Sink(Protocol):
|
|
|
122
156
|
"""
|
|
123
157
|
...
|
|
124
158
|
|
|
159
|
+
@abstractmethod
|
|
125
160
|
def close(self) -> None:
|
|
126
161
|
"""Flushes and releases any resources.
|
|
127
162
|
|
|
@@ -83,7 +83,7 @@ class ClickHouseSink:
|
|
|
83
83
|
self._table = valid_identifier(table)
|
|
84
84
|
self._chunk_size = chunk_size
|
|
85
85
|
self.max_retries = max(max_retries, 0)
|
|
86
|
-
self.
|
|
86
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
87
87
|
self.failed = 0
|
|
88
88
|
self._closed = False
|
|
89
89
|
self._lock = threading.Lock()
|
|
@@ -198,7 +198,7 @@ class ClickHouseSink:
|
|
|
198
198
|
return 1
|
|
199
199
|
except Exception as err:
|
|
200
200
|
if attempt < self.max_retries:
|
|
201
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
201
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
202
202
|
continue
|
|
203
203
|
with self._counter_lock:
|
|
204
204
|
self.failed += len(rows)
|
|
@@ -64,7 +64,7 @@ class AzureEventHubsSink:
|
|
|
64
64
|
)
|
|
65
65
|
self.producer = producer
|
|
66
66
|
self.max_retries = max(max_retries, 0)
|
|
67
|
-
self.
|
|
67
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
68
68
|
self.failed = 0
|
|
69
69
|
self.dropped_oversized = 0
|
|
70
70
|
self._counter_lock = threading.Lock()
|
|
@@ -201,7 +201,7 @@ class AzureEventHubsSink:
|
|
|
201
201
|
return 1
|
|
202
202
|
except Exception as err:
|
|
203
203
|
if attempt < self.max_retries:
|
|
204
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
204
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
205
205
|
continue
|
|
206
206
|
with self._counter_lock:
|
|
207
207
|
self.failed += len(event_batch)
|
|
@@ -105,7 +105,7 @@ class FilteringSink:
|
|
|
105
105
|
return True
|
|
106
106
|
|
|
107
107
|
@property
|
|
108
|
-
def
|
|
108
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
109
109
|
"""The worker's shutdown event, forwarded to whatever actually holds the retry loop.
|
|
110
110
|
|
|
111
111
|
The worker sets this on the configured sink (SPEC-027 FR-002), and a wrapper is not
|
|
@@ -124,8 +124,8 @@ class FilteringSink:
|
|
|
124
124
|
"""
|
|
125
125
|
return self._stop_signal
|
|
126
126
|
|
|
127
|
-
@
|
|
128
|
-
def
|
|
127
|
+
@log_foundry_stop_signal.setter
|
|
128
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
129
129
|
"""Forwards the stop signal to the inner sink.
|
|
130
130
|
|
|
131
131
|
Args:
|
|
@@ -139,7 +139,7 @@ class FilteringSink:
|
|
|
139
139
|
"""
|
|
140
140
|
self._stop_signal = signal
|
|
141
141
|
try:
|
|
142
|
-
self._inner.
|
|
142
|
+
self._inner.log_foundry_stop_signal = signal # type: ignore[attr-defined]
|
|
143
143
|
except Exception as err:
|
|
144
144
|
_diag.absorbed(
|
|
145
145
|
"handing the inner sink its stop signal",
|
|
@@ -69,7 +69,7 @@ class FirehoseSink:
|
|
|
69
69
|
self.delivery_stream = delivery_stream
|
|
70
70
|
self.client = client
|
|
71
71
|
self.max_retries = max(max_retries, 0)
|
|
72
|
-
self.
|
|
72
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
73
73
|
self.failed = 0
|
|
74
74
|
self.dropped_oversized = 0
|
|
75
75
|
self.dropped_unadjudicated = 0
|
|
@@ -224,7 +224,7 @@ class FirehoseSink:
|
|
|
224
224
|
if not records:
|
|
225
225
|
return sent
|
|
226
226
|
if attempt < self.max_retries:
|
|
227
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
227
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
228
228
|
if attempt >= self.max_retries:
|
|
229
229
|
with self._counter_lock:
|
|
230
230
|
self.failed += len(records)
|
|
@@ -132,7 +132,7 @@ class HTTPSink:
|
|
|
132
132
|
self.gzip = gzip
|
|
133
133
|
self.max_retries = max(max_retries, 0)
|
|
134
134
|
self.max_retry_after = max_retry_after
|
|
135
|
-
self.
|
|
135
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
136
136
|
self._opener = opener if opener is not None else urllib.request.urlopen
|
|
137
137
|
self.failed = 0
|
|
138
138
|
self.dropped_oversized = 0
|
|
@@ -364,7 +364,7 @@ class HTTPSink:
|
|
|
364
364
|
"""
|
|
365
365
|
server = clamp_server_delay(retry_after, self.max_retry_after)
|
|
366
366
|
delay = server if server is not None else _BACKOFF_BASE * (2**attempt)
|
|
367
|
-
wait(delay, self.
|
|
367
|
+
wait(delay, self.log_foundry_stop_signal)
|
|
368
368
|
|
|
369
369
|
def _abandon(self, reason: str) -> NoReturn:
|
|
370
370
|
"""Counts and logs a request abandoned past the retry bound, then raises (FR-012).
|
|
@@ -78,7 +78,7 @@ class KinesisSink:
|
|
|
78
78
|
self.client = client
|
|
79
79
|
self.partition_key_field = partition_key_field
|
|
80
80
|
self.max_retries = max(max_retries, 0)
|
|
81
|
-
self.
|
|
81
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
82
82
|
self.failed = 0
|
|
83
83
|
self.dropped_oversized = 0
|
|
84
84
|
self.dropped_unadjudicated = 0
|
|
@@ -231,7 +231,7 @@ class KinesisSink:
|
|
|
231
231
|
if not records:
|
|
232
232
|
return sent
|
|
233
233
|
if attempt < self.max_retries:
|
|
234
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
234
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
235
235
|
if attempt >= self.max_retries:
|
|
236
236
|
with self._counter_lock:
|
|
237
237
|
self.failed += len(records)
|
|
@@ -130,7 +130,7 @@ class LogstashSink:
|
|
|
130
130
|
self._socket.close()
|
|
131
131
|
|
|
132
132
|
@property
|
|
133
|
-
def
|
|
133
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
134
134
|
"""The worker's shutdown event, forwarded to whatever actually holds the retry loop.
|
|
135
135
|
|
|
136
136
|
The worker sets this on the configured sink (SPEC-027 FR-002), and a wrapper is not
|
|
@@ -149,8 +149,8 @@ class LogstashSink:
|
|
|
149
149
|
"""
|
|
150
150
|
return self._stop_signal
|
|
151
151
|
|
|
152
|
-
@
|
|
153
|
-
def
|
|
152
|
+
@log_foundry_stop_signal.setter
|
|
153
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
154
154
|
"""Forwards the stop signal to the active backend.
|
|
155
155
|
|
|
156
156
|
Args:
|
|
@@ -164,9 +164,9 @@ class LogstashSink:
|
|
|
164
164
|
"""
|
|
165
165
|
self._stop_signal = signal
|
|
166
166
|
if self._http is not None:
|
|
167
|
-
self._http.
|
|
167
|
+
self._http.log_foundry_stop_signal = signal
|
|
168
168
|
elif self._socket is not None:
|
|
169
|
-
self._socket.
|
|
169
|
+
self._socket.log_foundry_stop_signal = signal
|
|
170
170
|
|
|
171
171
|
@property
|
|
172
172
|
def failed(self) -> int:
|
|
@@ -71,7 +71,7 @@ class MongoDBSink:
|
|
|
71
71
|
self._client = client
|
|
72
72
|
self._collection = client[database][collection]
|
|
73
73
|
self.max_retries = max(max_retries, 0)
|
|
74
|
-
self.
|
|
74
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
75
75
|
self.failed = 0
|
|
76
76
|
self.dropped_oversized = 0
|
|
77
77
|
self._closed = False
|
|
@@ -150,7 +150,7 @@ class MongoDBSink:
|
|
|
150
150
|
) from None
|
|
151
151
|
return
|
|
152
152
|
if attempt < self.max_retries:
|
|
153
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
153
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
154
154
|
continue
|
|
155
155
|
with self._counter_lock:
|
|
156
156
|
self.failed += len(documents)
|
|
@@ -97,7 +97,7 @@ class MultiSink:
|
|
|
97
97
|
raise first_error
|
|
98
98
|
|
|
99
99
|
@property
|
|
100
|
-
def
|
|
100
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
101
101
|
"""The worker's shutdown event, forwarded to whatever actually holds the retry loop.
|
|
102
102
|
|
|
103
103
|
The worker sets this on the configured sink (SPEC-027 FR-002), and a wrapper is not
|
|
@@ -116,8 +116,8 @@ class MultiSink:
|
|
|
116
116
|
"""
|
|
117
117
|
return self._stop_signal
|
|
118
118
|
|
|
119
|
-
@
|
|
120
|
-
def
|
|
119
|
+
@log_foundry_stop_signal.setter
|
|
120
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
121
121
|
"""Forwards the stop signal to every child.
|
|
122
122
|
|
|
123
123
|
Children are not probed with ``hasattr`` first: setting it on a child that never reads
|
|
@@ -135,7 +135,7 @@ class MultiSink:
|
|
|
135
135
|
self._stop_signal = signal
|
|
136
136
|
for sink in self._sinks:
|
|
137
137
|
try:
|
|
138
|
-
sink.
|
|
138
|
+
sink.log_foundry_stop_signal = signal # type: ignore[attr-defined]
|
|
139
139
|
except Exception as err:
|
|
140
140
|
_diag.absorbed(
|
|
141
141
|
"handing a MultiSink child its stop signal",
|
|
@@ -67,7 +67,7 @@ class PostgresSink:
|
|
|
67
67
|
self._table = valid_identifier(table)
|
|
68
68
|
self._chunk_size = chunk_size
|
|
69
69
|
self.max_retries = max(max_retries, 0)
|
|
70
|
-
self.
|
|
70
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
71
71
|
self.failed = 0
|
|
72
72
|
self._closed = False
|
|
73
73
|
self._lock = threading.Lock()
|
|
@@ -156,7 +156,7 @@ class PostgresSink:
|
|
|
156
156
|
except Exception as err:
|
|
157
157
|
self._conn.rollback()
|
|
158
158
|
if attempt < self.max_retries:
|
|
159
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
159
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
160
160
|
continue
|
|
161
161
|
with self._counter_lock:
|
|
162
162
|
self.failed += len(batch)
|
|
@@ -66,7 +66,7 @@ class RabbitMQSink:
|
|
|
66
66
|
self._routing_key = routing_key
|
|
67
67
|
self._url = url
|
|
68
68
|
self._max_retries = max(max_retries, 0)
|
|
69
|
-
self.
|
|
69
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
70
70
|
self._owns_connection = connection is None
|
|
71
71
|
self._connection = connection if connection is not None else self._connect()
|
|
72
72
|
self._channel: Any = None
|
|
@@ -181,7 +181,7 @@ class RabbitMQSink:
|
|
|
181
181
|
except Exception as err:
|
|
182
182
|
self._reset()
|
|
183
183
|
if attempt < self._max_retries:
|
|
184
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
184
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
185
185
|
continue
|
|
186
186
|
with self._counter_lock:
|
|
187
187
|
self.failed += 1
|
|
@@ -59,7 +59,7 @@ class _RedisSink:
|
|
|
59
59
|
client = redis.Redis.from_url(url) if url else redis.Redis()
|
|
60
60
|
self.client = client
|
|
61
61
|
self.max_retries = max(max_retries, 0)
|
|
62
|
-
self.
|
|
62
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
63
63
|
self.failed = 0
|
|
64
64
|
self._counter_lock = threading.Lock()
|
|
65
65
|
self._closed = False
|
|
@@ -118,7 +118,7 @@ class _RedisSink:
|
|
|
118
118
|
return
|
|
119
119
|
except Exception as err:
|
|
120
120
|
if attempt < self.max_retries:
|
|
121
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
121
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
122
122
|
continue
|
|
123
123
|
with self._counter_lock:
|
|
124
124
|
self.failed += len(batch)
|
|
@@ -48,7 +48,7 @@ class SentrySink:
|
|
|
48
48
|
dsn: str | None = None,
|
|
49
49
|
*,
|
|
50
50
|
min_level: str = "ERROR",
|
|
51
|
-
|
|
51
|
+
client: Any = None,
|
|
52
52
|
opener: Any = None,
|
|
53
53
|
max_retries: int = 3,
|
|
54
54
|
) -> None:
|
|
@@ -58,7 +58,7 @@ class SentrySink:
|
|
|
58
58
|
dsn: The Sentry DSN. It is required for the fallback, which needs it to know where to
|
|
59
59
|
POST.
|
|
60
60
|
min_level: The lowest level worth sending.
|
|
61
|
-
|
|
61
|
+
client: A ``sentry_sdk``-shaped object to use instead of importing one.
|
|
62
62
|
opener: A ``urlopen``-shaped callable for the fallback, for tests.
|
|
63
63
|
max_retries: Retries the fallback's HTTP transport makes.
|
|
64
64
|
|
|
@@ -74,10 +74,10 @@ class SentrySink:
|
|
|
74
74
|
self.skipped = 0
|
|
75
75
|
self.transport_errors = 0
|
|
76
76
|
self._counter_lock = threading.Lock()
|
|
77
|
-
self.
|
|
77
|
+
self.client = client if client is not None else _import_sdk()
|
|
78
78
|
self._http: HTTPSink | None = None
|
|
79
79
|
self._auth_header = ""
|
|
80
|
-
if self.
|
|
80
|
+
if self.client is None:
|
|
81
81
|
if dsn is None:
|
|
82
82
|
raise ValueError(
|
|
83
83
|
"SentrySink without sentry-sdk requires a dsn for the HTTP-envelope fallback"
|
|
@@ -143,7 +143,7 @@ class SentrySink:
|
|
|
143
143
|
self._http.close()
|
|
144
144
|
|
|
145
145
|
@property
|
|
146
|
-
def
|
|
146
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
147
147
|
"""The worker's shutdown event, forwarded to whatever actually holds the retry loop.
|
|
148
148
|
|
|
149
149
|
The worker sets this on the configured sink (SPEC-027 FR-002), and a wrapper is not
|
|
@@ -162,8 +162,8 @@ class SentrySink:
|
|
|
162
162
|
"""
|
|
163
163
|
return self._stop_signal
|
|
164
164
|
|
|
165
|
-
@
|
|
166
|
-
def
|
|
165
|
+
@log_foundry_stop_signal.setter
|
|
166
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
167
167
|
"""Forwards the stop signal to the HTTP fallback, which is what waits.
|
|
168
168
|
|
|
169
169
|
Args:
|
|
@@ -177,7 +177,7 @@ class SentrySink:
|
|
|
177
177
|
"""
|
|
178
178
|
self._stop_signal = signal
|
|
179
179
|
if self._http is not None:
|
|
180
|
-
self._http.
|
|
180
|
+
self._http.log_foundry_stop_signal = signal
|
|
181
181
|
|
|
182
182
|
@property
|
|
183
183
|
def failed(self) -> int:
|
|
@@ -236,8 +236,8 @@ class SentrySink:
|
|
|
236
236
|
Only the exception type is ever written (arch §6).
|
|
237
237
|
"""
|
|
238
238
|
try:
|
|
239
|
-
if self.
|
|
240
|
-
self.
|
|
239
|
+
if self.client is not None:
|
|
240
|
+
self.client.capture_event(self._sentry_event(event))
|
|
241
241
|
else:
|
|
242
242
|
self._post_envelope(event)
|
|
243
243
|
except SinkDeliveryError:
|
|
@@ -61,7 +61,7 @@ class SNSSink:
|
|
|
61
61
|
self.topic_arn = topic_arn
|
|
62
62
|
self.client = client
|
|
63
63
|
self.max_retries = max(max_retries, 0)
|
|
64
|
-
self.
|
|
64
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
65
65
|
self.failed = 0
|
|
66
66
|
self.dropped_oversized = 0
|
|
67
67
|
self._counter_lock = threading.Lock()
|
|
@@ -171,7 +171,7 @@ class SNSSink:
|
|
|
171
171
|
failed_ids = {entry["Id"] for entry in failed}
|
|
172
172
|
entries = [entry for entry in entries if entry["Id"] in failed_ids]
|
|
173
173
|
if attempt < self.max_retries:
|
|
174
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
174
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
175
175
|
if attempt >= self.max_retries:
|
|
176
176
|
with self._counter_lock:
|
|
177
177
|
self.failed += len(entries)
|
|
@@ -103,8 +103,8 @@ class SQSSink:
|
|
|
103
103
|
def __init__(
|
|
104
104
|
self,
|
|
105
105
|
queue_url: str,
|
|
106
|
-
client: Any = None,
|
|
107
106
|
*,
|
|
107
|
+
client: Any = None,
|
|
108
108
|
max_retries: int = 3,
|
|
109
109
|
fifo: bool | None = None,
|
|
110
110
|
message_group_id: GroupIdSource = None,
|
|
@@ -144,7 +144,7 @@ class SQSSink:
|
|
|
144
144
|
self.queue_url = queue_url
|
|
145
145
|
self.client = client
|
|
146
146
|
self.max_retries = max(max_retries, 0)
|
|
147
|
-
self.
|
|
147
|
+
self.log_foundry_stop_signal: threading.Event | None = None
|
|
148
148
|
self.fifo = queue_url.endswith(".fifo") if fifo is None else fifo
|
|
149
149
|
self.message_group_id = message_group_id
|
|
150
150
|
self.message_deduplication_id = message_deduplication_id
|
|
@@ -376,7 +376,7 @@ class SQSSink:
|
|
|
376
376
|
return accepted, False
|
|
377
377
|
entries = [entry for entry in entries if entry["Id"] in retryable_ids]
|
|
378
378
|
if attempt < self.max_retries:
|
|
379
|
-
wait(_BACKOFF_BASE * (2**attempt), self.
|
|
379
|
+
wait(_BACKOFF_BASE * (2**attempt), self.log_foundry_stop_signal)
|
|
380
380
|
if attempt >= self.max_retries:
|
|
381
381
|
with self._counter_lock:
|
|
382
382
|
self.failed += len(entries)
|
|
@@ -124,7 +124,7 @@ class SyslogSink:
|
|
|
124
124
|
self._socket.close()
|
|
125
125
|
|
|
126
126
|
@property
|
|
127
|
-
def
|
|
127
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
128
128
|
"""The worker's shutdown event, forwarded to whatever actually holds the retry loop.
|
|
129
129
|
|
|
130
130
|
The worker sets this on the configured sink (SPEC-027 FR-002), and a wrapper is not
|
|
@@ -143,8 +143,8 @@ class SyslogSink:
|
|
|
143
143
|
"""
|
|
144
144
|
return self._stop_signal
|
|
145
145
|
|
|
146
|
-
@
|
|
147
|
-
def
|
|
146
|
+
@log_foundry_stop_signal.setter
|
|
147
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
148
148
|
"""Forwards the stop signal to the socket transport.
|
|
149
149
|
|
|
150
150
|
Args:
|
|
@@ -157,7 +157,7 @@ class SyslogSink:
|
|
|
157
157
|
None.
|
|
158
158
|
"""
|
|
159
159
|
self._stop_signal = signal
|
|
160
|
-
self._socket.
|
|
160
|
+
self._socket.log_foundry_stop_signal = signal
|
|
161
161
|
|
|
162
162
|
@property
|
|
163
163
|
def failed(self) -> int:
|
|
@@ -73,7 +73,7 @@ class TransformSink:
|
|
|
73
73
|
self._inner.emit(transformed)
|
|
74
74
|
|
|
75
75
|
@property
|
|
76
|
-
def
|
|
76
|
+
def log_foundry_stop_signal(self) -> threading.Event | None:
|
|
77
77
|
"""The worker's shutdown event, forwarded to whatever actually holds the retry loop.
|
|
78
78
|
|
|
79
79
|
The worker sets this on the configured sink (SPEC-027 FR-002), and a wrapper is not
|
|
@@ -92,8 +92,8 @@ class TransformSink:
|
|
|
92
92
|
"""
|
|
93
93
|
return self._stop_signal
|
|
94
94
|
|
|
95
|
-
@
|
|
96
|
-
def
|
|
95
|
+
@log_foundry_stop_signal.setter
|
|
96
|
+
def log_foundry_stop_signal(self, signal: threading.Event | None) -> None:
|
|
97
97
|
"""Forwards the stop signal to the inner sink.
|
|
98
98
|
|
|
99
99
|
Args:
|
|
@@ -107,7 +107,7 @@ class TransformSink:
|
|
|
107
107
|
"""
|
|
108
108
|
self._stop_signal = signal
|
|
109
109
|
try:
|
|
110
|
-
self._inner.
|
|
110
|
+
self._inner.log_foundry_stop_signal = signal # type: ignore[attr-defined]
|
|
111
111
|
except Exception as err:
|
|
112
112
|
_diag.absorbed(
|
|
113
113
|
"handing the inner sink its stop signal",
|
|
@@ -251,7 +251,7 @@ class Worker:
|
|
|
251
251
|
None.
|
|
252
252
|
|
|
253
253
|
Raises:
|
|
254
|
-
None. A sink whose ``
|
|
254
|
+
None. A sink whose ``log_foundry_stop_signal`` is a read-only property, or whose
|
|
255
255
|
``__setattr__`` objects, loses interruptibility rather than preventing the worker
|
|
256
256
|
from starting.
|
|
257
257
|
"""
|
|
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.dev40 → log_foundry-0.10.2.dev42}/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
|