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.
Files changed (56) hide show
  1. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/PKG-INFO +62 -9
  2. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/README.md +61 -8
  3. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/__init__.py +7 -2
  5. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/_lifecycle.py +4 -3
  6. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/api.py +1 -1
  7. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/config.py +9 -2
  8. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/context.py +33 -3
  9. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/decorator.py +1 -1
  10. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_socket.py +2 -2
  11. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/base.py +36 -1
  12. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/clickhouse.py +2 -2
  13. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/eventhubs.py +2 -2
  14. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/filtering.py +4 -4
  15. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/firehose.py +2 -2
  16. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/http.py +2 -2
  17. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/kinesis.py +2 -2
  18. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/logstash.py +5 -5
  19. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/mongodb.py +2 -2
  20. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/multi.py +4 -4
  21. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/postgres.py +2 -2
  22. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/rabbitmq.py +2 -2
  23. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/redis.py +2 -2
  24. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sentry.py +10 -10
  25. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sns.py +2 -2
  26. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sqs.py +3 -3
  27. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/syslog.py +4 -4
  28. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/transform.py +4 -4
  29. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/worker.py +1 -1
  30. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/LICENSE +0 -0
  31. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/_diag.py +0 -0
  32. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/console.py +0 -0
  33. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/ids.py +0 -0
  34. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/model.py +0 -0
  35. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/py.typed +0 -0
  36. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sanitize.py +0 -0
  37. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/__init__.py +0 -0
  38. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_batch.py +0 -0
  39. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_chunk.py +0 -0
  40. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_retry.py +0 -0
  41. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/_time.py +0 -0
  42. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/callback.py +0 -0
  43. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/datadog.py +0 -0
  44. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/elasticsearch.py +0 -0
  45. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/file.py +0 -0
  46. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/honeycomb.py +0 -0
  47. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/kafka.py +0 -0
  48. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/logging_sink.py +0 -0
  49. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/loki.py +0 -0
  50. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/nats.py +0 -0
  51. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/newrelic.py +0 -0
  52. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/pubsub.py +0 -0
  53. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/splunk.py +0 -0
  54. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/sqlite.py +0 -0
  55. {log_foundry-0.10.2.dev40 → log_foundry-0.10.2.dev42}/src/log_foundry/sinks/stdout.py +0 -0
  56. {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.dev40
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
- ```python
469
- class Sink(Protocol):
470
- def emit(self, batch: list[dict[str, object]]) -> None: ...
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()`. Sinks are **not** re-exported at the top level import each
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.sinks.base`) or any exception of your own — the contract is that *something*
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.sinks.base import SinkDeliveryError, SinkLosses
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
- ```python
433
- class Sink(Protocol):
434
- def emit(self, batch: list[dict[str, object]]) -> None: ...
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()`. Sinks are **not** re-exported at the top level import each
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.sinks.base`) or any exception of your own — the contract is that *something*
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.sinks.base import SinkDeliveryError, SinkLosses
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
@@ -20,7 +20,7 @@ dependencies = [
20
20
  ]
21
21
 
22
22
  # Optional features. Install with: pip install log-foundry[aws]
23
- version = "0.10.2.dev40"
23
+ version = "0.10.2.dev42"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -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 ``stop_signal`` is a read-only property, or whose ``__setattr__``
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, "stop_signal"):
173
- sink.stop_signal = stop
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.get_baggage()
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 singleton.
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, which must not be mutated in place.
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, to be treated as read-only.
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(get_baggage())
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.get_baggage())
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.stop_signal: threading.Event | None = None
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.stop_signal)
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. ``emit([])`` is a no-op and never raises, since an empty
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.stop_signal: threading.Event | None = None
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.stop_signal)
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.stop_signal: threading.Event | None = None
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.stop_signal)
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 stop_signal(self) -> threading.Event | None:
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
- @stop_signal.setter
128
- def stop_signal(self, signal: threading.Event | None) -> None:
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.stop_signal = signal # type: ignore[attr-defined]
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.stop_signal: threading.Event | None = None
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.stop_signal)
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.stop_signal: threading.Event | None = None
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.stop_signal)
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.stop_signal: threading.Event | None = None
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.stop_signal)
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 stop_signal(self) -> threading.Event | None:
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
- @stop_signal.setter
153
- def stop_signal(self, signal: threading.Event | None) -> None:
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.stop_signal = signal
167
+ self._http.log_foundry_stop_signal = signal
168
168
  elif self._socket is not None:
169
- self._socket.stop_signal = signal
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.stop_signal: threading.Event | None = None
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.stop_signal)
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 stop_signal(self) -> threading.Event | None:
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
- @stop_signal.setter
120
- def stop_signal(self, signal: threading.Event | None) -> None:
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.stop_signal = signal # type: ignore[attr-defined]
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.stop_signal: threading.Event | None = None
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.stop_signal)
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.stop_signal: threading.Event | None = None
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.stop_signal)
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.stop_signal: threading.Event | None = None
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.stop_signal)
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
- sdk: Any = None,
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
- sdk: A ``sentry_sdk``-shaped object to use instead of importing one.
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._sdk = sdk if sdk is not None else _import_sdk()
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._sdk is None:
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 stop_signal(self) -> threading.Event | None:
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
- @stop_signal.setter
166
- def stop_signal(self, signal: threading.Event | None) -> None:
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.stop_signal = signal
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._sdk is not None:
240
- self._sdk.capture_event(self._sentry_event(event))
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.stop_signal: threading.Event | None = None
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.stop_signal)
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.stop_signal: threading.Event | None = None
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.stop_signal)
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 stop_signal(self) -> threading.Event | None:
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
- @stop_signal.setter
147
- def stop_signal(self, signal: threading.Event | None) -> None:
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.stop_signal = signal
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 stop_signal(self) -> threading.Event | None:
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
- @stop_signal.setter
96
- def stop_signal(self, signal: threading.Event | None) -> None:
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.stop_signal = signal # type: ignore[attr-defined]
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 ``stop_signal`` is a read-only property, or 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
  """