log-foundry 0.10.2.dev61__tar.gz → 0.10.2.dev63__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 (60) hide show
  1. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/PKG-INFO +4 -1
  2. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/README.md +3 -0
  3. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/_fork.py +100 -14
  5. log_foundry-0.10.2.dev63/src/log_foundry/_lifecycle.py +734 -0
  6. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/config.py +8 -1
  7. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/decorator.py +43 -1
  8. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/base.py +24 -6
  9. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/file.py +10 -5
  10. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/filtering.py +1 -1
  11. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/logstash.py +1 -1
  12. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/multi.py +1 -1
  13. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/sentry.py +1 -1
  14. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/transform.py +1 -1
  15. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/worker.py +18 -0
  16. log_foundry-0.10.2.dev61/src/log_foundry/_lifecycle.py +0 -212
  17. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/LICENSE +0 -0
  18. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/__init__.py +0 -0
  19. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/_diag.py +0 -0
  20. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/api.py +0 -0
  21. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/console.py +0 -0
  22. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/context.py +0 -0
  23. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/ids.py +0 -0
  24. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/model.py +0 -0
  25. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/py.typed +0 -0
  26. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/results.py +0 -0
  27. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sanitize.py +0 -0
  28. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/__init__.py +0 -0
  29. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/_batch.py +0 -0
  30. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/_chunk.py +0 -0
  31. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/_retry.py +0 -0
  32. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/_socket.py +0 -0
  33. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/_time.py +0 -0
  34. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/callback.py +0 -0
  35. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/clickhouse.py +0 -0
  36. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/datadog.py +0 -0
  37. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/elasticsearch.py +0 -0
  38. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/eventhubs.py +0 -0
  39. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/firehose.py +0 -0
  40. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/honeycomb.py +0 -0
  41. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/http.py +0 -0
  42. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/kafka.py +0 -0
  43. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/kinesis.py +0 -0
  44. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/logging_sink.py +0 -0
  45. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/loki.py +0 -0
  46. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/memory.py +0 -0
  47. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/mongodb.py +0 -0
  48. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/nats.py +0 -0
  49. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/newrelic.py +0 -0
  50. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/null.py +0 -0
  51. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/postgres.py +0 -0
  52. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/pubsub.py +0 -0
  53. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/rabbitmq.py +0 -0
  54. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/redis.py +0 -0
  55. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/sns.py +0 -0
  56. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/splunk.py +0 -0
  57. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/sqlite.py +0 -0
  58. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/sqs.py +0 -0
  59. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/src/log_foundry/sinks/stdout.py +0 -0
  60. {log_foundry-0.10.2.dev61 → log_foundry-0.10.2.dev63}/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.dev61
3
+ Version: 0.10.2.dev63
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -951,6 +951,8 @@ if (
951
951
 
952
952
  `closing_sinks` is deliberately not a term here: it is briefly non-zero during a perfectly healthy
953
953
  sink swap, so a single reading is not a fault. Watch it over time instead — see the table below.
954
+ `inherited_sink` is not a term either, for a different reason: it reports a *state* the process is
955
+ in rather than a loss it took, and in a prefork deployment it is `True` on every worker by design.
954
956
 
955
957
  They tell you different things, and they want different responses:
956
958
 
@@ -963,6 +965,7 @@ They tell you different things, and they want different responses:
963
965
  | `sink.failed` | The sink attempted delivery and could not confirm it — abandoned requests, partially-failed batches, responses it could not adjudicate. | Fix the destination. |
964
966
  | `retired` + `submitted_after_shutdown` | `shutdown()` was called and the process **kept logging**. Those events are queued where nothing will drain them — total loss, for as long as the process runs. | Use `flush()`, not `shutdown()`, in a process that logs again. This is the serverless mistake below. |
965
967
  | `incomplete_swaps` | A late `configure(sink=...)` could not confirm the previous sink was drained. The swap took effect; that sink was left open and some queued events may have gone to the new one. | Investigate the previous sink — it was hung or failing. Configure the sink before the first log where you can. |
968
+ | `inherited_sink` | This process is delivering to a sink it **inherited across a `fork`** and may not release, so it will not be closed here. Not a loss and not an alert term. | Nothing, usually. It explains a handle still open after `shutdown()`, and tells you a deployment shares one sink across a fork at all. `True` for a shared `StdoutSink` too, whose `close()` only flushes — so a `True` is not by itself evidence that anything is held. If you want the child to own its transport, build the sink in the worker process (see Forking). |
966
969
  | `closing_sinks` | Swapped-out sinks inside `close()` **right now** — a live gauge, not a counter, and the only field that falls as well as rises. Non-zero on a single read is normal during a swap. | Nothing, unless it stays non-zero. That means a destination is stuck in `close()` and will not release its resources. |
967
970
 
968
971
  `h.sink` is a `SinkLosses(dropped, failed)` or `None` — `None` when no worker exists yet, or when
@@ -915,6 +915,8 @@ if (
915
915
 
916
916
  `closing_sinks` is deliberately not a term here: it is briefly non-zero during a perfectly healthy
917
917
  sink swap, so a single reading is not a fault. Watch it over time instead — see the table below.
918
+ `inherited_sink` is not a term either, for a different reason: it reports a *state* the process is
919
+ in rather than a loss it took, and in a prefork deployment it is `True` on every worker by design.
918
920
 
919
921
  They tell you different things, and they want different responses:
920
922
 
@@ -927,6 +929,7 @@ They tell you different things, and they want different responses:
927
929
  | `sink.failed` | The sink attempted delivery and could not confirm it — abandoned requests, partially-failed batches, responses it could not adjudicate. | Fix the destination. |
928
930
  | `retired` + `submitted_after_shutdown` | `shutdown()` was called and the process **kept logging**. Those events are queued where nothing will drain them — total loss, for as long as the process runs. | Use `flush()`, not `shutdown()`, in a process that logs again. This is the serverless mistake below. |
929
931
  | `incomplete_swaps` | A late `configure(sink=...)` could not confirm the previous sink was drained. The swap took effect; that sink was left open and some queued events may have gone to the new one. | Investigate the previous sink — it was hung or failing. Configure the sink before the first log where you can. |
932
+ | `inherited_sink` | This process is delivering to a sink it **inherited across a `fork`** and may not release, so it will not be closed here. Not a loss and not an alert term. | Nothing, usually. It explains a handle still open after `shutdown()`, and tells you a deployment shares one sink across a fork at all. `True` for a shared `StdoutSink` too, whose `close()` only flushes — so a `True` is not by itself evidence that anything is held. If you want the child to own its transport, build the sink in the worker process (see Forking). |
930
933
  | `closing_sinks` | Swapped-out sinks inside `close()` **right now** — a live gauge, not a counter, and the only field that falls as well as rises. Non-zero on a single read is normal during a swap. | Nothing, unless it stays non-zero. That means a destination is stuck in `close()` and will not release its resources. |
931
934
 
932
935
  `h.sink` is a `SinkLosses(dropped, failed)` or `None` — `None` when no worker exists yet, or when
@@ -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.dev61"
23
+ version = "0.10.2.dev63"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -37,6 +37,36 @@ and all three would have to import this module for it to reach them; instead the
37
37
  it, so this module imports nothing that imports it and there is no cycle to break later.
38
38
  """
39
39
 
40
+ _FORK_SKIP = ("reacquired_in_child",)
41
+ """This module's own opt-out from the walk below (:data:`_SKIP_ATTRIBUTE`).
42
+
43
+ :data:`reacquired_in_child` holds **sinks**, so without this the next fork's walk enters the
44
+ previous fork's roster and re-collects every sink in it — including ones ``configure()``
45
+ superseded in between. Measured: a child's walk collected a sink two ``configure()`` calls out
46
+ of date, its hook was called, and the reclaim that follows then stamped it for the grandchild's
47
+ pid, so a sink no descendant ever acquired read ``releasable``. A ratchet, not a one-off: once a
48
+ hook-implementing sink enters a roster, every descendant's walk finds it again.
49
+
50
+ Exactly the hazard ``_lifecycle._owned`` already declares an opt-out for, reached by adding a
51
+ second global that holds sinks. The rebind below is the belt to this brace.
52
+ """
53
+
54
+ reacquired_in_child: list[Any] = []
55
+ """Sinks whose re-acquisition hook returned normally in this child (SPEC-042 FR-005).
56
+
57
+ Published rather than acted on, because what it means is an *ownership* claim and ownership is
58
+ ``_lifecycle``'s: a sink that returns from the hook holds a transport of its own, so a child may
59
+ release it.
60
+
61
+ Rebound — never mutated in place — **before the repair walk runs**, so a second fork cannot
62
+ inherit the first one's roster even if the opt-out above were removed. The first version of this
63
+ claimed the rebind happened "at the start of every child repair" while doing it in the middle,
64
+ after the walk; both halves were false and nothing tested either.
65
+
66
+ This module still imports nothing but ``_diag`` (SPEC-039 FR-006); the reader registers a
67
+ handler and reads this, which is the same inversion :data:`_child_handlers` exists for.
68
+ """
69
+
40
70
  _installed = False
41
71
  """Whether :func:`install` has already registered the child handler in this process.
42
72
 
@@ -330,8 +360,12 @@ def _fresh_primitive(value: Any, memo: dict[int, Any], keepalive: list[Any]) ->
330
360
  return fresh
331
361
 
332
362
 
333
- _DISCARD_HOOK = "discard_buffered_after_fork"
334
- """The optional sink member a forked child asks to strand the parent's pending bytes.
363
+ _REACQUIRE_HOOK = "reacquire_after_fork"
364
+ """The optional sink member a forked child asks to take its transport over.
365
+
366
+ Named for the larger half of what it does (SPEC-042 FR-005): stranding the parent's pending
367
+ bytes is one consequence, and re-acquiring the transport is the step — a sink that returns from
368
+ it has claimed the transport as this process's own, which is what makes releasing it safe.
335
369
 
336
370
  Probed by name, as ``losses()`` and ``log_foundry_stop_signal`` are, so no existing sink stops
337
371
  satisfying ``Sink`` by not having it. ``sinks/base.py`` states the contract; this is the only
@@ -339,7 +373,7 @@ place the name is read.
339
373
  """
340
374
 
341
375
 
342
- def _offers_discard(holder: object) -> bool:
376
+ def _offers_reacquire(holder: object) -> bool:
343
377
  """Whether this object carries FR-004's buffer hook, without letting the question raise.
344
378
 
345
379
  Every other read the walk makes is guarded individually, and this one has to be too: an
@@ -361,11 +395,48 @@ def _offers_discard(holder: object) -> bool:
361
395
  None.
362
396
  """
363
397
  try:
364
- return callable(getattr(holder, _DISCARD_HOOK, None))
398
+ return callable(getattr(holder, _REACQUIRE_HOOK, None))
365
399
  except Exception:
366
400
  return False
367
401
 
368
402
 
403
+ _SKIP_ATTRIBUTE = "_FORK_SKIP"
404
+ """A module's opt-out: attribute names the walk must not read or descend into.
405
+
406
+ **Bookkeeping that pins objects is not live state to repair**, and the difference is invisible
407
+ to a walk that only sees a container. ``_lifecycle._owned`` is the case: SPEC-042 FR-001 requires
408
+ it to hold a **strong** reference to every sink this process ever acquired, so without an opt-out
409
+ the walk reaches sinks the process abandoned several ``configure()`` calls ago — replacing their
410
+ locks, which is merely wasteful, and calling their fork hooks, which is not. Measured: a forked
411
+ child announced a buffer-discard failure for a sink that had been superseded, and a ``FileSink``
412
+ in that state would have its file reopened on every fork for the life of the process.
413
+
414
+ Declared by the module that owns the state rather than listed here, which keeps this module's
415
+ rule that it imports nothing but ``_diag`` (SPEC-039 FR-006). Nothing is lost by skipping: a sink
416
+ that is still *live* is reached through the config and the worker, which are not opted out.
417
+ """
418
+
419
+
420
+ def _skipped_names(holder: object) -> frozenset[str]:
421
+ """Returns the attribute names a holder has opted out of the walk.
422
+
423
+ Args:
424
+ holder: The module, class or instance the walk is about to read.
425
+
426
+ Returns:
427
+ The opted-out names, empty for anything that declares none.
428
+
429
+ Raises:
430
+ None. A malformed or unreadable declaration opts nothing out, so the walk does more work
431
+ rather than less — the safe direction for a repair whose absence is a hang.
432
+ """
433
+ try:
434
+ declared = getattr(holder, _SKIP_ATTRIBUTE, None)
435
+ return frozenset(declared) if declared else frozenset()
436
+ except Exception:
437
+ return frozenset()
438
+
439
+
369
440
  def _reinit_primitives() -> list[Any]:
370
441
  """Replaces every lock and event this package owns, wherever the walk reaches one.
371
442
 
@@ -391,7 +462,7 @@ def _reinit_primitives() -> list[Any]:
391
462
  None.
392
463
 
393
464
  Returns:
394
- The owned instances carrying :data:`_DISCARD_HOOK`, in the order the walk reached them.
465
+ The owned instances carrying :data:`_REACQUIRE_HOOK`, in the order the walk reached them.
395
466
 
396
467
  Raises:
397
468
  None.
@@ -415,9 +486,12 @@ def _reinit_primitives() -> list[Any]:
415
486
  if not isinstance(holder, types.ModuleType | type):
416
487
  if not _is_owned(holder):
417
488
  continue
418
- if _offers_discard(holder):
489
+ if _offers_reacquire(holder):
419
490
  buffered.append(holder)
491
+ skip = _skipped_names(holder)
420
492
  for name, value in _namespace_items(holder):
493
+ if name in skip:
494
+ continue
421
495
  fresh = _fresh_primitive(value, memo, keepalive)
422
496
  if fresh is not None:
423
497
  _assign(holder, name, fresh)
@@ -426,19 +500,25 @@ def _reinit_primitives() -> list[Any]:
426
500
  return buffered
427
501
 
428
502
 
429
- def _discard_buffers(holders: list[Any]) -> None:
430
- """Asks each sink that owns a buffered stream to strand what it inherited (FR-004).
503
+ def _reacquire_transports(holders: list[Any]) -> list[Any]:
504
+ """Asks each sink that owns a buffered stream to re-acquire it in this child (FR-004).
431
505
 
432
506
  A fork landing inside ``emit`` — after the write loop, before the flush — leaves the child
433
507
  holding the parent's unflushed bytes, which both processes then write: measured, the event
434
508
  at the fork point appeared on disk twice. Without a ``before`` handler there is nowhere to
435
- empty the buffer from (FR-001), so the child throws its copy away instead.
509
+ empty the buffer from (FR-001), so the child re-opens instead, which strands the copy.
510
+
511
+ **Which hooks returned normally is published rather than acted on** (SPEC-042 FR-005). A
512
+ sink that returns from the hook has claimed the transport as this process's own, which is an
513
+ ownership fact — but ownership lives in ``_lifecycle``, and this module imports nothing but
514
+ ``_diag``. So the roster is handed back and the handler ``_lifecycle`` registers re-stamps
515
+ it.
436
516
 
437
517
  Args:
438
518
  holders: What :func:`_reinit_primitives` collected on its walk.
439
519
 
440
520
  Returns:
441
- None.
521
+ The holders whose hook returned normally, in the order they were called.
442
522
 
443
523
  Raises:
444
524
  None. One sink's failure is absorbed separately from the rest, so a hook that cannot
@@ -446,15 +526,19 @@ def _discard_buffers(holders: list[Any]) -> None:
446
526
  sink its repair — and the consequence is named, because an absorbed failure is
447
527
  invisible apart from that line.
448
528
  """
529
+ reacquired: list[Any] = []
449
530
  for holder in holders:
450
531
  try:
451
- getattr(holder, _DISCARD_HOOK)()
532
+ getattr(holder, _REACQUIRE_HOOK)()
533
+ reacquired.append(holder)
452
534
  except Exception as exc:
453
535
  _diag.absorbed(
454
- "discarding an inherited buffer after a fork",
536
+ "re-acquiring a transport after a fork",
455
537
  exc,
456
- f"{type(holder).__name__} may write the parent's pending bytes again",
538
+ f"{type(holder).__name__} may write the parent's pending bytes again, and this "
539
+ f"child will not release it",
457
540
  )
541
+ return reacquired
458
542
 
459
543
 
460
544
  def register_child_handler(fn: Callable[[], None]) -> None:
@@ -515,12 +599,14 @@ def _reinit_after_fork() -> None:
515
599
  registers another simply causes it to run — and :func:`register_child_handler` is the
516
600
  only writer, appending only.
517
601
  """
602
+ global reacquired_in_child
603
+ reacquired_in_child = []
518
604
  buffered: list[Any] = []
519
605
  try:
520
606
  buffered = _reinit_primitives()
521
607
  except Exception as exc:
522
608
  _diag.absorbed("repairing the library after a fork", exc, "this child may block or lose")
523
- _discard_buffers(buffered)
609
+ reacquired_in_child = _reacquire_transports(buffered)
524
610
  for handler in _child_handlers:
525
611
  try:
526
612
  handler()