log-foundry 0.10.2.dev12__tar.gz → 0.10.2.dev13__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 (54) hide show
  1. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/_diag.py +4 -1
  4. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/decorator.py +5 -22
  5. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/worker.py +23 -29
  6. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/LICENSE +0 -0
  7. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/README.md +0 -0
  8. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/__init__.py +0 -0
  9. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/api.py +0 -0
  10. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/config.py +0 -0
  11. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/console.py +0 -0
  12. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/context.py +0 -0
  13. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/ids.py +0 -0
  14. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/model.py +0 -0
  15. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/py.typed +0 -0
  16. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sanitize.py +0 -0
  17. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/__init__.py +0 -0
  18. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_batch.py +0 -0
  19. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_chunk.py +0 -0
  20. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_socket.py +0 -0
  21. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/_time.py +0 -0
  22. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/base.py +0 -0
  23. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/callback.py +0 -0
  24. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/clickhouse.py +0 -0
  25. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/datadog.py +0 -0
  26. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/elasticsearch.py +0 -0
  27. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/eventhubs.py +0 -0
  28. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/file.py +0 -0
  29. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/filtering.py +0 -0
  30. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/firehose.py +0 -0
  31. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/honeycomb.py +0 -0
  32. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/http.py +0 -0
  33. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/kafka.py +0 -0
  34. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/kinesis.py +0 -0
  35. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/logging_sink.py +0 -0
  36. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/logstash.py +0 -0
  37. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/loki.py +0 -0
  38. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/mongodb.py +0 -0
  39. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/multi.py +0 -0
  40. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/nats.py +0 -0
  41. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/newrelic.py +0 -0
  42. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/postgres.py +0 -0
  43. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/pubsub.py +0 -0
  44. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/rabbitmq.py +0 -0
  45. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/redis.py +0 -0
  46. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sentry.py +0 -0
  47. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sns.py +0 -0
  48. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/splunk.py +0 -0
  49. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sqlite.py +0 -0
  50. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/sqs.py +0 -0
  51. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/stdout.py +0 -0
  52. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/syslog.py +0 -0
  53. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/src/log_foundry/sinks/transform.py +0 -0
  54. {log_foundry-0.10.2.dev12 → log_foundry-0.10.2.dev13}/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.10.2.dev12
3
+ Version: 0.10.2.dev13
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.dev12"
23
+ version = "0.10.2.dev13"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -94,7 +94,10 @@ def lost(what: str, count: int, detail: str = "") -> None:
94
94
  Args:
95
95
  what: Singular noun for the unit lost — ``"event"``, ``"message"``, ``"row"``. Rendered
96
96
  with the corpus-wide ``(s)`` suffix, so it must be a literal, never a runtime value.
97
- count: How many were lost. The caller's counter has already moved by this much.
97
+ count: How many this line reports as lost normally the increment the caller's counter
98
+ has just taken. A **throttled** site instead passes its running total, because a line
99
+ written on every thousandth drop that said "lost 1" would read as one loss rather than
100
+ a thousand; such a site says so in ``detail``. Recorded before this call either way.
98
101
  detail: Optional circumstances — the sink's class name, the attempt count, an ``errno``,
99
102
  an exception *type*. Escaped and truncated to ``_MAX_DETAIL``.
100
103
  """
@@ -24,7 +24,6 @@ from __future__ import annotations
24
24
  import asyncio
25
25
  import atexit
26
26
  import functools
27
- import sys
28
27
  import threading
29
28
  from collections.abc import Callable
30
29
  from time import monotonic
@@ -47,9 +46,6 @@ if TYPE_CHECKING:
47
46
 
48
47
  __all__ = ["continue_trace", "trace"]
49
48
 
50
- # Bound on how much of a rejected inbound value is echoed into a stderr warning.
51
- _MAX_REJECTED_ECHO = 64
52
-
53
49
  # One background worker per process (SPEC-004), created lazily from the configured sink on the
54
50
  # first flush. The double-checked lock makes concurrent first-flushes create exactly one.
55
51
  _worker: Worker | None = None
@@ -86,19 +82,6 @@ def _open_span(name: str, defaults: dict[str, object] | None) -> Span:
86
82
  return span
87
83
 
88
84
 
89
- def _warn_rejected(reason: str, value: object) -> None:
90
- """Report a rejected inbound context on stderr, as ``worker`` and ``SQSSink`` do.
91
-
92
- The offending value is echoed only as a **bounded ``repr``**. Unbounded is a log-injection
93
- surface — the value is attacker-controllable, and `repr` additionally escapes newlines and
94
- control characters so it cannot forge a second log line in an operator's console.
95
- """
96
- shown = repr(value)
97
- if len(shown) > _MAX_REJECTED_ECHO:
98
- shown = shown[:_MAX_REJECTED_ECHO] + "…"
99
- sys.stderr.write(f"log-foundry: ignoring inbound trace context ({reason}): {shown}\n")
100
-
101
-
102
85
  def continue_trace(
103
86
  traceparent: str | None = None,
104
87
  *,
@@ -144,19 +127,19 @@ def continue_trace(
144
127
  if traceparent is not None:
145
128
  if trace_id is not None or parent_span_id is not None:
146
129
  # A programming error, not bad input: the caller supplied the same thing twice.
147
- _warn_rejected("both traceparent and explicit ids given; traceparent wins", traceparent)
130
+ _diag.rejected("both traceparent and explicit ids given; traceparent wins", traceparent)
148
131
  parsed = parse_traceparent(traceparent)
149
132
  if parsed is None:
150
- _warn_rejected("unparseable traceparent", traceparent)
133
+ _diag.rejected("unparseable traceparent", traceparent)
151
134
  else:
152
135
  adopted = parsed
153
136
  elif trace_id is not None:
154
137
  if not is_valid_trace_id(trace_id):
155
- _warn_rejected("invalid trace_id", trace_id)
138
+ _diag.rejected("invalid trace_id", trace_id)
156
139
  elif parent_span_id is not None and not is_valid_span_id(parent_span_id):
157
140
  # Drop just the parent and join as another root rather than reject the whole
158
141
  # context: being in the right trace without a parent beats being in a fresh one.
159
- _warn_rejected("invalid parent_span_id; joining as a root", parent_span_id)
142
+ _diag.rejected("invalid parent_span_id; joining as a root", parent_span_id)
160
143
  adopted = (trace_id, None)
161
144
  else:
162
145
  # parent_span_id may legitimately be omitted — a consumer that knows the trace but
@@ -175,7 +158,7 @@ def continue_trace(
175
158
  if parsed_baggage is None:
176
159
  # Deliberately independent of the trace context above: losing correlating fields is
177
160
  # bad, and losing the trace join because one field was malformed is worse.
178
- _warn_rejected("unusable baggage header", baggage)
161
+ _diag.rejected("unusable baggage header", baggage)
179
162
  else:
180
163
  context.set_baggage(**parsed_baggage)
181
164
 
@@ -20,7 +20,6 @@ nothing about spans or context (the same dumbness that makes sinks swappable).
20
20
  from __future__ import annotations
21
21
 
22
22
  import queue
23
- import sys
24
23
  import threading
25
24
  import time
26
25
  from typing import TYPE_CHECKING, NamedTuple, cast
@@ -162,15 +161,7 @@ class Worker:
162
161
  # submitters. Lines may therefore interleave out of order under concurrency; the
163
162
  # counts they carry are still exact.
164
163
  if total == 1 or total % _DROP_WARN_EVERY == 0:
165
- try:
166
- sys.stderr.write(
167
- f"log-foundry: log queue full, dropped {total} submission(s) so far\n"
168
- )
169
- except Exception: # submit() runs on the *caller's* thread, so an
170
- # unwritable stderr (closed fd, broken pipe, daemonized process) would raise
171
- # straight into the app. A diagnostic about dropped logs must never itself be
172
- # the reason a decorated function fails. The counter is already recorded.
173
- pass
164
+ _diag.lost("submission", total, "log queue full; count is cumulative")
174
165
 
175
166
  def health(self) -> Health:
176
167
  """Snapshot the delivery counters (SPEC-017 FR-005, SPEC-019 FR-003). Never raises.
@@ -323,36 +314,36 @@ class Worker:
323
314
 
324
315
  Recording precedes announcing: stderr may be closed or wedged, and unlike the overflow
325
316
  warning this line is written exactly once and cannot be re-emitted later, so the record
326
- must not be able to ride on it. The exception's *type* is reported and its message is
327
- not a sink's exception text can carry event data, and arch §6 keeps caller data out of
328
- places it was not asked for (the same rule behind ``sanitize``'s type-name placeholder).
317
+ must not be able to ride on it. The exception's *type* is reported and its message is not
318
+ the rule ``_diag`` now applies to every line the library writes (SPEC-029), and the
319
+ reason this site had it first.
329
320
 
330
- The count reports what was *in hand* and what was still *queued behind it* (SPEC-021
331
- FR-002). Held alone under-reads the loss: nothing will drain the queue either, so an
332
- operator reading "1 undrained event-list(s)" could conclude far less was lost than was.
321
+ The announcement is an :func:`~log_foundry._diag.absorbed`, not a fourth kind of line: the
322
+ thread's death *is* an exception this method caught and did not propagate, and what it
323
+ cost belongs in the detail. The count reports what was *in hand* and what was still
324
+ *queued behind it* (SPEC-021 FR-002). Held alone under-reads the loss: nothing will drain
325
+ the queue either, so an operator reading "1 undrained event-list(s)" could conclude far
326
+ less was lost than was.
333
327
 
334
328
  The queued figure is "items", not "event-lists", and says so: like ``Health.queued`` it is
335
329
  read without stopping the world, so it counts any internal flush/shutdown marker sitting
336
330
  alongside real submissions, and a producer thread can add to the queue between the death
337
331
  and the read. It is a floor on what was lost, which is the useful direction.
338
332
  """
339
- name = type(exc).__name__
340
333
  with self._lock:
341
- self.stopped_reason = name
334
+ self.stopped_reason = type(exc).__name__
342
335
  try:
343
336
  # In its own guard, and after the record: ``qsize()`` is not guaranteed on every
344
337
  # platform's queue, and a diagnostic must not be the reason the diagnosis is lost.
345
338
  queued: object = self._queue.qsize()
346
339
  except Exception:
347
340
  queued = "?"
348
- try:
349
- sys.stderr.write(
350
- f"log-foundry: worker thread stopped on {name}; {undrained} undrained "
351
- f"event-list(s) held and {queued} queued item(s) undelivered, nothing further "
352
- f"will be delivered\n"
353
- )
354
- except Exception: # best-effort: the record above is what an operator reads.
355
- pass
341
+ _diag.absorbed(
342
+ "draining the log queue",
343
+ exc,
344
+ f"worker thread stopped; {undrained} undrained event-list(s) held and {queued} "
345
+ f"queued item(s) undelivered, nothing further will be delivered",
346
+ )
356
347
 
357
348
  def _drain(self, pending: list[list[dict[str, object]]]) -> None:
358
349
  """The drain loop proper. ``pending`` is owned by :meth:`_run`, which reports its size.
@@ -477,9 +468,12 @@ class Worker:
477
468
  # than a half-updated pair. No deadlock: shutdown() releases before join().
478
469
  with self._lock:
479
470
  self.failed_batches += 1
480
- sys.stderr.write(
481
- f"log-foundry: abandoned a batch of {len(batch)} event(s) after "
482
- f"{retries + 1} failed emit attempts\n"
471
+ # Through ``_diag`` so the write is guarded: unguarded, a broken stderr raised
472
+ # out of here, through ``_drain``, into ``_run``'s handler, and the drain
473
+ # thread died for good — a diagnostic about one lost batch costing every batch
474
+ # after it (SPEC-029 FR-003).
475
+ _diag.lost(
476
+ "event", len(batch), f"batch abandoned after {retries + 1} emit attempts"
483
477
  )
484
478
  return
485
479
  # Backoff between attempts; _stop.wait returns at once during shutdown, so a