log-foundry 0.10.2.dev5__tar.gz → 0.10.2.dev7__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.dev5 → log_foundry-0.10.2.dev7}/PKG-INFO +49 -4
  2. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/README.md +48 -3
  3. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/context.py +37 -3
  5. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/decorator.py +7 -0
  6. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/LICENSE +0 -0
  7. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/__init__.py +0 -0
  8. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/api.py +0 -0
  9. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/ids.py +0 -0
  12. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/model.py +0 -0
  13. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/py.typed +0 -0
  14. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sanitize.py +0 -0
  15. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/__init__.py +0 -0
  16. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/_batch.py +0 -0
  17. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/_chunk.py +0 -0
  18. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/_socket.py +0 -0
  19. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/_time.py +0 -0
  20. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/base.py +0 -0
  21. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/callback.py +0 -0
  22. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/clickhouse.py +0 -0
  23. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/datadog.py +0 -0
  24. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/elasticsearch.py +0 -0
  25. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/eventhubs.py +0 -0
  26. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/file.py +0 -0
  27. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/filtering.py +0 -0
  28. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/firehose.py +0 -0
  29. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/honeycomb.py +0 -0
  30. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/http.py +0 -0
  31. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/kafka.py +0 -0
  32. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/kinesis.py +0 -0
  33. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/logging_sink.py +0 -0
  34. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/logstash.py +0 -0
  35. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/loki.py +0 -0
  36. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/mongodb.py +0 -0
  37. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/multi.py +0 -0
  38. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/nats.py +0 -0
  39. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/newrelic.py +0 -0
  40. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/postgres.py +0 -0
  41. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/pubsub.py +0 -0
  42. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/rabbitmq.py +0 -0
  43. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/redis.py +0 -0
  44. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/sentry.py +0 -0
  45. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/sns.py +0 -0
  46. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/splunk.py +0 -0
  47. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/sqlite.py +0 -0
  48. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/sqs.py +0 -0
  49. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/stdout.py +0 -0
  50. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/syslog.py +0 -0
  51. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/transform.py +0 -0
  52. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/src/log_foundry/sinks/util.py +0 -0
  53. {log_foundry-0.10.2.dev5 → log_foundry-0.10.2.dev7}/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.dev5
3
+ Version: 0.10.2.dev7
4
4
  Summary: Generate logs for your console and JSON events for downstream consumption.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -184,8 +184,10 @@ span: if one exists, the new span copies its `trace_id` and records its `span_id
184
184
  new span is then pushed, so anything it calls sees *it* as the parent. On exit the span is
185
185
  popped by restoring the stack to its exact prior state (via a token, not a blind pop), which
186
186
  stays correct even when code branches into concurrent tasks. Because the stack lives in a
187
- context variable, every thread and asyncio task gets its own isolated copy — so `asyncio.gather`
188
- children share their parent's trace, and baggage set in one task never leaks into a sibling.
187
+ context variable, every asyncio task gets its own isolated copy — so `asyncio.gather` children
188
+ share their parent's trace, and baggage set in one task never leaks into a sibling. A new thread
189
+ gets a fresh context rather than a copy, so nothing follows it there unless the caller copies one
190
+ (as `asyncio.to_thread` does).
189
191
 
190
192
  ### When the worker flushes
191
193
 
@@ -291,7 +293,14 @@ def process_payment(user_id: int) -> str:
291
293
 
292
294
  - **`set_baggage(**kv)`** — attach trace-scoped context that is merged into the `fields` of
293
295
  every subsequent event in the same execution flow. Precedence, lowest to highest: config
294
- `defaults` → span `defaults` → baggage → per-call `fields`.
296
+ `defaults` → span `defaults` → baggage → per-call `fields`. **Trace-scoped means it ends
297
+ with the trace:** when the outermost `@trace` call returns *or raises*, the baggage in effect
298
+ before it is restored, so one request's keys do not reach the next request's events. Nested
299
+ calls do not reset — baggage set three calls deep stays visible to its parent and to the
300
+ siblings after it. Set with no span open it becomes a process-level default that later traces
301
+ inherit and restore to (`configure(defaults=...)` is the better tool for that) — and a process
302
+ that logs *without* `@trace` has no root span to release anything, so it needs
303
+ [`reset_context()`](#clearing-context-in-a-long-lived-process).
295
304
  - **`echo=True`** — *additionally* write a human-readable `LEVEL message` line to the console
296
305
  (`sys.stderr` by default), synchronously, without waiting for the async flush. The event
297
306
  still rides the normal pipeline to the sink — echo never redirects.
@@ -339,6 +348,7 @@ def handler(event, context):
339
348
  | `current_traceparent()` | This span as a W3C `traceparent` string, or `None` if no span is active. |
340
349
  | `current_trace_context()` | `(trace_id, span_id)`, for when moving two fields beats moving a string. |
341
350
  | `current_baggage_header()` | Current baggage in W3C `baggage` format (`""` when empty). |
351
+ | `reset_context()` | Clear baggage and any adopted context. `@trace` users do not need it. Never raises. |
342
352
 
343
353
  Details worth knowing:
344
354
 
@@ -363,10 +373,45 @@ Details worth knowing:
363
373
  join because one field was malformed is worse. Headers over 8192 bytes are rejected. Values
364
374
  are percent-encoded, so `,` `=` and non-ASCII round-trip; non-string values are serialized
365
375
  with `str()`, so a dict arrives as its repr.
376
+ - **An adopted context is consumed by one root span.** It applies to the next root span opened
377
+ and does not survive it, so the invocation after it starts a fresh trace unless it adopts
378
+ again. That is what stops a warm container from logging every later invocation into the first
379
+ caller's trace. A batch that fans out to several *sibling* root spans therefore needs one
380
+ `continue_trace()` per item — or, better, one `@trace` entry point so the items are nested
381
+ spans of a single trace.
366
382
  - **Sampling is not honoured.** `traceparent`'s flags byte is parsed and ignored, and outbound
367
383
  is always `01`: this library records every span, so respecting another system's sampling
368
384
  decision would mean dropping them.
369
385
 
386
+ #### Clearing context in a long-lived process
387
+
388
+ `@trace` releases both baggage and the adopted context when the outermost decorated call returns
389
+ or raises, so **most callers never need `reset_context()`**. It exists for the two cases where no
390
+ root-span exit releases them in *your* context:
391
+
392
+ ```python
393
+ lf.reset_context() # clears baggage *and* any adopted trace context
394
+ ```
395
+
396
+ - **You use the emitters without `@trace`.** An orphan log opens no span, so nothing releases
397
+ what `set_baggage()` or `continue_trace()` set. In a process that reuses one thread across
398
+ requests — the main thread, a pooled worker, a warm Lambda container — that state reaches the
399
+ next request. Call `reset_context()` when a unit of work ends. (An orphan log never joins an
400
+ adopted trace either: it mints its own `trace_id`, and the adoption simply waits to claim the
401
+ next root span, whenever one happens to run.)
402
+ - **You adopt outside the span and dispatch into a task.** The release runs in whichever
403
+ context the root span's `finally` runs in, so `continue_trace()` here followed by
404
+ `asyncio.run(main())` clears the adoption in the task's copy of the context while this one
405
+ keeps it. `contextvars` has no way to write back to a parent context, so clear it yourself.
406
+ Adopting on the entry point's first line — the documented placement — is inside the span and
407
+ needs nothing.
408
+
409
+ It clears rather than restores: a process-level baggage default set before any span is erased
410
+ too — permanently when you call it outside a span. Prefer that. Called *inside* a span it also
411
+ empties the `span.start` / `span.end` events of baggage, because those are stamped with the
412
+ span's *final* baggage at close, and that span's exit then restores the pre-span baggage anyway,
413
+ undoing the erasure. It never raises.
414
+
370
415
  ### Sinks
371
416
 
372
417
  A **sink** is the swappable output transport — any object satisfying the `Sink` protocol. It
@@ -148,8 +148,10 @@ span: if one exists, the new span copies its `trace_id` and records its `span_id
148
148
  new span is then pushed, so anything it calls sees *it* as the parent. On exit the span is
149
149
  popped by restoring the stack to its exact prior state (via a token, not a blind pop), which
150
150
  stays correct even when code branches into concurrent tasks. Because the stack lives in a
151
- context variable, every thread and asyncio task gets its own isolated copy — so `asyncio.gather`
152
- children share their parent's trace, and baggage set in one task never leaks into a sibling.
151
+ context variable, every asyncio task gets its own isolated copy — so `asyncio.gather` children
152
+ share their parent's trace, and baggage set in one task never leaks into a sibling. A new thread
153
+ gets a fresh context rather than a copy, so nothing follows it there unless the caller copies one
154
+ (as `asyncio.to_thread` does).
153
155
 
154
156
  ### When the worker flushes
155
157
 
@@ -255,7 +257,14 @@ def process_payment(user_id: int) -> str:
255
257
 
256
258
  - **`set_baggage(**kv)`** — attach trace-scoped context that is merged into the `fields` of
257
259
  every subsequent event in the same execution flow. Precedence, lowest to highest: config
258
- `defaults` → span `defaults` → baggage → per-call `fields`.
260
+ `defaults` → span `defaults` → baggage → per-call `fields`. **Trace-scoped means it ends
261
+ with the trace:** when the outermost `@trace` call returns *or raises*, the baggage in effect
262
+ before it is restored, so one request's keys do not reach the next request's events. Nested
263
+ calls do not reset — baggage set three calls deep stays visible to its parent and to the
264
+ siblings after it. Set with no span open it becomes a process-level default that later traces
265
+ inherit and restore to (`configure(defaults=...)` is the better tool for that) — and a process
266
+ that logs *without* `@trace` has no root span to release anything, so it needs
267
+ [`reset_context()`](#clearing-context-in-a-long-lived-process).
259
268
  - **`echo=True`** — *additionally* write a human-readable `LEVEL message` line to the console
260
269
  (`sys.stderr` by default), synchronously, without waiting for the async flush. The event
261
270
  still rides the normal pipeline to the sink — echo never redirects.
@@ -303,6 +312,7 @@ def handler(event, context):
303
312
  | `current_traceparent()` | This span as a W3C `traceparent` string, or `None` if no span is active. |
304
313
  | `current_trace_context()` | `(trace_id, span_id)`, for when moving two fields beats moving a string. |
305
314
  | `current_baggage_header()` | Current baggage in W3C `baggage` format (`""` when empty). |
315
+ | `reset_context()` | Clear baggage and any adopted context. `@trace` users do not need it. Never raises. |
306
316
 
307
317
  Details worth knowing:
308
318
 
@@ -327,10 +337,45 @@ Details worth knowing:
327
337
  join because one field was malformed is worse. Headers over 8192 bytes are rejected. Values
328
338
  are percent-encoded, so `,` `=` and non-ASCII round-trip; non-string values are serialized
329
339
  with `str()`, so a dict arrives as its repr.
340
+ - **An adopted context is consumed by one root span.** It applies to the next root span opened
341
+ and does not survive it, so the invocation after it starts a fresh trace unless it adopts
342
+ again. That is what stops a warm container from logging every later invocation into the first
343
+ caller's trace. A batch that fans out to several *sibling* root spans therefore needs one
344
+ `continue_trace()` per item — or, better, one `@trace` entry point so the items are nested
345
+ spans of a single trace.
330
346
  - **Sampling is not honoured.** `traceparent`'s flags byte is parsed and ignored, and outbound
331
347
  is always `01`: this library records every span, so respecting another system's sampling
332
348
  decision would mean dropping them.
333
349
 
350
+ #### Clearing context in a long-lived process
351
+
352
+ `@trace` releases both baggage and the adopted context when the outermost decorated call returns
353
+ or raises, so **most callers never need `reset_context()`**. It exists for the two cases where no
354
+ root-span exit releases them in *your* context:
355
+
356
+ ```python
357
+ lf.reset_context() # clears baggage *and* any adopted trace context
358
+ ```
359
+
360
+ - **You use the emitters without `@trace`.** An orphan log opens no span, so nothing releases
361
+ what `set_baggage()` or `continue_trace()` set. In a process that reuses one thread across
362
+ requests — the main thread, a pooled worker, a warm Lambda container — that state reaches the
363
+ next request. Call `reset_context()` when a unit of work ends. (An orphan log never joins an
364
+ adopted trace either: it mints its own `trace_id`, and the adoption simply waits to claim the
365
+ next root span, whenever one happens to run.)
366
+ - **You adopt outside the span and dispatch into a task.** The release runs in whichever
367
+ context the root span's `finally` runs in, so `continue_trace()` here followed by
368
+ `asyncio.run(main())` clears the adoption in the task's copy of the context while this one
369
+ keeps it. `contextvars` has no way to write back to a parent context, so clear it yourself.
370
+ Adopting on the entry point's first line — the documented placement — is inside the span and
371
+ needs nothing.
372
+
373
+ It clears rather than restores: a process-level baggage default set before any span is erased
374
+ too — permanently when you call it outside a span. Prefer that. Called *inside* a span it also
375
+ empties the `span.start` / `span.end` events of baggage, because those are stamped with the
376
+ span's *final* baggage at close, and that span's exit then restores the pre-span baggage anyway,
377
+ undoing the erasure. It never raises.
378
+
334
379
  ### Sinks
335
380
 
336
381
  A **sink** is the swappable output transport — any object satisfying the `Sink` protocol. It
@@ -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.dev5"
23
+ version = "0.10.2.dev7"
24
24
 
25
25
  [project.optional-dependencies]
26
26
  aws = ["boto3>=1.43.61"] # SQSSink, SNSSink, KinesisSink, FirehoseSink
@@ -5,9 +5,30 @@ inherits its own copy — so ``log_foundry.info(...)`` can find "the span I'm in
5
5
  manual passing. This module holds a *stack* of active spans (the top is the current span and
6
6
  the parent of the next nested call) plus trace-scoped baggage.
7
7
 
8
+ **The three variables and how long each lives** (SPEC-024 — before it, the last two lived as
9
+ long as the context did, so one request's data reached the next request's events):
10
+
11
+ ``_span_stack``
12
+ One entry per active decorated call. Pushed on enter, popped in the ``finally`` on exit.
13
+
14
+ ``_baggage``
15
+ Restored to its pre-span value when the **root** span closes — the span opened when no
16
+ other was active. Nested spans do not reset, so baggage set three calls deep stays visible
17
+ to its parent and to later siblings in the same trace: that is what "trace-scoped" means.
18
+ Baggage set before any span is a process-level default and is restored *to*, not erased.
19
+
20
+ ``_adopted``
21
+ Cleared when the root span closes, and consumed by that one span — an inbound context is a
22
+ one-shot handoff to the trace it names, not a standing setting. Restoring it instead would
23
+ leave a warm container joining the first caller's trace forever.
24
+
25
+ The last two are released together by :func:`pop_baggage_scope`; the span stack is released by
26
+ :func:`pop_span`. A caller who opens no span clears both with :func:`reset_context`.
27
+
8
28
  Two footguns, both avoided below:
9
29
  * Never mutate a ContextVar's default mutable value — the ``()`` / ``{}`` defaults are shared
10
- across all contexts. Always ``.set()`` a new tuple/dict.
30
+ across all contexts. Always ``.set()`` a new tuple/dict. The same applies to any value a
31
+ *parent* context can still see: emptying a baggage dict in place reaches back into it.
11
32
  * Use the token/``reset`` pattern, not a manual pop — ``reset(token)`` restores the exact
12
33
  prior state even when tasks branch.
13
34
  """
@@ -75,12 +96,25 @@ def pop_span(token: contextvars.Token[tuple[Span, ...]]) -> None:
75
96
 
76
97
 
77
98
  def get_baggage() -> dict[str, object]:
78
- """Return the current trace's baggage (do not mutate the returned dict in place)."""
99
+ """Return the current trace's baggage (do not mutate the returned dict in place).
100
+
101
+ The trace's own keys are discarded when the enclosing **root** span closes, restoring the
102
+ baggage in effect before it — so a later trace sees only a process-level default set before
103
+ any span opened. With no span open at all nothing releases them and they accumulate for the
104
+ life of the context; :func:`reset_context` is the release on that path.
105
+ """
79
106
  return _baggage.get()
80
107
 
81
108
 
82
109
  def set_baggage(**kv: object) -> None:
83
- """Merge key/values into the current trace's baggage (replaces with a new dict)."""
110
+ """Merge key/values into the current trace's baggage (replaces with a new dict).
111
+
112
+ The keys live until the enclosing **root** span closes, then the baggage in effect before
113
+ that span is restored — so they ride every event at or below this point in the trace and
114
+ none of the next trace's. Called with no span open they become a process-level default that
115
+ later traces inherit and restore to; :func:`reset_context` is what erases those, and
116
+ ``configure(defaults=...)`` is the better tool for setting them in the first place.
117
+ """
84
118
  _baggage.set({**_baggage.get(), **kv})
85
119
 
86
120
 
@@ -120,6 +120,13 @@ def continue_trace(
120
120
  batch that fans out to several *sibling* root spans needs one call per item — or, better, a
121
121
  single ``@trace`` entry point so the items are nested spans of one trace.
122
122
 
123
+ One constraint on that release: it happens in whichever context the root span's ``finally``
124
+ runs in. Adopt here and then dispatch the span into a **child** context — any
125
+ ``asyncio.Task``, including the one ``asyncio.run`` creates — and the clear lands in the
126
+ copy while this context keeps the adoption. Calling this on the entry point's first line, as
127
+ above, is inside the span and unaffected; a caller who adopts before dispatching should call
128
+ :func:`~log_foundry.reset_context` when the work is done.
129
+
123
130
  ``baggage`` is a W3C ``baggage`` header merged into the current context. It succeeds or
124
131
  fails independently of the trace context.
125
132