log-foundry 0.10.2.dev7__tar.gz → 0.10.2.dev8__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.dev7 → log_foundry-0.10.2.dev8}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/pyproject.toml +1 -1
  3. log_foundry-0.10.2.dev8/src/log_foundry/_diag.py +53 -0
  4. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/context.py +14 -2
  5. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/decorator.py +122 -26
  6. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/LICENSE +0 -0
  7. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/README.md +0 -0
  8. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/__init__.py +0 -0
  9. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/api.py +0 -0
  10. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/config.py +0 -0
  11. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/console.py +0 -0
  12. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/ids.py +0 -0
  13. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/model.py +0 -0
  14. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/py.typed +0 -0
  15. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sanitize.py +0 -0
  16. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/__init__.py +0 -0
  17. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/_batch.py +0 -0
  18. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/_chunk.py +0 -0
  19. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/_socket.py +0 -0
  20. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/_time.py +0 -0
  21. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/base.py +0 -0
  22. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/callback.py +0 -0
  23. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/clickhouse.py +0 -0
  24. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/datadog.py +0 -0
  25. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/elasticsearch.py +0 -0
  26. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/eventhubs.py +0 -0
  27. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/file.py +0 -0
  28. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/filtering.py +0 -0
  29. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/firehose.py +0 -0
  30. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/honeycomb.py +0 -0
  31. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/http.py +0 -0
  32. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/kafka.py +0 -0
  33. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/kinesis.py +0 -0
  34. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/logging_sink.py +0 -0
  35. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/logstash.py +0 -0
  36. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/loki.py +0 -0
  37. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/mongodb.py +0 -0
  38. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/multi.py +0 -0
  39. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/nats.py +0 -0
  40. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/newrelic.py +0 -0
  41. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/postgres.py +0 -0
  42. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/pubsub.py +0 -0
  43. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/rabbitmq.py +0 -0
  44. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/redis.py +0 -0
  45. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/sentry.py +0 -0
  46. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/sns.py +0 -0
  47. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/splunk.py +0 -0
  48. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/sqlite.py +0 -0
  49. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/sqs.py +0 -0
  50. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/stdout.py +0 -0
  51. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/syslog.py +0 -0
  52. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/transform.py +0 -0
  53. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/src/log_foundry/sinks/util.py +0 -0
  54. {log_foundry-0.10.2.dev7 → log_foundry-0.10.2.dev8}/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.dev7
3
+ Version: 0.10.2.dev8
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.dev7"
23
+ version = "0.10.2.dev8"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -0,0 +1,53 @@
1
+ """The library's own diagnostic channel — one line on stderr, for failures it absorbed.
2
+
3
+ This library *is* the logger, so it cannot report its own faults through itself: an event
4
+ describing a broken sink would be handed to that same broken sink. Stderr is the channel of last
5
+ resort, and every site that swallows an exception to keep architecture §4's promise (logging never
6
+ breaks the application) announces it through here, so the wording and the safety rules are decided
7
+ once rather than per call site.
8
+
9
+ Two rules, both load-bearing:
10
+
11
+ * **The exception's type, never its message.** A message can carry a value from the event that
12
+ provoked it, and arch §6 keeps user data out of anything the library emits about itself — the
13
+ same rule SPEC-019 applies to ``Health.stopped_reason``. A type name is enough to tell a
14
+ ``ConnectionError`` from a ``TypeError``, which is what the reader needs.
15
+ * **No stream fault escapes.** It is called from ``except`` and ``finally`` blocks whose entire
16
+ purpose is to stop an exception reaching the caller, so a failure here — a closed stderr at
17
+ interpreter shutdown, a stream that rejects the write, ``sys.stderr`` set to ``None`` — must not
18
+ become the exception those guards existed to prevent. A ``BaseException`` still passes through,
19
+ as everywhere else in this library: a ``KeyboardInterrupt`` landing mid-write is the operator's
20
+ intent, not a stream fault to swallow.
21
+
22
+ SPEC-029 takes ownership of this module and moves the ``repr(exception)`` sink sites onto it.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import sys
28
+
29
+ __all__ = ["absorbed"]
30
+
31
+
32
+ def absorbed(where: str, exc: BaseException, detail: str = "") -> None:
33
+ """Report a failure the library swallowed rather than propagated.
34
+
35
+ Never raises on a stream fault; a ``BaseException`` from the write still propagates.
36
+
37
+ Args:
38
+ where: What was being attempted, as a participle phrase that reads after "while" —
39
+ ``"closing a span"``, ``"emitting an orphan log"``.
40
+ exc: The absorbed exception. Only ``type(exc).__name__`` is written.
41
+ detail: Optional consequence for the reader, appended after a semicolon — most usefully
42
+ what was lost, since an absorbed failure is invisible apart from this line.
43
+ """
44
+ try:
45
+ suffix = f"; {detail}" if detail else ""
46
+ sys.stderr.write(
47
+ f"log-foundry: absorbed a failure while {where} ({type(exc).__name__}){suffix}\n"
48
+ )
49
+ except Exception:
50
+ # The channel of last resort has no fallback of its own. Reporting is best-effort by
51
+ # construction: losing the line is bad, raising from the guard that exists to protect
52
+ # the caller's exception would be worse (arch §4).
53
+ pass
@@ -91,8 +91,20 @@ def push_span(span: Span) -> contextvars.Token[tuple[Span, ...]]:
91
91
 
92
92
 
93
93
  def pop_span(token: contextvars.Token[tuple[Span, ...]]) -> None:
94
- """Restore the span stack to its state before the matching :func:`push_span`."""
95
- _span_stack.reset(token)
94
+ """Restore the span stack to its state before the matching :func:`push_span`.
95
+
96
+ Total — never raises, on the same terms and for the same reason as
97
+ :func:`pop_baggage_scope`: it runs in the decorator's ``finally``, where an exception would
98
+ replace the one the caller's own function raised (arch §4, SPEC-025).
99
+ """
100
+ try:
101
+ _span_stack.reset(token)
102
+ except Exception:
103
+ # `reset` rejects a token minted in another context and one already used. Restoring the
104
+ # captured stack directly is equivalent; anything that is not a tuple of spans cannot
105
+ # have come from `push_span`, so it falls back to empty rather than corrupting the stack.
106
+ old = getattr(token, "old_value", None)
107
+ _span_stack.set(old if isinstance(old, tuple) else ())
96
108
 
97
109
 
98
110
  def get_baggage() -> dict[str, object]:
@@ -28,9 +28,9 @@ import sys
28
28
  import threading
29
29
  from collections.abc import Callable
30
30
  from time import monotonic
31
- from typing import Any, TypeVar, cast, overload
31
+ from typing import TYPE_CHECKING, Any, TypeVar, cast, overload
32
32
 
33
- from log_foundry import context
33
+ from log_foundry import _diag, context
34
34
  from log_foundry.config import _ensure_sink
35
35
  from log_foundry.ids import (
36
36
  is_valid_span_id,
@@ -42,6 +42,9 @@ from log_foundry.ids import (
42
42
  from log_foundry.model import Span, backfill_baggage, end_event, start_event
43
43
  from log_foundry.worker import Health, Worker
44
44
 
45
+ if TYPE_CHECKING:
46
+ import contextvars
47
+
45
48
  __all__ = ["continue_trace", "trace"]
46
49
 
47
50
  # Bound on how much of a rejected inbound value is echoed into a stderr warning.
@@ -272,6 +275,97 @@ def _flush(span: Span) -> None:
272
275
  _get_worker().submit(span.events)
273
276
 
274
277
 
278
+ type _SpanScope = tuple[
279
+ Span | None,
280
+ contextvars.Token[tuple[Span, ...]] | None,
281
+ contextvars.Token[dict[str, object]] | None,
282
+ ]
283
+
284
+
285
+ def _begin(name: str, defaults: dict[str, object] | None) -> _SpanScope:
286
+ """Open a span and make it current, degrading rather than failing. Never raises.
287
+
288
+ Everything here runs **before** the caller's function body, so a failure would mean the
289
+ library prevented the application from doing its work at all — the worst reading of arch §4,
290
+ and worse than the close-path faults (SPEC-025 FR-001) because nothing has run yet.
291
+
292
+ Whatever succeeded is kept, and :func:`_end` releases exactly that much. Four shapes reach
293
+ it, and the *order of the three steps* is what keeps every one of them coherent:
294
+
295
+ * ``(span, token, scope)`` — everything worked; ``scope`` is ``None`` for a nested call,
296
+ which has no root scope to open.
297
+ * ``(span, token, None)`` — a nested call, or a root whose scope failed. **The scope is
298
+ taken first for this reason**: opened last, a root call that lost only its scope would
299
+ keep the span *and* leak its baggage into the next request, which is the SPEC-024 defect
300
+ reappearing through a failure path. Taken first, a lost scope means no span either.
301
+ * ``(None, None, scope)`` — the span could not be opened; the call runs untraced and the
302
+ scope is still released, so nothing leaks.
303
+ * ``(None, None, None)`` — nothing was set up.
304
+
305
+ A span that exists but was never pushed (``token is None``) is still closed and flushed by
306
+ :func:`_end`: it is not *current*, so nested calls will not parent to it and its own events
307
+ are all it carries, but that is more than discarding it would preserve.
308
+
309
+ The root test reads :func:`~log_foundry.context.current_span` before :func:`_open_span`,
310
+ which is what makes the new span current (SPEC-024).
311
+ """
312
+ span: Span | None = None
313
+ token: contextvars.Token[tuple[Span, ...]] | None = None
314
+ scope: contextvars.Token[dict[str, object]] | None = None
315
+ try:
316
+ if context.current_span() is None:
317
+ scope = context.push_baggage_scope()
318
+ span = _open_span(name, defaults)
319
+ token = context.push_span(span)
320
+ except Exception as exc:
321
+ _diag.absorbed(
322
+ "opening a span",
323
+ exc,
324
+ "this call runs untraced" if span is None else "this call is traced incompletely",
325
+ )
326
+ return span, token, scope
327
+
328
+
329
+ def _end(
330
+ span: Span | None,
331
+ token: contextvars.Token[tuple[Span, ...]] | None,
332
+ scope: contextvars.Token[dict[str, object]] | None,
333
+ status: str,
334
+ error: BaseException | None,
335
+ ) -> None:
336
+ """Close the span and release the context. Never raises on a library fault.
337
+
338
+ Called from the wrappers' ``finally`` **once** per span, with the outcome the body actually
339
+ had (SPEC-025 FR-002) — the previous shape closed once in the ``try`` and again in the
340
+ ``except``, so a close that failed on the success path emitted a second, contradicting
341
+ ``span.end`` for a call that had returned normally.
342
+
343
+ The catch is ``Exception``, never ``BaseException``: a ``KeyboardInterrupt`` or
344
+ ``SystemExit`` arriving here is the operator's or the runtime's intent and must still reach
345
+ the caller. That is the same line SPEC-019 drew in the opposite direction for the worker
346
+ thread, where the *absence* of a handler was the defect.
347
+
348
+ One ordering constraint, and only one: the baggage scope is released **after** the close, so
349
+ SPEC-015's backfill still reads the baggage that was live inside the span. The stack pop is
350
+ order-independent — ``_close_span`` reads the span it is handed and the current baggage,
351
+ never the stack — and simply mirrors the setup. Both releases are total in their own right
352
+ (SPEC-024, SPEC-025), so neither needs a guard here.
353
+
354
+ The ``is not None`` checks are load-bearing, not defensive tidiness: ``pop_span(None)`` would
355
+ fall through that function's own guard to ``set(())`` and wipe the whole stack, detaching the
356
+ parent of an untraced *nested* call and splitting its trace.
357
+ """
358
+ if span is not None:
359
+ try:
360
+ _close_span(span, status, error)
361
+ except Exception as exc:
362
+ _diag.absorbed("closing a span", exc, "the span's events were lost")
363
+ if token is not None:
364
+ context.pop_span(token)
365
+ if scope is not None:
366
+ context.pop_baggage_scope(scope)
367
+
368
+
275
369
  def _close_span(span: Span, status: str, exc: BaseException | None) -> None:
276
370
  """Append the end event, complete the boundary events' baggage, then flush.
277
371
 
@@ -311,47 +405,49 @@ def trace(
311
405
 
312
406
  @functools.wraps(fn)
313
407
  async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
314
- # Before _open_span, which is what makes the new span current (SPEC-024).
315
- is_root = context.current_span() is None
316
- span = _open_span(name or fn.__qualname__, defaults)
317
- token = context.push_span(span)
318
- scope = context.push_baggage_scope() if is_root else None
408
+ span, token, scope = _begin(name or fn.__qualname__, defaults)
409
+ status, error = "ok", None
319
410
  try:
320
411
  result = await fn(*args, **kwargs)
321
- _close_span(span, "ok", None)
322
- return result
323
412
  except BaseException as exc:
324
- # Non-swallowing (arch §4): record the error, then re-raise unchanged.
413
+ # Non-swallowing (arch §4): record the outcome, then re-raise unchanged.
325
414
  # BaseException covers asyncio.CancelledError — a cancelled coroutine is
326
415
  # recorded as an error end event, never left with an unclosed span.
327
- _close_span(span, "error", exc)
416
+ status, error = "error", exc
328
417
  raise
329
418
  finally:
330
- context.pop_span(token)
331
- if scope is not None:
332
- context.pop_baggage_scope(scope)
419
+ try:
420
+ _end(span, token, scope, status, error)
421
+ finally:
422
+ # `except ... as exc` auto-deletes its target for exactly this reason.
423
+ # `error` holds an exception whose traceback holds this frame, so
424
+ # keeping it past the handler builds a cycle only the collector can
425
+ # break — and pins the caller's own frames and locals along with it.
426
+ del status, error
427
+ return result
333
428
 
334
429
  return cast("F", async_wrapper)
335
430
 
336
431
  @functools.wraps(fn)
337
432
  def wrapper(*args: Any, **kwargs: Any) -> Any:
338
- # Before _open_span, which is what makes the new span current (SPEC-024).
339
- is_root = context.current_span() is None
340
- span = _open_span(name or fn.__qualname__, defaults)
341
- token = context.push_span(span)
342
- scope = context.push_baggage_scope() if is_root else None
433
+ span, token, scope = _begin(name or fn.__qualname__, defaults)
434
+ status, error = "ok", None
343
435
  try:
344
436
  result = fn(*args, **kwargs)
345
- _close_span(span, "ok", None)
346
- return result
347
437
  except BaseException as exc:
348
- # Non-swallowing (arch §4): record the error, then re-raise unchanged.
349
- _close_span(span, "error", exc)
438
+ # Non-swallowing (arch §4): record the outcome, then re-raise unchanged.
439
+ status, error = "error", exc
350
440
  raise
351
441
  finally:
352
- context.pop_span(token)
353
- if scope is not None:
354
- context.pop_baggage_scope(scope)
442
+ try:
443
+ _end(span, token, scope, status, error)
444
+ finally:
445
+ # `except ... as exc` auto-deletes its target for exactly this reason.
446
+ # `error` holds an exception whose traceback holds this frame, so keeping
447
+ # it past the handler builds a cycle only the collector can break — and
448
+ # pins the caller's own frames and locals along with it.
449
+ del status, error
450
+ return result
355
451
 
356
452
  return cast("F", wrapper)
357
453