log-foundry 0.5.1.dev8__tar.gz → 0.5.1.dev9__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 (52) hide show
  1. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/PKG-INFO +20 -2
  2. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/README.md +19 -1
  3. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/pyproject.toml +1 -1
  4. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/__init__.py +24 -0
  5. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/decorator.py +14 -1
  6. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/worker.py +49 -4
  7. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/LICENSE +0 -0
  8. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/api.py +0 -0
  9. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/context.py +0 -0
  12. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/ids.py +0 -0
  13. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/model.py +0 -0
  14. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/py.typed +0 -0
  15. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sanitize.py +0 -0
  16. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/__init__.py +0 -0
  17. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/_chunk.py +0 -0
  18. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/_socket.py +0 -0
  19. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/_time.py +0 -0
  20. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/base.py +0 -0
  21. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/callback.py +0 -0
  22. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/clickhouse.py +0 -0
  23. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/datadog.py +0 -0
  24. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/elasticsearch.py +0 -0
  25. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/eventhubs.py +0 -0
  26. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/file.py +0 -0
  27. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/filtering.py +0 -0
  28. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/firehose.py +0 -0
  29. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/honeycomb.py +0 -0
  30. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/http.py +0 -0
  31. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/kafka.py +0 -0
  32. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/kinesis.py +0 -0
  33. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/logging_sink.py +0 -0
  34. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/logstash.py +0 -0
  35. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/loki.py +0 -0
  36. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/mongodb.py +0 -0
  37. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/multi.py +0 -0
  38. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/nats.py +0 -0
  39. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/newrelic.py +0 -0
  40. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/postgres.py +0 -0
  41. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/pubsub.py +0 -0
  42. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/rabbitmq.py +0 -0
  43. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/redis.py +0 -0
  44. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/sentry.py +0 -0
  45. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/sns.py +0 -0
  46. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/splunk.py +0 -0
  47. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/sqlite.py +0 -0
  48. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/sqs.py +0 -0
  49. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/stdout.py +0 -0
  50. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/syslog.py +0 -0
  51. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/transform.py +0 -0
  52. {log_foundry-0.5.1.dev8 → log_foundry-0.5.1.dev9}/src/log_foundry/sinks/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: log-foundry
3
- Version: 0.5.1.dev8
3
+ Version: 0.5.1.dev9
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -395,6 +395,9 @@ A few conventions hold across every sink below:
395
395
  open for you to manage.
396
396
  - **Never crashes the app.** A failing sink is retried with backoff and then counted (`.failed`,
397
397
  `.dropped_oversized`, …) rather than raised — a broken destination degrades logging, nothing more.
398
+ The one deliberate exception is a `MultiSink` whose children *all* failed: it re-raises so the
399
+ worker's retry engages, since nothing was delivered and there are no duplicates to risk. That
400
+ still doesn't reach your code — the worker is what catches it.
398
401
 
399
402
  #### Built-in, zero-dependency
400
403
 
@@ -612,7 +615,22 @@ background worker via a fast, non-blocking submit — your function returns with
612
615
  the sink. The worker batches events (by count and time), emits them on its own thread, retries
613
616
  a failing sink with backoff, and applies backpressure so a slow or down sink can never block or
614
617
  back-pressure the app: when its bounded queue is full it drops the newest submissions and counts
615
- them (`worker.dropped`) rather than stalling.
618
+ them rather than stalling.
619
+
620
+ Those losses are deliberate, so the library gives you a way to notice them. `log_foundry.health()`
621
+ returns a snapshot of the worker's counters:
622
+
623
+ ```python
624
+ h = log_foundry.health()
625
+ if h.dropped or h.failed_batches:
626
+ ... # logs were silently lost — worth an alert
627
+ ```
628
+
629
+ `dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
630
+ abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
631
+ every thousandth after it, since overflow is a high-rate condition and a line per drop would be its
632
+ own outage. A process that has never logged has no worker, and asking after its health does not
633
+ create one.
616
634
 
617
635
  Because delivery is asynchronous, drain before the process exits. There are two drains, and
618
636
  which one you want depends on whether the process is about to end:
@@ -359,6 +359,9 @@ A few conventions hold across every sink below:
359
359
  open for you to manage.
360
360
  - **Never crashes the app.** A failing sink is retried with backoff and then counted (`.failed`,
361
361
  `.dropped_oversized`, …) rather than raised — a broken destination degrades logging, nothing more.
362
+ The one deliberate exception is a `MultiSink` whose children *all* failed: it re-raises so the
363
+ worker's retry engages, since nothing was delivered and there are no duplicates to risk. That
364
+ still doesn't reach your code — the worker is what catches it.
362
365
 
363
366
  #### Built-in, zero-dependency
364
367
 
@@ -576,7 +579,22 @@ background worker via a fast, non-blocking submit — your function returns with
576
579
  the sink. The worker batches events (by count and time), emits them on its own thread, retries
577
580
  a failing sink with backoff, and applies backpressure so a slow or down sink can never block or
578
581
  back-pressure the app: when its bounded queue is full it drops the newest submissions and counts
579
- them (`worker.dropped`) rather than stalling.
582
+ them rather than stalling.
583
+
584
+ Those losses are deliberate, so the library gives you a way to notice them. `log_foundry.health()`
585
+ returns a snapshot of the worker's counters:
586
+
587
+ ```python
588
+ h = log_foundry.health()
589
+ if h.dropped or h.failed_batches:
590
+ ... # logs were silently lost — worth an alert
591
+ ```
592
+
593
+ `dropped` counts submissions discarded because the queue filled; `failed_batches` counts batches
594
+ abandoned after the retry budget was spent. Overflow also warns on stderr — on the first drop and
595
+ every thousandth after it, since overflow is a high-rate condition and a line per drop would be its
596
+ own outage. A process that has never logged has no worker, and asking after its health does not
597
+ create one.
580
598
 
581
599
  Because delivery is asynchronous, drain before the process exits. There are two drains, and
582
600
  which one you want depends on whether the process is about to end:
@@ -20,7 +20,7 @@ dependencies = [
20
20
  ]
21
21
 
22
22
  # Optional features. Install with: pip install log-foundry[aws]
23
- version = "0.5.1.dev8"
23
+ version = "0.5.1.dev9"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.34"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -18,6 +18,7 @@ from log_foundry.context import (
18
18
  current_traceparent,
19
19
  )
20
20
  from log_foundry.decorator import continue_trace, trace
21
+ from log_foundry.worker import Health
21
22
 
22
23
  try:
23
24
  # Distribution name ("log-foundry") differs from the import name ("log_foundry").
@@ -49,6 +50,27 @@ def flush(timeout: float | None = 5.0) -> bool:
49
50
  return _flush_worker(timeout)
50
51
 
51
52
 
53
+ def health() -> Health:
54
+ """Snapshot the background worker's delivery counters (SPEC-017 FR-005). Never raises.
55
+
56
+ Returns ``queued`` / ``dropped`` / ``failed_batches``. A non-zero ``dropped`` means the queue
57
+ filled and submissions were discarded to keep your code non-blocking; a non-zero
58
+ ``failed_batches`` means a sink stayed broken through the whole retry budget. Both are
59
+ losses the library absorbs on purpose, and this is how you notice them::
60
+
61
+ h = log_foundry.health()
62
+ if h.dropped or h.failed_batches:
63
+ ... # raise an alert; logs were silently lost
64
+
65
+ A process that has never logged has no worker, and asking after its health does not create
66
+ one — the snapshot is simply zeroed. Valid after :func:`shutdown`, which leaves the final
67
+ counters readable.
68
+ """
69
+ from log_foundry.decorator import _worker_health
70
+
71
+ return _worker_health()
72
+
73
+
52
74
  def shutdown() -> None:
53
75
  """Flush buffered events and close the sink, blocking until drained. Idempotent.
54
76
 
@@ -80,5 +102,7 @@ __all__ = [
80
102
  "current_baggage_header",
81
103
  "flush",
82
104
  "shutdown",
105
+ "health",
106
+ "Health",
83
107
  "__version__",
84
108
  ]
@@ -40,7 +40,7 @@ from log_foundry.ids import (
40
40
  parse_traceparent,
41
41
  )
42
42
  from log_foundry.model import Span, backfill_baggage, end_event, start_event
43
- from log_foundry.worker import Worker
43
+ from log_foundry.worker import Health, Worker
44
44
 
45
45
  __all__ = ["trace", "continue_trace"]
46
46
 
@@ -235,6 +235,19 @@ def _flush_worker(timeout: float | None = 5.0) -> bool:
235
235
  return False
236
236
 
237
237
 
238
+ def _worker_health() -> Health:
239
+ """Snapshot the process worker's counters, or zeros if none was ever created.
240
+
241
+ Backs :func:`log_foundry.health` (SPEC-017 FR-005). Like :func:`_flush_worker` this
242
+ deliberately does *not* call :func:`_get_worker`: starting a thread and registering an
243
+ ``atexit`` drain in order to report three zeros would be pure cost.
244
+ """
245
+ worker = _worker
246
+ if worker is None:
247
+ return Health(queued=0, dropped=0, failed_batches=0)
248
+ return worker.health()
249
+
250
+
238
251
  def _flush(span: Span) -> None:
239
252
  """Hand the finished span's events to the background worker — non-blocking (FR-001).
240
253
 
@@ -23,16 +23,36 @@ import queue
23
23
  import sys
24
24
  import threading
25
25
  import time
26
- from typing import cast
26
+ from typing import NamedTuple, cast
27
27
 
28
28
  from log_foundry.sinks.base import Sink
29
29
 
30
- __all__ = ["Worker"]
30
+ __all__ = ["Health", "Worker"]
31
31
 
32
32
  # Sentinel enqueued by shutdown() to wake a worker blocked in queue.get() so it stops promptly
33
33
  # instead of waiting out the flush_interval. It is never emitted.
34
34
  _SHUTDOWN = object()
35
35
 
36
+ # Queue overflow is a high-rate condition by nature: a line per dropped submission would be its
37
+ # own outage. Warn on the first drop, then every this-many-th (SPEC-017 FR-005).
38
+ _DROP_WARN_EVERY = 1000
39
+
40
+
41
+ class Health(NamedTuple):
42
+ """A point-in-time snapshot of the worker's delivery counters (SPEC-017 FR-005).
43
+
44
+ Attributes:
45
+ queued: Submissions currently buffered. Approximate by nature — it is read without
46
+ stopping the world, and briefly counts the internal flush/shutdown markers
47
+ alongside real submissions.
48
+ dropped: Submissions discarded because the queue was full (backpressure).
49
+ failed_batches: Batches abandoned after the retry budget was spent.
50
+ """
51
+
52
+ queued: int
53
+ dropped: int
54
+ failed_batches: int
55
+
36
56
 
37
57
  class _FlushMarker:
38
58
  """A drain request travelling the queue in FIFO order (SPEC-013 FR-002).
@@ -87,13 +107,35 @@ class Worker:
87
107
 
88
108
  Enqueues via ``put_nowait`` and returns immediately without touching the sink. When the
89
109
  queue is full, drops this submission (drop-newest) and counts it in ``dropped`` rather
90
- than blocking the caller (FR-001, FR-004).
110
+ than blocking the caller (FR-001, FR-004), warning on a throttle (SPEC-017 FR-005).
91
111
  """
92
112
  try:
93
113
  self._queue.put_nowait(events)
94
114
  except queue.Full:
95
115
  with self._lock:
96
116
  self.dropped += 1
117
+ total = self.dropped # read under the lock: every value is produced exactly once
118
+ # Written outside the lock deliberately. stderr can block on a slow reader, and
119
+ # ``_lock`` also guards flush()/shutdown()'s once-only flag — holding it across a
120
+ # blocking write would let a wedged console stall the drain path, not just other
121
+ # submitters. Lines may therefore interleave out of order under concurrency; the
122
+ # counts they carry are still exact.
123
+ if total == 1 or total % _DROP_WARN_EVERY == 0:
124
+ sys.stderr.write(
125
+ f"log-foundry: log queue full, dropped {total} submission(s) so far\n"
126
+ )
127
+
128
+ def health(self) -> Health:
129
+ """Snapshot the delivery counters (SPEC-017 FR-005). Never raises.
130
+
131
+ Valid after :meth:`shutdown` — the counters are plain integers that outlive the thread,
132
+ and the final drain consumes the queue, so ``queued`` reads 0 rather than a stale marker.
133
+ """
134
+ with self._lock:
135
+ dropped, failed_batches = self.dropped, self.failed_batches
136
+ return Health(
137
+ queued=self._queue.qsize(), dropped=dropped, failed_batches=failed_batches
138
+ )
97
139
 
98
140
  def flush(self, timeout: float | None = 5.0) -> bool:
99
141
  """Drain everything submitted before this call through the sink, without stopping.
@@ -221,7 +263,10 @@ class Worker:
221
263
  return
222
264
  except Exception: # noqa: BLE001 — any sink failure must not kill the worker thread
223
265
  if attempt >= self.max_retries:
224
- self.failed_batches += 1
266
+ # Under the lock so a concurrent health() sees a coherent snapshot rather
267
+ # than a half-updated pair. No deadlock: shutdown() releases before join().
268
+ with self._lock:
269
+ self.failed_batches += 1
225
270
  sys.stderr.write(
226
271
  f"log-foundry: abandoned a batch of {len(batch)} event(s) after "
227
272
  f"{self.max_retries + 1} failed emit attempts\n"