log-foundry 0.10.2.dev75__tar.gz → 0.10.2.dev76__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.dev75 → log_foundry-0.10.2.dev76}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/decorator.py +119 -17
  4. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/LICENSE +0 -0
  5. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/README.md +0 -0
  6. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/__init__.py +0 -0
  7. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/_diag.py +0 -0
  8. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/_fork.py +0 -0
  9. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/_lifecycle.py +0 -0
  10. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/api.py +0 -0
  11. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/config.py +0 -0
  12. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/console.py +0 -0
  13. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/context.py +0 -0
  14. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/ids.py +0 -0
  15. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/model.py +0 -0
  16. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/py.typed +0 -0
  17. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/results.py +0 -0
  18. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sanitize.py +0 -0
  19. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/__init__.py +0 -0
  20. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/_batch.py +0 -0
  21. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/_chunk.py +0 -0
  22. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/_retry.py +0 -0
  23. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/_socket.py +0 -0
  24. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/_time.py +0 -0
  25. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/base.py +0 -0
  26. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/callback.py +0 -0
  27. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/clickhouse.py +0 -0
  28. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/datadog.py +0 -0
  29. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/elasticsearch.py +0 -0
  30. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/eventhubs.py +0 -0
  31. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/file.py +0 -0
  32. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/filtering.py +0 -0
  33. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/firehose.py +0 -0
  34. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/honeycomb.py +0 -0
  35. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/http.py +0 -0
  36. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/kafka.py +0 -0
  37. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/kinesis.py +0 -0
  38. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/logging_sink.py +0 -0
  39. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/logstash.py +0 -0
  40. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/loki.py +0 -0
  41. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/memory.py +0 -0
  42. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/mongodb.py +0 -0
  43. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/multi.py +0 -0
  44. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/nats.py +0 -0
  45. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/newrelic.py +0 -0
  46. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/null.py +0 -0
  47. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/postgres.py +0 -0
  48. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/pubsub.py +0 -0
  49. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/rabbitmq.py +0 -0
  50. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/redis.py +0 -0
  51. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/sentry.py +0 -0
  52. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/sns.py +0 -0
  53. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/splunk.py +0 -0
  54. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/sqlite.py +0 -0
  55. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/sqs.py +0 -0
  56. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/stdout.py +0 -0
  57. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/syslog.py +0 -0
  58. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/sinks/transform.py +0 -0
  59. {log_foundry-0.10.2.dev75 → log_foundry-0.10.2.dev76}/src/log_foundry/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log-foundry
3
- Version: 0.10.2.dev75
3
+ Version: 0.10.2.dev76
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.dev75"
23
+ version = "0.10.2.dev76"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -309,13 +309,16 @@ def _get_worker() -> Worker:
309
309
  Exception: Whatever constructing the sink or worker raises.
310
310
  """
311
311
  global _worker, _orphan_sink
312
- if _worker is None:
312
+ worker = _worker_exists()
313
+ if worker is None:
313
314
  with _worker_lock:
314
- if _worker is None:
315
+ worker = _worker_exists()
316
+ if worker is None:
315
317
  _register_exit_handler()
316
- _worker = Worker(_ensure_sink())
318
+ worker = Worker(_ensure_sink())
319
+ _worker = worker
317
320
  _orphan_sink = None
318
- return _worker
321
+ return worker
319
322
 
320
323
 
321
324
  def _register_exit_handler() -> None:
@@ -414,7 +417,7 @@ def _rebuild_worker_after_fork() -> None:
414
417
  None. ``_fork`` absorbs and announces a handler's failure, so a child whose worker cannot
415
418
  be rebuilt still has working locks.
416
419
  """
417
- worker = _worker
420
+ worker = _worker_exists()
418
421
  if worker is None:
419
422
  return
420
423
  resume = not worker.retired
@@ -424,8 +427,103 @@ def _rebuild_worker_after_fork() -> None:
424
427
  _fork.register_child_handler(_rebuild_worker_after_fork)
425
428
 
426
429
 
430
+ def _worker_exists() -> Worker | None:
431
+ """Existence — is there a worker at all, and therefore anything to do (arch §9.2).
432
+
433
+ The first of the four questions a guard may ask, and the weakest: it says only that this
434
+ process built a worker, never that the worker still delivers or that it owns any particular
435
+ sink. A retired worker is still the process worker, which is why rebuilding one here would
436
+ fight a process trying to exit (SPEC-019) and why :func:`_shutdown_worker`,
437
+ :func:`_flush_worker` and :func:`_worker_health` all answer from it rather than from
438
+ :func:`_live_worker`.
439
+
440
+ **No lock is taken**, and callers must not assume one. Four of this module's guards ask a
441
+ question with ``_worker_lock`` already held (:func:`_get_worker`'s inner check,
442
+ :func:`_close_orphan_sink`, :func:`_swap_sink`, and :func:`_offer_orphan_signal` through all
443
+ three of its callers), so acquiring a non-reentrant lock here would deadlock them; and
444
+ :func:`_get_worker`'s **outer** check is deliberately unlocked on the ``@trace`` hot path,
445
+ where a lock would serialize every span flush in the process. The read is a single reference
446
+ load, which is atomic — a caller needing consistency across two reads takes ``_worker_lock``
447
+ itself, exactly as it does today.
448
+
449
+ Its own definition is the one of the four the roster does not file, because the body is a
450
+ bare ``return _worker`` and ``_boolean_positions`` excludes a plain ``Name`` return by
451
+ design. That is not a hole: every rewrite that would change the category introduces a
452
+ boolean the walker does see — an ``IfExp`` test, or a call to :func:`_live_worker`.
453
+
454
+ Args:
455
+ None.
456
+
457
+ Returns:
458
+ The process worker if one was ever built, retired or not, otherwise ``None``.
459
+
460
+ Raises:
461
+ None.
462
+ """
463
+ return _worker
464
+
465
+
466
+ def _worker_owns(sink: Sink) -> bool:
467
+ """Ownership — who *owns* a close, which a retired worker still does (arch §9.2).
468
+
469
+ The third question, and the one three reviewers each named a different site for. It is not
470
+ liveness: :meth:`Worker.swap_sink` returns early once ``_shutdown_done``, so a retired worker
471
+ keeps its old sink forever, and answering "who closes this" with :func:`_live_worker` closes
472
+ it a second time on a clean shutdown and closes it **under a live writer** on an expired one
473
+ — both measured (SPEC-033 FR-002).
474
+
475
+ Takes no lock, for the reason :func:`_worker_exists` states; :func:`_close_orphan_sink` and
476
+ :func:`_swap_sink` both ask it under ``_worker_lock``, which is where the consistency they
477
+ need comes from.
478
+
479
+ Args:
480
+ sink: The sink whose owner is in question.
481
+
482
+ Returns:
483
+ Whether the process worker holds that sink, retired or not.
484
+
485
+ Raises:
486
+ None.
487
+ """
488
+ worker = _worker
489
+ return worker is not None and worker.sink is sink
490
+
491
+
492
+ def _worker_owns_now(sink: Sink) -> bool:
493
+ """Ownership ∧ moment — whose stop event the sink should be holding *now* (arch §9.2).
494
+
495
+ The fourth question, and a **conjunction** rather than a new subject, which is why it is named
496
+ for both terms. It exists because neither of its halves is right alone at
497
+ :func:`_offer_orphan_signal`, the one site that asks it: bare ownership skips the offer for a
498
+ worker whose shutdown has *finished*, leaving a live sink holding a set event that can never
499
+ clear, while liveness alone un-skips for the whole drain and hands the drain thread a fresh
500
+ event nobody will set — ``retired`` latches on **entry** to :meth:`Worker.shutdown`, not at
501
+ its completion. Both were measured (SPEC-035 FR-001), and the identity term is what stops an
502
+ orphan log to sink Y being skipped merely because a live worker is draining into sink X.
503
+
504
+ Takes no lock. Its one caller is :func:`_offer_orphan_signal`, which does not acquire
505
+ ``_worker_lock`` itself — all three of *its* callers hold it, and that is the obligation a
506
+ fourth caller would have to satisfy.
507
+
508
+ Args:
509
+ sink: The sink about to be offered a stop signal.
510
+
511
+ Returns:
512
+ Whether the process worker holds that sink *and* is still draining into it.
513
+
514
+ Raises:
515
+ None.
516
+ """
517
+ worker = _worker
518
+ return worker is not None and worker.sink is sink and worker.draining
519
+
520
+
427
521
  def _live_worker() -> Worker | None:
428
- """Returns the process worker only while it is still delivering (SPEC-033 FR-002).
522
+ """Liveness — who *performs* an action, and a retired worker performs nothing (arch §9.2).
523
+
524
+ The second of the four questions (SPEC-033 FR-002). Reading ``retired`` in order to *report*
525
+ it — as :func:`_worker_health` does — is this same question asked for a different purpose,
526
+ not a fifth category.
429
527
 
430
528
  Three guards ask who owns a sink, and two of them mean a *live* owner. A retired worker holds
431
529
  its sink forever — :meth:`Worker.swap_sink` returns early once shut down — so keying on a
@@ -439,6 +537,10 @@ def _live_worker() -> Worker | None:
439
537
  ownership is exactly what must make it decline, since an expired shutdown leaves the drain
440
538
  thread possibly still inside that sink's ``emit``.
441
539
 
540
+ Takes no lock, for the reason :func:`_worker_exists` states. :func:`_swap_sink` asks it
541
+ under ``_worker_lock``; :func:`_flush_live_sink` asks it under no lock at all, which is
542
+ sound because it consumes the answer immediately and holds nothing across it.
543
+
442
544
  Args:
443
545
  None.
444
546
 
@@ -501,8 +603,7 @@ def _offer_orphan_signal(sink: Sink) -> None:
501
603
  None.
502
604
  """
503
605
  global _orphan_stop
504
- worker = _worker
505
- if worker is not None and worker.sink is sink and worker.draining:
606
+ if _worker_owns_now(sink):
506
607
  return
507
608
  if _orphan_stop.is_set():
508
609
  _orphan_stop = threading.Event()
@@ -554,7 +655,7 @@ def _close_orphan_sink() -> None:
554
655
  owed = _orphan_sink
555
656
  if owed is None:
556
657
  return
557
- if _worker is not None and _worker.sink is owed:
658
+ if _worker_owns(owed):
558
659
  return
559
660
  _orphan_sink = None
560
661
  _orphan_closed_sink = owed
@@ -605,8 +706,9 @@ def _shutdown_worker(timeout: float | None = DEFAULT_SHUTDOWN_TIMEOUT) -> None:
605
706
  _orphan_retired = True
606
707
  _orphan_stop.set()
607
708
  deadline = None if timeout is None else monotonic() + timeout
608
- if _worker is not None:
609
- _worker.shutdown(timeout)
709
+ worker = _worker_exists()
710
+ if worker is not None:
711
+ worker.shutdown(timeout)
610
712
  _close_orphan_sink()
611
713
  return
612
714
  _close_orphan_sink()
@@ -690,7 +792,7 @@ def _swap_sink(new_sink: Sink, timeout: float | None = DEFAULT_SWAP_TIMEOUT) ->
690
792
  return
691
793
  _orphan_sink = new_sink
692
794
  _offer_orphan_signal(new_sink)
693
- if _worker is None or _worker.sink is not old:
795
+ if not _worker_owns(old):
694
796
  _orphan_closed_sink = old
695
797
  closer = _lifecycle.release(old, detached=True)
696
798
  if worker is not None:
@@ -863,8 +965,8 @@ def _flush_live_sink() -> bool:
863
965
  None. A failure is reported as a ``FlushResult`` reason by the caller, never raised: a
864
966
  flush is the call most likely to be made in a ``finally``.
865
967
  """
866
- worker = _worker
867
- sink = worker.sink if worker is not None and not worker.retired else _orphan_sink
968
+ worker = _live_worker()
969
+ sink = worker.sink if worker is not None else _orphan_sink
868
970
  if sink is None:
869
971
  return True
870
972
  try:
@@ -915,7 +1017,7 @@ def _flush_worker(timeout: float | None = 5.0) -> FlushResult:
915
1017
  except Exception as exc:
916
1018
  _diag.absorbed("sweeping open spans for a flush", exc, "buffered events were not swept")
917
1019
  swept = False
918
- worker = _worker
1020
+ worker = _worker_exists()
919
1021
  drained: FlushResult = FlushResult(ok=True)
920
1022
  thread_died = False
921
1023
  if worker is not None:
@@ -1025,7 +1127,7 @@ def _delivering_to_an_inherited_sink() -> bool:
1025
1127
  forked.
1026
1128
  """
1027
1129
  try:
1028
- worker = _worker
1130
+ worker = _worker_exists()
1029
1131
  sink = worker.sink if worker is not None else (_orphan_sink or _live_config().sink)
1030
1132
  return sink is not None and not _lifecycle.releasable(sink)
1031
1133
  except Exception:
@@ -1074,7 +1176,7 @@ def _worker_health() -> Health:
1074
1176
  Raises:
1075
1177
  None.
1076
1178
  """
1077
- worker = _worker
1179
+ worker = _worker_exists()
1078
1180
  orphan_lost, in_span_lost = _read_losses()
1079
1181
  if worker is None:
1080
1182
  return Health(