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
+ ]