log-foundry 0.10.2.dev4__tar.gz → 0.10.2.dev6__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.dev4 → log_foundry-0.10.2.dev6}/PKG-INFO +49 -4
  2. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/README.md +48 -3
  3. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/pyproject.toml +1 -1
  4. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/__init__.py +5 -2
  5. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/context.py +70 -3
  6. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/decorator.py +7 -0
  7. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/LICENSE +0 -0
  8. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/api.py +0 -0
  9. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/config.py +0 -0
  10. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/console.py +0 -0
  11. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/ids.py +0 -0
  12. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/model.py +0 -0
  13. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/py.typed +0 -0
  14. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sanitize.py +0 -0
  15. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/__init__.py +0 -0
  16. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/_batch.py +0 -0
  17. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/_chunk.py +0 -0
  18. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/_socket.py +0 -0
  19. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/_time.py +0 -0
  20. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/base.py +0 -0
  21. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/callback.py +0 -0
  22. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/clickhouse.py +0 -0
  23. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/datadog.py +0 -0
  24. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/elasticsearch.py +0 -0
  25. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/eventhubs.py +0 -0
  26. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/file.py +0 -0
  27. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/filtering.py +0 -0
  28. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/firehose.py +0 -0
  29. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/honeycomb.py +0 -0
  30. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/http.py +0 -0
  31. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/kafka.py +0 -0
  32. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/kinesis.py +0 -0
  33. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/logging_sink.py +0 -0
  34. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/logstash.py +0 -0
  35. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/loki.py +0 -0
  36. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/mongodb.py +0 -0
  37. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/multi.py +0 -0
  38. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/nats.py +0 -0
  39. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/newrelic.py +0 -0
  40. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/postgres.py +0 -0
  41. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/pubsub.py +0 -0
  42. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/rabbitmq.py +0 -0
  43. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/redis.py +0 -0
  44. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/sentry.py +0 -0
  45. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/sns.py +0 -0
  46. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/splunk.py +0 -0
  47. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/sqlite.py +0 -0
  48. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/sqs.py +0 -0
  49. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/stdout.py +0 -0
  50. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/syslog.py +0 -0
  51. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/transform.py +0 -0
  52. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/src/log_foundry/sinks/util.py +0 -0
  53. {log_foundry-0.10.2.dev4 → log_foundry-0.10.2.dev6}/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.dev4
3
+ Version: 0.10.2.dev6
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.dev4"
23
+ version = "0.10.2.dev6"
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",
@@ -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
  """
@@ -33,6 +54,7 @@ __all__ = [
33
54
  "pop_span",
34
55
  "push_baggage_scope",
35
56
  "push_span",
57
+ "reset_context",
36
58
  "set_baggage",
37
59
  ]
38
60
 
@@ -74,12 +96,25 @@ def pop_span(token: contextvars.Token[tuple[Span, ...]]) -> None:
74
96
 
75
97
 
76
98
  def get_baggage() -> dict[str, object]:
77
- """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
+ """
78
106
  return _baggage.get()
79
107
 
80
108
 
81
109
  def set_baggage(**kv: object) -> None:
82
- """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
+ """
83
118
  _baggage.set({**_baggage.get(), **kv})
84
119
 
85
120
 
@@ -133,6 +168,38 @@ def pop_baggage_scope(token: contextvars.Token[dict[str, object]]) -> None:
133
168
  _baggage.set(old if isinstance(old, dict) else {})
134
169
 
135
170
 
171
+ def reset_context() -> None:
172
+ """Clear baggage and any adopted trace context outright (SPEC-024 FR-003).
173
+
174
+ For the caller who uses the emitters **without** ``@trace``: the orphan path opens no span,
175
+ so there is no root-span exit to hang the release on, and both values would otherwise live
176
+ as long as the context does. A ``@trace`` user does not need this —
177
+ :func:`pop_baggage_scope` already runs at every root span's exit.
178
+
179
+ It is also the remedy for the one case that scope cannot reach: an adoption made outside a
180
+ span whose root span then runs in a child context, where the clear lands in the copy. Call
181
+ this in the context that made the adoption.
182
+
183
+ One function rather than two because the two values have the same lifetime and the same
184
+ failure mode, and a caller who wants one almost always wants the other.
185
+
186
+ Unlike the scope release this **clears** rather than restores — a process-level baggage
187
+ default set before any span is erased too, which is the point of an explicit reset. Safe
188
+ with nothing ever set, with no span open, and inside an open span. Never raises, like every
189
+ other entry point on this path (arch §4).
190
+
191
+ Prefer calling it **outside** a span. Inside one it does the obvious thing to the events
192
+ that follow, but SPEC-015 backfills ``span.start`` and ``span.end`` from the baggage live at
193
+ *close*, so a mid-span reset also empties the two boundary events — which describe the whole
194
+ span and carry ``duration_ms``/``status`` — of baggage that was live for most of it. That
195
+ span's exit then restores the baggage from before the span, as it always does.
196
+ """
197
+ # Cleared in the same order as `pop_baggage_scope`, for the reason given there: a stale
198
+ # trace id is wrong data, a stale baggage field is only stale.
199
+ _adopted.set(None)
200
+ _baggage.set({})
201
+
202
+
136
203
  # -- adopted inbound context (SPEC-014) --------------------------------------------------
137
204
 
138
205
 
@@ -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