log-foundry 0.10.2.dev31__tar.gz → 0.10.2.dev33__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 (55) hide show
  1. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/worker.py +106 -13
  4. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/LICENSE +0 -0
  5. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/README.md +0 -0
  6. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/__init__.py +0 -0
  7. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/_diag.py +0 -0
  8. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/api.py +0 -0
  9. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/context.py +0 -0
  12. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/decorator.py +0 -0
  13. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/ids.py +0 -0
  14. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/model.py +0 -0
  15. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/py.typed +0 -0
  16. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sanitize.py +0 -0
  17. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/__init__.py +0 -0
  18. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_batch.py +0 -0
  19. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_chunk.py +0 -0
  20. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_retry.py +0 -0
  21. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_socket.py +0 -0
  22. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/_time.py +0 -0
  23. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/base.py +0 -0
  24. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/callback.py +0 -0
  25. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/clickhouse.py +0 -0
  26. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/datadog.py +0 -0
  27. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/elasticsearch.py +0 -0
  28. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/eventhubs.py +0 -0
  29. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/file.py +0 -0
  30. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/filtering.py +0 -0
  31. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/firehose.py +0 -0
  32. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/honeycomb.py +0 -0
  33. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/http.py +0 -0
  34. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/kafka.py +0 -0
  35. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/kinesis.py +0 -0
  36. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/logging_sink.py +0 -0
  37. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/logstash.py +0 -0
  38. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/loki.py +0 -0
  39. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/mongodb.py +0 -0
  40. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/multi.py +0 -0
  41. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/nats.py +0 -0
  42. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/newrelic.py +0 -0
  43. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/postgres.py +0 -0
  44. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/pubsub.py +0 -0
  45. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/rabbitmq.py +0 -0
  46. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/redis.py +0 -0
  47. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sentry.py +0 -0
  48. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sns.py +0 -0
  49. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/splunk.py +0 -0
  50. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sqlite.py +0 -0
  51. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/sqs.py +0 -0
  52. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/stdout.py +0 -0
  53. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/syslog.py +0 -0
  54. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/src/log_foundry/sinks/transform.py +0 -0
  55. {log_foundry-0.10.2.dev31 → log_foundry-0.10.2.dev33}/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.dev31
3
+ Version: 0.10.2.dev33
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.dev31"
23
+ version = "0.10.2.dev33"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -76,8 +76,10 @@ class Health(NamedTuple):
76
76
 
77
77
  Attributes:
78
78
  queued: Submissions currently buffered. Approximate by nature: it is read without
79
- stopping the world, and briefly counts the internal flush/shutdown markers alongside
80
- real submissions.
79
+ stopping the world, and counts the internal flush/shutdown markers alongside real
80
+ submissions — normally only in passing, but a ``flush()`` marker stranded by racing
81
+ ``shutdown()`` is answered and then counted for the life of the process, since
82
+ ``Queue`` offers no way to remove one specific item.
81
83
  dropped: Submissions discarded because the queue was full (backpressure).
82
84
  failed_batches: Batches abandoned after the retry budget was spent.
83
85
  stopped_reason: The exception type name that terminated the drain thread, or ``None`` if
@@ -227,6 +229,7 @@ class Worker:
227
229
  self._closers: list[threading.Thread] = []
228
230
  self._queue: queue.Queue[object] = queue.Queue(maxsize=max_queue)
229
231
  self._stop = threading.Event()
232
+ self._drain_finished = threading.Event()
230
233
  self._shutdown_done = False
231
234
  self._sink_closed = False
232
235
  self._lock = threading.Lock()
@@ -338,8 +341,11 @@ class Worker:
338
341
  """Snapshots the delivery counters (SPEC-017 FR-005, SPEC-019 FR-003).
339
342
 
340
343
  This stays valid after :meth:`shutdown`: the counters are plain integers that outlive
341
- the thread, and the final drain consumes the queue, so ``queued`` reads 0 rather than a
342
- stale marker. The same applies to ``stopped_reason``, since a caller finding a dead
344
+ the thread, and the final drain consumes the queue. ``queued`` therefore reads 0 for a
345
+ worker nothing logged to afterwards but not always: submissions accepted after the
346
+ shutdown stay queued on purpose (SPEC-030), and a ``flush()`` marker stranded by
347
+ racing it is answered and then counted, as ``Health.queued`` records. The same applies
348
+ to ``stopped_reason``, since a caller finding a dead
343
349
  worker will usually call ``shutdown()`` next. Reading it after a shutdown is in fact
344
350
  the point of ``retired`` and ``submitted_after_shutdown`` (SPEC-030 FR-001), which
345
351
  report a state only a retired worker can be in.
@@ -408,6 +414,24 @@ class Worker:
408
414
  queue ``put_nowait`` would skip the flush and return as though it had succeeded, the
409
415
  one outcome a flush must never produce silently.
410
416
 
417
+ The drain's completion is re-checked **after** the put, and that second look is what
418
+ makes a ``timeout=None`` call safe. The checks above can both pass microseconds before
419
+ the drain finishes, leaving this marker queued behind something that will never read it
420
+ — a bounded caller then waits out its timeout, which SPEC-021 accepts as correct either
421
+ way, but an unbounded one waits forever.
422
+
423
+ It tests ``_drain_finished`` and not only ``is_alive()``, because the two are not the
424
+ same instant and the gap between them is where the hang survives: the terminal-failure
425
+ path sweeps for markers and *then* returns, so a marker queued after that sweep sits
426
+ behind a thread still reading as alive. The flag is set **before** the sweep, and a
427
+ ``put`` and the sweep's snapshot both take the queue's own mutex, so a marker either
428
+ lands before the snapshot and is answered, or lands after it and finds the flag set.
429
+
430
+ Reporting is by the **marker**, never by the check alone. A drain that answered this
431
+ marker and then exited has delivered, and saying otherwise would be a false failure —
432
+ one ``swap_sink`` reads as an unconfirmed drain, counting ``incomplete_swaps``, leaving
433
+ the previous sink open and writing a loss line for a swap that in fact completed.
434
+
411
435
  Args:
412
436
  timeout: Seconds bounding the whole call — one deadline shared by the put and the
413
437
  wait, so the two cannot add up to twice the timeout. ``None`` waits indefinitely.
@@ -433,6 +457,8 @@ class Worker:
433
457
  self._queue.put(marker, timeout=timeout)
434
458
  except queue.Full:
435
459
  return False
460
+ if self._drain_finished.is_set() or not self._thread.is_alive():
461
+ return marker.event.is_set() and marker.delivered
436
462
  remaining = None if deadline is None else max(0.0, deadline - time.monotonic())
437
463
  if not marker.event.wait(remaining):
438
464
  return False
@@ -638,6 +664,36 @@ class Worker:
638
664
  ``close()`` twice on a sink that may have partially released its resources; what
639
665
  SPEC-025 FR-004 changed is that the failure is announced rather than swallowed.
640
666
 
667
+ **The sentinel is queued before ``_stop`` is set, and while the drain loop is running
668
+ that order is what makes it impossible to strand.** Both ways of leaving the loop —
669
+ taking the sentinel, or seeing ``_stop`` — can only happen once it is already in the
670
+ queue, so either that ``get`` consumes it or :meth:`_final_drain` does. The reverse
671
+ order left a window in which the loop read ``_stop``, exited, and finished its final
672
+ drain before the sentinel landed. It never lost an event, but left ``health().queued``
673
+ reading 1 for the life of the process. The rate is load-dependent and not worth
674
+ quoting as a property — rare when idle, and repeatedly reproduced between roughly one
675
+ shutdown in 14 and one in 50 with spinner threads and a tightened switch interval.
676
+
677
+ Paying for that order needs :meth:`_drain` to break on the sentinel rather than loop, or
678
+ a thread taking it before ``_stop`` was set would block for another ``flush_interval``
679
+ — measured stalling low single-digit percentages of shutdowns under load for the entire
680
+ budget, latching a ``stopped_reason`` of ``"ShutdownTimeout"``, which is far worse than
681
+ the cosmetic problem being fixed.
682
+
683
+ The premise is the loop, so the put is skipped once the drain has stopped reading. A
684
+ drain that died terminally (SPEC-019) is not coming back for a wake-up, and queueing one
685
+ for it would strand it permanently — reintroducing the symptom on the one path the
686
+ ordering cannot reach. The gate is ``_drain_finished`` rather than ``is_alive()``,
687
+ because they are not the same instant: the thread is still alive throughout
688
+ :meth:`_terminal_failure`, which writes to stderr and can block on a slow reader, and a
689
+ liveness test would queue a sentinel through that whole window. The flag is set before
690
+ that call, so this one does not.
691
+
692
+ :meth:`_release_waiters` runs on the way out for the sibling case the ordering cannot
693
+ reach: a ``flush()`` that passed its liveness check microseconds before the thread
694
+ finished can still queue a marker nothing will answer, and with ``timeout=None`` that
695
+ caller waits forever rather than merely too long.
696
+
641
697
  The worker does not come back, and :meth:`submit` keeps accepting afterwards — so a
642
698
  caller that logs again queues events nothing will drain. That is reported rather than
643
699
  prevented, through ``retired`` and ``submitted_after_shutdown`` (SPEC-030 FR-001) and
@@ -664,11 +720,12 @@ class Worker:
664
720
  self._close_if_owed()
665
721
  self._join_closers(None if deadline is None else max(0.0, deadline - time.monotonic()))
666
722
  return
723
+ if not self._drain_finished.is_set():
724
+ try:
725
+ self._queue.put_nowait(_SHUTDOWN)
726
+ except queue.Full:
727
+ pass
667
728
  self._stop.set()
668
- try:
669
- self._queue.put_nowait(_SHUTDOWN)
670
- except queue.Full:
671
- pass
672
729
  self._thread.join(timeout)
673
730
  if self._thread.is_alive():
674
731
  queued = self._queued_or_unknown()
@@ -682,6 +739,7 @@ class Worker:
682
739
  f"because the worker thread is still using it",
683
740
  )
684
741
  return
742
+ self._release_waiters()
685
743
  self._close_if_owed()
686
744
  self._join_closers(None if deadline is None else max(0.0, deadline - time.monotonic()))
687
745
 
@@ -826,6 +884,18 @@ class Worker:
826
884
  ``BaseException``. It records and exits; looping onward past a ``KeyboardInterrupt``
827
885
  would be a worse failure than the one this prevents.
828
886
 
887
+ The two ``finally`` blocks are nested rather than merged, and the order they impose is
888
+ load-bearing twice over. ``_drain_finished`` is set the instant the loop stops reading
889
+ the queue — *before* :meth:`_terminal_failure`, which writes to stderr and can block on
890
+ a slow reader — so a ``shutdown()`` arriving during that window sees a drain that is
891
+ already finished and declines to queue a sentinel nothing would consume. And it is set
892
+ before :meth:`_release_waiters`, so a marker either lands ahead of that sweep's snapshot
893
+ and is answered by it, or lands behind it and finds the flag set; both take the queue's
894
+ own mutex, which is what leaves no gap between the two. Sweeping here rather than only
895
+ in :meth:`Worker.shutdown` is what covers the paths ``shutdown`` never reaches — a
896
+ terminal failure, and a bounded shutdown that expired while this thread was still
897
+ inside an emit.
898
+
829
899
  Args:
830
900
  None.
831
901
 
@@ -837,9 +907,13 @@ class Worker:
837
907
  """
838
908
  pending: list[list[dict[str, object]]] = []
839
909
  try:
840
- self._drain(pending)
910
+ try:
911
+ self._drain(pending)
912
+ finally:
913
+ self._drain_finished.set()
841
914
  except BaseException as exc:
842
915
  self._terminal_failure(exc, len(pending))
916
+ finally:
843
917
  self._release_waiters()
844
918
 
845
919
  def _release_waiters(self) -> None:
@@ -852,10 +926,20 @@ class Worker:
852
926
  ``health().queued`` and the terminal line report; each keeps its pessimistic
853
927
  ``delivered``, which is the truth here.
854
928
 
929
+ It is called from two places. The terminal-failure path is the original one. The clean
930
+ :meth:`shutdown` path was added because that same enqueue-after-the-drain race happens
931
+ there too, and hurts more: measured stranding a marker in 13 of 400 shutdowns raced
932
+ against a ``flush()`` under load, where the caller sat out its whole timeout — and
933
+ ``flush(timeout=None)``, which the API documents as supported, waits forever rather
934
+ than too long. The marker keeps its pessimistic ``delivered``, which is the honest
935
+ answer: the drain that would have carried it is gone.
936
+
855
937
  One residual race, stated rather than papered over: a ``flush()`` that passed its
856
- liveness check microseconds before the thread died can still enqueue a marker after
857
- this sweep, and that one waits out its timeout — then returns False, which is correct
858
- either way.
938
+ liveness check microseconds before *this* sweep can still enqueue a marker after it,
939
+ and that one waits out its timeout — then returns False, which is correct either way.
940
+ A marker left queued is also still counted by ``health().queued``, which describes
941
+ submissions; removing it would mean deleting a specific item, which ``Queue`` has no
942
+ public way to do, and the read above is the access ``architecture.md`` §13 sanctions.
859
943
 
860
944
  The reliance on ``queue.Queue``'s private ``mutex`` and ``queue`` is deliberate and is
861
945
  recorded in ``architecture.md`` §13 Known Constraints (SPEC-031 FR-005): there is no
@@ -928,6 +1012,13 @@ class Worker:
928
1012
  advanced even when idle: otherwise the timeout collapses to zero and ``get`` busy-spins
929
1013
  a core.
930
1014
 
1015
+ The shutdown sentinel **breaks** rather than falling through to the loop condition.
1016
+ :meth:`shutdown` queues it before setting ``_stop``, so a thread that takes it may find
1017
+ ``_stop`` still clear; continuing would re-enter ``get`` and block for another
1018
+ ``flush_interval``, which is a slow shutdown rather than a prompt one. Leaving
1019
+ immediately is safe because the only thing after the loop is :meth:`_final_drain`, which
1020
+ collects whatever is still queued — the sentinel is a wake-up, never a fence.
1021
+
931
1022
  Args:
932
1023
  pending: The accumulator owned by :meth:`_run`, which reports its size on a terminal
933
1024
  failure. It is mutated in place rather than rebound, so that count is accurate.
@@ -953,7 +1044,9 @@ class Worker:
953
1044
  last_flush = time.monotonic()
954
1045
  item.event.set()
955
1046
  continue
956
- if item is not None and item is not _SHUTDOWN:
1047
+ if item is _SHUTDOWN:
1048
+ break
1049
+ if item is not None:
957
1050
  pending.append(cast("list[dict[str, object]]", item))
958
1051
  now = time.monotonic()
959
1052
  if len(pending) >= self.batch_size or now - last_flush >= self.flush_interval: