log-foundry 0.10.2.dev53__tar.gz → 0.10.2.dev54__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.dev53 → log_foundry-0.10.2.dev54}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/_fork.py +61 -5
  4. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/decorator.py +38 -1
  5. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/worker.py +100 -1
  6. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/LICENSE +0 -0
  7. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/README.md +0 -0
  8. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/__init__.py +0 -0
  9. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/_diag.py +0 -0
  10. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/_lifecycle.py +0 -0
  11. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/api.py +0 -0
  12. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/config.py +0 -0
  13. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/console.py +0 -0
  14. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/context.py +0 -0
  15. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/ids.py +0 -0
  16. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/model.py +0 -0
  17. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/py.typed +0 -0
  18. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/results.py +0 -0
  19. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sanitize.py +0 -0
  20. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/__init__.py +0 -0
  21. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/_batch.py +0 -0
  22. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/_chunk.py +0 -0
  23. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/_retry.py +0 -0
  24. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/_socket.py +0 -0
  25. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/_time.py +0 -0
  26. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/base.py +0 -0
  27. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/callback.py +0 -0
  28. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/clickhouse.py +0 -0
  29. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/datadog.py +0 -0
  30. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/elasticsearch.py +0 -0
  31. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/eventhubs.py +0 -0
  32. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/file.py +0 -0
  33. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/filtering.py +0 -0
  34. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/firehose.py +0 -0
  35. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/honeycomb.py +0 -0
  36. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/http.py +0 -0
  37. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/kafka.py +0 -0
  38. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/kinesis.py +0 -0
  39. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/logging_sink.py +0 -0
  40. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/logstash.py +0 -0
  41. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/loki.py +0 -0
  42. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/memory.py +0 -0
  43. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/mongodb.py +0 -0
  44. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/multi.py +0 -0
  45. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/nats.py +0 -0
  46. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/newrelic.py +0 -0
  47. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/null.py +0 -0
  48. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/postgres.py +0 -0
  49. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/pubsub.py +0 -0
  50. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/rabbitmq.py +0 -0
  51. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/redis.py +0 -0
  52. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/sentry.py +0 -0
  53. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/sns.py +0 -0
  54. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/splunk.py +0 -0
  55. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/sqlite.py +0 -0
  56. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/sqs.py +0 -0
  57. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/stdout.py +0 -0
  58. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/syslog.py +0 -0
  59. {log_foundry-0.10.2.dev53 → log_foundry-0.10.2.dev54}/src/log_foundry/sinks/transform.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log-foundry
3
- Version: 0.10.2.dev53
3
+ Version: 0.10.2.dev54
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.dev53"
23
+ version = "0.10.2.dev54"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -7,10 +7,13 @@ import os
7
7
  import sys
8
8
  import threading
9
9
  import types
10
- from typing import Any
10
+ from typing import TYPE_CHECKING, Any
11
11
 
12
12
  from log_foundry import _diag
13
13
 
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Callable
16
+
14
17
  _PACKAGE = __name__.rpartition(".")[0]
15
18
  """This package's import name, derived rather than written.
16
19
 
@@ -25,6 +28,15 @@ _LOCK_TYPES: tuple[type, ...] = (type(threading.Lock()), _RLOCK_TYPE)
25
28
 
26
29
  _CONTAINER_TYPES: tuple[type, ...] = (list, tuple, set, frozenset, dict, collections.deque)
27
30
 
31
+ _child_handlers: list[Callable[[], None]] = []
32
+ """What runs in the child once its locks are its own again, in registration order.
33
+
34
+ **An inverted registry, and that is what keeps the dependency arrow pointing one way**
35
+ (FR-006). The work a child needs done belongs to ``decorator``, ``_lifecycle`` and the sinks,
36
+ and all three would have to import this module for it to reach them; instead they register with
37
+ it, so this module imports nothing that imports it and there is no cycle to break later.
38
+ """
39
+
28
40
  _installed = False
29
41
  """Whether :func:`install` has already registered the child handler in this process.
30
42
 
@@ -367,12 +379,46 @@ def _reinit_primitives() -> None:
367
379
  stack.append(value)
368
380
 
369
381
 
382
+ def register_child_handler(fn: Callable[[], None]) -> None:
383
+ """Adds work to be done in a forked child, after its locks have been re-initialised.
384
+
385
+ A handler may take any of the library's locks, which is what the ordering in
386
+ :func:`_reinit_after_fork` buys it and why registering is the only way in. What it must not
387
+ assume is that it is alone: handlers run in registration order and an earlier one may have
388
+ started a thread — ``decorator``'s rebuild does exactly that — so only the *first* runs in a
389
+ genuinely single-threaded child. None of them may block, since nothing has returned from
390
+ ``fork`` yet.
391
+
392
+ Registering the same function twice is a no-op, compared by **identity**: ``in`` would ask
393
+ a registered object's ``__eq__``, which is not this function's to trust and would raise into
394
+ a caller documented as raising nothing. The exposure is the one :data:`_installed` records
395
+ for the fork registration itself: a reload of a module whose body registers here would
396
+ otherwise stack a second handler, and for the worker rebuild that means two drain threads,
397
+ the first bound to a queue nothing writes to.
398
+
399
+ Args:
400
+ fn: Called with no arguments in the child. A failure is absorbed and announced, and the
401
+ remaining handlers still run.
402
+
403
+ Returns:
404
+ None.
405
+
406
+ Raises:
407
+ None.
408
+ """
409
+ if not any(handler is fn for handler in _child_handlers):
410
+ _child_handlers.append(fn)
411
+
412
+
370
413
  def _reinit_after_fork() -> None:
371
414
  """Repairs the library in a child that has just returned from ``fork``.
372
415
 
373
- **The order of work here is the contract** (FR-001 AC-2): locks and events first, because
374
- anything running afterwards may take one, and a lock re-initialised after a handler that
375
- takes it is a handler that hangs.
416
+ **The order of work here is the contract** (FR-001 AC-2): locks and events first, then the
417
+ registered handlers. A lock re-initialised *after* a handler that takes it is a handler
418
+ that hangs, and it hangs on the child's only thread with nothing to interrupt it. Work that
419
+ must happen before *any* handler belongs inline between the two steps rather than registered
420
+ — the buffer discard FR-004 adds is the case, and registering it would put it after
421
+ ``decorator``'s rebuild, which has started a live drain thread by then.
376
422
 
377
423
  Args:
378
424
  None.
@@ -383,12 +429,22 @@ def _reinit_after_fork() -> None:
383
429
  Raises:
384
430
  None. A fork handler that raises has its exception printed by CPython with a full
385
431
  traceback, carrying the message arch §6 keeps out of anything the library says about
386
- itself — and it would leave the rest of the repair undone.
432
+ itself — and it would leave the rest of the repair undone. One handler's failure is
433
+ absorbed separately from the rest for the same reason: a child that cannot rebuild its
434
+ worker should still have working locks. The list is iterated live rather than copied,
435
+ which is safe because CPython's list iterator is index-based, so a handler that
436
+ registers another simply causes it to run — and :func:`register_child_handler` is the
437
+ only writer, appending only.
387
438
  """
388
439
  try:
389
440
  _reinit_primitives()
390
441
  except Exception as exc:
391
442
  _diag.absorbed("repairing the library after a fork", exc, "this child may block or lose")
443
+ for handler in _child_handlers:
444
+ try:
445
+ handler()
446
+ except Exception as exc:
447
+ _diag.absorbed("running a fork handler", exc, "this child may not deliver")
392
448
 
393
449
 
394
450
  def install() -> None:
@@ -11,7 +11,7 @@ from dataclasses import replace
11
11
  from time import monotonic
12
12
  from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
13
13
 
14
- from log_foundry import _diag, _lifecycle, context
14
+ from log_foundry import _diag, _fork, _lifecycle, context
15
15
  from log_foundry.config import _ensure_sink
16
16
  from log_foundry.ids import (
17
17
  is_valid_span_id,
@@ -332,6 +332,43 @@ def _note_orphan_emit(sink: Sink) -> None:
332
332
  _orphan_sink = sink
333
333
 
334
334
 
335
+ def _rebuild_worker_after_fork() -> None:
336
+ """Gives a forked child a drain thread of its own, or a retired worker (SPEC-039 FR-002).
337
+
338
+ Registered with ``_fork`` at import rather than reached for by it, which is what keeps that
339
+ module free of an import of this one (FR-006). It runs on the child's only thread, after the
340
+ locks are its own again, so it may take them.
341
+
342
+ The predicate is deliberately **not** ``_live_worker()``, even though the two agree today.
343
+ What is being asked is whether to start a thread, and the answer must survive a later
344
+ reading of ``retired`` — so it is hoisted into ``resume`` and passed, where the roster files
345
+ it as its own decision rather than as a call whose category a reader has to chase.
346
+
347
+ ``_worker_lock`` is deliberately **not** taken. There is one thread here by construction,
348
+ and the lock was re-initialised moments ago: a child that took it would be taking a lock no
349
+ other thread can contend for, at the one moment in the process's life when that is provably
350
+ true.
351
+
352
+ Args:
353
+ None.
354
+
355
+ Returns:
356
+ None.
357
+
358
+ Raises:
359
+ None. ``_fork`` absorbs and announces a handler's failure, so a child whose worker cannot
360
+ be rebuilt still has working locks.
361
+ """
362
+ worker = _worker
363
+ if worker is None:
364
+ return
365
+ resume = not worker.retired
366
+ worker._reinit_after_fork(resume=resume)
367
+
368
+
369
+ _fork.register_child_handler(_rebuild_worker_after_fork)
370
+
371
+
335
372
  def _live_worker() -> Worker | None:
336
373
  """Returns the process worker only while it is still delivering (SPEC-033 FR-002).
337
374
 
@@ -81,7 +81,10 @@ class Health:
81
81
  report. Non-``None`` is categorically worse than the two counters above: they measure
82
82
  loss the worker absorbed and kept running through, this one means the worker is gone
83
83
  (SPEC-019 FR-003). Also ``"ShutdownTimeout"`` when a bounded :meth:`Worker.shutdown`
84
- expired before the drain finished (SPEC-027 FR-004), the same thing to a reader.
84
+ expired before the drain finished (SPEC-027 FR-004), and the type name of whatever
85
+ stopped a forked child's rebuild from starting a thread at all (SPEC-039 FR-002) — a
86
+ case where the drain thread never ran rather than died. All three are the same thing to
87
+ a reader, which is why they share the field: nothing is delivering.
85
88
  sink: The configured sink's own loss counters, or ``None`` when there is no worker or
86
89
  the sink reports nothing (SPEC-026 FR-003). Nested rather than folded into the
87
90
  integers above because they count different things: ``dropped`` here is backpressure
@@ -229,6 +232,7 @@ class Worker:
229
232
  self.stopped_reason: str | None = None
230
233
  self.submitted_after_shutdown = 0
231
234
  self.incomplete_swaps = 0
235
+ self._max_queue = max_queue
232
236
  self._queue: queue.Queue[object] = queue.Queue(maxsize=max_queue)
233
237
  self._stop = threading.Event()
234
238
  self._drain_finished = threading.Event()
@@ -242,6 +246,101 @@ class Worker:
242
246
  )
243
247
  self._thread.start()
244
248
 
249
+ def _reinit_after_fork(self, *, resume: bool) -> None:
250
+ """Rebuilds this worker **in place** for a child that has just forked (SPEC-039 FR-002).
251
+
252
+ The child inherits a ``Worker`` whose thread does not exist, so ``submit`` goes on
253
+ enqueueing and nothing drains: measured, six events never delivered, ``atexit`` closing
254
+ the sink without a drain, and ``health()`` reading ``queued=2`` with every other field
255
+ clean — the documented alert idiom blind. Rebuilding rather than retiring is the design:
256
+ a prefork server's child is a working process, and a child that silently stops logging
257
+ is the failure this arc exists to remove.
258
+
259
+ **In place, never a new object** (FR-002 AC-5). The ownership guards keyed on
260
+ ``_worker.sink is X`` and ``_lifecycle``'s registry are identity comparisons, so
261
+ replacing the worker would leave every one of them answering about something else.
262
+
263
+ **The queue is replaced, not drained.** Emptying it would keep ``queue.Queue``'s own
264
+ mutex and its three ``Condition``s, which the fork walk cannot reach — it enters no
265
+ standard-library container — and a fork landing inside that mutex leaves the child's
266
+ very next ``submit`` blocked on the application's thread. That is why the replacement
267
+ happens even when nothing is resumed: a retired worker still *accepts* submissions
268
+ (SPEC-030 FR-001), so a retired child would block on the same mutex. Starting empty is
269
+ also what stops the parent's undelivered backlog being sent twice (AC-2).
270
+
271
+ ``stopped_reason`` is cleared rather than set to ``"Forked"`` (AC-6). SPEC-019 defines
272
+ that field as "the drain thread died", and this child's drain thread is about to be
273
+ running — the alternative reads as the more honest one and is not.
274
+
275
+ The two drain events are **set or cleared to match what this child will actually do**,
276
+ never simply inherited. Resuming clears them, so ``draining`` and ``flush``'s gate
277
+ describe the thread starting here rather than the one that did not survive the fork;
278
+ retiring **sets** them, because no thread will ever set them and a child forked while a
279
+ ``shutdown()`` was mid-join otherwise inherits them unset with nothing to settle them.
280
+ Two consequences, both measured: that child paid the whole 30 s budget at exit and with
281
+ ``shutdown(timeout=None)`` would never exit at all, and — reaching further —
282
+ ``_offer_orphan_signal`` reads it as still draining and skips, leaving the sink holding
283
+ a **set** stop event, so every later backoff returns instantly against a destination
284
+ that is already refusing (SPEC-033 FR-004). ``_drain_finished`` has no reader on a
285
+ retired worker and is set for the invariant rather than for an observable: settled with
286
+ finished clear is what :attr:`draining` defines as an *abandoned* drain, and a child
287
+ reporting no ``stopped_reason`` must not read that way. The stop signal is re-offered for
288
+ the sink the fork walk cannot reach: a **third-party** sink is outside its ownership
289
+ boundary, so it would keep pointing at the pre-fork event while this worker sets a new
290
+ one — SPEC-027's guarantee broken by the repair meant to preserve it.
291
+
292
+ **The new thread is only installed once it has started**, which ``__init__`` never had
293
+ to consider: a constructor whose ``start`` raises lets no ``Worker`` escape, while here
294
+ the worker is already the process's. Assigning first would leave an unstarted ``Thread``
295
+ on a live worker reading ``draining`` forever, and the next ``shutdown()`` would take a
296
+ ``RuntimeError`` from ``join`` straight out of a public call documented to raise nothing.
297
+ The inherited thread object is kept instead, which is safe because CPython repairs it
298
+ across the fork — measured, it reports dead and both a bounded and an unbounded ``join``
299
+ return in 0.0000 s — and the failure is recorded as a ``stopped_reason``, which is
300
+ exactly SPEC-019's vocabulary for "nothing is delivering". Setting both drain events is
301
+ what keeps :meth:`shutdown` from queueing a sentinel into a queue no thread will read.
302
+
303
+ The success path assigns ``self._thread`` *after* the start, so for an instant a live
304
+ drain thread coexists with the inherited dead one in that attribute. That is safe only
305
+ because the drain thread never reads it — ``_run``, ``_drain``, ``_terminal_failure``
306
+ and ``_release_waiters`` do not, and the three readers are all on caller threads — which
307
+ is an invariant this note states rather than one anything enforces.
308
+
309
+ Args:
310
+ resume: Whether to start a drain thread. ``False`` for a retired parent, which forks
311
+ a retired child (AC-4): a fork does not undo a ``shutdown()``, and reviving a worker
312
+ the caller terminated would be the library overruling them.
313
+
314
+ Returns:
315
+ None.
316
+
317
+ Raises:
318
+ None.
319
+ """
320
+ self._queue = queue.Queue(maxsize=self._max_queue)
321
+ self.dropped = 0
322
+ self.failed_batches = 0
323
+ self.submitted_after_shutdown = 0
324
+ self.incomplete_swaps = 0
325
+ self.stopped_reason = None
326
+ if not resume:
327
+ self._drain_finished.set()
328
+ self._drain_settled.set()
329
+ return
330
+ self._drain_finished.clear()
331
+ self._drain_settled.clear()
332
+ self._offer_stop_signal()
333
+ thread = threading.Thread(target=self._run, name="log-foundry-worker", daemon=True)
334
+ try:
335
+ thread.start()
336
+ except Exception as exc:
337
+ self.stopped_reason = type(exc).__name__
338
+ self._drain_finished.set()
339
+ self._drain_settled.set()
340
+ _diag.absorbed("starting this child's drain thread", exc, "it will deliver nothing")
341
+ return
342
+ self._thread = thread
343
+
245
344
  def _offer_stop_signal(self) -> None:
246
345
  """Gives the sink this worker's shutdown event, if it advertises somewhere to put it.
247
346