log-foundry 0.10.2.dev60__tar.gz → 0.10.2.dev61__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 (59) hide show
  1. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/_lifecycle.py +39 -3
  4. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/decorator.py +2 -2
  5. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/filtering.py +6 -2
  6. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/logstash.py +7 -1
  7. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/multi.py +7 -3
  8. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/sentry.py +5 -3
  9. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/transform.py +6 -2
  10. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/worker.py +2 -2
  11. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/LICENSE +0 -0
  12. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/README.md +0 -0
  13. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/__init__.py +0 -0
  14. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/_diag.py +0 -0
  15. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/_fork.py +0 -0
  16. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/api.py +0 -0
  17. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/config.py +0 -0
  18. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/console.py +0 -0
  19. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/context.py +0 -0
  20. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/ids.py +0 -0
  21. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/model.py +0 -0
  22. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/py.typed +0 -0
  23. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/results.py +0 -0
  24. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sanitize.py +0 -0
  25. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/__init__.py +0 -0
  26. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/_batch.py +0 -0
  27. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/_chunk.py +0 -0
  28. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/_retry.py +0 -0
  29. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/_socket.py +0 -0
  30. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/_time.py +0 -0
  31. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/base.py +0 -0
  32. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/callback.py +0 -0
  33. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/clickhouse.py +0 -0
  34. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/datadog.py +0 -0
  35. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/elasticsearch.py +0 -0
  36. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/eventhubs.py +0 -0
  37. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/file.py +0 -0
  38. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/firehose.py +0 -0
  39. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/honeycomb.py +0 -0
  40. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/http.py +0 -0
  41. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/kafka.py +0 -0
  42. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/kinesis.py +0 -0
  43. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/logging_sink.py +0 -0
  44. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/loki.py +0 -0
  45. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/memory.py +0 -0
  46. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/mongodb.py +0 -0
  47. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/nats.py +0 -0
  48. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/newrelic.py +0 -0
  49. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/null.py +0 -0
  50. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/postgres.py +0 -0
  51. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/pubsub.py +0 -0
  52. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/rabbitmq.py +0 -0
  53. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/redis.py +0 -0
  54. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/sns.py +0 -0
  55. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/splunk.py +0 -0
  56. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/sqlite.py +0 -0
  57. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/sqs.py +0 -0
  58. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/stdout.py +0 -0
  59. {log_foundry-0.10.2.dev60 → log_foundry-0.10.2.dev61}/src/log_foundry/sinks/syslog.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log-foundry
3
- Version: 0.10.2.dev60
3
+ Version: 0.10.2.dev61
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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.dev60"
23
+ version = "0.10.2.dev61"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -24,7 +24,42 @@ _closers: list[threading.Thread] = []
24
24
  _closers_lock = threading.Lock()
25
25
 
26
26
 
27
- def close_detached(sink: Sink) -> threading.Thread | None:
27
+ def release(sink: Sink, *, detached: bool = False) -> threading.Thread | None:
28
+ """Closes a sink on the library's behalf — the one path by which it ever does (SPEC-042 FR-002).
29
+
30
+ Eight sites closed a sink directly before this existed, and guarding only the three the
31
+ lifecycle owns was measurably insufficient: a forked child that wraps an **inherited** sink in
32
+ a ``MultiSink`` of its own reaches the inner sink through the wrapper, so the parent's
33
+ structural sink was closed twice with all three lifecycle sites guarded. Routing every
34
+ library closer through one function is what gives the ownership question one home.
35
+
36
+ **The guard moves here; the error handling does not.** This propagates whatever ``close()``
37
+ raises, because the callers do not agree today and must not be made to: four absorb, under
38
+ three distinct ``_diag`` texts naming the site (SPEC-029), ``MultiSink`` also increments its
39
+ ``failed`` counter, and ``FilteringSink``/``TransformSink``/``LogstashSink`` propagate under
40
+ a documented ``Raises:``. Folding the ``try/except`` in here would drop absorbed close
41
+ failures out of ``Health.sink.failed`` — a SPEC-026 regression — and falsify those three.
42
+
43
+ Args:
44
+ sink: The sink to close.
45
+ detached: Whether to close on a daemon thread rather than inline. Detached is for a sink
46
+ the caller has stopped delivering to and must not block on (SPEC-030 FR-003).
47
+
48
+ Returns:
49
+ The started closer thread for a detached release, or ``None`` — both when an inline close
50
+ completed and when the platform would not give the process another thread.
51
+
52
+ Raises:
53
+ Exception: Whatever an inline ``close()`` raised. A detached release raises nothing: the
54
+ thread body absorbs, since there is no caller left to hand it to.
55
+ """
56
+ if detached:
57
+ return _start_closer(sink)
58
+ sink.close()
59
+ return None
60
+
61
+
62
+ def _start_closer(sink: Sink) -> threading.Thread | None:
28
63
  """Starts a daemon close of a sink no longer being delivered to (SPEC-030 FR-003).
29
64
 
30
65
  The thread is returned rather than joined, so a caller holding a lock can start under it and
@@ -79,7 +114,8 @@ def _close_guarded(sink: Sink) -> None:
79
114
  The guard is what makes the thread safe to leave unattended: an exception escaping here
80
115
  would reach CPython's thread bootstrap, which prints a full traceback carrying the
81
116
  exception's message — the user data arch §6 keeps out of anything the library says about
82
- itself.
117
+ itself. It goes back through :func:`release` rather than calling ``close()`` itself, so the
118
+ thread body is one of the eight callers rather than a ninth close (SPEC-042 FR-002).
83
119
 
84
120
  Args:
85
121
  sink: The sink to close.
@@ -91,7 +127,7 @@ def _close_guarded(sink: Sink) -> None:
91
127
  None.
92
128
  """
93
129
  try:
94
- sink.close()
130
+ release(sink)
95
131
  except Exception as exc:
96
132
  _diag.absorbed("closing a swapped-out sink", exc, "it may still hold its resources")
97
133
 
@@ -504,7 +504,7 @@ def _close_orphan_sink() -> None:
504
504
  _orphan_sink = None
505
505
  _orphan_closed_sink = owed
506
506
  try:
507
- owed.close()
507
+ _lifecycle.release(owed)
508
508
  except Exception as exc:
509
509
  _diag.absorbed("closing the sink", exc, "it may still hold its resources")
510
510
 
@@ -637,7 +637,7 @@ def _swap_sink(new_sink: Sink, timeout: float | None = DEFAULT_SWAP_TIMEOUT) ->
637
637
  _offer_orphan_signal(new_sink)
638
638
  if _worker is None or _worker.sink is not old:
639
639
  _orphan_closed_sink = old
640
- closer = _lifecycle.close_detached(old)
640
+ closer = _lifecycle.release(old, detached=True)
641
641
  if worker is not None:
642
642
  try:
643
643
  worker_holds_sink = worker.swap_sink(new_sink, timeout)
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from log_foundry import _diag
7
+ from log_foundry import _diag, _lifecycle
8
8
  from log_foundry.sinks.base import read_losses
9
9
 
10
10
  if TYPE_CHECKING:
@@ -172,6 +172,10 @@ class FilteringSink:
172
172
  def close(self) -> None:
173
173
  """Closes the inner sink (FR-003).
174
174
 
175
+ Routed through ``_lifecycle.release`` so a sink this process may not release is refused
176
+ here as it is at the lifecycle's own closers (SPEC-042 FR-002). The propagation is
177
+ unchanged: the helper re-raises, and this method's ``Raises:`` is the reason it does.
178
+
175
179
  Args:
176
180
  None.
177
181
 
@@ -181,4 +185,4 @@ class FilteringSink:
181
185
  Raises:
182
186
  Exception: Whatever the inner sink raises on close.
183
187
  """
184
- self._inner.close()
188
+ _lifecycle.release(self._inner)
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
8
8
  if TYPE_CHECKING:
9
9
  import threading
10
10
 
11
+ from log_foundry import _lifecycle
11
12
  from log_foundry.sinks._socket import DEFAULT_MAX_DATAGRAM_BYTES, SocketTransport
12
13
  from log_foundry.sinks.base import SinkLosses
13
14
  from log_foundry.sinks.http import HTTPSink
@@ -124,6 +125,11 @@ class LogstashSink:
124
125
  def close(self) -> None:
125
126
  """Closes whichever backend is held (FR-005).
126
127
 
128
+ The two branches are deliberately not symmetric. The HTTP backend is a ``Sink`` and goes
129
+ through ``_lifecycle.release``, so a forked child cannot release one it inherited
130
+ (SPEC-042 FR-002); the socket is a ``SocketTransport`` this sink built and owns
131
+ outright, which no sink-ownership record describes.
132
+
127
133
  Args:
128
134
  None.
129
135
 
@@ -134,7 +140,7 @@ class LogstashSink:
134
140
  Exception: Whatever the backend raises on close.
135
141
  """
136
142
  if self._http is not None:
137
- self._http.close()
143
+ _lifecycle.release(self._http)
138
144
  elif self._socket is not None:
139
145
  self._socket.close()
140
146
 
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import threading
6
6
  from typing import TYPE_CHECKING
7
7
 
8
- from log_foundry import _diag
8
+ from log_foundry import _diag, _lifecycle
9
9
  from log_foundry.sinks.base import SinkLosses, read_losses
10
10
 
11
11
  if TYPE_CHECKING:
@@ -178,7 +178,11 @@ class MultiSink:
178
178
  """Closes every child, isolating a failing one so the rest still close (FR-002).
179
179
 
180
180
  Unlike :meth:`emit` this keeps the unconditional isolate-and-continue behaviour even on
181
- total failure, because a failed close has nothing to retry.
181
+ total failure, because a failed close has nothing to retry. Each child goes through
182
+ ``_lifecycle.release`` rather than being closed here, so a child this process may not
183
+ release is refused by the same guard the lifecycle's own closers consult (SPEC-042
184
+ FR-002) — the wrapper route is how a forked child reached a parent's sink with all three
185
+ lifecycle sites already guarded.
182
186
 
183
187
  Args:
184
188
  None.
@@ -191,7 +195,7 @@ class MultiSink:
191
195
  """
192
196
  for sink in self._sinks:
193
197
  try:
194
- sink.close()
198
+ _lifecycle.release(sink)
195
199
  except Exception as err:
196
200
  with self._counter_lock:
197
201
  self.failed += 1
@@ -8,7 +8,7 @@ import uuid
8
8
  from typing import Any
9
9
  from urllib.parse import urlparse
10
10
 
11
- from log_foundry import _diag
11
+ from log_foundry import _diag, _lifecycle
12
12
  from log_foundry.sinks.base import SinkDeliveryError, SinkLosses
13
13
  from log_foundry.sinks.http import HTTPSink
14
14
 
@@ -128,7 +128,9 @@ class SentrySink:
128
128
  Idempotent, and it releases nothing — which is why the class docstring's post-close claim
129
129
  holds despite this method calling a ``close()``. ``HTTPSink.close`` is a documented no-op,
130
130
  since ``urllib`` builds a fresh connection per request; the forward exists so a future
131
- ``HTTPSink`` that *did* hold a pool would be released here rather than leaked.
131
+ ``HTTPSink`` that *did* hold a pool would be released here rather than leaked. It goes
132
+ through ``_lifecycle.release`` for that same future: the day the forward releases
133
+ something is the day a forked child must not perform it (SPEC-042 FR-002).
132
134
 
133
135
  Args:
134
136
  None.
@@ -140,7 +142,7 @@ class SentrySink:
140
142
  None.
141
143
  """
142
144
  if self._http is not None:
143
- self._http.close()
145
+ _lifecycle.release(self._http)
144
146
 
145
147
  @property
146
148
  def log_foundry_stop_signal(self) -> threading.Event | None:
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from log_foundry import _diag
7
+ from log_foundry import _diag, _lifecycle
8
8
  from log_foundry.sinks.base import read_losses
9
9
 
10
10
  if TYPE_CHECKING:
@@ -140,6 +140,10 @@ class TransformSink:
140
140
  def close(self) -> None:
141
141
  """Closes the inner sink (FR-004).
142
142
 
143
+ Routed through ``_lifecycle.release`` so a sink this process may not release is refused
144
+ here as it is at the lifecycle's own closers (SPEC-042 FR-002). The propagation is
145
+ unchanged: the helper re-raises, and this method's ``Raises:`` is the reason it does.
146
+
143
147
  Args:
144
148
  None.
145
149
 
@@ -149,4 +153,4 @@ class TransformSink:
149
153
  Raises:
150
154
  Exception: Whatever the inner sink raises on close.
151
155
  """
152
- self._inner.close()
156
+ _lifecycle.release(self._inner)
@@ -790,7 +790,7 @@ class Worker:
790
790
  wait this method exists to remove, in the one situation where the process is
791
791
  already under resource pressure.
792
792
  """
793
- closer = _lifecycle.close_detached(sink)
793
+ closer = _lifecycle.release(sink, detached=True)
794
794
  if closer is not None:
795
795
  closer.join(timeout)
796
796
 
@@ -1009,7 +1009,7 @@ class Worker:
1009
1009
  None.
1010
1010
  """
1011
1011
  try:
1012
- self.sink.close()
1012
+ _lifecycle.release(self.sink)
1013
1013
  except Exception as exc:
1014
1014
  _diag.absorbed("closing the sink", exc, "it may still hold its resources")
1015
1015