log-foundry 0.10.2.dev83__tar.gz → 0.10.2.dev84__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.dev83 → log_foundry-0.10.2.dev84}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/_lifecycle.py +149 -7
  4. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/LICENSE +0 -0
  5. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/README.md +0 -0
  6. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/__init__.py +0 -0
  7. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/_diag.py +0 -0
  8. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/_fork.py +0 -0
  9. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/api.py +0 -0
  10. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/config.py +0 -0
  11. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/console.py +0 -0
  12. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/context.py +0 -0
  13. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/decorator.py +0 -0
  14. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/ids.py +0 -0
  15. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/model.py +0 -0
  16. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/py.typed +0 -0
  17. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/results.py +0 -0
  18. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sanitize.py +0 -0
  19. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/__init__.py +0 -0
  20. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/_batch.py +0 -0
  21. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/_chunk.py +0 -0
  22. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/_retry.py +0 -0
  23. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/_socket.py +0 -0
  24. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/_time.py +0 -0
  25. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/base.py +0 -0
  26. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/callback.py +0 -0
  27. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/clickhouse.py +0 -0
  28. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/datadog.py +0 -0
  29. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/elasticsearch.py +0 -0
  30. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/eventhubs.py +0 -0
  31. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/file.py +0 -0
  32. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/filtering.py +0 -0
  33. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/firehose.py +0 -0
  34. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/honeycomb.py +0 -0
  35. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/http.py +0 -0
  36. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/kafka.py +0 -0
  37. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/kinesis.py +0 -0
  38. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/logging_sink.py +0 -0
  39. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/logstash.py +0 -0
  40. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/loki.py +0 -0
  41. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/memory.py +0 -0
  42. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/mongodb.py +0 -0
  43. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/multi.py +0 -0
  44. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/nats.py +0 -0
  45. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/newrelic.py +0 -0
  46. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/null.py +0 -0
  47. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/postgres.py +0 -0
  48. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/pubsub.py +0 -0
  49. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/rabbitmq.py +0 -0
  50. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/redis.py +0 -0
  51. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/sentry.py +0 -0
  52. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/sns.py +0 -0
  53. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/splunk.py +0 -0
  54. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/sqlite.py +0 -0
  55. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/sqs.py +0 -0
  56. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/stdout.py +0 -0
  57. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/syslog.py +0 -0
  58. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/src/log_foundry/sinks/transform.py +0 -0
  59. {log_foundry-0.10.2.dev83 → log_foundry-0.10.2.dev84}/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.dev83
3
+ Version: 0.10.2.dev84
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.dev83"
23
+ version = "0.10.2.dev84"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -1365,6 +1365,92 @@ def _offer_orphan_signal(sink: Sink) -> None:
1365
1365
  if _closing(sink):
1366
1366
  return
1367
1367
  offer_stop_signal(sink, _state.refresh_stop_signal())
1368
+ def _close_owed(sink: Sink) -> None:
1369
+ """Closes one sink the orphan path owed, absorbing whatever the close raised (SPEC-046).
1370
+
1371
+ Factored out so the inline close and the threaded ones are provably the same call, and
1372
+ because a thread body must not raise: an exception escaping one reaches CPython's bootstrap,
1373
+ which prints a traceback carrying the message arch §6 keeps out of anything the library says
1374
+ about itself. It is the same guard :func:`_close_guarded` applies for a swapped-out sink, with
1375
+ the ``_diag`` text of the site it actually is (SPEC-029).
1376
+
1377
+ It records nothing in the closed-sink latch. :func:`_close_orphan_sink` **empties the owed
1378
+ record** under ``_state._lock`` before any close starts, which is what stops two callers
1379
+ performing the same close. The closed-sink latch is a single slot and holds only the last of
1380
+ them — unchanged by SPEC-046, and why a racing emit can still re-arm one of the others is
1381
+ recorded in ``architecture.md`` §13.
1382
+
1383
+ Args:
1384
+ sink: The sink to close.
1385
+
1386
+ Returns:
1387
+ None.
1388
+
1389
+ Raises:
1390
+ None. ``Exception``, never ``BaseException`` — the SPEC-025 line. On the calling thread that
1391
+ keeps a ``KeyboardInterrupt`` or ``SystemExit`` reaching the caller as that decision
1392
+ requires; on a fan-out thread it cannot, because there is no caller to reach. CPython's
1393
+ ``threading.excepthook`` announces an interrupt itself and discards a ``SystemExit``, so
1394
+ one raised by a threaded close is reported by the runtime rather than by this library and
1395
+ does not propagate. That is :func:`_close_guarded`'s shipped behaviour too; it is stated
1396
+ here rather than left to be discovered.
1397
+ """
1398
+ try:
1399
+ release(sink)
1400
+ except Exception as exc:
1401
+ _diag.absorbed("closing the sink", exc, "it may still hold its resources")
1402
+
1403
+
1404
+ def _live_config_sink() -> Sink | None:
1405
+ """Returns the configured sink, for callers that must not import config at module scope.
1406
+
1407
+ Args:
1408
+ None.
1409
+
1410
+ Returns:
1411
+ The sink the config names, or None when none is set.
1412
+
1413
+ Raises:
1414
+ None.
1415
+ """
1416
+ from log_foundry.config import _live_config
1417
+
1418
+ return _live_config().sink
1419
+
1420
+
1421
+ def _inline_close_choice(owed: list[Sink]) -> Sink:
1422
+ """Picks the owed sink whose close stays on the calling thread (SPEC-046 FR-001).
1423
+
1424
+ The **configured** sink where it is among those owed, and otherwise the most recently armed.
1425
+ The config is the authority for which sink is being delivered to, and keeping that one inline
1426
+ is what preserves SPEC-030's decision that ``shutdown()``'s own close stays inline. The
1427
+ fallback is what keeps the single-owed-sink case free of a thread it does not need.
1428
+
1429
+ Membership is by **identity**, never ``in``. ``list.__contains__`` is ``x is e or x == e``, so
1430
+ a sink with a value ``__eq__`` — a dataclass, say, which ``Sink`` permits and no shipped sink
1431
+ happens to be — would match an object the record never armed: the inline close would then run
1432
+ against a sink that was never latched, and every genuinely owed sink would go to a thread.
1433
+
1434
+ This deliberately does not copy :func:`_delivering_to_an_inherited_sink`, which takes the
1435
+ record's last entry and whose own docstring says neither end of the record is authoritative
1436
+ for "installed".
1437
+
1438
+ Args:
1439
+ owed: The sinks owed a close, in arming order and never empty.
1440
+
1441
+ Returns:
1442
+ The one to close on the calling thread.
1443
+
1444
+ Raises:
1445
+ None.
1446
+ """
1447
+ configured = _live_config_sink()
1448
+ for sink in owed:
1449
+ if sink is configured:
1450
+ return sink
1451
+ return owed[-1]
1452
+
1453
+
1368
1454
  def _close_orphan_sink() -> None:
1369
1455
  """Closes a sink only the orphan path ever wrote to, once (SPEC-031 FR-006).
1370
1456
 
@@ -1394,6 +1480,41 @@ def _close_orphan_sink() -> None:
1394
1480
  The once-only flag is set ahead of the close, as ``Worker.shutdown``'s is: a second
1395
1481
  ``close()`` on a sink that partially released its resources is worse than an unclosed one.
1396
1482
 
1483
+ **The owed closes run concurrently and every one is joined** (SPEC-046). Draining the record
1484
+ in sequence made this cost one slow close *times* the number owed — measured against
1485
+ ``shutdown(timeout=1.0)`` with 2-second closes, one owed sink 2.00 s and four 8.02 s — which
1486
+ is a multiplication SPEC-045 introduced when it made the record a set. One sink closes on the
1487
+ calling thread (:func:`_inline_close_choice` picks which, and why) and the rest get a thread
1488
+ each, so the cost is the slowest rather than the sum.
1489
+
1490
+ **Joined, deliberately not detached.** Routing them through :func:`_start_closer` and
1491
+ :func:`join_closers` is the obvious reuse and it loses data, in two independent ways. The
1492
+ grace is what remains of the shutdown's budget, which a slow inline close can exhaust
1493
+ entirely — measured completing **1 of 4** against a 1.0 s budget. And it caps at
1494
+ :data:`DEFAULT_CLOSER_GRACE` regardless, so even inside a generous budget a sink whose
1495
+ ``close()`` outlasts two seconds is abandoned and killed at interpreter exit — measured, a
1496
+ 3-second close delivered nothing where it delivers today. It also recharges the double grace SPEC-044 measured, and runs into
1497
+ the §13 entry recording that a daemon close of *this* sink was built and reverted because
1498
+ exit can kill it inside ``SQLiteSink.commit()``. Joining every close avoids all three, and is
1499
+ strictly better than the sequential drain on both axes: the cost falls and the loss stays
1500
+ zero. The threads are daemons only so that one which somehow outlives the join cannot keep the
1501
+ interpreter alive; the join, not the flag, is what guarantees each close completes.
1502
+
1503
+ **The join is in a ``finally``**, so a ``BaseException`` — the ``KeyboardInterrupt`` SPEC-025
1504
+ requires to reach the caller — waits for the started closes before it propagates. Without it,
1505
+ a Ctrl-C during ``shutdown()`` returned with every fan-out close abandoned *mid-write*, where
1506
+ the sequential drain abandoned one and had merely not started the rest: measured 4 killed
1507
+ mid-write against 1, which trades a leaked resource for a corrupt one and is the wrong side
1508
+ of the ordering §13 records for exactly this hazard. The interrupt still reaches the caller;
1509
+ it is delayed by the closes already in flight, which is the same wait the inline close has
1510
+ always imposed.
1511
+
1512
+ A thread that will not start is closed **inline** instead, which is the opposite of
1513
+ :func:`_start_closer`'s refusal and deliberately so: that helper is spending a caller's
1514
+ bounded budget, and falling back to an inline close there would reintroduce the unbounded
1515
+ wait it exists to remove. This path has no budget left to protect — it is the exit — so the
1516
+ choice is between closing inline and never closing at all.
1517
+
1397
1518
  Args:
1398
1519
  None.
1399
1520
 
@@ -1412,11 +1533,32 @@ def _close_orphan_sink() -> None:
1412
1533
  for sink in owed:
1413
1534
  del _state._orphan_owed[id(sink)]
1414
1535
  _state._orphan_closed_sink = sink
1415
- for sink in owed:
1416
- try:
1417
- release(sink)
1418
- except Exception as exc:
1419
- _diag.absorbed("closing the sink", exc, "it may still hold its resources")
1536
+ if not owed:
1537
+ return
1538
+ inline = _inline_close_choice(owed)
1539
+ started: list[threading.Thread] = []
1540
+ try:
1541
+ for sink in owed:
1542
+ if sink is inline:
1543
+ continue
1544
+ closer = threading.Thread(
1545
+ target=_close_owed, args=(sink,), name="log-foundry-owed-close", daemon=True
1546
+ )
1547
+ try:
1548
+ closer.start()
1549
+ except Exception as exc:
1550
+ _diag.absorbed(
1551
+ "starting the thread that closes an owed sink",
1552
+ exc,
1553
+ "it is closed inline instead",
1554
+ )
1555
+ _close_owed(sink)
1556
+ else:
1557
+ started.append(closer)
1558
+ _close_owed(inline)
1559
+ finally:
1560
+ for closer in started:
1561
+ closer.join()
1420
1562
  def _shutdown_worker(timeout: float | None = DEFAULT_SHUTDOWN_TIMEOUT) -> None:
1421
1563
  """Drains and closes the process worker, or closes an orphan-only sink, backing ``shutdown()``.
1422
1564
 
@@ -1792,8 +1934,8 @@ def _delivering_to_an_inherited_sink() -> bool:
1792
1934
  order is ``[live, superseded]``. Neither end of the record is authoritative for "installed" —
1793
1935
  arming order is emit order, which is a different question — and the config is. The answer is
1794
1936
  therefore unchanged from before SPEC-045 and can still name a superseded sink; that limit is
1795
- recorded in ``architecture.md`` §13 rather than quietly fixed here, because correcting it
1796
- changes a documented ``Health`` field on a path this spec does not otherwise touch.
1937
+ an open item in ``architecture.md`` §12 rather than quietly fixed here, because correcting it
1938
+ changes a documented ``Health`` field on a path that spec did not otherwise touch.
1797
1939
 
1798
1940
  With no sink resolved at all there is nothing installed and nothing inherited, so the answer
1799
1941
  is ``False`` rather than a guess.