agentlens-py 0.1.0__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.
- agentlens_py-0.1.0/.gitignore +170 -0
- agentlens_py-0.1.0/PKG-INFO +11 -0
- agentlens_py-0.1.0/README.md +120 -0
- agentlens_py-0.1.0/agentlens/__init__.py +62 -0
- agentlens_py-0.1.0/agentlens/client.py +137 -0
- agentlens_py-0.1.0/agentlens/context.py +30 -0
- agentlens_py-0.1.0/agentlens/decorators.py +208 -0
- agentlens_py-0.1.0/agentlens/instruments/anthropic.py +359 -0
- agentlens_py-0.1.0/agentlens/instruments/openai.py +367 -0
- agentlens_py-0.1.0/agentlens/pricing.py +37 -0
- agentlens_py-0.1.0/pyproject.toml +27 -0
- agentlens_py-0.1.0/tests/__init__.py +0 -0
- agentlens_py-0.1.0/tests/integration/.gitkeep +0 -0
- agentlens_py-0.1.0/tests/integration/__init__.py +0 -0
- agentlens_py-0.1.0/tests/property/__init__.py +0 -0
- agentlens_py-0.1.0/tests/property/test_pricing_properties.py +46 -0
- agentlens_py-0.1.0/tests/property/test_sdk_properties.py +382 -0
- agentlens_py-0.1.0/tests/unit/__init__.py +0 -0
- agentlens_py-0.1.0/tests/unit/test_queue.py +47 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Python
|
|
3
|
+
# ============================================================
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
*.so
|
|
8
|
+
*.egg
|
|
9
|
+
*.egg-info/
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
eggs/
|
|
13
|
+
parts/
|
|
14
|
+
var/
|
|
15
|
+
sdist/
|
|
16
|
+
develop-eggs/
|
|
17
|
+
.installed.cfg
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
.eggs/
|
|
21
|
+
|
|
22
|
+
# Virtual environments
|
|
23
|
+
.venv/
|
|
24
|
+
venv/
|
|
25
|
+
ENV/
|
|
26
|
+
env/
|
|
27
|
+
.env
|
|
28
|
+
.env.local
|
|
29
|
+
.env.*.local
|
|
30
|
+
|
|
31
|
+
# pytest / coverage
|
|
32
|
+
.pytest_cache/
|
|
33
|
+
.coverage
|
|
34
|
+
.coverage.*
|
|
35
|
+
htmlcov/
|
|
36
|
+
coverage.xml
|
|
37
|
+
*.cover
|
|
38
|
+
.hypothesis/
|
|
39
|
+
|
|
40
|
+
# mypy / pyright
|
|
41
|
+
.mypy_cache/
|
|
42
|
+
.dmypy.json
|
|
43
|
+
dmypy.json
|
|
44
|
+
.pyright/
|
|
45
|
+
|
|
46
|
+
# ruff / flake8
|
|
47
|
+
.ruff_cache/
|
|
48
|
+
|
|
49
|
+
# Distribution / packaging
|
|
50
|
+
MANIFEST
|
|
51
|
+
|
|
52
|
+
# ============================================================
|
|
53
|
+
# Node / Next.js
|
|
54
|
+
# ============================================================
|
|
55
|
+
node_modules/
|
|
56
|
+
.next/
|
|
57
|
+
out/
|
|
58
|
+
.nuxt/
|
|
59
|
+
.cache/
|
|
60
|
+
.parcel-cache/
|
|
61
|
+
.turbo/
|
|
62
|
+
|
|
63
|
+
# Build output
|
|
64
|
+
dist/
|
|
65
|
+
build/
|
|
66
|
+
|
|
67
|
+
# Logs
|
|
68
|
+
npm-debug.log*
|
|
69
|
+
yarn-debug.log*
|
|
70
|
+
yarn-error.log*
|
|
71
|
+
pnpm-debug.log*
|
|
72
|
+
lerna-debug.log*
|
|
73
|
+
|
|
74
|
+
# Runtime data
|
|
75
|
+
pids/
|
|
76
|
+
*.pid
|
|
77
|
+
*.seed
|
|
78
|
+
*.pid.lock
|
|
79
|
+
|
|
80
|
+
# TypeScript
|
|
81
|
+
*.tsbuildinfo
|
|
82
|
+
next-env.d.ts
|
|
83
|
+
|
|
84
|
+
# ============================================================
|
|
85
|
+
# Docker
|
|
86
|
+
# ============================================================
|
|
87
|
+
.dockerignore
|
|
88
|
+
|
|
89
|
+
# ============================================================
|
|
90
|
+
# OS / Editor
|
|
91
|
+
# ============================================================
|
|
92
|
+
.DS_Store
|
|
93
|
+
Thumbs.db
|
|
94
|
+
*.swp
|
|
95
|
+
*.swo
|
|
96
|
+
*~
|
|
97
|
+
.idea/
|
|
98
|
+
.vscode/
|
|
99
|
+
*.sublime-project
|
|
100
|
+
*.sublime-workspace
|
|
101
|
+
|
|
102
|
+
# ============================================================
|
|
103
|
+
# Secrets / credentials (never commit these)
|
|
104
|
+
# ============================================================
|
|
105
|
+
.env
|
|
106
|
+
.env.local
|
|
107
|
+
.env.development.local
|
|
108
|
+
.env.test.local
|
|
109
|
+
.env.production.local
|
|
110
|
+
*.pem
|
|
111
|
+
*.key
|
|
112
|
+
secrets/
|
|
113
|
+
|
|
114
|
+
# ============================================================
|
|
115
|
+
# Alembic
|
|
116
|
+
# ============================================================
|
|
117
|
+
# Keep migrations tracked; ignore only generated artefacts
|
|
118
|
+
apps/api/migrations/__pycache__/
|
|
119
|
+
apps/api/migrations/versions/__pycache__/
|
|
120
|
+
|
|
121
|
+
# ============================================================
|
|
122
|
+
# Misc
|
|
123
|
+
# ============================================================
|
|
124
|
+
*.log
|
|
125
|
+
*.tmp
|
|
126
|
+
*.bak
|
|
127
|
+
|
|
128
|
+
# ============================================================
|
|
129
|
+
# Pre-launch release cleanup additions
|
|
130
|
+
# ============================================================
|
|
131
|
+
.Python
|
|
132
|
+
downloads/
|
|
133
|
+
wheels/
|
|
134
|
+
*.pyo
|
|
135
|
+
.env/
|
|
136
|
+
apps/api/.env
|
|
137
|
+
apps/web/.env
|
|
138
|
+
apps/web/.env.local
|
|
139
|
+
secrets.txt
|
|
140
|
+
apps/web/.next/
|
|
141
|
+
apps/web/out/
|
|
142
|
+
apps/web/node_modules/
|
|
143
|
+
*.code-workspace
|
|
144
|
+
*.tar
|
|
145
|
+
logs/
|
|
146
|
+
scratch_test.py
|
|
147
|
+
ws_test_delay.py
|
|
148
|
+
test_trace.py
|
|
149
|
+
|
|
150
|
+
# Internal planning documents
|
|
151
|
+
ANTIGRAVITY_PROMPT.md
|
|
152
|
+
agentlens_project_handover_summary.md
|
|
153
|
+
*_handover_*.md
|
|
154
|
+
|
|
155
|
+
# Manual test scripts
|
|
156
|
+
test_agent.py
|
|
157
|
+
verify_dashboard_data.py
|
|
158
|
+
test_trace.py
|
|
159
|
+
|
|
160
|
+
# Kiro AI IDE specs (internal)
|
|
161
|
+
.kiro/
|
|
162
|
+
|
|
163
|
+
# Editor
|
|
164
|
+
.vscode/
|
|
165
|
+
|
|
166
|
+
# Hypothesis test data
|
|
167
|
+
apps/api/.hypothesis/
|
|
168
|
+
apps/api/tmp/
|
|
169
|
+
apps/api/unicode_data/
|
|
170
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentlens-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AgentLens Python SDK — zero-overhead observability for AI agents
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: httpx==0.28.1
|
|
7
|
+
Requires-Dist: structlog==25.5.0
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: hypothesis==6.152.9; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest-asyncio==1.3.0; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest==9.0.3; extra == 'dev'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# AgentLens Python SDK
|
|
2
|
+
|
|
3
|
+
The official Python client for AgentLens. Easily trace agent execution steps, nested tool calls, and LLM completions with non-blocking, asynchronous ingestion.
|
|
4
|
+
|
|
5
|
+
## Key Features
|
|
6
|
+
|
|
7
|
+
- **Micro-Batched Background Ingestion**: Uses a thread-safe daemon worker (`SpanQueue` wrapping a safe `queue.Queue`) to buffer and flush spans concurrently without delaying agent execution.
|
|
8
|
+
- **Zero-Dependency Exception Guard**: The SDK never propagates internal networking or API exceptions, ensuring your production agent runs remain robust.
|
|
9
|
+
- **Automatic Cost & Token Estimation**: Immediate calculations of token consumption and pricing for popular OpenAI and Anthropic models.
|
|
10
|
+
- **Auto-Instrumentation**: Monkey-patches OpenAI and Anthropic clients to capture inputs, outputs, tokens, and streaming responses out-of-the-box.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Install the package via `pip` (or `uv` / `poetry`):
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install agentlens-py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> [!NOTE]
|
|
23
|
+
> Note that the package is installed via `agentlens-py`, but the Python import name remains `import agentlens as al`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick Start (2-Line Integration)
|
|
28
|
+
|
|
29
|
+
Instrument your application to capture all OpenAI completions automatically:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import agentlens as al
|
|
33
|
+
|
|
34
|
+
# Initialize the global client and instrument the OpenAI SDK
|
|
35
|
+
al.init(api_key="al_live_YOUR_API_KEY_HERE", base_url="http://localhost:8000")
|
|
36
|
+
al.instrument_openai()
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Detailed Usage
|
|
42
|
+
|
|
43
|
+
### Decorators & Context Managers
|
|
44
|
+
|
|
45
|
+
Trace custom Python functions or specific logical blocks inside your agents:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import agentlens as al
|
|
49
|
+
|
|
50
|
+
# 1. Trace a high-level agent loop
|
|
51
|
+
@al.trace(name="Customer Assistant Agent")
|
|
52
|
+
def run_assistant(user_query: str):
|
|
53
|
+
print("Agent started...")
|
|
54
|
+
|
|
55
|
+
# 2. Trace tool calls or sub-steps using context managers
|
|
56
|
+
with al.span(name="Database Lookup", span_type="tool") as s:
|
|
57
|
+
s.set_metadata("query", user_query)
|
|
58
|
+
result = db_search(user_query)
|
|
59
|
+
s.set_output({"results_count": len(result)})
|
|
60
|
+
|
|
61
|
+
return result
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Auto-Instrumentation Examples
|
|
65
|
+
|
|
66
|
+
#### OpenAI (Both Streaming & Non-Streaming)
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import openai
|
|
70
|
+
import agentlens as al
|
|
71
|
+
|
|
72
|
+
al.init(api_key="al_live_...")
|
|
73
|
+
al.instrument_openai()
|
|
74
|
+
|
|
75
|
+
client = openai.OpenAI()
|
|
76
|
+
|
|
77
|
+
# Spans are automatically created for completions, including token counts and costs
|
|
78
|
+
response = client.chat.completions.create(
|
|
79
|
+
model="gpt-4o",
|
|
80
|
+
messages=[{"role": "user", "content": "Explain quantizing neural networks."}]
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
#### Anthropic (Both Streaming & Non-Streaming)
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
import anthropic
|
|
88
|
+
import agentlens as al
|
|
89
|
+
|
|
90
|
+
al.init(api_key="al_live_...")
|
|
91
|
+
al.instrument_anthropic()
|
|
92
|
+
|
|
93
|
+
client = anthropic.Anthropic()
|
|
94
|
+
|
|
95
|
+
response = client.messages.create(
|
|
96
|
+
model="claude-3-5-sonnet-20241022",
|
|
97
|
+
max_tokens=1000,
|
|
98
|
+
messages=[{"role": "user", "content": "What is reinforcement learning?"}]
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Public API Reference
|
|
105
|
+
|
|
106
|
+
### `al.init(api_key: str, base_url: str = "http://localhost:8000", debug: bool = False, enabled: bool = True) -> None`
|
|
107
|
+
Configures the global AgentLens client, spins up the thread-safe background ingestion worker, and registers an `atexit` hook to flush remaining spans on application termination.
|
|
108
|
+
- `api_key`: Your active AgentLens project API key (prefixed with `al_live_`).
|
|
109
|
+
- `base_url`: Endpoint of your FastAPI backend.
|
|
110
|
+
- `debug`: Enable internal logging for SDK debugging.
|
|
111
|
+
- `enabled`: If set to `False`, all tracing operations, monkey-patches, and decorators gracefully fallback to no-ops.
|
|
112
|
+
|
|
113
|
+
### `al.trace(name: str | None = None, tags: list[str] | None = None)`
|
|
114
|
+
Decorator to declare a new root trace context. Propagates the trace context down the execution tree.
|
|
115
|
+
|
|
116
|
+
### `al.span(name: str, span_type: str = "custom")`
|
|
117
|
+
Context manager to record a logical sub-step inside an active trace context. Supports updating metadata and final outputs.
|
|
118
|
+
|
|
119
|
+
### `al.flush(timeout: float = 5.0) -> None`
|
|
120
|
+
Manually blocks and flushes any enqueued spans to the API backend up to the specified timeout. Excellent for short-running serverless function environments.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""AgentLens Python SDK — zero-overhead observability for AI agents.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import atexit
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from agentlens.client import AgentLensClient
|
|
11
|
+
from agentlens.decorators import span, trace
|
|
12
|
+
from agentlens.instruments.anthropic import instrument_anthropic
|
|
13
|
+
from agentlens.instruments.openai import instrument_openai
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"init",
|
|
17
|
+
"instrument_openai",
|
|
18
|
+
"instrument_anthropic",
|
|
19
|
+
"trace",
|
|
20
|
+
"span",
|
|
21
|
+
"flush",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
_global_client: Optional[AgentLensClient] = None
|
|
25
|
+
logger = logging.getLogger("agentlens")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def init(
|
|
29
|
+
api_key: str,
|
|
30
|
+
base_url: str = "http://localhost:8000",
|
|
31
|
+
debug: bool = False,
|
|
32
|
+
enabled: bool = True,
|
|
33
|
+
) -> AgentLensClient:
|
|
34
|
+
"""Initialize the global AgentLens client."""
|
|
35
|
+
global _global_client
|
|
36
|
+
|
|
37
|
+
if _global_client is not None:
|
|
38
|
+
_global_client.stop()
|
|
39
|
+
|
|
40
|
+
_global_client = AgentLensClient(
|
|
41
|
+
api_key=api_key,
|
|
42
|
+
base_url=base_url,
|
|
43
|
+
debug=debug,
|
|
44
|
+
enabled=enabled,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Register exit handler to flush queue on shutdown
|
|
48
|
+
if enabled:
|
|
49
|
+
atexit.register(flush)
|
|
50
|
+
|
|
51
|
+
return _global_client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def flush(timeout: float = 5.0) -> None:
|
|
55
|
+
"""Flush all currently buffered spans in the global client."""
|
|
56
|
+
global _global_client
|
|
57
|
+
if _global_client is not None and _global_client.enabled:
|
|
58
|
+
try:
|
|
59
|
+
_global_client.flush(timeout=timeout)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
if _global_client.debug:
|
|
62
|
+
logger.debug(f"Failed to flush spans: {e}", exc_info=True)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""AgentLens SDK Client for background batch span ingestion.
|
|
2
|
+
"""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import atexit
|
|
7
|
+
import logging
|
|
8
|
+
import queue
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger("agentlens")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AgentLensClient:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str,
|
|
22
|
+
base_url: str = "http://localhost:8000",
|
|
23
|
+
debug: bool = False,
|
|
24
|
+
enabled: bool = True,
|
|
25
|
+
):
|
|
26
|
+
self.api_key = api_key
|
|
27
|
+
self.base_url = base_url.rstrip("/")
|
|
28
|
+
self.debug = debug
|
|
29
|
+
self.enabled = enabled
|
|
30
|
+
|
|
31
|
+
self._queue: queue.Queue[Dict[str, Any]] = queue.Queue(maxsize=10000)
|
|
32
|
+
self._stop_event = threading.Event()
|
|
33
|
+
self._worker_thread: Optional[threading.Thread] = None
|
|
34
|
+
|
|
35
|
+
if self.enabled:
|
|
36
|
+
self.start()
|
|
37
|
+
|
|
38
|
+
def start(self):
|
|
39
|
+
if self._worker_thread is not None and self._worker_thread.is_alive():
|
|
40
|
+
return
|
|
41
|
+
self._stop_event.clear()
|
|
42
|
+
self._worker_thread = threading.Thread(target=self._worker, daemon=True)
|
|
43
|
+
self._worker_thread.start()
|
|
44
|
+
|
|
45
|
+
def enqueue(self, item: Dict[str, Any]):
|
|
46
|
+
if not self.enabled:
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
self._queue.put_nowait(item)
|
|
50
|
+
except queue.Full:
|
|
51
|
+
logger.warning("AgentLens queue full. Draining one item to make space.")
|
|
52
|
+
try:
|
|
53
|
+
self._queue.get_nowait()
|
|
54
|
+
self._queue.task_done()
|
|
55
|
+
except (queue.Empty, ValueError):
|
|
56
|
+
pass
|
|
57
|
+
try:
|
|
58
|
+
self._queue.put_nowait(item)
|
|
59
|
+
except queue.Full:
|
|
60
|
+
pass # Fail-safe
|
|
61
|
+
|
|
62
|
+
def _safe_enqueue(self, item: Dict[str, Any]):
|
|
63
|
+
try:
|
|
64
|
+
self.enqueue(item)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
if self.debug:
|
|
67
|
+
logger.debug(f"Failed to enqueue span: {e}", exc_info=True)
|
|
68
|
+
|
|
69
|
+
def _send_batch(self, batch: List[Dict[str, Any]]):
|
|
70
|
+
if not batch:
|
|
71
|
+
return
|
|
72
|
+
try:
|
|
73
|
+
url = f"{self.base_url}/v1/ingest"
|
|
74
|
+
headers = {
|
|
75
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
}
|
|
78
|
+
payload = {"spans": batch}
|
|
79
|
+
|
|
80
|
+
# Sync request since we are on background worker thread
|
|
81
|
+
with httpx.Client(timeout=10.0) as client:
|
|
82
|
+
resp = client.post(url, json=payload, headers=headers)
|
|
83
|
+
if resp.status_code != 200:
|
|
84
|
+
if self.debug:
|
|
85
|
+
logger.debug(
|
|
86
|
+
f"Ingest failed with status {resp.status_code}: {resp.text}"
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
if self.debug:
|
|
90
|
+
logger.debug(f"Error sending batch to AgentLens: {e}", exc_info=True)
|
|
91
|
+
|
|
92
|
+
def _worker(self):
|
|
93
|
+
while not self._stop_event.is_set():
|
|
94
|
+
batch = []
|
|
95
|
+
start_time = time.time()
|
|
96
|
+
while len(batch) < 100:
|
|
97
|
+
elapsed = time.time() - start_time
|
|
98
|
+
if elapsed >= 0.5:
|
|
99
|
+
break
|
|
100
|
+
timeout = 0.5 - elapsed
|
|
101
|
+
try:
|
|
102
|
+
item = self._queue.get(timeout=timeout)
|
|
103
|
+
batch.append(item)
|
|
104
|
+
except queue.Empty:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
if batch:
|
|
108
|
+
self._send_batch(batch)
|
|
109
|
+
for _ in range(len(batch)):
|
|
110
|
+
self._queue.task_done()
|
|
111
|
+
else:
|
|
112
|
+
time.sleep(0.05)
|
|
113
|
+
|
|
114
|
+
def flush(self, timeout: float = 5.0):
|
|
115
|
+
start_time = time.time()
|
|
116
|
+
while not self._queue.empty():
|
|
117
|
+
if time.time() - start_time > timeout:
|
|
118
|
+
break
|
|
119
|
+
batch = []
|
|
120
|
+
while len(batch) < 100:
|
|
121
|
+
try:
|
|
122
|
+
item = self._queue.get_nowait()
|
|
123
|
+
batch.append(item)
|
|
124
|
+
except queue.Empty:
|
|
125
|
+
break
|
|
126
|
+
if batch:
|
|
127
|
+
self._send_batch(batch)
|
|
128
|
+
for _ in range(len(batch)):
|
|
129
|
+
self._queue.task_done()
|
|
130
|
+
else:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
def stop(self, timeout: float = 5.0):
|
|
134
|
+
self._stop_event.set()
|
|
135
|
+
self.flush(timeout=timeout)
|
|
136
|
+
if self._worker_thread is not None:
|
|
137
|
+
self._worker_thread.join(timeout=timeout)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""ContextVar-based trace/span ID propagation.
|
|
2
|
+
|
|
3
|
+
These vars are set by the @trace decorator and span() context manager so that
|
|
4
|
+
nested calls automatically inherit the current trace and span context without
|
|
5
|
+
any explicit passing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from contextvars import ContextVar
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
_current_trace_id: ContextVar[Optional[str]] = ContextVar("_current_trace_id", default=None)
|
|
14
|
+
_current_span_id: ContextVar[Optional[str]] = ContextVar("_current_span_id", default=None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_trace_id() -> Optional[str]:
|
|
18
|
+
return _current_trace_id.get()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_span_id() -> Optional[str]:
|
|
22
|
+
return _current_span_id.get()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def set_trace_id(value: Optional[str]):
|
|
26
|
+
return _current_trace_id.set(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_span_id(value: Optional[str]):
|
|
30
|
+
return _current_span_id.set(value)
|