log-foundry 0.10.2.dev112__tar.gz → 0.10.2.dev114__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.dev112 → log_foundry-0.10.2.dev114}/PKG-INFO +2 -2
  2. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/README.md +1 -1
  3. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/worker.py +83 -5
  5. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/LICENSE +0 -0
  6. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/__init__.py +0 -0
  7. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/_diag.py +0 -0
  8. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/_fork.py +0 -0
  9. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/_lifecycle.py +0 -0
  10. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/api.py +0 -0
  11. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/config.py +0 -0
  12. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/console.py +0 -0
  13. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/context.py +0 -0
  14. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/decorator.py +0 -0
  15. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/ids.py +0 -0
  16. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/model.py +0 -0
  17. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/py.typed +0 -0
  18. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/results.py +0 -0
  19. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sanitize.py +0 -0
  20. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/__init__.py +0 -0
  21. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/_batch.py +0 -0
  22. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/_chunk.py +0 -0
  23. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/_retry.py +0 -0
  24. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/_socket.py +0 -0
  25. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/_time.py +0 -0
  26. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/base.py +0 -0
  27. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/callback.py +0 -0
  28. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/clickhouse.py +0 -0
  29. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/datadog.py +0 -0
  30. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/elasticsearch.py +0 -0
  31. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/eventhubs.py +0 -0
  32. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/file.py +0 -0
  33. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/filtering.py +0 -0
  34. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/firehose.py +0 -0
  35. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/honeycomb.py +0 -0
  36. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/http.py +0 -0
  37. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/kafka.py +0 -0
  38. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/kinesis.py +0 -0
  39. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/logging_sink.py +0 -0
  40. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/logstash.py +0 -0
  41. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/loki.py +0 -0
  42. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/memory.py +0 -0
  43. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/mongodb.py +0 -0
  44. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/multi.py +0 -0
  45. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/nats.py +0 -0
  46. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/newrelic.py +0 -0
  47. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/null.py +0 -0
  48. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/postgres.py +0 -0
  49. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/pubsub.py +0 -0
  50. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/rabbitmq.py +0 -0
  51. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/redis.py +0 -0
  52. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/sentry.py +0 -0
  53. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/sns.py +0 -0
  54. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/splunk.py +0 -0
  55. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/sqlite.py +0 -0
  56. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/sqs.py +0 -0
  57. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/stdout.py +0 -0
  58. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/src/log_foundry/sinks/syslog.py +0 -0
  59. {log_foundry-0.10.2.dev112 → log_foundry-0.10.2.dev114}/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.dev112
3
+ Version: 0.10.2.dev114
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -1208,7 +1208,7 @@ Falsy carries a `reason` saying which of these happened, because they need diffe
1208
1208
  | `"retired"` | `shutdown()` was already called. Your lifecycle is wrong, not the sink. |
1209
1209
  | `"thread-died"` | The drain thread is gone; see `health().stopped_reason`. |
1210
1210
  | `"queue-full"` | Backpressure — the queue could not even accept the marker. |
1211
- | `"abandoned"` | A batch was given up on after its retry budget. The destination is broken. |
1211
+ | `"abandoned"` | The drain that would have carried those events did not confirm delivery of them. A broken destination is one cause, but not the only one — an expiring bounded `shutdown()` answers waiting calls pessimistically, and the drain may still deliver afterwards. So this is not by itself evidence that anything was lost: `health()` is what says whether it was. |
1212
1212
  | `"sink-flush"` | Everything queued reached the sink, but the sink's own `flush()` raised — a client-side buffer that did not go out. Distinct from `"abandoned"`: the library delivered, the sink did not. |
1213
1213
 
1214
1214
  ```python
@@ -1157,7 +1157,7 @@ Falsy carries a `reason` saying which of these happened, because they need diffe
1157
1157
  | `"retired"` | `shutdown()` was already called. Your lifecycle is wrong, not the sink. |
1158
1158
  | `"thread-died"` | The drain thread is gone; see `health().stopped_reason`. |
1159
1159
  | `"queue-full"` | Backpressure — the queue could not even accept the marker. |
1160
- | `"abandoned"` | A batch was given up on after its retry budget. The destination is broken. |
1160
+ | `"abandoned"` | The drain that would have carried those events did not confirm delivery of them. A broken destination is one cause, but not the only one — an expiring bounded `shutdown()` answers waiting calls pessimistically, and the drain may still deliver afterwards. So this is not by itself evidence that anything was lost: `health()` is what says whether it was. |
1161
1161
  | `"sink-flush"` | Everything queued reached the sink, but the sink's own `flush()` raised — a client-side buffer that did not go out. Distinct from `"abandoned"`: the library delivered, the sink did not. |
1162
1162
 
1163
1163
  ```python
@@ -74,7 +74,7 @@ keywords = [
74
74
  # vulnerability-reporting channel. The repository is still named `log-forge` — the ORIGINAL name,
75
75
  # which PyPI rejected for the distribution — so these URLs deliberately do not match the package
76
76
  # name. See the note on `name` above before "correcting" them.
77
- version = "0.10.2.dev112"
77
+ version = "0.10.2.dev114"
78
78
 
79
79
  [project.urls]
80
80
  Homepage = "https://github.com/agriffi10/log-forge"
@@ -270,9 +270,15 @@ class Worker:
270
270
  mechanics only, and knows nothing about spans or context.
271
271
  """
272
272
 
273
- _FORK_SKIP = ("_unclosed_swaps",)
273
+ _FORK_SKIP = ("_unclosed_swaps", "_taken_markers")
274
274
  """Attribute names ``_fork``'s repair walk must not read or descend into (SPEC-050 FR-004).
275
275
 
276
+ ``_taken_markers`` names ``flush()`` callers **in the parent**, whose ``Event`` the walk would
277
+ otherwise replace on an object nothing in the child will ever wait on. The skip stops that
278
+ work and :meth:`_reinit_after_fork` drops the references, because a skip alone leaves them
279
+ held — measured, a child inherited the parent's in-flight marker permanently and kept it
280
+ through fifty further flushes of its own.
281
+
276
282
  ``_unclosed_swaps`` holds sinks this process has **stopped** delivering to, which is exactly
277
283
  the shape ``_fork._SKIP_ATTRIBUTE`` describes: bookkeeping that pins objects is not live state
278
284
  to repair. Without the opt-out the walk reaches a superseded sink, replaces its locks — merely
@@ -345,6 +351,7 @@ class Worker:
345
351
  self._shutdown_done = False
346
352
  self._sink_closed = sink_released
347
353
  self._closing: threading.Event | None = None
354
+ self._taken_markers: list[_FlushMarker] = []
348
355
  self._unclosed_swaps: list[Sink] = []
349
356
  self._lock = threading.Lock()
350
357
  self._offer_stop_signal()
@@ -385,6 +392,9 @@ class Worker:
385
392
  release at every child exit. Emptying is what "a child inherits no promise" means for the
386
393
  record as well as for the close.
387
394
 
395
+ ``_taken_markers`` and ``_unclosed_swaps`` are emptied with it: a child inherits no
396
+ ``flush()`` caller to answer and stranded no sink, so both are references it can only hold.
397
+
388
398
  ``_closing`` is emptied unconditionally, on both branches (SPEC-050 FR-002). It names a
389
399
  close running on a thread that did not survive the fork, so the child inherits a promise
390
400
  nothing can keep: ``_fork._fresh_primitive`` carries an ``Event``'s set state across, so
@@ -447,6 +457,7 @@ class Worker:
447
457
  self.incomplete_swaps = 0
448
458
  self.stopped_reason = None
449
459
  self._closing = None
460
+ self._taken_markers = []
450
461
  self._unclosed_swaps = []
451
462
  if not resume:
452
463
  self._drain_finished.set()
@@ -1342,8 +1353,67 @@ class Worker:
1342
1353
  finally:
1343
1354
  self._release_waiters()
1344
1355
 
1356
+ def _take_marker(self, marker: _FlushMarker) -> None:
1357
+ """Records a marker the drain thread has taken out of the queue (SPEC-050 FR-001).
1358
+
1359
+ :meth:`_release_waiters` answers markers by reading ``self._queue.queue``, so it can only
1360
+ reach one that is still *in* the queue. A marker the drain has already dequeued is held in
1361
+ that thread's local while it emits — and if the sink's ``emit`` never returns, nothing
1362
+ answers it and a ``flush(timeout=None)`` waits forever, which is the defect FR-001 exists
1363
+ to remove. Registering it here is what puts it back within reach.
1364
+
1365
+ Args:
1366
+ marker: The marker this thread is about to work on.
1367
+
1368
+ Returns:
1369
+ None.
1370
+
1371
+ Raises:
1372
+ None.
1373
+ """
1374
+ with self._lock:
1375
+ self._taken_markers.append(marker)
1376
+ settled = self._drain_settled.is_set()
1377
+ if settled:
1378
+ marker.event.set()
1379
+
1380
+ def _release_marker(self, marker: _FlushMarker) -> None:
1381
+ """Drops a marker the drain thread has finished with, before it is answered.
1382
+
1383
+ **It runs after ``event.set()``, and that order is load-bearing** — the reverse survives
1384
+ the suite, which is this repo's evidence that nothing covers it rather than that it is
1385
+ safe. An earlier draft of this docstring read the green as proof of equivalence and said
1386
+ so; it is not. Deregistering first leaves a window in which the marker is in neither the
1387
+ queue nor the record, so an async ``BaseException`` landing there — the one ``_run``
1388
+ catches, on this thread — strands a ``flush(timeout=None)`` that ``_run``'s own closing
1389
+ sweep can no longer reach. Answering first inverts the failure into a leaked list entry on
1390
+ a worker that is already dead. This is the reasoning :meth:`_final_drain` already applies
1391
+ to its own ``finally``.
1392
+
1393
+ Args:
1394
+ marker: The marker this thread has finished with.
1395
+
1396
+ Returns:
1397
+ None.
1398
+
1399
+ Raises:
1400
+ None.
1401
+ """
1402
+ with self._lock:
1403
+ self._taken_markers = [m for m in self._taken_markers if m is not marker]
1404
+
1345
1405
  def _release_waiters(self) -> None:
1346
- """Answers every ``flush()`` marker still queued, so no caller waits out its timeout.
1406
+ """Answers every outstanding ``flush()`` marker, so no caller waits out its timeout.
1407
+
1408
+ **Two populations, and the second is not optional** (SPEC-050 FR-001). A marker still in
1409
+ the queue is read from it; a marker the drain thread has already **taken** is read from
1410
+ :attr:`_taken_markers`, because between dequeuing one and returning from ``sink.emit`` the
1411
+ drain holds it in a local where a queue read cannot see it. Answering only the first is
1412
+ what the audit prescribed and it does not cover the audit's own probe. The taken markers
1413
+ are answered **before** the queue read and outside its ``try``: that read reaches into
1414
+ ``Queue``'s privates, a risk ``architecture.md`` §13 accepts on the understanding that a
1415
+ CPython change costs *timed-out* waiters, and letting the new mechanism inherit it would
1416
+ upgrade that cost to waiters that never return at all.
1347
1417
 
1348
1418
  A ``BaseException`` from the main loop skips :meth:`_final_drain` entirely, which is
1349
1419
  where queued markers are normally answered, leaving a waiter to sit for its full
@@ -1352,7 +1422,7 @@ class Worker:
1352
1422
  ``health().queued`` and the terminal line report; each keeps its pessimistic
1353
1423
  ``delivered``, which is the truth here.
1354
1424
 
1355
- It is called from two places. The terminal-failure path is the original one. The clean
1425
+ It is called from three places. The terminal-failure path is the original one. The clean
1356
1426
  :meth:`shutdown` path was added because that same enqueue-after-the-drain race happens
1357
1427
  there too, and hurts more: measured stranding a marker in 13 of 400 shutdowns raced
1358
1428
  against a ``flush()`` under load, where the caller sat out its whole timeout — and
@@ -1382,10 +1452,14 @@ class Worker:
1382
1452
  Raises:
1383
1453
  None. This runs after the record and the stderr line, neither of which may be lost.
1384
1454
  """
1455
+ with self._lock:
1456
+ taken = list(self._taken_markers)
1457
+ for marker in taken:
1458
+ marker.event.set()
1385
1459
  try:
1386
1460
  with self._queue.mutex:
1387
- markers = [i for i in self._queue.queue if isinstance(i, _FlushMarker)]
1388
- for marker in markers:
1461
+ queued = [i for i in self._queue.queue if isinstance(i, _FlushMarker)]
1462
+ for marker in queued:
1389
1463
  marker.event.set()
1390
1464
  except Exception:
1391
1465
  pass
@@ -1463,12 +1537,14 @@ class Worker:
1463
1537
  except queue.Empty:
1464
1538
  item = None
1465
1539
  if isinstance(item, _FlushMarker):
1540
+ self._take_marker(item)
1466
1541
  try:
1467
1542
  self._emit_pending(pending)
1468
1543
  item.delivered = self._nothing_lost_since(item)
1469
1544
  finally:
1470
1545
  last_flush = time.monotonic()
1471
1546
  item.event.set()
1547
+ self._release_marker(item)
1472
1548
  continue
1473
1549
  if item is _SHUTDOWN:
1474
1550
  break
@@ -1546,6 +1622,7 @@ class Worker:
1546
1622
  break
1547
1623
  if isinstance(item, _FlushMarker):
1548
1624
  markers.append(item)
1625
+ self._take_marker(item)
1549
1626
  continue
1550
1627
  if item is not None and item is not _SHUTDOWN:
1551
1628
  pending.append(cast("list[dict[str, object]]", item))
@@ -1556,6 +1633,7 @@ class Worker:
1556
1633
  finally:
1557
1634
  for marker in markers:
1558
1635
  marker.event.set()
1636
+ self._release_marker(marker)
1559
1637
 
1560
1638
  def _emit(self, event_lists: list[list[dict[str, object]]]) -> None:
1561
1639
  """Flattens queued per-span event-lists into one batch and emits it, retrying.