traceact 0.1.0__py3-none-any.whl
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.
- traceact/__init__.py +43 -0
- traceact/budget.py +103 -0
- traceact/config.py +198 -0
- traceact/context.py +130 -0
- traceact/decorators.py +430 -0
- traceact/helpers.py +103 -0
- traceact/ids.py +62 -0
- traceact/sinks.py +177 -0
- traceact/trace.py +1176 -0
- traceact-0.1.0.dist-info/METADATA +94 -0
- traceact-0.1.0.dist-info/RECORD +14 -0
- traceact-0.1.0.dist-info/WHEEL +5 -0
- traceact-0.1.0.dist-info/licenses/LICENSE +21 -0
- traceact-0.1.0.dist-info/top_level.txt +1 -0
traceact/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# __init__.py
|
|
2
|
+
#
|
|
3
|
+
# Public API for the traceact package.
|
|
4
|
+
#
|
|
5
|
+
# Everything a developer needs to use TraceAct is exported from here. Importing
|
|
6
|
+
# from the submodules directly (e.g. from traceact.trace import ActionTrace) is
|
|
7
|
+
# supported but not the intended pattern — this file is the stable public surface.
|
|
8
|
+
#
|
|
9
|
+
# Public API (v1):
|
|
10
|
+
#
|
|
11
|
+
# ActionTrace — the live trace object. Use with ActionTrace.start() for manual
|
|
12
|
+
# tracing inside a with-block.
|
|
13
|
+
#
|
|
14
|
+
# TraceConfig — settings object. Pass to configure() or to @traced_action.
|
|
15
|
+
#
|
|
16
|
+
# TraceBudget — limits object. Pass to configure() or to @traced_action.
|
|
17
|
+
#
|
|
18
|
+
# configure() — set package-wide defaults for all future traces.
|
|
19
|
+
#
|
|
20
|
+
# reset_config()— restore package defaults (use in test teardown).
|
|
21
|
+
#
|
|
22
|
+
# traced_action — decorator. The primary way to add tracing to a function.
|
|
23
|
+
#
|
|
24
|
+
# JsonlSink — writes traces to a .jsonl file.
|
|
25
|
+
#
|
|
26
|
+
# ConsoleSink — prints traces to stdout.
|
|
27
|
+
|
|
28
|
+
from traceact.config import configure, reset_config, TraceConfig
|
|
29
|
+
from traceact.budget import TraceBudget
|
|
30
|
+
from traceact.trace import ActionTrace
|
|
31
|
+
from traceact.decorators import traced_action
|
|
32
|
+
from traceact.sinks import JsonlSink, ConsoleSink
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"ActionTrace",
|
|
36
|
+
"TraceConfig",
|
|
37
|
+
"TraceBudget",
|
|
38
|
+
"configure",
|
|
39
|
+
"reset_config",
|
|
40
|
+
"traced_action",
|
|
41
|
+
"JsonlSink",
|
|
42
|
+
"ConsoleSink",
|
|
43
|
+
]
|
traceact/budget.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# budget.py
|
|
2
|
+
#
|
|
3
|
+
# Defines TraceBudget — the limits object that controls how much TraceAct
|
|
4
|
+
# records before stopping and flagging budget_hit on the trace.
|
|
5
|
+
#
|
|
6
|
+
# Why a separate budget from config?
|
|
7
|
+
# Config controls *behaviour* (is tracing on? what mode? strict or not?).
|
|
8
|
+
# Budget controls *volume* (how many events? how deep? what size payloads?).
|
|
9
|
+
# Keeping them separate makes it easy to tune one without touching the other,
|
|
10
|
+
# and makes the inherited-vs-overridden distinction clear at each trace boundary.
|
|
11
|
+
#
|
|
12
|
+
# How budget inheritance works:
|
|
13
|
+
# Like TraceConfig, TraceBudget uses None to mean "not specified." When a child
|
|
14
|
+
# trace specifies TraceBudget(max_events=300), only max_events is overridden —
|
|
15
|
+
# all other fields are inherited from the parent trace or the package default.
|
|
16
|
+
#
|
|
17
|
+
# This is key because a specific trace (say, an agent loop) might need more
|
|
18
|
+
# events than the default, but still wants the global max_depth and
|
|
19
|
+
# always_trace_errors settings to apply.
|
|
20
|
+
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TraceBudget:
|
|
25
|
+
"""
|
|
26
|
+
Controls the recording limits for a trace.
|
|
27
|
+
|
|
28
|
+
All fields default to None, which means "not specified — inherit from the
|
|
29
|
+
parent trace or use the package default." See _resolve_budget() in trace.py
|
|
30
|
+
for how inheritance is applied.
|
|
31
|
+
|
|
32
|
+
Fields:
|
|
33
|
+
max_events:
|
|
34
|
+
Maximum number of events to record in a single trace. Once this
|
|
35
|
+
limit is reached, further calls to trace.event() are silently
|
|
36
|
+
dropped and budget_hit is set to True on the trace. The wrapped
|
|
37
|
+
function continues to run normally.
|
|
38
|
+
|
|
39
|
+
max_steps:
|
|
40
|
+
Maximum number of steps to record. Same behaviour as max_events
|
|
41
|
+
once the limit is hit.
|
|
42
|
+
|
|
43
|
+
max_depth:
|
|
44
|
+
Maximum nesting depth for child traces. If a @traced_action
|
|
45
|
+
decorator would create a trace at depth > max_depth, the decorator
|
|
46
|
+
behaves as if tracing is disabled for that call. The function still
|
|
47
|
+
runs normally; it just produces no trace record.
|
|
48
|
+
|
|
49
|
+
max_payload_bytes:
|
|
50
|
+
Maximum size (in bytes, after JSON serialisation) of any single
|
|
51
|
+
captured value — an input field, an event result field, or an
|
|
52
|
+
output field. Values that exceed this are replaced with a
|
|
53
|
+
"[truncated: N chars]" summary.
|
|
54
|
+
|
|
55
|
+
sample_rate:
|
|
56
|
+
The fraction of successful traces to record. 1.0 means record
|
|
57
|
+
everything. 0.1 means record approximately 10% of traces. Failed
|
|
58
|
+
traces (status="failed") are always recorded when always_trace_errors
|
|
59
|
+
is True, regardless of sample_rate.
|
|
60
|
+
|
|
61
|
+
Sampling happens before a trace object is created. If a trace is
|
|
62
|
+
sampled out, the ContextVar is set to a skip sentinel so that any
|
|
63
|
+
nested @traced_action calls are also skipped. Nothing is written.
|
|
64
|
+
|
|
65
|
+
always_trace_errors:
|
|
66
|
+
When True (default), failed traces are always recorded even if
|
|
67
|
+
sample_rate would otherwise drop them. This ensures that errors are
|
|
68
|
+
never silently lost because of sampling.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
max_events: Optional[int] = None,
|
|
74
|
+
max_steps: Optional[int] = None,
|
|
75
|
+
max_depth: Optional[int] = None,
|
|
76
|
+
max_payload_bytes: Optional[int] = None,
|
|
77
|
+
sample_rate: Optional[float] = None,
|
|
78
|
+
always_trace_errors: Optional[bool] = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.max_events = max_events
|
|
81
|
+
self.max_steps = max_steps
|
|
82
|
+
self.max_depth = max_depth
|
|
83
|
+
self.max_payload_bytes = max_payload_bytes
|
|
84
|
+
self.sample_rate = sample_rate
|
|
85
|
+
self.always_trace_errors = always_trace_errors
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Package-level defaults
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
#
|
|
92
|
+
# These are the values used when no package config and no trace-level override
|
|
93
|
+
# specifies a budget field. They are intentionally generous so that TraceAct
|
|
94
|
+
# works well in small local tools without any configuration at all.
|
|
95
|
+
|
|
96
|
+
BUDGET_DEFAULTS = {
|
|
97
|
+
"max_events": 100,
|
|
98
|
+
"max_steps": 50,
|
|
99
|
+
"max_depth": 10,
|
|
100
|
+
"max_payload_bytes": 8192,
|
|
101
|
+
"sample_rate": 1.0,
|
|
102
|
+
"always_trace_errors": True,
|
|
103
|
+
}
|
traceact/config.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# config.py
|
|
2
|
+
#
|
|
3
|
+
# Defines TraceConfig — the settings object that controls how TraceAct behaves —
|
|
4
|
+
# and the package-level configure() / reset_config() functions.
|
|
5
|
+
#
|
|
6
|
+
# Why a separate config module?
|
|
7
|
+
# Configuration is shared across the whole package. Every trace, decorator, and
|
|
8
|
+
# sink needs to read the same settings. Centralising them here means there is one
|
|
9
|
+
# place to look when a setting needs to change, and one place to reset when a
|
|
10
|
+
# test has left state behind.
|
|
11
|
+
#
|
|
12
|
+
# How config inheritance works:
|
|
13
|
+
# TraceConfig uses None to mean "not specified". A None field means "inherit from
|
|
14
|
+
# the parent trace's config, or fall back to the package default." This allows a
|
|
15
|
+
# single decorator to override only the fields it cares about, rather than
|
|
16
|
+
# replacing the entire config object.
|
|
17
|
+
#
|
|
18
|
+
# The resolution order (lowest to highest priority) is:
|
|
19
|
+
# Package defaults → package-level configure() → parent trace config → decorator override
|
|
20
|
+
#
|
|
21
|
+
# See trace.py for the _resolve_config() function that applies this ordering.
|
|
22
|
+
|
|
23
|
+
from typing import Any, List, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TraceConfig:
|
|
27
|
+
"""
|
|
28
|
+
Controls the behaviour of TraceAct tracing.
|
|
29
|
+
|
|
30
|
+
All fields default to None, which means "not specified — use the package
|
|
31
|
+
default or inherit from the parent trace." Only fields you explicitly set
|
|
32
|
+
will override the inherited value.
|
|
33
|
+
|
|
34
|
+
Fields:
|
|
35
|
+
enabled:
|
|
36
|
+
Whether tracing is active. When False, @traced_action decorators
|
|
37
|
+
run their wrapped function with almost no overhead — no trace is
|
|
38
|
+
created, no context is touched, no sink is called. This is the
|
|
39
|
+
global kill switch.
|
|
40
|
+
|
|
41
|
+
sink_mode:
|
|
42
|
+
Controls when traces are written to sinks.
|
|
43
|
+
"blocking" — write immediately when a trace finishes. Best for
|
|
44
|
+
local development and tests.
|
|
45
|
+
"buffered" — accumulate finished traces in memory and flush them
|
|
46
|
+
later (or on program exit). Best default for small apps.
|
|
47
|
+
"disabled" — never write anything, regardless of sink configuration.
|
|
48
|
+
Use as a performance kill switch that still leaves
|
|
49
|
+
decorators in place.
|
|
50
|
+
|
|
51
|
+
strict:
|
|
52
|
+
When True, any exception raised inside TraceAct's own tracing logic
|
|
53
|
+
(not inside the function being traced) will propagate into the
|
|
54
|
+
application. When False (default), tracing failures are silently
|
|
55
|
+
swallowed so the app is never broken by its own observability layer.
|
|
56
|
+
|
|
57
|
+
redact_by_default:
|
|
58
|
+
When True, field names that match known sensitive patterns (such as
|
|
59
|
+
"password", "token", "secret", "api_key") are replaced with the
|
|
60
|
+
string "[redacted]" before being stored in inputs, events, or outputs.
|
|
61
|
+
This applies to both automatic input capture and manual trace.event()
|
|
62
|
+
calls.
|
|
63
|
+
|
|
64
|
+
capture_inputs:
|
|
65
|
+
Controls the automatic input-capture behaviour of @traced_action.
|
|
66
|
+
|
|
67
|
+
None / not set — use the package default, which is False (no capture).
|
|
68
|
+
False — disable automatic capture globally. When set at the
|
|
69
|
+
package level via configure(), individual decorators
|
|
70
|
+
cannot re-enable automatic capture. Manual
|
|
71
|
+
trace.input() calls still work.
|
|
72
|
+
True — capture all named function arguments automatically,
|
|
73
|
+
applying redaction and payload limits.
|
|
74
|
+
list of strings — capture only the named arguments in the list.
|
|
75
|
+
This is the safest opt-in form: you choose exactly
|
|
76
|
+
which fields are recorded.
|
|
77
|
+
|
|
78
|
+
capture_outputs:
|
|
79
|
+
When True (default), calls to trace.output() are recorded on the
|
|
80
|
+
trace. When False, trace.output() is a no-op.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(
|
|
84
|
+
self,
|
|
85
|
+
enabled: Optional[bool] = None,
|
|
86
|
+
sink_mode: Optional[str] = None,
|
|
87
|
+
strict: Optional[bool] = None,
|
|
88
|
+
redact_by_default: Optional[bool] = None,
|
|
89
|
+
capture_inputs: Any = None,
|
|
90
|
+
capture_outputs: Optional[bool] = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
self.enabled = enabled
|
|
93
|
+
self.sink_mode = sink_mode
|
|
94
|
+
self.strict = strict
|
|
95
|
+
self.redact_by_default = redact_by_default
|
|
96
|
+
self.capture_inputs = capture_inputs
|
|
97
|
+
self.capture_outputs = capture_outputs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# Package-level state
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
#
|
|
104
|
+
# These three variables hold the current package-wide configuration, budget,
|
|
105
|
+
# and list of sinks. They start as None / empty, which means "use defaults."
|
|
106
|
+
# configure() sets them. reset_config() clears them back to None / empty.
|
|
107
|
+
#
|
|
108
|
+
# Why module-level variables instead of a class?
|
|
109
|
+
# A module is itself a singleton in Python — imported once and cached. Module-
|
|
110
|
+
# level variables are the simplest, most readable way to hold package state
|
|
111
|
+
# without introducing a global object that callers must instantiate.
|
|
112
|
+
|
|
113
|
+
_package_config: Optional[TraceConfig] = None
|
|
114
|
+
_package_budget: Any = None # Optional[TraceBudget] — typed as Any to avoid circular import
|
|
115
|
+
_package_sinks: List[Any] = [] # List[Sink]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def configure(
|
|
119
|
+
config: Optional[TraceConfig] = None,
|
|
120
|
+
budget: Any = None,
|
|
121
|
+
sinks: Optional[List[Any]] = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Set package-wide defaults for all future traces.
|
|
125
|
+
|
|
126
|
+
Call this once at application startup. Settings applied here become the
|
|
127
|
+
baseline for every trace unless a specific trace or decorator overrides them.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config: A TraceConfig instance. Only non-None fields are applied; the
|
|
131
|
+
rest keep their current values (or package defaults).
|
|
132
|
+
budget: A TraceBudget instance. Only non-None fields are applied.
|
|
133
|
+
sinks: A list of sink objects (JsonlSink, ConsoleSink, etc.). Replaces
|
|
134
|
+
the current sink list entirely.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
configure(
|
|
138
|
+
config=TraceConfig(enabled=True, sink_mode="buffered"),
|
|
139
|
+
budget=TraceBudget(max_events=200),
|
|
140
|
+
sinks=[JsonlSink("traces.jsonl")],
|
|
141
|
+
)
|
|
142
|
+
"""
|
|
143
|
+
global _package_config, _package_budget, _package_sinks
|
|
144
|
+
|
|
145
|
+
if config is not None:
|
|
146
|
+
_package_config = config
|
|
147
|
+
|
|
148
|
+
if budget is not None:
|
|
149
|
+
_package_budget = budget
|
|
150
|
+
|
|
151
|
+
if sinks is not None:
|
|
152
|
+
_package_sinks = list(sinks)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def reset_config() -> None:
|
|
156
|
+
"""
|
|
157
|
+
Restore all package-level state to its initial defaults.
|
|
158
|
+
|
|
159
|
+
Use this in test teardown to prevent one test's configure() call from
|
|
160
|
+
leaking into the next test. reset_config() also clears the active trace
|
|
161
|
+
ContextVar so no orphaned trace context carries over between tests.
|
|
162
|
+
|
|
163
|
+
What it resets:
|
|
164
|
+
- TraceConfig → package defaults (enabled, buffered, strict=False, etc.)
|
|
165
|
+
- TraceBudget → package defaults
|
|
166
|
+
- Sink list → empty (no sinks)
|
|
167
|
+
- Active trace ContextVar → None (no active trace)
|
|
168
|
+
|
|
169
|
+
What it does NOT reset:
|
|
170
|
+
- Traces that have already been written to a sink. Those are gone.
|
|
171
|
+
- Files on disk. JsonlSink output is not deleted.
|
|
172
|
+
"""
|
|
173
|
+
global _package_config, _package_budget, _package_sinks
|
|
174
|
+
|
|
175
|
+
_package_config = None
|
|
176
|
+
_package_budget = None
|
|
177
|
+
_package_sinks = []
|
|
178
|
+
|
|
179
|
+
# Clear the active trace context so tests start with a clean slate.
|
|
180
|
+
# The deferred import avoids a circular dependency at module load time
|
|
181
|
+
# (context.py does not import config.py).
|
|
182
|
+
from traceact.context import _active_trace
|
|
183
|
+
_active_trace.set(None)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def get_package_config() -> Optional[TraceConfig]:
|
|
187
|
+
"""Return the package-level TraceConfig, or None if not set."""
|
|
188
|
+
return _package_config
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_package_budget() -> Any:
|
|
192
|
+
"""Return the package-level TraceBudget, or None if not set."""
|
|
193
|
+
return _package_budget
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_package_sinks() -> List[Any]:
|
|
197
|
+
"""Return the current list of configured sinks."""
|
|
198
|
+
return _package_sinks
|
traceact/context.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# context.py
|
|
2
|
+
#
|
|
3
|
+
# Manages the active trace using Python's contextvars.ContextVar.
|
|
4
|
+
#
|
|
5
|
+
# Why ContextVar?
|
|
6
|
+
# Python's contextvars module (introduced in 3.7) provides context-local
|
|
7
|
+
# storage that is automatically isolated across:
|
|
8
|
+
# - Threads (each thread gets its own copy)
|
|
9
|
+
# - asyncio Tasks (each task gets its own copy, inherited from its parent)
|
|
10
|
+
# - Nested function calls (the ContextVar token mechanism allows exact restore)
|
|
11
|
+
#
|
|
12
|
+
# This makes it the right tool for tracking "which trace is currently running"
|
|
13
|
+
# without requiring the developer to pass a trace object through every function
|
|
14
|
+
# call manually.
|
|
15
|
+
#
|
|
16
|
+
# The public API never exposes the ContextVar directly. Developers interact with
|
|
17
|
+
# traces through @traced_action and ActionTrace.start(), not through this module.
|
|
18
|
+
#
|
|
19
|
+
# How the skip sentinel works:
|
|
20
|
+
# When a trace is sampled out (sample_rate < 1.0 decides to skip it), we still
|
|
21
|
+
# set the ContextVar — but we set it to the SKIP sentinel rather than to a real
|
|
22
|
+
# ActionTrace. Any nested @traced_action call checks the ContextVar first: if it
|
|
23
|
+
# sees SKIP, it also skips. This ensures that a sampled-out parent silently
|
|
24
|
+
# suppresses all its children without requiring any coordination between traces.
|
|
25
|
+
#
|
|
26
|
+
# Without the skip sentinel, a sampled-out parent would leave the ContextVar
|
|
27
|
+
# empty, and a nested @traced_action would see "no active parent" and create a
|
|
28
|
+
# new root trace — which would then appear in the sink without its parent, making
|
|
29
|
+
# the output confusing and incomplete.
|
|
30
|
+
|
|
31
|
+
from contextvars import ContextVar
|
|
32
|
+
from typing import Any, Optional
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Skip sentinel
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
#
|
|
39
|
+
# A sentinel is a unique object used as a special signal value. We use a class
|
|
40
|
+
# rather than a string like "SKIP" so that nothing can accidentally match it.
|
|
41
|
+
|
|
42
|
+
class _SkipSentinel:
|
|
43
|
+
"""
|
|
44
|
+
Marker placed in the ContextVar when a trace has been sampled out.
|
|
45
|
+
|
|
46
|
+
Any code that reads the ContextVar and finds this object should treat the
|
|
47
|
+
current execution context as "tracing suppressed" and do nothing.
|
|
48
|
+
"""
|
|
49
|
+
__slots__ = ()
|
|
50
|
+
|
|
51
|
+
def __repr__(self) -> str:
|
|
52
|
+
return "<TraceAct: sampled out>"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# The single instance of the skip sentinel. This is what gets stored in the
|
|
56
|
+
# ContextVar when a trace is sampled out.
|
|
57
|
+
SKIP: _SkipSentinel = _SkipSentinel()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_skip(value: Any) -> bool:
|
|
61
|
+
"""Return True if the given value is the skip sentinel."""
|
|
62
|
+
return isinstance(value, _SkipSentinel)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# The ContextVar
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
#
|
|
69
|
+
# This variable holds one of three things at any point in time:
|
|
70
|
+
# None — no active trace (we are outside any traced function)
|
|
71
|
+
# ActionTrace — a live trace that @traced_action or ActionTrace.start() created
|
|
72
|
+
# SKIP — we are inside a sampled-out trace; everything is suppressed
|
|
73
|
+
#
|
|
74
|
+
# The variable is module-level so it is truly global (one per interpreter), but
|
|
75
|
+
# ContextVar ensures each asyncio Task and each thread sees its own independent
|
|
76
|
+
# value. Two concurrent requests will never interfere with each other's active
|
|
77
|
+
# trace.
|
|
78
|
+
|
|
79
|
+
_active_trace: ContextVar[Optional[Any]] = ContextVar(
|
|
80
|
+
"traceact_active_trace",
|
|
81
|
+
default=None,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_active_trace() -> Optional[Any]:
|
|
86
|
+
"""
|
|
87
|
+
Return whatever is currently stored in the active trace ContextVar.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
None — no active trace
|
|
91
|
+
ActionTrace — the currently running trace
|
|
92
|
+
SKIP sentinel — we are inside a sampled-out parent
|
|
93
|
+
"""
|
|
94
|
+
return _active_trace.get()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def push_trace(trace_or_skip: Any) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Set a new value as the active trace and return a token for restoring the
|
|
100
|
+
previous value later.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
trace_or_skip: Either an ActionTrace or the SKIP sentinel.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
A ContextVar Token. Pass this to pop_trace() when the trace finishes
|
|
107
|
+
to restore whatever was active before.
|
|
108
|
+
|
|
109
|
+
How the token mechanism works:
|
|
110
|
+
Python's ContextVar.set() returns a Token object that remembers the
|
|
111
|
+
previous value. Calling ContextVar.reset(token) restores that exact
|
|
112
|
+
previous value — even if something else has changed the ContextVar in
|
|
113
|
+
between. This is what makes nested traces work correctly: each level
|
|
114
|
+
saves its own token and restores independently.
|
|
115
|
+
"""
|
|
116
|
+
return _active_trace.set(trace_or_skip)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def pop_trace(token: Any) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Restore the active trace to whatever it was before the matching push_trace()
|
|
122
|
+
call.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
token: The Token returned by the corresponding push_trace() call.
|
|
126
|
+
|
|
127
|
+
This should always be called in a finally block so that the context is
|
|
128
|
+
restored even if the traced function raises an exception.
|
|
129
|
+
"""
|
|
130
|
+
_active_trace.reset(token)
|