spanlens 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.
- spanlens-0.1.0/.gitignore +38 -0
- spanlens-0.1.0/CHANGELOG.md +15 -0
- spanlens-0.1.0/LICENSE +21 -0
- spanlens-0.1.0/PKG-INFO +261 -0
- spanlens-0.1.0/README.md +214 -0
- spanlens-0.1.0/pyproject.toml +113 -0
- spanlens-0.1.0/spanlens/__init__.py +48 -0
- spanlens-0.1.0/spanlens/client.py +89 -0
- spanlens-0.1.0/spanlens/integrations/__init__.py +40 -0
- spanlens-0.1.0/spanlens/integrations/anthropic.py +99 -0
- spanlens-0.1.0/spanlens/integrations/gemini.py +119 -0
- spanlens-0.1.0/spanlens/integrations/openai.py +106 -0
- spanlens-0.1.0/spanlens/observe.py +243 -0
- spanlens-0.1.0/spanlens/parsers.py +141 -0
- spanlens-0.1.0/spanlens/span.py +231 -0
- spanlens-0.1.0/spanlens/trace.py +156 -0
- spanlens-0.1.0/spanlens/transport.py +207 -0
- spanlens-0.1.0/spanlens/types.py +98 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
.next/
|
|
3
|
+
dist/
|
|
4
|
+
build/
|
|
5
|
+
.turbo/
|
|
6
|
+
*.tsbuildinfo
|
|
7
|
+
|
|
8
|
+
# env
|
|
9
|
+
.env
|
|
10
|
+
.env.local
|
|
11
|
+
.env.production
|
|
12
|
+
.env.*.local
|
|
13
|
+
|
|
14
|
+
# supabase
|
|
15
|
+
supabase/.branches/
|
|
16
|
+
supabase/.temp/
|
|
17
|
+
|
|
18
|
+
# pnpm
|
|
19
|
+
.pnpm-store/
|
|
20
|
+
|
|
21
|
+
# npm auth tokens (never commit)
|
|
22
|
+
.npmrc
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# logs
|
|
29
|
+
*.log
|
|
30
|
+
npm-debug.log*
|
|
31
|
+
|
|
32
|
+
# IDE
|
|
33
|
+
.vscode/
|
|
34
|
+
.idea/
|
|
35
|
+
*.swp
|
|
36
|
+
|
|
37
|
+
# Vercel
|
|
38
|
+
.vercel
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
Initial release of the Spanlens Python SDK.
|
|
6
|
+
|
|
7
|
+
* `SpanlensClient`, `TraceHandle`, `SpanHandle` — core tracing primitives
|
|
8
|
+
* Context-manager support so `end()` is called automatically
|
|
9
|
+
* `observe()`, `observe_openai()`, `observe_anthropic()`, `observe_gemini()`
|
|
10
|
+
— boilerplate-free helpers with auto-parsed usage
|
|
11
|
+
* `create_openai()`, `create_anthropic()`, `create_gemini()`,
|
|
12
|
+
`configure_gemini()` — proxy-mode integrations
|
|
13
|
+
* Background ingest with timeout + ordering guarantees so observability
|
|
14
|
+
never blocks user code or loses spans to race conditions
|
|
15
|
+
* Sync **and** async callables supported by every `observe*()` helper
|
spanlens-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Spanlens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
spanlens-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spanlens
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spanlens SDK — agent tracing, LLM usage capture, and cost observability for Python.
|
|
5
|
+
Project-URL: Homepage, https://spanlens.io
|
|
6
|
+
Project-URL: Documentation, https://spanlens.io/docs/sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/sunes26/Spanlens
|
|
8
|
+
Project-URL: Issues, https://github.com/sunes26/Spanlens/issues
|
|
9
|
+
Author-email: Spanlens <support@spanlens.io>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent-tracing,ai-agents,anthropic,claude,cost-tracking,gemini,helicone-alternative,langchain,langfuse-alternative,langsmith-alternative,llamaindex,llm,llm-monitoring,llm-observability,llm-tracing,observability,openai,proxy,spanlens,token-tracking,tracing
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Topic :: System :: Monitoring
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: httpx>=0.24.0
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: anthropic>=0.18.0; extra == 'all'
|
|
31
|
+
Requires-Dist: google-generativeai>=0.5.0; extra == 'all'
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == 'all'
|
|
33
|
+
Provides-Extra: anthropic
|
|
34
|
+
Requires-Dist: anthropic>=0.18.0; extra == 'anthropic'
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: mypy>=1.8.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest>=7.4.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: respx>=0.20.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
42
|
+
Provides-Extra: gemini
|
|
43
|
+
Requires-Dist: google-generativeai>=0.5.0; extra == 'gemini'
|
|
44
|
+
Provides-Extra: openai
|
|
45
|
+
Requires-Dist: openai>=1.0.0; extra == 'openai'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# Spanlens Python SDK
|
|
49
|
+
|
|
50
|
+
LLM observability for Python. Trace agent runs, capture token usage and cost,
|
|
51
|
+
and link calls back to your Spanlens dashboard with one line of code.
|
|
52
|
+
|
|
53
|
+
[](https://pypi.org/project/spanlens/)
|
|
54
|
+
[](https://opensource.org/licenses/MIT)
|
|
55
|
+
[](https://pypi.org/project/spanlens/)
|
|
56
|
+
|
|
57
|
+
> **Spanlens** is the open-source LLM observability platform. This is the
|
|
58
|
+
> official Python SDK — for the dashboard, signup, and proxy docs, head to
|
|
59
|
+
> [spanlens.io](https://spanlens.io).
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install spanlens
|
|
67
|
+
|
|
68
|
+
# Or with provider integrations:
|
|
69
|
+
pip install "spanlens[openai]"
|
|
70
|
+
pip install "spanlens[anthropic]"
|
|
71
|
+
pip install "spanlens[gemini]"
|
|
72
|
+
pip install "spanlens[all]"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Two ways to use it
|
|
76
|
+
|
|
77
|
+
| Mode | Best for | Setup |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| **Proxy** | Single-call observability — drop-in for the OpenAI/Anthropic SDK | Replace `base_url` |
|
|
80
|
+
| **SDK tracing** | Multi-step agents, RAG, tool calls, manual spans | `SpanlensClient(...)` |
|
|
81
|
+
|
|
82
|
+
You can mix both. The proxy logs the raw request; the SDK groups multiple
|
|
83
|
+
requests into a single trace with parent / child spans.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Mode 1 — Proxy (zero-code)
|
|
88
|
+
|
|
89
|
+
Get a Spanlens API key from your dashboard, then point your provider SDK at
|
|
90
|
+
the Spanlens proxy:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import os
|
|
94
|
+
from spanlens.integrations.openai import create_openai
|
|
95
|
+
|
|
96
|
+
# Reads SPANLENS_API_KEY from the environment
|
|
97
|
+
client = create_openai()
|
|
98
|
+
|
|
99
|
+
response = client.chat.completions.create(
|
|
100
|
+
model="gpt-4o-mini",
|
|
101
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Spanlens automatically logs the request, response, latency, token counts,
|
|
106
|
+
and cost — viewable in the dashboard under **Requests**.
|
|
107
|
+
|
|
108
|
+
### Tagging requests with a prompt version
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from spanlens.integrations.openai import create_openai, with_prompt_version
|
|
112
|
+
|
|
113
|
+
client = create_openai()
|
|
114
|
+
res = client.chat.completions.create(
|
|
115
|
+
model="gpt-4o-mini",
|
|
116
|
+
messages=[...],
|
|
117
|
+
**with_prompt_version("chatbot-system@3"),
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The same pattern works for Anthropic — see
|
|
122
|
+
[`spanlens.integrations.anthropic`](./spanlens/integrations/anthropic.py).
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Mode 2 — SDK tracing (multi-step agents)
|
|
127
|
+
|
|
128
|
+
Use the SDK when one user request spans multiple LLM calls, retrieval, tool
|
|
129
|
+
use, etc. Spans appear nested under a single trace in the dashboard.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from spanlens import SpanlensClient
|
|
133
|
+
|
|
134
|
+
client = SpanlensClient(api_key="sl_live_...")
|
|
135
|
+
|
|
136
|
+
with client.start_trace("rag_pipeline", metadata={"user_id": "u_42"}) as trace:
|
|
137
|
+
with trace.span("retrieve", span_type="retrieval") as span:
|
|
138
|
+
docs = vector_store.similarity_search(query, k=5)
|
|
139
|
+
span.end(output={"doc_count": len(docs)})
|
|
140
|
+
|
|
141
|
+
with trace.span("generate", span_type="llm") as span:
|
|
142
|
+
response = openai_client.chat.completions.create(
|
|
143
|
+
model="gpt-4o-mini",
|
|
144
|
+
messages=build_prompt(query, docs),
|
|
145
|
+
extra_headers=span.trace_headers(), # links proxy log to this span
|
|
146
|
+
)
|
|
147
|
+
usage = response.usage
|
|
148
|
+
span.end(
|
|
149
|
+
output=response.choices[0].message.content,
|
|
150
|
+
prompt_tokens=usage.prompt_tokens,
|
|
151
|
+
completion_tokens=usage.completion_tokens,
|
|
152
|
+
total_tokens=usage.total_tokens,
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
When a span / trace context manager exits with an exception, the span is
|
|
157
|
+
automatically marked `error` with the exception message.
|
|
158
|
+
|
|
159
|
+
### Helper: `observe_openai`
|
|
160
|
+
|
|
161
|
+
Boilerplate-free version of the LLM span — auto-injects trace headers,
|
|
162
|
+
auto-parses `usage`, and auto-ends the span:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from spanlens import observe_openai
|
|
166
|
+
|
|
167
|
+
result = observe_openai(trace, "answer", lambda headers:
|
|
168
|
+
openai_client.chat.completions.create(
|
|
169
|
+
model="gpt-4o-mini",
|
|
170
|
+
messages=messages,
|
|
171
|
+
extra_headers=headers,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The same shape exists for Anthropic (`observe_anthropic`) and Gemini
|
|
177
|
+
(`observe_gemini`).
|
|
178
|
+
|
|
179
|
+
### Async support
|
|
180
|
+
|
|
181
|
+
`observe()` and `observe_*()` detect coroutines automatically. Pass an async
|
|
182
|
+
callable and `await` the result:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
async def go():
|
|
186
|
+
result = await observe_openai(trace, "answer", lambda h:
|
|
187
|
+
async_openai.chat.completions.create(..., extra_headers=h),
|
|
188
|
+
)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Configuration reference
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
SpanlensClient(
|
|
197
|
+
api_key="sl_live_...", # required
|
|
198
|
+
base_url=None, # default: https://spanlens-server.vercel.app
|
|
199
|
+
timeout_ms=3000, # ingest timeout per call
|
|
200
|
+
silent=True, # swallow errors so observability never crashes user code
|
|
201
|
+
on_error=None, # callback (err, context) for non-silent monitoring
|
|
202
|
+
)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Environment variables:
|
|
206
|
+
|
|
207
|
+
* `SPANLENS_API_KEY` — picked up by `create_openai()`, `create_anthropic()`,
|
|
208
|
+
`create_gemini()` when `api_key=` is omitted.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Why the SDK is non-blocking
|
|
213
|
+
|
|
214
|
+
Every `trace.end()` / `span.end()` call returns immediately. Network I/O
|
|
215
|
+
runs on a background thread pool with a configurable timeout, so:
|
|
216
|
+
|
|
217
|
+
* Your hot path (the LLM call itself) is never slowed down.
|
|
218
|
+
* The Spanlens server being slow / down does not crash your app.
|
|
219
|
+
* Order is still preserved: a span POST always waits for its parent trace
|
|
220
|
+
POST to finish — the server's ownership check would otherwise 404 and the
|
|
221
|
+
span would be silently lost.
|
|
222
|
+
|
|
223
|
+
For short-lived scripts, call `client.close()` before exit (or use
|
|
224
|
+
`with SpanlensClient(...) as client:`) to drain the queue.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Compatibility
|
|
229
|
+
|
|
230
|
+
* Python 3.9, 3.10, 3.11, 3.12, 3.13
|
|
231
|
+
* `openai` >= 1.0
|
|
232
|
+
* `anthropic` >= 0.18
|
|
233
|
+
* `google-generativeai` >= 0.5
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Self-hosting
|
|
238
|
+
|
|
239
|
+
Point the SDK and proxy helpers at your own deployment:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
client = SpanlensClient(
|
|
243
|
+
api_key="...",
|
|
244
|
+
base_url="https://spanlens.mycompany.com",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
openai = create_openai(base_url="https://spanlens.mycompany.com/proxy/openai/v1")
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT — see [LICENSE](./LICENSE).
|
|
255
|
+
|
|
256
|
+
## Links
|
|
257
|
+
|
|
258
|
+
* [Spanlens dashboard](https://spanlens.io)
|
|
259
|
+
* [Proxy docs](https://spanlens.io/docs/proxy)
|
|
260
|
+
* [TypeScript SDK](https://www.npmjs.com/package/@spanlens/sdk)
|
|
261
|
+
* [GitHub](https://github.com/sunes26/Spanlens)
|
spanlens-0.1.0/README.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Spanlens Python SDK
|
|
2
|
+
|
|
3
|
+
LLM observability for Python. Trace agent runs, capture token usage and cost,
|
|
4
|
+
and link calls back to your Spanlens dashboard with one line of code.
|
|
5
|
+
|
|
6
|
+
[](https://pypi.org/project/spanlens/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
[](https://pypi.org/project/spanlens/)
|
|
9
|
+
|
|
10
|
+
> **Spanlens** is the open-source LLM observability platform. This is the
|
|
11
|
+
> official Python SDK — for the dashboard, signup, and proxy docs, head to
|
|
12
|
+
> [spanlens.io](https://spanlens.io).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install spanlens
|
|
20
|
+
|
|
21
|
+
# Or with provider integrations:
|
|
22
|
+
pip install "spanlens[openai]"
|
|
23
|
+
pip install "spanlens[anthropic]"
|
|
24
|
+
pip install "spanlens[gemini]"
|
|
25
|
+
pip install "spanlens[all]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Two ways to use it
|
|
29
|
+
|
|
30
|
+
| Mode | Best for | Setup |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| **Proxy** | Single-call observability — drop-in for the OpenAI/Anthropic SDK | Replace `base_url` |
|
|
33
|
+
| **SDK tracing** | Multi-step agents, RAG, tool calls, manual spans | `SpanlensClient(...)` |
|
|
34
|
+
|
|
35
|
+
You can mix both. The proxy logs the raw request; the SDK groups multiple
|
|
36
|
+
requests into a single trace with parent / child spans.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Mode 1 — Proxy (zero-code)
|
|
41
|
+
|
|
42
|
+
Get a Spanlens API key from your dashboard, then point your provider SDK at
|
|
43
|
+
the Spanlens proxy:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import os
|
|
47
|
+
from spanlens.integrations.openai import create_openai
|
|
48
|
+
|
|
49
|
+
# Reads SPANLENS_API_KEY from the environment
|
|
50
|
+
client = create_openai()
|
|
51
|
+
|
|
52
|
+
response = client.chat.completions.create(
|
|
53
|
+
model="gpt-4o-mini",
|
|
54
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
55
|
+
)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Spanlens automatically logs the request, response, latency, token counts,
|
|
59
|
+
and cost — viewable in the dashboard under **Requests**.
|
|
60
|
+
|
|
61
|
+
### Tagging requests with a prompt version
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from spanlens.integrations.openai import create_openai, with_prompt_version
|
|
65
|
+
|
|
66
|
+
client = create_openai()
|
|
67
|
+
res = client.chat.completions.create(
|
|
68
|
+
model="gpt-4o-mini",
|
|
69
|
+
messages=[...],
|
|
70
|
+
**with_prompt_version("chatbot-system@3"),
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The same pattern works for Anthropic — see
|
|
75
|
+
[`spanlens.integrations.anthropic`](./spanlens/integrations/anthropic.py).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Mode 2 — SDK tracing (multi-step agents)
|
|
80
|
+
|
|
81
|
+
Use the SDK when one user request spans multiple LLM calls, retrieval, tool
|
|
82
|
+
use, etc. Spans appear nested under a single trace in the dashboard.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from spanlens import SpanlensClient
|
|
86
|
+
|
|
87
|
+
client = SpanlensClient(api_key="sl_live_...")
|
|
88
|
+
|
|
89
|
+
with client.start_trace("rag_pipeline", metadata={"user_id": "u_42"}) as trace:
|
|
90
|
+
with trace.span("retrieve", span_type="retrieval") as span:
|
|
91
|
+
docs = vector_store.similarity_search(query, k=5)
|
|
92
|
+
span.end(output={"doc_count": len(docs)})
|
|
93
|
+
|
|
94
|
+
with trace.span("generate", span_type="llm") as span:
|
|
95
|
+
response = openai_client.chat.completions.create(
|
|
96
|
+
model="gpt-4o-mini",
|
|
97
|
+
messages=build_prompt(query, docs),
|
|
98
|
+
extra_headers=span.trace_headers(), # links proxy log to this span
|
|
99
|
+
)
|
|
100
|
+
usage = response.usage
|
|
101
|
+
span.end(
|
|
102
|
+
output=response.choices[0].message.content,
|
|
103
|
+
prompt_tokens=usage.prompt_tokens,
|
|
104
|
+
completion_tokens=usage.completion_tokens,
|
|
105
|
+
total_tokens=usage.total_tokens,
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
When a span / trace context manager exits with an exception, the span is
|
|
110
|
+
automatically marked `error` with the exception message.
|
|
111
|
+
|
|
112
|
+
### Helper: `observe_openai`
|
|
113
|
+
|
|
114
|
+
Boilerplate-free version of the LLM span — auto-injects trace headers,
|
|
115
|
+
auto-parses `usage`, and auto-ends the span:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from spanlens import observe_openai
|
|
119
|
+
|
|
120
|
+
result = observe_openai(trace, "answer", lambda headers:
|
|
121
|
+
openai_client.chat.completions.create(
|
|
122
|
+
model="gpt-4o-mini",
|
|
123
|
+
messages=messages,
|
|
124
|
+
extra_headers=headers,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The same shape exists for Anthropic (`observe_anthropic`) and Gemini
|
|
130
|
+
(`observe_gemini`).
|
|
131
|
+
|
|
132
|
+
### Async support
|
|
133
|
+
|
|
134
|
+
`observe()` and `observe_*()` detect coroutines automatically. Pass an async
|
|
135
|
+
callable and `await` the result:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
async def go():
|
|
139
|
+
result = await observe_openai(trace, "answer", lambda h:
|
|
140
|
+
async_openai.chat.completions.create(..., extra_headers=h),
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Configuration reference
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
SpanlensClient(
|
|
150
|
+
api_key="sl_live_...", # required
|
|
151
|
+
base_url=None, # default: https://spanlens-server.vercel.app
|
|
152
|
+
timeout_ms=3000, # ingest timeout per call
|
|
153
|
+
silent=True, # swallow errors so observability never crashes user code
|
|
154
|
+
on_error=None, # callback (err, context) for non-silent monitoring
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Environment variables:
|
|
159
|
+
|
|
160
|
+
* `SPANLENS_API_KEY` — picked up by `create_openai()`, `create_anthropic()`,
|
|
161
|
+
`create_gemini()` when `api_key=` is omitted.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Why the SDK is non-blocking
|
|
166
|
+
|
|
167
|
+
Every `trace.end()` / `span.end()` call returns immediately. Network I/O
|
|
168
|
+
runs on a background thread pool with a configurable timeout, so:
|
|
169
|
+
|
|
170
|
+
* Your hot path (the LLM call itself) is never slowed down.
|
|
171
|
+
* The Spanlens server being slow / down does not crash your app.
|
|
172
|
+
* Order is still preserved: a span POST always waits for its parent trace
|
|
173
|
+
POST to finish — the server's ownership check would otherwise 404 and the
|
|
174
|
+
span would be silently lost.
|
|
175
|
+
|
|
176
|
+
For short-lived scripts, call `client.close()` before exit (or use
|
|
177
|
+
`with SpanlensClient(...) as client:`) to drain the queue.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Compatibility
|
|
182
|
+
|
|
183
|
+
* Python 3.9, 3.10, 3.11, 3.12, 3.13
|
|
184
|
+
* `openai` >= 1.0
|
|
185
|
+
* `anthropic` >= 0.18
|
|
186
|
+
* `google-generativeai` >= 0.5
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Self-hosting
|
|
191
|
+
|
|
192
|
+
Point the SDK and proxy helpers at your own deployment:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
client = SpanlensClient(
|
|
196
|
+
api_key="...",
|
|
197
|
+
base_url="https://spanlens.mycompany.com",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
openai = create_openai(base_url="https://spanlens.mycompany.com/proxy/openai/v1")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## License
|
|
206
|
+
|
|
207
|
+
MIT — see [LICENSE](./LICENSE).
|
|
208
|
+
|
|
209
|
+
## Links
|
|
210
|
+
|
|
211
|
+
* [Spanlens dashboard](https://spanlens.io)
|
|
212
|
+
* [Proxy docs](https://spanlens.io/docs/proxy)
|
|
213
|
+
* [TypeScript SDK](https://www.npmjs.com/package/@spanlens/sdk)
|
|
214
|
+
* [GitHub](https://github.com/sunes26/Spanlens)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spanlens"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Spanlens SDK — agent tracing, LLM usage capture, and cost observability for Python."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Spanlens", email = "support@spanlens.io" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"llm",
|
|
15
|
+
"llm-observability",
|
|
16
|
+
"llm-monitoring",
|
|
17
|
+
"llm-tracing",
|
|
18
|
+
"openai",
|
|
19
|
+
"anthropic",
|
|
20
|
+
"claude",
|
|
21
|
+
"gemini",
|
|
22
|
+
"observability",
|
|
23
|
+
"tracing",
|
|
24
|
+
"agent-tracing",
|
|
25
|
+
"ai-agents",
|
|
26
|
+
"cost-tracking",
|
|
27
|
+
"token-tracking",
|
|
28
|
+
"langchain",
|
|
29
|
+
"llamaindex",
|
|
30
|
+
"helicone-alternative",
|
|
31
|
+
"langfuse-alternative",
|
|
32
|
+
"langsmith-alternative",
|
|
33
|
+
"proxy",
|
|
34
|
+
"spanlens",
|
|
35
|
+
]
|
|
36
|
+
classifiers = [
|
|
37
|
+
"Development Status :: 4 - Beta",
|
|
38
|
+
"Intended Audience :: Developers",
|
|
39
|
+
"License :: OSI Approved :: MIT License",
|
|
40
|
+
"Operating System :: OS Independent",
|
|
41
|
+
"Programming Language :: Python",
|
|
42
|
+
"Programming Language :: Python :: 3",
|
|
43
|
+
"Programming Language :: Python :: 3.9",
|
|
44
|
+
"Programming Language :: Python :: 3.10",
|
|
45
|
+
"Programming Language :: Python :: 3.11",
|
|
46
|
+
"Programming Language :: Python :: 3.12",
|
|
47
|
+
"Programming Language :: Python :: 3.13",
|
|
48
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
49
|
+
"Topic :: System :: Monitoring",
|
|
50
|
+
"Typing :: Typed",
|
|
51
|
+
]
|
|
52
|
+
dependencies = [
|
|
53
|
+
"httpx>=0.24.0",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.optional-dependencies]
|
|
57
|
+
openai = ["openai>=1.0.0"]
|
|
58
|
+
anthropic = ["anthropic>=0.18.0"]
|
|
59
|
+
gemini = ["google-generativeai>=0.5.0"]
|
|
60
|
+
all = [
|
|
61
|
+
"openai>=1.0.0",
|
|
62
|
+
"anthropic>=0.18.0",
|
|
63
|
+
"google-generativeai>=0.5.0",
|
|
64
|
+
]
|
|
65
|
+
dev = [
|
|
66
|
+
"pytest>=7.4.0",
|
|
67
|
+
"pytest-asyncio>=0.23.0",
|
|
68
|
+
"pytest-mock>=3.12.0",
|
|
69
|
+
"respx>=0.20.0",
|
|
70
|
+
"ruff>=0.4.0",
|
|
71
|
+
"mypy>=1.8.0",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[project.urls]
|
|
75
|
+
Homepage = "https://spanlens.io"
|
|
76
|
+
Documentation = "https://spanlens.io/docs/sdk"
|
|
77
|
+
Repository = "https://github.com/sunes26/Spanlens"
|
|
78
|
+
Issues = "https://github.com/sunes26/Spanlens/issues"
|
|
79
|
+
|
|
80
|
+
[tool.hatch.build.targets.wheel]
|
|
81
|
+
packages = ["spanlens"]
|
|
82
|
+
|
|
83
|
+
[tool.hatch.build.targets.sdist]
|
|
84
|
+
include = [
|
|
85
|
+
"spanlens",
|
|
86
|
+
"README.md",
|
|
87
|
+
"LICENSE",
|
|
88
|
+
"CHANGELOG.md",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[tool.pytest.ini_options]
|
|
92
|
+
testpaths = ["tests"]
|
|
93
|
+
asyncio_mode = "auto"
|
|
94
|
+
filterwarnings = ["error::DeprecationWarning"]
|
|
95
|
+
|
|
96
|
+
[tool.ruff]
|
|
97
|
+
line-length = 100
|
|
98
|
+
target-version = "py39"
|
|
99
|
+
|
|
100
|
+
[tool.ruff.lint]
|
|
101
|
+
select = ["E", "F", "I", "B", "UP", "N"]
|
|
102
|
+
# UP006/UP007/UP035/UP045 push PEP 604 / PEP 585 syntax that we deliberately
|
|
103
|
+
# avoid: while ``from __future__ import annotations`` makes ``list[int]`` and
|
|
104
|
+
# ``X | None`` legal at parse time on Python 3.9, runtime introspection
|
|
105
|
+
# (``typing.get_type_hints``) still breaks. Stick with ``Optional[X]`` /
|
|
106
|
+
# ``List[X]`` until the minimum supported version is 3.10.
|
|
107
|
+
ignore = ["E501", "UP006", "UP007", "UP035", "UP045"]
|
|
108
|
+
|
|
109
|
+
[tool.mypy]
|
|
110
|
+
python_version = "3.9"
|
|
111
|
+
strict = true
|
|
112
|
+
warn_return_any = true
|
|
113
|
+
warn_unused_configs = true
|