zespan-flask 1.0.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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# Dependencies
|
|
4
|
+
node_modules
|
|
5
|
+
.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
langfuse-main/
|
|
11
|
+
langwatch-main/
|
|
12
|
+
.claude
|
|
13
|
+
.gemini
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Testing
|
|
17
|
+
coverage
|
|
18
|
+
|
|
19
|
+
# Turbo
|
|
20
|
+
.turbo
|
|
21
|
+
|
|
22
|
+
# Vercel
|
|
23
|
+
.vercel
|
|
24
|
+
|
|
25
|
+
# Wrangler (Cloudflare dev artifacts — not used in VPS deploy)
|
|
26
|
+
.wrangler/
|
|
27
|
+
|
|
28
|
+
# Build Outputs
|
|
29
|
+
.next/
|
|
30
|
+
.open-next/
|
|
31
|
+
out/
|
|
32
|
+
build
|
|
33
|
+
dist
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Debug
|
|
37
|
+
npm-debug.log*
|
|
38
|
+
yarn-debug.log*
|
|
39
|
+
yarn-error.log*
|
|
40
|
+
|
|
41
|
+
# Misc
|
|
42
|
+
.DS_Store
|
|
43
|
+
*.pem
|
|
44
|
+
|
|
45
|
+
.hypothesis
|
|
46
|
+
.kiro
|
|
47
|
+
.vscode
|
|
48
|
+
|
|
49
|
+
lumiqtrace-landing
|
|
50
|
+
# Production secrets — never commit
|
|
51
|
+
ops/cloudflared/creds.json
|
|
52
|
+
|
|
53
|
+
# Added by code-review-graph
|
|
54
|
+
.code-review-graph/
|
|
55
|
+
.gitnexus
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zespan-flask
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Zespan observability middleware for Flask
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Requires-Dist: flask>=2.0
|
|
7
|
+
Requires-Dist: requests>=2.28
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# zespan-flask
|
|
14
|
+
|
|
15
|
+
Automatic LLM observability middleware for Flask. Traces every request, LLM call, and custom span with zero code changes for basic usage.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install zespan-flask
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from flask import Flask
|
|
27
|
+
from zespan_flask import ZespanFlaskExtension, ZespanConfig
|
|
28
|
+
|
|
29
|
+
app = Flask(__name__)
|
|
30
|
+
|
|
31
|
+
config = ZespanConfig(api_key="zsp_your_api_key")
|
|
32
|
+
lumiq = ZespanFlaskExtension(app, config)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every HTTP request is now automatically traced.
|
|
36
|
+
|
|
37
|
+
## Application Factory Pattern
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
lumiq = ZespanFlaskExtension()
|
|
41
|
+
|
|
42
|
+
def create_app():
|
|
43
|
+
app = Flask(__name__)
|
|
44
|
+
lumiq.init_app(app)
|
|
45
|
+
return app
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Trace LLM Calls
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from zespan_flask import observe_llm
|
|
52
|
+
|
|
53
|
+
@observe_llm(model="gpt-4o", provider="openai")
|
|
54
|
+
def generate_response(prompt: str):
|
|
55
|
+
response = openai.chat.completions.create(
|
|
56
|
+
model="gpt-4o",
|
|
57
|
+
messages=[{"role": "user", "content": prompt}]
|
|
58
|
+
)
|
|
59
|
+
return response
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Trace Custom Spans
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from zespan_flask import observe_span
|
|
66
|
+
|
|
67
|
+
@observe_span("fetch_context", span_type="retrieval")
|
|
68
|
+
def fetch_context(query: str):
|
|
69
|
+
return vector_db.search(query)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Set Custom Attributes
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from zespan_flask import set_attribute
|
|
76
|
+
|
|
77
|
+
@app.route("/chat")
|
|
78
|
+
def chat():
|
|
79
|
+
set_attribute("user.id", current_user.id)
|
|
80
|
+
set_attribute("session.id", session.get("id"))
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Configuration
|
|
84
|
+
|
|
85
|
+
| Option | Default | Description |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `api_key` | required | Zespan API key (`zsp_...`) |
|
|
88
|
+
| `base_url` | `https://api.zespan.com` | API endpoint |
|
|
89
|
+
| `enabled` | `True` | Toggle tracing on/off |
|
|
90
|
+
| `sample_rate` | `1.0` | Fraction of requests to trace (0.0–1.0) |
|
|
91
|
+
| `capture_request_body` | `False` | Include request body in traces |
|
|
92
|
+
| `capture_response_body` | `False` | Include response body in traces |
|
|
93
|
+
| `redact_fields` | `["password","token","api_key"]` | Fields redacted automatically |
|
|
94
|
+
| `debug` | `False` | Print flush errors to stdout |
|
|
95
|
+
|
|
96
|
+
Get your API key at [zespan.com](https://zespan.com).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# zespan-flask
|
|
2
|
+
|
|
3
|
+
Automatic LLM observability middleware for Flask. Traces every request, LLM call, and custom span with zero code changes for basic usage.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install zespan-flask
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from flask import Flask
|
|
15
|
+
from zespan_flask import ZespanFlaskExtension, ZespanConfig
|
|
16
|
+
|
|
17
|
+
app = Flask(__name__)
|
|
18
|
+
|
|
19
|
+
config = ZespanConfig(api_key="zsp_your_api_key")
|
|
20
|
+
lumiq = ZespanFlaskExtension(app, config)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every HTTP request is now automatically traced.
|
|
24
|
+
|
|
25
|
+
## Application Factory Pattern
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
lumiq = ZespanFlaskExtension()
|
|
29
|
+
|
|
30
|
+
def create_app():
|
|
31
|
+
app = Flask(__name__)
|
|
32
|
+
lumiq.init_app(app)
|
|
33
|
+
return app
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Trace LLM Calls
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from zespan_flask import observe_llm
|
|
40
|
+
|
|
41
|
+
@observe_llm(model="gpt-4o", provider="openai")
|
|
42
|
+
def generate_response(prompt: str):
|
|
43
|
+
response = openai.chat.completions.create(
|
|
44
|
+
model="gpt-4o",
|
|
45
|
+
messages=[{"role": "user", "content": prompt}]
|
|
46
|
+
)
|
|
47
|
+
return response
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Trace Custom Spans
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from zespan_flask import observe_span
|
|
54
|
+
|
|
55
|
+
@observe_span("fetch_context", span_type="retrieval")
|
|
56
|
+
def fetch_context(query: str):
|
|
57
|
+
return vector_db.search(query)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Set Custom Attributes
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from zespan_flask import set_attribute
|
|
64
|
+
|
|
65
|
+
@app.route("/chat")
|
|
66
|
+
def chat():
|
|
67
|
+
set_attribute("user.id", current_user.id)
|
|
68
|
+
set_attribute("session.id", session.get("id"))
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Configuration
|
|
72
|
+
|
|
73
|
+
| Option | Default | Description |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `api_key` | required | Zespan API key (`zsp_...`) |
|
|
76
|
+
| `base_url` | `https://api.zespan.com` | API endpoint |
|
|
77
|
+
| `enabled` | `True` | Toggle tracing on/off |
|
|
78
|
+
| `sample_rate` | `1.0` | Fraction of requests to trace (0.0–1.0) |
|
|
79
|
+
| `capture_request_body` | `False` | Include request body in traces |
|
|
80
|
+
| `capture_response_body` | `False` | Include response body in traces |
|
|
81
|
+
| `redact_fields` | `["password","token","api_key"]` | Fields redacted automatically |
|
|
82
|
+
| `debug` | `False` | Print flush errors to stdout |
|
|
83
|
+
|
|
84
|
+
Get your API key at [zespan.com](https://zespan.com).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zespan/middleware-flask",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zespan trace middleware for Flask - Automatic LLM observability",
|
|
5
|
+
"main": "src/zespan_flask/__init__.py",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "echo 'Python package - no build step required'",
|
|
8
|
+
"dev": "echo 'Python package - no dev server'"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["zespan", "flask", "middleware", "llm", "observability", "tracing"],
|
|
11
|
+
"license": "MIT"
|
|
12
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zespan-flask"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Zespan observability middleware for Flask"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"flask>=2.0",
|
|
13
|
+
"requests>=2.28",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0",
|
|
19
|
+
"pytest-asyncio>=0.23",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.version]
|
|
23
|
+
path = "src/zespan_flask/__init__.py"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/zespan_flask"]
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Zespan Trace - Flask Middleware
|
|
3
|
+
Automatic Agent observability for Flask applications
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "1.0.0"
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
import json
|
|
11
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
12
|
+
from functools import wraps
|
|
13
|
+
from contextvars import ContextVar
|
|
14
|
+
|
|
15
|
+
# Context for current trace
|
|
16
|
+
current_trace: ContextVar[Optional["TraceContext"]] = ContextVar("current_trace", default=None)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TraceContext:
|
|
20
|
+
"""Context for a single trace/span"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, trace_id: str, span_id: str, name: str = "", parent_id: Optional[str] = None):
|
|
23
|
+
self.trace_id = trace_id
|
|
24
|
+
self.span_id = span_id
|
|
25
|
+
self.parent_id = parent_id
|
|
26
|
+
self.name = name
|
|
27
|
+
self.start_time = time.time()
|
|
28
|
+
self.attributes: Dict[str, Any] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ZespanConfig:
|
|
32
|
+
"""Configuration for Zespan middleware"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
api_key: str,
|
|
37
|
+
base_url: str = "https://api.zespan.com",
|
|
38
|
+
enabled: bool = True,
|
|
39
|
+
sample_rate: float = 1.0,
|
|
40
|
+
capture_request_body: bool = False,
|
|
41
|
+
capture_response_body: bool = False,
|
|
42
|
+
redact_fields: Optional[List[str]] = None,
|
|
43
|
+
debug: bool = False
|
|
44
|
+
):
|
|
45
|
+
self.api_key = api_key
|
|
46
|
+
self.base_url = base_url
|
|
47
|
+
self.enabled = enabled
|
|
48
|
+
self.sample_rate = sample_rate
|
|
49
|
+
self.capture_request_body = capture_request_body
|
|
50
|
+
self.capture_response_body = capture_response_body
|
|
51
|
+
self.redact_fields = redact_fields or ["password", "token", "api_key"]
|
|
52
|
+
self.debug = debug
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ZespanFlaskExtension:
|
|
56
|
+
"""
|
|
57
|
+
Flask extension for automatic LLM observability.
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
from flask import Flask
|
|
61
|
+
from zespan_flask import ZespanFlaskExtension, ZespanConfig
|
|
62
|
+
|
|
63
|
+
app = Flask(__name__)
|
|
64
|
+
config = ZespanConfig(api_key="your-api-key")
|
|
65
|
+
zespan = ZespanFlaskExtension(app, config)
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, app=None, config: Optional[ZespanConfig] = None):
|
|
69
|
+
self.app = app
|
|
70
|
+
self.config = config or ZespanConfig(api_key="")
|
|
71
|
+
self._queue: List[Dict[str, Any]] = []
|
|
72
|
+
self._flush_interval = 5.0
|
|
73
|
+
self._last_flush = time.time()
|
|
74
|
+
|
|
75
|
+
if app is not None:
|
|
76
|
+
self.init_app(app)
|
|
77
|
+
|
|
78
|
+
def init_app(self, app):
|
|
79
|
+
"""Initialize the extension with a Flask app"""
|
|
80
|
+
self.app = app
|
|
81
|
+
|
|
82
|
+
@app.before_request
|
|
83
|
+
def before_request():
|
|
84
|
+
if not self.config.enabled:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
trace_id = f"trace_{uuid.uuid4().hex}"
|
|
88
|
+
span_id = f"span_{uuid.uuid4().hex[:16]}"
|
|
89
|
+
|
|
90
|
+
from flask import request
|
|
91
|
+
trace_ctx = TraceContext(
|
|
92
|
+
trace_id=trace_id,
|
|
93
|
+
span_id=span_id,
|
|
94
|
+
name=f"{request.method} {request.path}",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Store start time
|
|
98
|
+
trace_ctx.attributes["http.method"] = request.method
|
|
99
|
+
trace_ctx.attributes["http.url"] = request.url
|
|
100
|
+
trace_ctx.attributes["http.route"] = request.path
|
|
101
|
+
trace_ctx.attributes["http.user_agent"] = request.headers.get("User-Agent")
|
|
102
|
+
|
|
103
|
+
current_trace.set(trace_ctx)
|
|
104
|
+
|
|
105
|
+
@app.after_request
|
|
106
|
+
def after_request(response):
|
|
107
|
+
if not self.config.enabled:
|
|
108
|
+
return response
|
|
109
|
+
|
|
110
|
+
trace_ctx = current_trace.get()
|
|
111
|
+
if not trace_ctx:
|
|
112
|
+
return response
|
|
113
|
+
|
|
114
|
+
duration_ms = (time.time() - trace_ctx.start_time) * 1000
|
|
115
|
+
|
|
116
|
+
trace_data = {
|
|
117
|
+
"trace_id": trace_ctx.trace_id,
|
|
118
|
+
"span_id": trace_ctx.span_id,
|
|
119
|
+
"name": trace_ctx.name,
|
|
120
|
+
"type": "http_request",
|
|
121
|
+
"duration_ms": duration_ms,
|
|
122
|
+
"status": "success" if response.status_code < 400 else "error",
|
|
123
|
+
"http_status": response.status_code,
|
|
124
|
+
"attributes": trace_ctx.attributes,
|
|
125
|
+
"timestamp": time.time(),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
self._queue.append(trace_data)
|
|
129
|
+
|
|
130
|
+
# Flush if needed
|
|
131
|
+
if len(self._queue) >= 10 or time.time() - self._last_flush > self._flush_interval:
|
|
132
|
+
self._flush()
|
|
133
|
+
|
|
134
|
+
return response
|
|
135
|
+
|
|
136
|
+
@app.teardown_appcontext
|
|
137
|
+
def teardown(exception):
|
|
138
|
+
# Final flush on request end
|
|
139
|
+
if self._queue:
|
|
140
|
+
self._flush()
|
|
141
|
+
|
|
142
|
+
def _flush(self):
|
|
143
|
+
"""Send queued traces to Zespan"""
|
|
144
|
+
if not self._queue:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
traces = self._queue[:]
|
|
148
|
+
self._queue = []
|
|
149
|
+
self._last_flush = time.time()
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
import requests
|
|
153
|
+
requests.post(
|
|
154
|
+
f"{self.config.base_url}/v1/ingest/batch",
|
|
155
|
+
json={"traces": traces},
|
|
156
|
+
headers={"X-API-Key": self.config.api_key},
|
|
157
|
+
timeout=5.0
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
if self.config.debug:
|
|
161
|
+
print(f"[Zespan] Failed to send traces: {e}")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def observe_span(name: str, span_type: str = "custom"):
|
|
165
|
+
"""
|
|
166
|
+
Decorator to observe a function as a span.
|
|
167
|
+
|
|
168
|
+
Usage:
|
|
169
|
+
@observe_span("my_llm_call", span_type="llm")
|
|
170
|
+
def my_llm_function(prompt: str):
|
|
171
|
+
return call_llm(prompt)
|
|
172
|
+
"""
|
|
173
|
+
def decorator(func: Callable) -> Callable:
|
|
174
|
+
@wraps(func)
|
|
175
|
+
def wrapper(*args, **kwargs):
|
|
176
|
+
trace_ctx = current_trace.get()
|
|
177
|
+
if not trace_ctx:
|
|
178
|
+
return func(*args, **kwargs)
|
|
179
|
+
|
|
180
|
+
span_id = f"span_{uuid.uuid4().hex[:16]}"
|
|
181
|
+
start = time.time()
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
result = func(*args, **kwargs)
|
|
185
|
+
duration = (time.time() - start) * 1000
|
|
186
|
+
|
|
187
|
+
# Add to parent trace
|
|
188
|
+
trace_ctx.attributes[f"span.{name}.duration_ms"] = duration
|
|
189
|
+
trace_ctx.attributes[f"span.{name}.status"] = "success"
|
|
190
|
+
|
|
191
|
+
return result
|
|
192
|
+
except Exception as e:
|
|
193
|
+
duration = (time.time() - start) * 1000
|
|
194
|
+
trace_ctx.attributes[f"span.{name}.duration_ms"] = duration
|
|
195
|
+
trace_ctx.attributes[f"span.{name}.status"] = "error"
|
|
196
|
+
trace_ctx.attributes[f"span.{name}.error"] = str(e)
|
|
197
|
+
raise
|
|
198
|
+
|
|
199
|
+
return wrapper
|
|
200
|
+
return decorator
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def observe_llm(model: Optional[str] = None, provider: Optional[str] = None):
|
|
204
|
+
"""
|
|
205
|
+
Decorator specifically for LLM calls with automatic token tracking.
|
|
206
|
+
|
|
207
|
+
Usage:
|
|
208
|
+
@observe_llm(model="gpt-4", provider="openai")
|
|
209
|
+
def generate_text(prompt: str):
|
|
210
|
+
response = openai.chat.completions.create(...)
|
|
211
|
+
return response
|
|
212
|
+
"""
|
|
213
|
+
def decorator(func: Callable) -> Callable:
|
|
214
|
+
@wraps(func)
|
|
215
|
+
def wrapper(*args, **kwargs):
|
|
216
|
+
trace_ctx = current_trace.get()
|
|
217
|
+
|
|
218
|
+
span_id = f"span_{uuid.uuid4().hex[:16]}"
|
|
219
|
+
start = time.time()
|
|
220
|
+
|
|
221
|
+
# Extract prompt from args/kwargs if possible
|
|
222
|
+
prompt = kwargs.get("prompt") or (args[0] if args else None)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
result = func(*args, **kwargs)
|
|
226
|
+
duration = (time.time() - start) * 1000
|
|
227
|
+
|
|
228
|
+
if trace_ctx:
|
|
229
|
+
trace_ctx.attributes.update({
|
|
230
|
+
"llm.model": model or "unknown",
|
|
231
|
+
"llm.provider": provider or "unknown",
|
|
232
|
+
"llm.duration_ms": duration,
|
|
233
|
+
"llm.prompt": str(prompt)[:1000] if prompt else None,
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
# Try to extract token usage from common response formats
|
|
237
|
+
if hasattr(result, "usage"):
|
|
238
|
+
usage = result.usage
|
|
239
|
+
trace_ctx.attributes["llm.tokens.input"] = getattr(usage, "prompt_tokens", None)
|
|
240
|
+
trace_ctx.attributes["llm.tokens.output"] = getattr(usage, "completion_tokens", None)
|
|
241
|
+
trace_ctx.attributes["llm.tokens.total"] = getattr(usage, "total_tokens", None)
|
|
242
|
+
elif isinstance(result, dict):
|
|
243
|
+
usage = result.get("usage", {})
|
|
244
|
+
trace_ctx.attributes["llm.tokens.input"] = usage.get("prompt_tokens")
|
|
245
|
+
trace_ctx.attributes["llm.tokens.output"] = usage.get("completion_tokens")
|
|
246
|
+
trace_ctx.attributes["llm.tokens.total"] = usage.get("total_tokens")
|
|
247
|
+
|
|
248
|
+
return result
|
|
249
|
+
except Exception as e:
|
|
250
|
+
if trace_ctx:
|
|
251
|
+
trace_ctx.attributes["llm.error"] = str(e)
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
return wrapper
|
|
255
|
+
return decorator
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def set_attribute(key: str, value: Any):
|
|
259
|
+
"""Set an attribute on the current trace context"""
|
|
260
|
+
trace_ctx = current_trace.get()
|
|
261
|
+
if trace_ctx:
|
|
262
|
+
trace_ctx.attributes[key] = value
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_current_trace_id() -> Optional[str]:
|
|
266
|
+
"""Get the current trace ID if available"""
|
|
267
|
+
trace_ctx = current_trace.get()
|
|
268
|
+
return trace_ctx.trace_id if trace_ctx else None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# Re-exports
|
|
272
|
+
__all__ = [
|
|
273
|
+
"ZespanFlaskExtension",
|
|
274
|
+
"ZespanConfig",
|
|
275
|
+
"observe_span",
|
|
276
|
+
"observe_llm",
|
|
277
|
+
"set_attribute",
|
|
278
|
+
"get_current_trace_id",
|
|
279
|
+
"TraceContext",
|
|
280
|
+
]
|