log-foundry 0.10.2.dev3__tar.gz → 0.10.2.dev5__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 (53) hide show
  1. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/PKG-INFO +1 -1
  2. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/pyproject.toml +1 -1
  3. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/__init__.py +5 -2
  4. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/context.py +85 -0
  5. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/decorator.py +16 -0
  6. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/LICENSE +0 -0
  7. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/README.md +0 -0
  8. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/api.py +0 -0
  9. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/ids.py +0 -0
  12. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/model.py +0 -0
  13. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/py.typed +0 -0
  14. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sanitize.py +0 -0
  15. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/__init__.py +0 -0
  16. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/_batch.py +0 -0
  17. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/_chunk.py +0 -0
  18. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/_socket.py +0 -0
  19. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/_time.py +0 -0
  20. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/base.py +0 -0
  21. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/callback.py +0 -0
  22. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/clickhouse.py +0 -0
  23. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/datadog.py +0 -0
  24. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/elasticsearch.py +0 -0
  25. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/eventhubs.py +0 -0
  26. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/file.py +0 -0
  27. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/filtering.py +0 -0
  28. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/firehose.py +0 -0
  29. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/honeycomb.py +0 -0
  30. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/http.py +0 -0
  31. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/kafka.py +0 -0
  32. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/kinesis.py +0 -0
  33. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/logging_sink.py +0 -0
  34. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/logstash.py +0 -0
  35. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/loki.py +0 -0
  36. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/mongodb.py +0 -0
  37. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/multi.py +0 -0
  38. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/nats.py +0 -0
  39. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/newrelic.py +0 -0
  40. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/postgres.py +0 -0
  41. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/pubsub.py +0 -0
  42. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/rabbitmq.py +0 -0
  43. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/redis.py +0 -0
  44. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/sentry.py +0 -0
  45. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/sns.py +0 -0
  46. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/splunk.py +0 -0
  47. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/sqlite.py +0 -0
  48. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/sqs.py +0 -0
  49. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/stdout.py +0 -0
  50. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/syslog.py +0 -0
  51. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/transform.py +0 -0
  52. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/src/log_foundry/sinks/util.py +0 -0
  53. {log_foundry-0.10.2.dev3 → log_foundry-0.10.2.dev5}/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.dev3
3
+ Version: 0.10.2.dev5
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.dev3"
23
+ version = "0.10.2.dev5"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -3,8 +3,9 @@
3
3
  Public façade (module-function shape). Exposes configuration, the ``@trace`` decorator, the
4
4
  ``debug/info/warning/error/critical`` emitters, ``set_baggage``, the cross-process propagation
5
5
  pair (``continue_trace`` to adopt an inbound context; ``current_traceparent`` /
6
- ``current_trace_context`` / ``current_baggage_header`` to publish this one), ``flush`` (drain and
7
- keep logging) and ``shutdown`` (drain, close the sink, and stop).
6
+ ``current_trace_context`` / ``current_baggage_header`` to publish this one), ``reset_context``
7
+ (clear both for a caller who opens no span), ``flush`` (drain and keep logging) and ``shutdown``
8
+ (drain, close the sink, and stop).
8
9
  """
9
10
 
10
11
  from importlib.metadata import PackageNotFoundError
@@ -16,6 +17,7 @@ from log_foundry.context import (
16
17
  current_baggage_header,
17
18
  current_trace_context,
18
19
  current_traceparent,
20
+ reset_context,
19
21
  )
20
22
  from log_foundry.decorator import continue_trace, trace
21
23
  from log_foundry.worker import Health
@@ -107,6 +109,7 @@ __all__ = [
107
109
  "get_config",
108
110
  "health",
109
111
  "info",
112
+ "reset_context",
110
113
  "set_baggage",
111
114
  "shutdown",
112
115
  "trace",
@@ -29,8 +29,11 @@ __all__ = [
29
29
  "current_trace_context",
30
30
  "current_traceparent",
31
31
  "get_baggage",
32
+ "pop_baggage_scope",
32
33
  "pop_span",
34
+ "push_baggage_scope",
33
35
  "push_span",
36
+ "reset_context",
34
37
  "set_baggage",
35
38
  ]
36
39
 
@@ -81,6 +84,88 @@ def set_baggage(**kv: object) -> None:
81
84
  _baggage.set({**_baggage.get(), **kv})
82
85
 
83
86
 
87
+ # -- the root-span scope (SPEC-024) ------------------------------------------------------
88
+
89
+
90
+ def push_baggage_scope() -> contextvars.Token[dict[str, object]]:
91
+ """Open a root span's baggage scope; hand the token back to :func:`pop_baggage_scope`.
92
+
93
+ Setting the variable to its own current value is what mints the token — there is no other
94
+ way to say "restore whatever was here", including the case where nothing was ever set. It
95
+ is safe because no baggage dict is ever mutated in place (see this module's docstring).
96
+ """
97
+ return _baggage.set(_baggage.get())
98
+
99
+
100
+ def pop_baggage_scope(token: contextvars.Token[dict[str, object]]) -> None:
101
+ """Close a root span's scope: restore baggage, discard any adopted trace context.
102
+
103
+ The two are deliberately **asymmetric**. Baggage is *restored*, so a process-level default
104
+ set before any span outlives the traces that run under it while a request's own keys do not
105
+ (SPEC-024 FR-001). The adopted context is *cleared*, because it is a one-shot handoff to the
106
+ trace it was adopted for: restoring it would leave the next invocation still joining the
107
+ previous caller's trace, which is the defect this scope exists to close (FR-002). A caller
108
+ who opens no span at all clears both with :func:`reset_context`.
109
+
110
+ Total — never raises, because a decorated function must not fail on the way out (arch §4).
111
+
112
+ One caveat the ``contextvars`` model makes unavoidable: both writes land in the context the
113
+ root span's ``finally`` runs in. An adoption made *outside* a span that then runs in a child
114
+ context — any ``asyncio.Task``, including the one ``asyncio.run`` creates — is cleared in
115
+ the copy, not in the parent that holds it. :func:`continue_trace` is documented to be called on
116
+ the entry point's first line, which is inside the span and unaffected; a caller who adopts
117
+ before dispatching across a task boundary clears it with :func:`reset_context`.
118
+ """
119
+ # Cleared first, and deliberately ahead of anything that could fail: a stale trace id puts
120
+ # *wrong* data in the log stream, while a missed baggage restore only leaves a stale field.
121
+ _adopted.set(None)
122
+ try:
123
+ _baggage.reset(token)
124
+ except Exception:
125
+ # `reset` rejects a token minted in another context (``ValueError``, reachable when a
126
+ # span body hands work to another thread) and one already used (``RuntimeError``).
127
+ # Setting the captured value directly is equivalent: this context never had the scope
128
+ # pushed onto it, so there is nothing else to unwind. Broad because the alternative is
129
+ # raising from a `finally` and replacing the caller's own exception (arch §4).
130
+ # `old_value` is ``Token.MISSING`` when the variable was unset at capture, and absent
131
+ # entirely if this was never a Token; neither can have come from `set_baggage`, so both
132
+ # land on empty rather than poisoning baggage with whatever was passed.
133
+ old = getattr(token, "old_value", None)
134
+ _baggage.set(old if isinstance(old, dict) else {})
135
+
136
+
137
+ def reset_context() -> None:
138
+ """Clear baggage and any adopted trace context outright (SPEC-024 FR-003).
139
+
140
+ For the caller who uses the emitters **without** ``@trace``: the orphan path opens no span,
141
+ so there is no root-span exit to hang the release on, and both values would otherwise live
142
+ as long as the context does. A ``@trace`` user does not need this —
143
+ :func:`pop_baggage_scope` already runs at every root span's exit.
144
+
145
+ It is also the remedy for the one case that scope cannot reach: an adoption made outside a
146
+ span whose root span then runs in a child context, where the clear lands in the copy. Call
147
+ this in the context that made the adoption.
148
+
149
+ One function rather than two because the two values have the same lifetime and the same
150
+ failure mode, and a caller who wants one almost always wants the other.
151
+
152
+ Unlike the scope release this **clears** rather than restores — a process-level baggage
153
+ default set before any span is erased too, which is the point of an explicit reset. Safe
154
+ with nothing ever set, with no span open, and inside an open span. Never raises, like every
155
+ other entry point on this path (arch §4).
156
+
157
+ Prefer calling it **outside** a span. Inside one it does the obvious thing to the events
158
+ that follow, but SPEC-015 backfills ``span.start`` and ``span.end`` from the baggage live at
159
+ *close*, so a mid-span reset also empties the two boundary events — which describe the whole
160
+ span and carry ``duration_ms``/``status`` — of baggage that was live for most of it. That
161
+ span's exit then restores the baggage from before the span, as it always does.
162
+ """
163
+ # Cleared in the same order as `pop_baggage_scope`, for the reason given there: a stale
164
+ # trace id is wrong data, a stale baggage field is only stale.
165
+ _adopted.set(None)
166
+ _baggage.set({})
167
+
168
+
84
169
  # -- adopted inbound context (SPEC-014) --------------------------------------------------
85
170
 
86
171
 
@@ -114,6 +114,12 @@ def continue_trace(
114
114
  Call it on the **first line** of the entry point: a child span that already finished has
115
115
  been handed to the worker and can no longer be rewritten.
116
116
 
117
+ The adoption is **consumed by that one root span** and does not survive it (SPEC-024): the
118
+ next root span opened with no fresh call of its own starts a new trace, which is what stops
119
+ a warm container from logging every later invocation into the first caller's trace. So a
120
+ batch that fans out to several *sibling* root spans needs one call per item — or, better, a
121
+ single ``@trace`` entry point so the items are nested spans of one trace.
122
+
117
123
  ``baggage`` is a W3C ``baggage`` header merged into the current context. It succeeds or
118
124
  fails independently of the trace context.
119
125
 
@@ -298,8 +304,11 @@ def trace(
298
304
 
299
305
  @functools.wraps(fn)
300
306
  async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
307
+ # Before _open_span, which is what makes the new span current (SPEC-024).
308
+ is_root = context.current_span() is None
301
309
  span = _open_span(name or fn.__qualname__, defaults)
302
310
  token = context.push_span(span)
311
+ scope = context.push_baggage_scope() if is_root else None
303
312
  try:
304
313
  result = await fn(*args, **kwargs)
305
314
  _close_span(span, "ok", None)
@@ -312,13 +321,18 @@ def trace(
312
321
  raise
313
322
  finally:
314
323
  context.pop_span(token)
324
+ if scope is not None:
325
+ context.pop_baggage_scope(scope)
315
326
 
316
327
  return cast("F", async_wrapper)
317
328
 
318
329
  @functools.wraps(fn)
319
330
  def wrapper(*args: Any, **kwargs: Any) -> Any:
331
+ # Before _open_span, which is what makes the new span current (SPEC-024).
332
+ is_root = context.current_span() is None
320
333
  span = _open_span(name or fn.__qualname__, defaults)
321
334
  token = context.push_span(span)
335
+ scope = context.push_baggage_scope() if is_root else None
322
336
  try:
323
337
  result = fn(*args, **kwargs)
324
338
  _close_span(span, "ok", None)
@@ -329,6 +343,8 @@ def trace(
329
343
  raise
330
344
  finally:
331
345
  context.pop_span(token)
346
+ if scope is not None:
347
+ context.pop_baggage_scope(scope)
332
348
 
333
349
  return cast("F", wrapper)
334
350