sigil-telemetry 0.2.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.
- sigil_telemetry-0.2.0/PKG-INFO +234 -0
- sigil_telemetry-0.2.0/README.md +182 -0
- sigil_telemetry-0.2.0/pyproject.toml +60 -0
- sigil_telemetry-0.2.0/setup.cfg +4 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry/__init__.py +24 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry/core.py +539 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry.egg-info/PKG-INFO +234 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry.egg-info/SOURCES.txt +9 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry.egg-info/dependency_links.txt +1 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry.egg-info/requires.txt +57 -0
- sigil_telemetry-0.2.0/src/sigil_telemetry.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sigil-telemetry
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Universal AI agent telemetry for Sigil — auto-instruments any LLM SDK and exports to the Sigil collector.
|
|
5
|
+
Author: Zurain Khan
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
10
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
11
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
12
|
+
Requires-Dist: opentelemetry-semantic-conventions>=0.41b0
|
|
13
|
+
Provides-Extra: anthropic
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.30.0; extra == "anthropic"
|
|
15
|
+
Provides-Extra: openai
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.30.0; extra == "openai"
|
|
17
|
+
Provides-Extra: langchain
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.30.0; extra == "langchain"
|
|
19
|
+
Provides-Extra: crewai
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.30.0; extra == "crewai"
|
|
21
|
+
Provides-Extra: llamaindex
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.30.0; extra == "llamaindex"
|
|
23
|
+
Provides-Extra: vertexai
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-vertexai>=0.30.0; extra == "vertexai"
|
|
25
|
+
Provides-Extra: mistral
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-mistralai>=0.30.0; extra == "mistral"
|
|
27
|
+
Provides-Extra: bedrock
|
|
28
|
+
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.30.0; extra == "bedrock"
|
|
29
|
+
Provides-Extra: litellm
|
|
30
|
+
Requires-Dist: openinference-instrumentation-litellm>=0.1.0; extra == "litellm"
|
|
31
|
+
Provides-Extra: fastapi
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0; extra == "fastapi"
|
|
33
|
+
Provides-Extra: flask
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.41b0; extra == "flask"
|
|
35
|
+
Provides-Extra: django
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == "django"
|
|
37
|
+
Provides-Extra: databricks
|
|
38
|
+
Requires-Dist: databricks-sql-connector>=4.0.0; extra == "databricks"
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.30.0; extra == "all"
|
|
41
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.30.0; extra == "all"
|
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.30.0; extra == "all"
|
|
43
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.30.0; extra == "all"
|
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.30.0; extra == "all"
|
|
45
|
+
Requires-Dist: opentelemetry-instrumentation-vertexai>=0.30.0; extra == "all"
|
|
46
|
+
Requires-Dist: opentelemetry-instrumentation-mistralai>=0.30.0; extra == "all"
|
|
47
|
+
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.30.0; extra == "all"
|
|
48
|
+
Requires-Dist: openinference-instrumentation-litellm>=0.1.0; extra == "all"
|
|
49
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0; extra == "all"
|
|
50
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.41b0; extra == "all"
|
|
51
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == "all"
|
|
52
|
+
|
|
53
|
+
# sigil-telemetry
|
|
54
|
+
|
|
55
|
+
Plug-and-play telemetry for AI agents. Install it, call `init()`, and every LLM call your agent makes is automatically tracked in Sigil.
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install sigil-telemetry[all]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from sigil_telemetry import init
|
|
65
|
+
init()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Set your agent's ID and collector endpoint
|
|
70
|
+
SIGIL_AGENT_ID=sigil-agent-your-agent-slug
|
|
71
|
+
SIGIL_COLLECTOR_URL=https://your-collector-endpoint/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it. Every LLM API call is now captured — tokens, model, latency, errors — and sent to the Sigil collector.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## What's New in v0.2.0
|
|
79
|
+
|
|
80
|
+
- **Web framework auto-instrumentation** — FastAPI, Flask, and Django are auto-detected and instrumented. All LLM calls within one HTTP request share a single `trace_id` (operation_Id), so you can count agent "runs" with `COUNT(DISTINCT trace_id)`.
|
|
81
|
+
- **Noise span filtering** — `http send` / `http send body` spans from web frameworks are silently dropped before they leave the process. They never reach your collector, so you're not billed for them.
|
|
82
|
+
- **Health check exclusion** — Routes like `/health`, `/healthz`, `/ready`, `/alive`, `/ping` are excluded from tracing entirely. No spans generated, no storage cost.
|
|
83
|
+
- **Graceful shutdown** — `atexit` handler flushes all pending spans when the process exits, so you never lose the last batch.
|
|
84
|
+
- **Lighter install** — Removed unnecessary dependencies from the core install.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Full Example: What Actually Happens
|
|
89
|
+
|
|
90
|
+
Here's a real agent that summarizes documents using Claude. Let's walk through exactly what the telemetry captures and where it ends up.
|
|
91
|
+
|
|
92
|
+
### 1. The Agent Code
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# document_summarizer.py
|
|
96
|
+
import anthropic
|
|
97
|
+
from sigil_telemetry import init
|
|
98
|
+
|
|
99
|
+
# Initialize telemetry — call this ONCE at startup
|
|
100
|
+
init()
|
|
101
|
+
|
|
102
|
+
# Your normal agent code — no changes needed
|
|
103
|
+
client = anthropic.Anthropic()
|
|
104
|
+
response = client.messages.create(
|
|
105
|
+
model="claude-sonnet-4-20250514",
|
|
106
|
+
max_tokens=1024,
|
|
107
|
+
messages=[
|
|
108
|
+
{"role": "user", "content": "Summarize this document: ..."}
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
print(response.content[0].text)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. What Gets Captured (Per LLM Call)
|
|
115
|
+
|
|
116
|
+
Every time `client.messages.create()` runs, a **span** is automatically created with:
|
|
117
|
+
|
|
118
|
+
| Field | Example Value | Description |
|
|
119
|
+
|-------|--------------|-------------|
|
|
120
|
+
| `operation_Id` | `a1b2c3d4e5f6...` | Trace ID — groups all LLM calls in a single agent run |
|
|
121
|
+
| `sigil.agent.id` | `sigil-agent-doc-summarizer` | Which agent made the call |
|
|
122
|
+
| `sigil.agent.version` | `sha-abc1234` | Agent version (set by deploy workflow) |
|
|
123
|
+
| `sigil.agent.frameworks` | `Anthropic,FastAPI` | Which SDKs and frameworks were detected |
|
|
124
|
+
| `gen_ai.system` | `anthropic` | LLM provider |
|
|
125
|
+
| `gen_ai.request.model` | `claude-sonnet-4-20250514` | Model used |
|
|
126
|
+
| `gen_ai.usage.input_tokens` | `1250` | Tokens sent |
|
|
127
|
+
| `gen_ai.usage.output_tokens` | `340` | Tokens received |
|
|
128
|
+
| `duration` | `2.3s` | How long the call took |
|
|
129
|
+
| `status` | `OK` or `ERROR` | Whether the call succeeded |
|
|
130
|
+
| `sigil.environment` | `production` | Environment |
|
|
131
|
+
| `sigil.agent.division` | `Sales` | Business division (if set) |
|
|
132
|
+
| `sigil.agent.risk_classification` | `low` | Risk level (if set) |
|
|
133
|
+
|
|
134
|
+
If the agent makes **multiple LLM calls** in one run (e.g., calls Claude then GPT-4), all calls share the same `operation_Id` so you can see the full trace.
|
|
135
|
+
|
|
136
|
+
### 3. How Trace Grouping Works
|
|
137
|
+
|
|
138
|
+
**API agents (FastAPI/Flask/Django):** The web framework instrumentor creates a root span per HTTP request. All LLM calls within that request automatically become child spans sharing the same `trace_id`. You don't need to do anything — `init()` handles it.
|
|
139
|
+
|
|
140
|
+
**Worker agents (scheduled jobs, listeners):** The template wraps your `main()` function in a root span. All LLM calls within one job or message share the same `trace_id`.
|
|
141
|
+
|
|
142
|
+
In both cases: `COUNT(DISTINCT trace_id)` = number of agent runs.
|
|
143
|
+
|
|
144
|
+
### 4. Where the Data Goes
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Agent makes LLM call
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
sigil-telemetry auto-captures it as an OpenTelemetry span
|
|
151
|
+
(noise spans like "http send" are filtered out here)
|
|
152
|
+
│
|
|
153
|
+
▼
|
|
154
|
+
Span is batched and sent via OTLP to:
|
|
155
|
+
→ Your configured collector endpoint
|
|
156
|
+
│
|
|
157
|
+
▼
|
|
158
|
+
Collector forwards to:
|
|
159
|
+
→ Your observability backend (Jaeger, Zipkin, Datadog, etc.)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Supported SDKs
|
|
165
|
+
|
|
166
|
+
Use `[all]` to install everything. Only the SDKs your agent actually uses get activated.
|
|
167
|
+
|
|
168
|
+
| SDK | Install Extra | What It Covers |
|
|
169
|
+
|-----|--------------|----------------|
|
|
170
|
+
| Anthropic | `[anthropic]` | Anthropic API |
|
|
171
|
+
| OpenAI | `[openai]` | OpenAI API (including compatible endpoints) |
|
|
172
|
+
| LangChain | `[langchain]` | LangChain, LangGraph, any LangChain-wrapped model |
|
|
173
|
+
| CrewAI | `[crewai]` | CrewAI multi-agent framework |
|
|
174
|
+
| LlamaIndex | `[llamaindex]` | LlamaIndex agents and pipelines |
|
|
175
|
+
| Vertex AI | `[vertexai]` | Google Vertex AI, Gemini models |
|
|
176
|
+
| Mistral AI | `[mistral]` | Mistral API |
|
|
177
|
+
| AWS Bedrock | `[bedrock]` | Claude, Llama, Titan via AWS |
|
|
178
|
+
| LiteLLM | `[litellm]` | Unified proxy across 100+ LLM providers |
|
|
179
|
+
|
|
180
|
+
## Web Framework Auto-Instrumentation
|
|
181
|
+
|
|
182
|
+
These are included in `[all]` and auto-detected by `init()`:
|
|
183
|
+
|
|
184
|
+
| Framework | Install Extra | What It Does |
|
|
185
|
+
|-----------|--------------|--------------|
|
|
186
|
+
| FastAPI | `[fastapi]` | Creates root span per HTTP request — all LLM calls in that request share one trace_id |
|
|
187
|
+
| Flask | `[flask]` | Same trace grouping for Flask apps |
|
|
188
|
+
| Django | `[django]` | Same trace grouping for Django apps |
|
|
189
|
+
|
|
190
|
+
Health check routes (`/health`, `/healthz`, `/ready`, `/alive`, `/ping`, `/startup`, `/liveness`, `/readiness`) are automatically excluded from tracing.
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
| Env Variable | Default | Description |
|
|
195
|
+
|-------------|---------|-------------|
|
|
196
|
+
| `SIGIL_AGENT_ID` | — | **Required.** Your agent's Sigil ID |
|
|
197
|
+
| `SIGIL_AGENT_VERSION` | `0.1.0` | Track deployments (set automatically by deploy workflow) |
|
|
198
|
+
| `SIGIL_COLLECTOR_URL` | — | **Required.** Your collector endpoint URL |
|
|
199
|
+
| `SIGIL_ENVIRONMENT` | `production` | `production`, `staging`, `development` |
|
|
200
|
+
| `SIGIL_CONSOLE_EXPORT` | `false` | Print spans to console for debugging |
|
|
201
|
+
| `SIGIL_DIVISION` | — | Business division (e.g., `Sales`, `Engineering`) |
|
|
202
|
+
| `SIGIL_RISK_CLASSIFICATION` | — | Agent risk level (`low`, `medium`, `high`) |
|
|
203
|
+
| `SIGIL_HOURS_SAVED` | — | Estimated hours saved per run |
|
|
204
|
+
|
|
205
|
+
Or pass config in code:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from sigil_telemetry import init, SigilConfig
|
|
209
|
+
|
|
210
|
+
init(SigilConfig(
|
|
211
|
+
agent_id="sigil-agent-my-agent",
|
|
212
|
+
environment="development",
|
|
213
|
+
console_export=True
|
|
214
|
+
))
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Custom Spans
|
|
218
|
+
|
|
219
|
+
Track things beyond LLM calls (document parsing, tool use, etc.):
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from sigil_telemetry import get_tracer, record_error
|
|
223
|
+
|
|
224
|
+
tracer = get_tracer()
|
|
225
|
+
|
|
226
|
+
with tracer.start_as_current_span("parse-contract") as span:
|
|
227
|
+
span.set_attribute("document.pages", 42)
|
|
228
|
+
try:
|
|
229
|
+
result = parse_pdf(file)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
record_error(span, e)
|
|
232
|
+
raise
|
|
233
|
+
```
|
|
234
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# sigil-telemetry
|
|
2
|
+
|
|
3
|
+
Plug-and-play telemetry for AI agents. Install it, call `init()`, and every LLM call your agent makes is automatically tracked in Sigil.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install sigil-telemetry[all]
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from sigil_telemetry import init
|
|
13
|
+
init()
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Set your agent's ID and collector endpoint
|
|
18
|
+
SIGIL_AGENT_ID=sigil-agent-your-agent-slug
|
|
19
|
+
SIGIL_COLLECTOR_URL=https://your-collector-endpoint/
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
That's it. Every LLM API call is now captured — tokens, model, latency, errors — and sent to the Sigil collector.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## What's New in v0.2.0
|
|
27
|
+
|
|
28
|
+
- **Web framework auto-instrumentation** — FastAPI, Flask, and Django are auto-detected and instrumented. All LLM calls within one HTTP request share a single `trace_id` (operation_Id), so you can count agent "runs" with `COUNT(DISTINCT trace_id)`.
|
|
29
|
+
- **Noise span filtering** — `http send` / `http send body` spans from web frameworks are silently dropped before they leave the process. They never reach your collector, so you're not billed for them.
|
|
30
|
+
- **Health check exclusion** — Routes like `/health`, `/healthz`, `/ready`, `/alive`, `/ping` are excluded from tracing entirely. No spans generated, no storage cost.
|
|
31
|
+
- **Graceful shutdown** — `atexit` handler flushes all pending spans when the process exits, so you never lose the last batch.
|
|
32
|
+
- **Lighter install** — Removed unnecessary dependencies from the core install.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Full Example: What Actually Happens
|
|
37
|
+
|
|
38
|
+
Here's a real agent that summarizes documents using Claude. Let's walk through exactly what the telemetry captures and where it ends up.
|
|
39
|
+
|
|
40
|
+
### 1. The Agent Code
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
# document_summarizer.py
|
|
44
|
+
import anthropic
|
|
45
|
+
from sigil_telemetry import init
|
|
46
|
+
|
|
47
|
+
# Initialize telemetry — call this ONCE at startup
|
|
48
|
+
init()
|
|
49
|
+
|
|
50
|
+
# Your normal agent code — no changes needed
|
|
51
|
+
client = anthropic.Anthropic()
|
|
52
|
+
response = client.messages.create(
|
|
53
|
+
model="claude-sonnet-4-20250514",
|
|
54
|
+
max_tokens=1024,
|
|
55
|
+
messages=[
|
|
56
|
+
{"role": "user", "content": "Summarize this document: ..."}
|
|
57
|
+
]
|
|
58
|
+
)
|
|
59
|
+
print(response.content[0].text)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. What Gets Captured (Per LLM Call)
|
|
63
|
+
|
|
64
|
+
Every time `client.messages.create()` runs, a **span** is automatically created with:
|
|
65
|
+
|
|
66
|
+
| Field | Example Value | Description |
|
|
67
|
+
|-------|--------------|-------------|
|
|
68
|
+
| `operation_Id` | `a1b2c3d4e5f6...` | Trace ID — groups all LLM calls in a single agent run |
|
|
69
|
+
| `sigil.agent.id` | `sigil-agent-doc-summarizer` | Which agent made the call |
|
|
70
|
+
| `sigil.agent.version` | `sha-abc1234` | Agent version (set by deploy workflow) |
|
|
71
|
+
| `sigil.agent.frameworks` | `Anthropic,FastAPI` | Which SDKs and frameworks were detected |
|
|
72
|
+
| `gen_ai.system` | `anthropic` | LLM provider |
|
|
73
|
+
| `gen_ai.request.model` | `claude-sonnet-4-20250514` | Model used |
|
|
74
|
+
| `gen_ai.usage.input_tokens` | `1250` | Tokens sent |
|
|
75
|
+
| `gen_ai.usage.output_tokens` | `340` | Tokens received |
|
|
76
|
+
| `duration` | `2.3s` | How long the call took |
|
|
77
|
+
| `status` | `OK` or `ERROR` | Whether the call succeeded |
|
|
78
|
+
| `sigil.environment` | `production` | Environment |
|
|
79
|
+
| `sigil.agent.division` | `Sales` | Business division (if set) |
|
|
80
|
+
| `sigil.agent.risk_classification` | `low` | Risk level (if set) |
|
|
81
|
+
|
|
82
|
+
If the agent makes **multiple LLM calls** in one run (e.g., calls Claude then GPT-4), all calls share the same `operation_Id` so you can see the full trace.
|
|
83
|
+
|
|
84
|
+
### 3. How Trace Grouping Works
|
|
85
|
+
|
|
86
|
+
**API agents (FastAPI/Flask/Django):** The web framework instrumentor creates a root span per HTTP request. All LLM calls within that request automatically become child spans sharing the same `trace_id`. You don't need to do anything — `init()` handles it.
|
|
87
|
+
|
|
88
|
+
**Worker agents (scheduled jobs, listeners):** The template wraps your `main()` function in a root span. All LLM calls within one job or message share the same `trace_id`.
|
|
89
|
+
|
|
90
|
+
In both cases: `COUNT(DISTINCT trace_id)` = number of agent runs.
|
|
91
|
+
|
|
92
|
+
### 4. Where the Data Goes
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Agent makes LLM call
|
|
96
|
+
│
|
|
97
|
+
▼
|
|
98
|
+
sigil-telemetry auto-captures it as an OpenTelemetry span
|
|
99
|
+
(noise spans like "http send" are filtered out here)
|
|
100
|
+
│
|
|
101
|
+
▼
|
|
102
|
+
Span is batched and sent via OTLP to:
|
|
103
|
+
→ Your configured collector endpoint
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
Collector forwards to:
|
|
107
|
+
→ Your observability backend (Jaeger, Zipkin, Datadog, etc.)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Supported SDKs
|
|
113
|
+
|
|
114
|
+
Use `[all]` to install everything. Only the SDKs your agent actually uses get activated.
|
|
115
|
+
|
|
116
|
+
| SDK | Install Extra | What It Covers |
|
|
117
|
+
|-----|--------------|----------------|
|
|
118
|
+
| Anthropic | `[anthropic]` | Anthropic API |
|
|
119
|
+
| OpenAI | `[openai]` | OpenAI API (including compatible endpoints) |
|
|
120
|
+
| LangChain | `[langchain]` | LangChain, LangGraph, any LangChain-wrapped model |
|
|
121
|
+
| CrewAI | `[crewai]` | CrewAI multi-agent framework |
|
|
122
|
+
| LlamaIndex | `[llamaindex]` | LlamaIndex agents and pipelines |
|
|
123
|
+
| Vertex AI | `[vertexai]` | Google Vertex AI, Gemini models |
|
|
124
|
+
| Mistral AI | `[mistral]` | Mistral API |
|
|
125
|
+
| AWS Bedrock | `[bedrock]` | Claude, Llama, Titan via AWS |
|
|
126
|
+
| LiteLLM | `[litellm]` | Unified proxy across 100+ LLM providers |
|
|
127
|
+
|
|
128
|
+
## Web Framework Auto-Instrumentation
|
|
129
|
+
|
|
130
|
+
These are included in `[all]` and auto-detected by `init()`:
|
|
131
|
+
|
|
132
|
+
| Framework | Install Extra | What It Does |
|
|
133
|
+
|-----------|--------------|--------------|
|
|
134
|
+
| FastAPI | `[fastapi]` | Creates root span per HTTP request — all LLM calls in that request share one trace_id |
|
|
135
|
+
| Flask | `[flask]` | Same trace grouping for Flask apps |
|
|
136
|
+
| Django | `[django]` | Same trace grouping for Django apps |
|
|
137
|
+
|
|
138
|
+
Health check routes (`/health`, `/healthz`, `/ready`, `/alive`, `/ping`, `/startup`, `/liveness`, `/readiness`) are automatically excluded from tracing.
|
|
139
|
+
|
|
140
|
+
## Configuration
|
|
141
|
+
|
|
142
|
+
| Env Variable | Default | Description |
|
|
143
|
+
|-------------|---------|-------------|
|
|
144
|
+
| `SIGIL_AGENT_ID` | — | **Required.** Your agent's Sigil ID |
|
|
145
|
+
| `SIGIL_AGENT_VERSION` | `0.1.0` | Track deployments (set automatically by deploy workflow) |
|
|
146
|
+
| `SIGIL_COLLECTOR_URL` | — | **Required.** Your collector endpoint URL |
|
|
147
|
+
| `SIGIL_ENVIRONMENT` | `production` | `production`, `staging`, `development` |
|
|
148
|
+
| `SIGIL_CONSOLE_EXPORT` | `false` | Print spans to console for debugging |
|
|
149
|
+
| `SIGIL_DIVISION` | — | Business division (e.g., `Sales`, `Engineering`) |
|
|
150
|
+
| `SIGIL_RISK_CLASSIFICATION` | — | Agent risk level (`low`, `medium`, `high`) |
|
|
151
|
+
| `SIGIL_HOURS_SAVED` | — | Estimated hours saved per run |
|
|
152
|
+
|
|
153
|
+
Or pass config in code:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from sigil_telemetry import init, SigilConfig
|
|
157
|
+
|
|
158
|
+
init(SigilConfig(
|
|
159
|
+
agent_id="sigil-agent-my-agent",
|
|
160
|
+
environment="development",
|
|
161
|
+
console_export=True
|
|
162
|
+
))
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Custom Spans
|
|
166
|
+
|
|
167
|
+
Track things beyond LLM calls (document parsing, tool use, etc.):
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from sigil_telemetry import get_tracer, record_error
|
|
171
|
+
|
|
172
|
+
tracer = get_tracer()
|
|
173
|
+
|
|
174
|
+
with tracer.start_as_current_span("parse-contract") as span:
|
|
175
|
+
span.set_attribute("document.pages", 42)
|
|
176
|
+
try:
|
|
177
|
+
result = parse_pdf(file)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
record_error(span, e)
|
|
180
|
+
raise
|
|
181
|
+
```
|
|
182
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sigil-telemetry"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Universal AI agent telemetry for Sigil — auto-instruments any LLM SDK and exports to the Sigil collector."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Zurain Khan"}
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
# Core dependencies — always installed
|
|
17
|
+
dependencies = [
|
|
18
|
+
"opentelemetry-api>=1.20.0",
|
|
19
|
+
"opentelemetry-sdk>=1.20.0",
|
|
20
|
+
"opentelemetry-exporter-otlp-proto-http>=1.20.0",
|
|
21
|
+
"opentelemetry-semantic-conventions>=0.41b0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
# Install only the instrumentors you need
|
|
26
|
+
anthropic = ["opentelemetry-instrumentation-anthropic>=0.30.0"]
|
|
27
|
+
openai = ["opentelemetry-instrumentation-openai>=0.30.0"]
|
|
28
|
+
langchain = ["opentelemetry-instrumentation-langchain>=0.30.0"]
|
|
29
|
+
crewai = ["opentelemetry-instrumentation-crewai>=0.30.0"]
|
|
30
|
+
llamaindex = ["opentelemetry-instrumentation-llamaindex>=0.30.0"]
|
|
31
|
+
vertexai = ["opentelemetry-instrumentation-vertexai>=0.30.0"]
|
|
32
|
+
mistral = ["opentelemetry-instrumentation-mistralai>=0.30.0"]
|
|
33
|
+
bedrock = ["opentelemetry-instrumentation-bedrock>=0.30.0"]
|
|
34
|
+
litellm = ["openinference-instrumentation-litellm>=0.1.0"]
|
|
35
|
+
# Web framework auto-instrumentation (for trace grouping)
|
|
36
|
+
fastapi = ["opentelemetry-instrumentation-fastapi>=0.41b0"]
|
|
37
|
+
flask = ["opentelemetry-instrumentation-flask>=0.41b0"]
|
|
38
|
+
django = ["opentelemetry-instrumentation-django>=0.41b0"]
|
|
39
|
+
|
|
40
|
+
# Databricks ingestion — only needed in the notebook, not in agents
|
|
41
|
+
databricks = ["databricks-sql-connector>=4.0.0"]
|
|
42
|
+
|
|
43
|
+
# Install all LLM instrumentors + web framework instrumentors
|
|
44
|
+
all = [
|
|
45
|
+
"opentelemetry-instrumentation-anthropic>=0.30.0",
|
|
46
|
+
"opentelemetry-instrumentation-openai>=0.30.0",
|
|
47
|
+
"opentelemetry-instrumentation-langchain>=0.30.0",
|
|
48
|
+
"opentelemetry-instrumentation-crewai>=0.30.0",
|
|
49
|
+
"opentelemetry-instrumentation-llamaindex>=0.30.0",
|
|
50
|
+
"opentelemetry-instrumentation-vertexai>=0.30.0",
|
|
51
|
+
"opentelemetry-instrumentation-mistralai>=0.30.0",
|
|
52
|
+
"opentelemetry-instrumentation-bedrock>=0.30.0",
|
|
53
|
+
"openinference-instrumentation-litellm>=0.1.0",
|
|
54
|
+
"opentelemetry-instrumentation-fastapi>=0.41b0",
|
|
55
|
+
"opentelemetry-instrumentation-flask>=0.41b0",
|
|
56
|
+
"opentelemetry-instrumentation-django>=0.41b0",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.setuptools.packages.find]
|
|
60
|
+
where = ["src"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sigil Telemetry — Universal AI Agent Telemetry Package
|
|
3
|
+
======================================================
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from sigil_telemetry import init
|
|
7
|
+
init()
|
|
8
|
+
|
|
9
|
+
That's it. The package auto-detects installed LLM SDKs,
|
|
10
|
+
instruments them, and exports telemetry to the Sigil collector.
|
|
11
|
+
|
|
12
|
+
Environment Variables:
|
|
13
|
+
SIGIL_AGENT_ID — Required. Your agent's unique ID (e.g. sigil-agent-my-agent)
|
|
14
|
+
SIGIL_AGENT_VERSION — Optional. Agent version for tracking deployments (default: "0.1.0")
|
|
15
|
+
SIGIL_COLLECTOR_URL — Required. Your collector endpoint URL
|
|
16
|
+
SIGIL_ENVIRONMENT — Optional. "production", "staging", "development" (default: "production")
|
|
17
|
+
SIGIL_CONSOLE_EXPORT — Optional. Set "true" to also print spans to console (for debugging)
|
|
18
|
+
OTEL_SERVICE_NAME — Optional. Override service name (defaults to SIGIL_AGENT_ID)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from sigil_telemetry.core import init, SigilConfig, get_tracer, record_error
|
|
22
|
+
|
|
23
|
+
__version__ = "0.2.0"
|
|
24
|
+
__all__ = ["init", "SigilConfig", "get_tracer", "record_error"]
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sigil Telemetry Core — auto-detect, instrument, and export.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Anthropic SDK
|
|
6
|
+
- OpenAI SDK (including compatible endpoints)
|
|
7
|
+
- LangChain / LangGraph
|
|
8
|
+
- CrewAI
|
|
9
|
+
- LlamaIndex
|
|
10
|
+
- Google Vertex AI / Gemini
|
|
11
|
+
- Mistral AI
|
|
12
|
+
- AWS Bedrock
|
|
13
|
+
- LiteLLM (unified LLM proxy)
|
|
14
|
+
- FastAPI / Flask auto-instrumentation (trace grouping)
|
|
15
|
+
- Multi-model runs (all calls share one trace/operation_Id)
|
|
16
|
+
- Model failures (captured as error spans with exception details)
|
|
17
|
+
- Custom span attributes for Sigil-specific metadata
|
|
18
|
+
- Noise span filtering (http send spans dropped before export)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import atexit
|
|
24
|
+
import logging
|
|
25
|
+
import importlib
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Optional
|
|
28
|
+
|
|
29
|
+
from opentelemetry import trace
|
|
30
|
+
from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
|
|
31
|
+
from opentelemetry.sdk.trace.export import (
|
|
32
|
+
BatchSpanProcessor,
|
|
33
|
+
ConsoleSpanExporter,
|
|
34
|
+
SpanExportResult,
|
|
35
|
+
)
|
|
36
|
+
from opentelemetry.sdk.resources import Resource
|
|
37
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
38
|
+
from opentelemetry.trace import StatusCode
|
|
39
|
+
|
|
40
|
+
logger = logging.getLogger("sigil_telemetry")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ──────────────────────────────────────────────
|
|
44
|
+
# Default collector endpoint — set via SIGIL_COLLECTOR_URL env var
|
|
45
|
+
# ──────────────────────────────────────────────
|
|
46
|
+
DEFAULT_COLLECTOR_URL = None
|
|
47
|
+
|
|
48
|
+
# ──────────────────────────────────────────────
|
|
49
|
+
# Noise span names to drop before export.
|
|
50
|
+
# FastAPI/Flask create these on every response —
|
|
51
|
+
# they carry no LLM data and pollute the collector.
|
|
52
|
+
# ──────────────────────────────────────────────
|
|
53
|
+
_NOISE_SPAN_NAMES = frozenset({
|
|
54
|
+
"http send",
|
|
55
|
+
"http send body",
|
|
56
|
+
"http receive",
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
# ──────────────────────────────────────────────
|
|
60
|
+
# Health check paths to exclude from tracing.
|
|
61
|
+
# These never need telemetry — they only add
|
|
62
|
+
# cost to the collector backend.
|
|
63
|
+
# ──────────────────────────────────────────────
|
|
64
|
+
_HEALTH_CHECK_PATHS = "health,healthz,ready,alive,ping,startup,liveness,readiness"
|
|
65
|
+
|
|
66
|
+
# ──────────────────────────────────────────────
|
|
67
|
+
# Supported LLM SDK instrumentors
|
|
68
|
+
# ──────────────────────────────────────────────
|
|
69
|
+
INSTRUMENTORS = [
|
|
70
|
+
{
|
|
71
|
+
"name": "Anthropic",
|
|
72
|
+
"sdk_module": "anthropic",
|
|
73
|
+
"instrumentor_module": "opentelemetry.instrumentation.anthropic",
|
|
74
|
+
"instrumentor_class": "AnthropicInstrumentor",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "OpenAI",
|
|
78
|
+
"sdk_module": "openai",
|
|
79
|
+
"instrumentor_module": "opentelemetry.instrumentation.openai",
|
|
80
|
+
"instrumentor_class": "OpenAIInstrumentor",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "LangChain / LangGraph",
|
|
84
|
+
"sdk_module": "langchain",
|
|
85
|
+
"instrumentor_module": "opentelemetry.instrumentation.langchain",
|
|
86
|
+
"instrumentor_class": "LangchainInstrumentor",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"name": "CrewAI",
|
|
90
|
+
"sdk_module": "crewai",
|
|
91
|
+
"instrumentor_module": "opentelemetry.instrumentation.crewai",
|
|
92
|
+
"instrumentor_class": "CrewAIInstrumentor",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"name": "LlamaIndex",
|
|
96
|
+
"sdk_module": "llama_index",
|
|
97
|
+
"instrumentor_module": "opentelemetry.instrumentation.llamaindex",
|
|
98
|
+
"instrumentor_class": "LlamaIndexInstrumentor",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"name": "Google Vertex AI / Gemini",
|
|
102
|
+
"sdk_module": "vertexai",
|
|
103
|
+
"instrumentor_module": "opentelemetry.instrumentation.vertexai",
|
|
104
|
+
"instrumentor_class": "VertexAIInstrumentor",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "Mistral AI",
|
|
108
|
+
"sdk_module": "mistralai",
|
|
109
|
+
"instrumentor_module": "opentelemetry.instrumentation.mistralai",
|
|
110
|
+
"instrumentor_class": "MistralAIInstrumentor",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "AWS Bedrock",
|
|
114
|
+
"sdk_module": "boto3",
|
|
115
|
+
"instrumentor_module": "opentelemetry.instrumentation.bedrock",
|
|
116
|
+
"instrumentor_class": "BedrockInstrumentor",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "LiteLLM",
|
|
120
|
+
"sdk_module": "litellm",
|
|
121
|
+
"instrumentor_module": "openinference.instrumentation.litellm",
|
|
122
|
+
"instrumentor_class": "LiteLLMInstrumentor",
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
# ──────────────────────────────────────────────
|
|
127
|
+
# Web framework instrumentors (for trace grouping)
|
|
128
|
+
# These create a root span per HTTP request so
|
|
129
|
+
# all LLM calls within one request share a trace_id.
|
|
130
|
+
# ──────────────────────────────────────────────
|
|
131
|
+
FRAMEWORK_INSTRUMENTORS = [
|
|
132
|
+
{
|
|
133
|
+
"name": "FastAPI",
|
|
134
|
+
"sdk_module": "fastapi",
|
|
135
|
+
"instrumentor_module": "opentelemetry.instrumentation.fastapi",
|
|
136
|
+
"instrumentor_class": "FastAPIInstrumentor",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
"name": "Flask",
|
|
140
|
+
"sdk_module": "flask",
|
|
141
|
+
"instrumentor_module": "opentelemetry.instrumentation.flask",
|
|
142
|
+
"instrumentor_class": "FlaskInstrumentor",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"name": "Django",
|
|
146
|
+
"sdk_module": "django",
|
|
147
|
+
"instrumentor_module": "opentelemetry.instrumentation.django",
|
|
148
|
+
"instrumentor_class": "DjangoInstrumentor",
|
|
149
|
+
},
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ──────────────────────────────────────────────
|
|
154
|
+
# Filtering SpanProcessor — drops noise spans
|
|
155
|
+
# before they reach the exporter, so the collector
|
|
156
|
+
# never sees or bills for them.
|
|
157
|
+
# ──────────────────────────────────────────────
|
|
158
|
+
class _FilteringSpanProcessor(SpanProcessor):
|
|
159
|
+
"""Wraps an inner SpanProcessor and silently drops spans
|
|
160
|
+
whose names match the noise set (http send, etc.)."""
|
|
161
|
+
|
|
162
|
+
def __init__(self, inner: SpanProcessor, drop_names: frozenset):
|
|
163
|
+
self._inner = inner
|
|
164
|
+
self._drop_names = drop_names
|
|
165
|
+
|
|
166
|
+
def on_start(self, span, parent_context=None):
|
|
167
|
+
self._inner.on_start(span, parent_context)
|
|
168
|
+
|
|
169
|
+
def on_end(self, span):
|
|
170
|
+
if span.name in self._drop_names:
|
|
171
|
+
return # silently drop — never exported
|
|
172
|
+
self._inner.on_end(span)
|
|
173
|
+
|
|
174
|
+
def shutdown(self):
|
|
175
|
+
self._inner.shutdown()
|
|
176
|
+
|
|
177
|
+
def force_flush(self, timeout_millis=None):
|
|
178
|
+
self._inner.force_flush(timeout_millis)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class SigilConfig:
|
|
183
|
+
"""Configuration for Sigil telemetry. All fields have sensible defaults
|
|
184
|
+
that read from environment variables, so most users never need to touch this."""
|
|
185
|
+
|
|
186
|
+
agent_id: Optional[str] = None
|
|
187
|
+
agent_version: str = "0.1.0"
|
|
188
|
+
collector_url: Optional[str] = None
|
|
189
|
+
environment: str = "production"
|
|
190
|
+
console_export: bool = False
|
|
191
|
+
service_name: Optional[str] = None
|
|
192
|
+
|
|
193
|
+
# Extra resource attributes to attach to every span
|
|
194
|
+
extra_attributes: dict = field(default_factory=dict)
|
|
195
|
+
|
|
196
|
+
def __post_init__(self):
|
|
197
|
+
"""Resolve values from environment variables if not explicitly set."""
|
|
198
|
+
self.agent_id = self.agent_id or os.environ.get("SIGIL_AGENT_ID")
|
|
199
|
+
self.agent_version = (
|
|
200
|
+
os.environ.get("SIGIL_AGENT_VERSION") or self.agent_version
|
|
201
|
+
)
|
|
202
|
+
self.collector_url = (
|
|
203
|
+
self.collector_url
|
|
204
|
+
or os.environ.get("SIGIL_COLLECTOR_URL")
|
|
205
|
+
)
|
|
206
|
+
self.environment = (
|
|
207
|
+
os.environ.get("SIGIL_ENVIRONMENT") or self.environment
|
|
208
|
+
)
|
|
209
|
+
self.console_export = (
|
|
210
|
+
os.environ.get("SIGIL_CONSOLE_EXPORT", "").lower() == "true"
|
|
211
|
+
or self.console_export
|
|
212
|
+
)
|
|
213
|
+
self.service_name = (
|
|
214
|
+
self.service_name
|
|
215
|
+
or os.environ.get("OTEL_SERVICE_NAME")
|
|
216
|
+
or self.agent_id
|
|
217
|
+
or "sigil-agent-unknown"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
# Track initialization state
|
|
222
|
+
_initialized = False
|
|
223
|
+
_provider = None # Keep reference for atexit shutdown
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def init(config: Optional[SigilConfig] = None) -> dict:
|
|
227
|
+
"""
|
|
228
|
+
Initialize Sigil telemetry. Call once at application startup.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
config: Optional SigilConfig. If None, reads everything from env vars.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
dict with keys:
|
|
235
|
+
- "status": "ok" or "error"
|
|
236
|
+
- "agent_id": the resolved agent ID
|
|
237
|
+
- "instrumented": list of SDK names that were instrumented
|
|
238
|
+
- "frameworks": list of web frameworks that were instrumented
|
|
239
|
+
- "skipped": list of SDK names that were skipped (not installed)
|
|
240
|
+
- "errors": list of errors encountered during instrumentation
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
from sigil_telemetry import init
|
|
244
|
+
|
|
245
|
+
# Simplest — reads everything from env vars
|
|
246
|
+
init()
|
|
247
|
+
|
|
248
|
+
# Or with explicit config
|
|
249
|
+
from sigil_telemetry import init, SigilConfig
|
|
250
|
+
init(SigilConfig(agent_id="sigil-agent-my-agent", console_export=True))
|
|
251
|
+
"""
|
|
252
|
+
global _initialized, _provider
|
|
253
|
+
|
|
254
|
+
if _initialized:
|
|
255
|
+
logger.warning("Sigil telemetry already initialized — skipping re-init")
|
|
256
|
+
return {"status": "ok", "message": "already initialized"}
|
|
257
|
+
|
|
258
|
+
cfg = config or SigilConfig()
|
|
259
|
+
|
|
260
|
+
result = {
|
|
261
|
+
"status": "ok",
|
|
262
|
+
"agent_id": cfg.agent_id,
|
|
263
|
+
"instrumented": [],
|
|
264
|
+
"frameworks": [],
|
|
265
|
+
"skipped": [],
|
|
266
|
+
"errors": [],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# ── Validate ──────────────────────────────────────
|
|
270
|
+
if not cfg.agent_id:
|
|
271
|
+
logger.warning(
|
|
272
|
+
"SIGIL_AGENT_ID not set. Telemetry will work but agent "
|
|
273
|
+
"identification in Sigil will show as 'unknown'. "
|
|
274
|
+
"Set the SIGIL_AGENT_ID environment variable or pass "
|
|
275
|
+
"SigilConfig(agent_id='...')"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
if not cfg.collector_url:
|
|
279
|
+
logger.error(
|
|
280
|
+
"SIGIL_COLLECTOR_URL not set. Telemetry has nowhere to send spans. "
|
|
281
|
+
"Set the SIGIL_COLLECTOR_URL environment variable or pass "
|
|
282
|
+
"SigilConfig(collector_url='https://your-collector-endpoint')"
|
|
283
|
+
)
|
|
284
|
+
result["status"] = "error"
|
|
285
|
+
result["errors"].append("No collector URL configured")
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
# ── Build OTel Resource ───────────────────────────
|
|
289
|
+
resource_attrs = {
|
|
290
|
+
"service.name": cfg.service_name,
|
|
291
|
+
"service.version": cfg.agent_version,
|
|
292
|
+
"deployment.environment": cfg.environment,
|
|
293
|
+
"sigil.agent.id": cfg.agent_id or "unknown",
|
|
294
|
+
"sigil.agent.version": cfg.agent_version,
|
|
295
|
+
"sigil.environment": cfg.environment,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Read additional Sigil governance metadata from env vars
|
|
299
|
+
for env_key, attr_key in [
|
|
300
|
+
("SIGIL_DIVISION", "sigil.agent.division"),
|
|
301
|
+
("SIGIL_RISK_CLASSIFICATION", "sigil.agent.risk_classification"),
|
|
302
|
+
("SIGIL_HOURS_SAVED", "sigil.agent.hours_saved"),
|
|
303
|
+
]:
|
|
304
|
+
val = os.environ.get(env_key)
|
|
305
|
+
if val:
|
|
306
|
+
resource_attrs[attr_key] = val
|
|
307
|
+
|
|
308
|
+
resource_attrs.update(cfg.extra_attributes)
|
|
309
|
+
|
|
310
|
+
# ── Auto-detect LLM SDKs ─────────────────────────
|
|
311
|
+
instrumented_set = set()
|
|
312
|
+
for spec in INSTRUMENTORS:
|
|
313
|
+
name = spec["name"]
|
|
314
|
+
shared = spec.get("shared_with")
|
|
315
|
+
|
|
316
|
+
# Skip if this instrumentor is shared with one already activated
|
|
317
|
+
if shared and shared in instrumented_set:
|
|
318
|
+
logger.debug(f" {name}: shares instrumentor with {shared}, skipping")
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Check if the target SDK is installed
|
|
322
|
+
if not _is_installed(spec["sdk_module"]):
|
|
323
|
+
result["skipped"].append(name)
|
|
324
|
+
logger.debug(f" {name}: SDK not installed, skipping")
|
|
325
|
+
continue
|
|
326
|
+
|
|
327
|
+
# Check if the instrumentor package is installed
|
|
328
|
+
if not _is_installed(spec["instrumentor_module"]):
|
|
329
|
+
msg = (
|
|
330
|
+
f"{name} SDK detected but instrumentor not installed. "
|
|
331
|
+
f"Install with: pip install sigil-telemetry[{spec['sdk_module'].split('.')[0]}]"
|
|
332
|
+
)
|
|
333
|
+
result["errors"].append(msg)
|
|
334
|
+
logger.warning(f" {name}: {msg}")
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
# Instrument
|
|
338
|
+
try:
|
|
339
|
+
module = importlib.import_module(spec["instrumentor_module"])
|
|
340
|
+
instrumentor_cls = getattr(module, spec["instrumentor_class"])
|
|
341
|
+
instrumentor = instrumentor_cls()
|
|
342
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
343
|
+
instrumentor.instrument()
|
|
344
|
+
instrumented_set.add(name)
|
|
345
|
+
result["instrumented"].append(name)
|
|
346
|
+
logger.info(f" ✓ {name} instrumented")
|
|
347
|
+
else:
|
|
348
|
+
logger.debug(f" {name}: already instrumented")
|
|
349
|
+
result["instrumented"].append(name)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
msg = f"Failed to instrument {name}: {e}"
|
|
352
|
+
result["errors"].append(msg)
|
|
353
|
+
logger.error(f" ✗ {msg}")
|
|
354
|
+
|
|
355
|
+
# ── Auto-detect web frameworks (for trace grouping) ──
|
|
356
|
+
for spec in FRAMEWORK_INSTRUMENTORS:
|
|
357
|
+
name = spec["name"]
|
|
358
|
+
|
|
359
|
+
if not _is_installed(spec["sdk_module"]):
|
|
360
|
+
logger.debug(f" {name}: not installed, skipping framework instrumentation")
|
|
361
|
+
continue
|
|
362
|
+
|
|
363
|
+
if not _is_installed(spec["instrumentor_module"]):
|
|
364
|
+
logger.debug(f" {name}: instrumentor not installed, skipping")
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
module = importlib.import_module(spec["instrumentor_module"])
|
|
369
|
+
instrumentor_cls = getattr(module, spec["instrumentor_class"])
|
|
370
|
+
instrumentor = instrumentor_cls()
|
|
371
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
372
|
+
instrumentor.instrument(excluded_urls=_HEALTH_CHECK_PATHS)
|
|
373
|
+
result["frameworks"].append(name)
|
|
374
|
+
logger.info(f" ✓ {name} framework instrumented (health checks excluded)")
|
|
375
|
+
else:
|
|
376
|
+
logger.debug(f" {name}: already instrumented")
|
|
377
|
+
result["frameworks"].append(name)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
msg = f"Failed to instrument {name} framework: {e}"
|
|
380
|
+
result["errors"].append(msg)
|
|
381
|
+
logger.error(f" ✗ {msg}")
|
|
382
|
+
|
|
383
|
+
# ── Record detected frameworks in resource attributes ──
|
|
384
|
+
all_instrumented = result["instrumented"] + result["frameworks"]
|
|
385
|
+
if all_instrumented:
|
|
386
|
+
resource_attrs["sigil.agent.frameworks"] = ",".join(all_instrumented)
|
|
387
|
+
else:
|
|
388
|
+
resource_attrs["sigil.agent.frameworks"] = "none"
|
|
389
|
+
logger.warning(
|
|
390
|
+
"No LLM SDKs were instrumented. Install at least one supported SDK "
|
|
391
|
+
"(anthropic, openai, langchain, crewai, llama-index, vertexai, "
|
|
392
|
+
"mistralai, bedrock, litellm) and its corresponding instrumentor."
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# ── Now build the Resource and TracerProvider ─────
|
|
396
|
+
resource = Resource.create(resource_attrs)
|
|
397
|
+
provider = TracerProvider(resource=resource)
|
|
398
|
+
_provider = provider
|
|
399
|
+
|
|
400
|
+
# OTLP exporter → Sigil collector
|
|
401
|
+
# Wrapped in _FilteringSpanProcessor to drop noise spans
|
|
402
|
+
# (http send, http send body) before they reach the collector.
|
|
403
|
+
otlp_endpoint = f"{cfg.collector_url}/v1/traces"
|
|
404
|
+
otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint)
|
|
405
|
+
batch_processor = BatchSpanProcessor(otlp_exporter)
|
|
406
|
+
filtering_processor = _FilteringSpanProcessor(batch_processor, _NOISE_SPAN_NAMES)
|
|
407
|
+
provider.add_span_processor(filtering_processor)
|
|
408
|
+
|
|
409
|
+
# Optional console exporter for debugging
|
|
410
|
+
if cfg.console_export:
|
|
411
|
+
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
412
|
+
logger.info("Console span export enabled (debug mode)")
|
|
413
|
+
|
|
414
|
+
trace.set_tracer_provider(provider)
|
|
415
|
+
|
|
416
|
+
# ── Register atexit handler to flush pending spans ──
|
|
417
|
+
atexit.register(_shutdown)
|
|
418
|
+
|
|
419
|
+
logger.info(
|
|
420
|
+
f"Sigil telemetry initialized — agent={cfg.agent_id}, "
|
|
421
|
+
f"collector={cfg.collector_url}, env={cfg.environment}, "
|
|
422
|
+
f"frameworks={resource_attrs.get('sigil.agent.frameworks')}"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
if result["errors"]:
|
|
426
|
+
result["status"] = "partial"
|
|
427
|
+
|
|
428
|
+
_initialized = True
|
|
429
|
+
|
|
430
|
+
# Print a clean startup banner
|
|
431
|
+
_print_banner(cfg, result)
|
|
432
|
+
|
|
433
|
+
return result
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def get_tracer(name: str = "sigil-telemetry") -> trace.Tracer:
|
|
437
|
+
"""Get an OTel tracer for creating custom spans.
|
|
438
|
+
|
|
439
|
+
Use this when you want to add custom instrumentation beyond
|
|
440
|
+
what the auto-instrumentors capture:
|
|
441
|
+
|
|
442
|
+
from sigil_telemetry import get_tracer
|
|
443
|
+
|
|
444
|
+
tracer = get_tracer()
|
|
445
|
+
with tracer.start_as_current_span("document-parsing") as span:
|
|
446
|
+
span.set_attribute("document.pages", 42)
|
|
447
|
+
span.set_attribute("document.type", "contract")
|
|
448
|
+
result = parse_document(doc)
|
|
449
|
+
"""
|
|
450
|
+
return trace.get_tracer(name)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def record_error(
|
|
454
|
+
span: trace.Span,
|
|
455
|
+
exception: Exception,
|
|
456
|
+
attributes: Optional[dict] = None,
|
|
457
|
+
) -> None:
|
|
458
|
+
"""Record an error on a span with Sigil-standard attributes.
|
|
459
|
+
|
|
460
|
+
Use this to capture model failures, API errors, rate limits, etc.:
|
|
461
|
+
|
|
462
|
+
from sigil_telemetry import get_tracer, record_error
|
|
463
|
+
|
|
464
|
+
tracer = get_tracer()
|
|
465
|
+
with tracer.start_as_current_span("llm-call") as span:
|
|
466
|
+
try:
|
|
467
|
+
response = client.messages.create(...)
|
|
468
|
+
except Exception as e:
|
|
469
|
+
record_error(span, e, {"retry_count": 2})
|
|
470
|
+
raise
|
|
471
|
+
"""
|
|
472
|
+
span.set_status(StatusCode.ERROR, str(exception))
|
|
473
|
+
span.record_exception(exception)
|
|
474
|
+
if attributes:
|
|
475
|
+
for k, v in attributes.items():
|
|
476
|
+
span.set_attribute(f"sigil.error.{k}", v)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _shutdown():
|
|
480
|
+
"""Flush all pending spans on process exit."""
|
|
481
|
+
global _provider
|
|
482
|
+
if _provider:
|
|
483
|
+
try:
|
|
484
|
+
_provider.shutdown()
|
|
485
|
+
except Exception:
|
|
486
|
+
pass # Best effort — process is exiting
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _is_installed(module_name: str) -> bool:
|
|
490
|
+
"""Check if a Python module is importable."""
|
|
491
|
+
try:
|
|
492
|
+
importlib.import_module(module_name)
|
|
493
|
+
return True
|
|
494
|
+
except ImportError:
|
|
495
|
+
return False
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _print_banner(cfg: SigilConfig, result: dict) -> None:
|
|
499
|
+
"""Print a clean startup banner."""
|
|
500
|
+
lines = [
|
|
501
|
+
"",
|
|
502
|
+
"╔══════════════════════════════════════════════════╗",
|
|
503
|
+
"║ Sigil Telemetry Initialized ║",
|
|
504
|
+
"╠══════════════════════════════════════════════════╣",
|
|
505
|
+
f"║ Agent ID : {(cfg.agent_id or 'not set'):<35} ║",
|
|
506
|
+
f"║ Version : {cfg.agent_version:<35} ║",
|
|
507
|
+
f"║ Env : {cfg.environment:<35} ║",
|
|
508
|
+
f"║ Collector : {'connected':<35} ║",
|
|
509
|
+
"╠══════════════════════════════════════════════════╣",
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
if result["instrumented"]:
|
|
513
|
+
lines.append(f"║ LLM SDKs: ║")
|
|
514
|
+
for sdk in result["instrumented"]:
|
|
515
|
+
lines.append(f"║ ✓ {sdk:<43} ║")
|
|
516
|
+
|
|
517
|
+
if result["frameworks"]:
|
|
518
|
+
lines.append(f"║ Web Frameworks: ║")
|
|
519
|
+
for fw in result["frameworks"]:
|
|
520
|
+
lines.append(f"║ ✓ {fw:<43} ║")
|
|
521
|
+
|
|
522
|
+
if not result["instrumented"] and not result["frameworks"]:
|
|
523
|
+
lines.append(f"║ ⚠ No SDKs instrumented ║")
|
|
524
|
+
|
|
525
|
+
if result["skipped"]:
|
|
526
|
+
lines.append(f"║ Skipped (not installed): ║")
|
|
527
|
+
for sdk in result["skipped"]:
|
|
528
|
+
lines.append(f"║ · {sdk:<43} ║")
|
|
529
|
+
|
|
530
|
+
if result["errors"]:
|
|
531
|
+
lines.append(f"║ Errors: ║")
|
|
532
|
+
for err in result["errors"]:
|
|
533
|
+
short = err[:43]
|
|
534
|
+
lines.append(f"║ ✗ {short:<43} ║")
|
|
535
|
+
|
|
536
|
+
lines.append("╚══════════════════════════════════════════════════╝")
|
|
537
|
+
lines.append("")
|
|
538
|
+
|
|
539
|
+
print("\n".join(lines))
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sigil-telemetry
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Universal AI agent telemetry for Sigil — auto-instruments any LLM SDK and exports to the Sigil collector.
|
|
5
|
+
Author: Zurain Khan
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
10
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
11
|
+
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
12
|
+
Requires-Dist: opentelemetry-semantic-conventions>=0.41b0
|
|
13
|
+
Provides-Extra: anthropic
|
|
14
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.30.0; extra == "anthropic"
|
|
15
|
+
Provides-Extra: openai
|
|
16
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.30.0; extra == "openai"
|
|
17
|
+
Provides-Extra: langchain
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.30.0; extra == "langchain"
|
|
19
|
+
Provides-Extra: crewai
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.30.0; extra == "crewai"
|
|
21
|
+
Provides-Extra: llamaindex
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.30.0; extra == "llamaindex"
|
|
23
|
+
Provides-Extra: vertexai
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-vertexai>=0.30.0; extra == "vertexai"
|
|
25
|
+
Provides-Extra: mistral
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-mistralai>=0.30.0; extra == "mistral"
|
|
27
|
+
Provides-Extra: bedrock
|
|
28
|
+
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.30.0; extra == "bedrock"
|
|
29
|
+
Provides-Extra: litellm
|
|
30
|
+
Requires-Dist: openinference-instrumentation-litellm>=0.1.0; extra == "litellm"
|
|
31
|
+
Provides-Extra: fastapi
|
|
32
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0; extra == "fastapi"
|
|
33
|
+
Provides-Extra: flask
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.41b0; extra == "flask"
|
|
35
|
+
Provides-Extra: django
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == "django"
|
|
37
|
+
Provides-Extra: databricks
|
|
38
|
+
Requires-Dist: databricks-sql-connector>=4.0.0; extra == "databricks"
|
|
39
|
+
Provides-Extra: all
|
|
40
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic>=0.30.0; extra == "all"
|
|
41
|
+
Requires-Dist: opentelemetry-instrumentation-openai>=0.30.0; extra == "all"
|
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-langchain>=0.30.0; extra == "all"
|
|
43
|
+
Requires-Dist: opentelemetry-instrumentation-crewai>=0.30.0; extra == "all"
|
|
44
|
+
Requires-Dist: opentelemetry-instrumentation-llamaindex>=0.30.0; extra == "all"
|
|
45
|
+
Requires-Dist: opentelemetry-instrumentation-vertexai>=0.30.0; extra == "all"
|
|
46
|
+
Requires-Dist: opentelemetry-instrumentation-mistralai>=0.30.0; extra == "all"
|
|
47
|
+
Requires-Dist: opentelemetry-instrumentation-bedrock>=0.30.0; extra == "all"
|
|
48
|
+
Requires-Dist: openinference-instrumentation-litellm>=0.1.0; extra == "all"
|
|
49
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.41b0; extra == "all"
|
|
50
|
+
Requires-Dist: opentelemetry-instrumentation-flask>=0.41b0; extra == "all"
|
|
51
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.41b0; extra == "all"
|
|
52
|
+
|
|
53
|
+
# sigil-telemetry
|
|
54
|
+
|
|
55
|
+
Plug-and-play telemetry for AI agents. Install it, call `init()`, and every LLM call your agent makes is automatically tracked in Sigil.
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install sigil-telemetry[all]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from sigil_telemetry import init
|
|
65
|
+
init()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Set your agent's ID and collector endpoint
|
|
70
|
+
SIGIL_AGENT_ID=sigil-agent-your-agent-slug
|
|
71
|
+
SIGIL_COLLECTOR_URL=https://your-collector-endpoint/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That's it. Every LLM API call is now captured — tokens, model, latency, errors — and sent to the Sigil collector.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## What's New in v0.2.0
|
|
79
|
+
|
|
80
|
+
- **Web framework auto-instrumentation** — FastAPI, Flask, and Django are auto-detected and instrumented. All LLM calls within one HTTP request share a single `trace_id` (operation_Id), so you can count agent "runs" with `COUNT(DISTINCT trace_id)`.
|
|
81
|
+
- **Noise span filtering** — `http send` / `http send body` spans from web frameworks are silently dropped before they leave the process. They never reach your collector, so you're not billed for them.
|
|
82
|
+
- **Health check exclusion** — Routes like `/health`, `/healthz`, `/ready`, `/alive`, `/ping` are excluded from tracing entirely. No spans generated, no storage cost.
|
|
83
|
+
- **Graceful shutdown** — `atexit` handler flushes all pending spans when the process exits, so you never lose the last batch.
|
|
84
|
+
- **Lighter install** — Removed unnecessary dependencies from the core install.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Full Example: What Actually Happens
|
|
89
|
+
|
|
90
|
+
Here's a real agent that summarizes documents using Claude. Let's walk through exactly what the telemetry captures and where it ends up.
|
|
91
|
+
|
|
92
|
+
### 1. The Agent Code
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
# document_summarizer.py
|
|
96
|
+
import anthropic
|
|
97
|
+
from sigil_telemetry import init
|
|
98
|
+
|
|
99
|
+
# Initialize telemetry — call this ONCE at startup
|
|
100
|
+
init()
|
|
101
|
+
|
|
102
|
+
# Your normal agent code — no changes needed
|
|
103
|
+
client = anthropic.Anthropic()
|
|
104
|
+
response = client.messages.create(
|
|
105
|
+
model="claude-sonnet-4-20250514",
|
|
106
|
+
max_tokens=1024,
|
|
107
|
+
messages=[
|
|
108
|
+
{"role": "user", "content": "Summarize this document: ..."}
|
|
109
|
+
]
|
|
110
|
+
)
|
|
111
|
+
print(response.content[0].text)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 2. What Gets Captured (Per LLM Call)
|
|
115
|
+
|
|
116
|
+
Every time `client.messages.create()` runs, a **span** is automatically created with:
|
|
117
|
+
|
|
118
|
+
| Field | Example Value | Description |
|
|
119
|
+
|-------|--------------|-------------|
|
|
120
|
+
| `operation_Id` | `a1b2c3d4e5f6...` | Trace ID — groups all LLM calls in a single agent run |
|
|
121
|
+
| `sigil.agent.id` | `sigil-agent-doc-summarizer` | Which agent made the call |
|
|
122
|
+
| `sigil.agent.version` | `sha-abc1234` | Agent version (set by deploy workflow) |
|
|
123
|
+
| `sigil.agent.frameworks` | `Anthropic,FastAPI` | Which SDKs and frameworks were detected |
|
|
124
|
+
| `gen_ai.system` | `anthropic` | LLM provider |
|
|
125
|
+
| `gen_ai.request.model` | `claude-sonnet-4-20250514` | Model used |
|
|
126
|
+
| `gen_ai.usage.input_tokens` | `1250` | Tokens sent |
|
|
127
|
+
| `gen_ai.usage.output_tokens` | `340` | Tokens received |
|
|
128
|
+
| `duration` | `2.3s` | How long the call took |
|
|
129
|
+
| `status` | `OK` or `ERROR` | Whether the call succeeded |
|
|
130
|
+
| `sigil.environment` | `production` | Environment |
|
|
131
|
+
| `sigil.agent.division` | `Sales` | Business division (if set) |
|
|
132
|
+
| `sigil.agent.risk_classification` | `low` | Risk level (if set) |
|
|
133
|
+
|
|
134
|
+
If the agent makes **multiple LLM calls** in one run (e.g., calls Claude then GPT-4), all calls share the same `operation_Id` so you can see the full trace.
|
|
135
|
+
|
|
136
|
+
### 3. How Trace Grouping Works
|
|
137
|
+
|
|
138
|
+
**API agents (FastAPI/Flask/Django):** The web framework instrumentor creates a root span per HTTP request. All LLM calls within that request automatically become child spans sharing the same `trace_id`. You don't need to do anything — `init()` handles it.
|
|
139
|
+
|
|
140
|
+
**Worker agents (scheduled jobs, listeners):** The template wraps your `main()` function in a root span. All LLM calls within one job or message share the same `trace_id`.
|
|
141
|
+
|
|
142
|
+
In both cases: `COUNT(DISTINCT trace_id)` = number of agent runs.
|
|
143
|
+
|
|
144
|
+
### 4. Where the Data Goes
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Agent makes LLM call
|
|
148
|
+
│
|
|
149
|
+
▼
|
|
150
|
+
sigil-telemetry auto-captures it as an OpenTelemetry span
|
|
151
|
+
(noise spans like "http send" are filtered out here)
|
|
152
|
+
│
|
|
153
|
+
▼
|
|
154
|
+
Span is batched and sent via OTLP to:
|
|
155
|
+
→ Your configured collector endpoint
|
|
156
|
+
│
|
|
157
|
+
▼
|
|
158
|
+
Collector forwards to:
|
|
159
|
+
→ Your observability backend (Jaeger, Zipkin, Datadog, etc.)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Supported SDKs
|
|
165
|
+
|
|
166
|
+
Use `[all]` to install everything. Only the SDKs your agent actually uses get activated.
|
|
167
|
+
|
|
168
|
+
| SDK | Install Extra | What It Covers |
|
|
169
|
+
|-----|--------------|----------------|
|
|
170
|
+
| Anthropic | `[anthropic]` | Anthropic API |
|
|
171
|
+
| OpenAI | `[openai]` | OpenAI API (including compatible endpoints) |
|
|
172
|
+
| LangChain | `[langchain]` | LangChain, LangGraph, any LangChain-wrapped model |
|
|
173
|
+
| CrewAI | `[crewai]` | CrewAI multi-agent framework |
|
|
174
|
+
| LlamaIndex | `[llamaindex]` | LlamaIndex agents and pipelines |
|
|
175
|
+
| Vertex AI | `[vertexai]` | Google Vertex AI, Gemini models |
|
|
176
|
+
| Mistral AI | `[mistral]` | Mistral API |
|
|
177
|
+
| AWS Bedrock | `[bedrock]` | Claude, Llama, Titan via AWS |
|
|
178
|
+
| LiteLLM | `[litellm]` | Unified proxy across 100+ LLM providers |
|
|
179
|
+
|
|
180
|
+
## Web Framework Auto-Instrumentation
|
|
181
|
+
|
|
182
|
+
These are included in `[all]` and auto-detected by `init()`:
|
|
183
|
+
|
|
184
|
+
| Framework | Install Extra | What It Does |
|
|
185
|
+
|-----------|--------------|--------------|
|
|
186
|
+
| FastAPI | `[fastapi]` | Creates root span per HTTP request — all LLM calls in that request share one trace_id |
|
|
187
|
+
| Flask | `[flask]` | Same trace grouping for Flask apps |
|
|
188
|
+
| Django | `[django]` | Same trace grouping for Django apps |
|
|
189
|
+
|
|
190
|
+
Health check routes (`/health`, `/healthz`, `/ready`, `/alive`, `/ping`, `/startup`, `/liveness`, `/readiness`) are automatically excluded from tracing.
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
| Env Variable | Default | Description |
|
|
195
|
+
|-------------|---------|-------------|
|
|
196
|
+
| `SIGIL_AGENT_ID` | — | **Required.** Your agent's Sigil ID |
|
|
197
|
+
| `SIGIL_AGENT_VERSION` | `0.1.0` | Track deployments (set automatically by deploy workflow) |
|
|
198
|
+
| `SIGIL_COLLECTOR_URL` | — | **Required.** Your collector endpoint URL |
|
|
199
|
+
| `SIGIL_ENVIRONMENT` | `production` | `production`, `staging`, `development` |
|
|
200
|
+
| `SIGIL_CONSOLE_EXPORT` | `false` | Print spans to console for debugging |
|
|
201
|
+
| `SIGIL_DIVISION` | — | Business division (e.g., `Sales`, `Engineering`) |
|
|
202
|
+
| `SIGIL_RISK_CLASSIFICATION` | — | Agent risk level (`low`, `medium`, `high`) |
|
|
203
|
+
| `SIGIL_HOURS_SAVED` | — | Estimated hours saved per run |
|
|
204
|
+
|
|
205
|
+
Or pass config in code:
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
from sigil_telemetry import init, SigilConfig
|
|
209
|
+
|
|
210
|
+
init(SigilConfig(
|
|
211
|
+
agent_id="sigil-agent-my-agent",
|
|
212
|
+
environment="development",
|
|
213
|
+
console_export=True
|
|
214
|
+
))
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Custom Spans
|
|
218
|
+
|
|
219
|
+
Track things beyond LLM calls (document parsing, tool use, etc.):
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from sigil_telemetry import get_tracer, record_error
|
|
223
|
+
|
|
224
|
+
tracer = get_tracer()
|
|
225
|
+
|
|
226
|
+
with tracer.start_as_current_span("parse-contract") as span:
|
|
227
|
+
span.set_attribute("document.pages", 42)
|
|
228
|
+
try:
|
|
229
|
+
result = parse_pdf(file)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
record_error(span, e)
|
|
232
|
+
raise
|
|
233
|
+
```
|
|
234
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/sigil_telemetry/__init__.py
|
|
4
|
+
src/sigil_telemetry/core.py
|
|
5
|
+
src/sigil_telemetry.egg-info/PKG-INFO
|
|
6
|
+
src/sigil_telemetry.egg-info/SOURCES.txt
|
|
7
|
+
src/sigil_telemetry.egg-info/dependency_links.txt
|
|
8
|
+
src/sigil_telemetry.egg-info/requires.txt
|
|
9
|
+
src/sigil_telemetry.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
opentelemetry-api>=1.20.0
|
|
2
|
+
opentelemetry-sdk>=1.20.0
|
|
3
|
+
opentelemetry-exporter-otlp-proto-http>=1.20.0
|
|
4
|
+
opentelemetry-semantic-conventions>=0.41b0
|
|
5
|
+
|
|
6
|
+
[all]
|
|
7
|
+
opentelemetry-instrumentation-anthropic>=0.30.0
|
|
8
|
+
opentelemetry-instrumentation-openai>=0.30.0
|
|
9
|
+
opentelemetry-instrumentation-langchain>=0.30.0
|
|
10
|
+
opentelemetry-instrumentation-crewai>=0.30.0
|
|
11
|
+
opentelemetry-instrumentation-llamaindex>=0.30.0
|
|
12
|
+
opentelemetry-instrumentation-vertexai>=0.30.0
|
|
13
|
+
opentelemetry-instrumentation-mistralai>=0.30.0
|
|
14
|
+
opentelemetry-instrumentation-bedrock>=0.30.0
|
|
15
|
+
openinference-instrumentation-litellm>=0.1.0
|
|
16
|
+
opentelemetry-instrumentation-fastapi>=0.41b0
|
|
17
|
+
opentelemetry-instrumentation-flask>=0.41b0
|
|
18
|
+
opentelemetry-instrumentation-django>=0.41b0
|
|
19
|
+
|
|
20
|
+
[anthropic]
|
|
21
|
+
opentelemetry-instrumentation-anthropic>=0.30.0
|
|
22
|
+
|
|
23
|
+
[bedrock]
|
|
24
|
+
opentelemetry-instrumentation-bedrock>=0.30.0
|
|
25
|
+
|
|
26
|
+
[crewai]
|
|
27
|
+
opentelemetry-instrumentation-crewai>=0.30.0
|
|
28
|
+
|
|
29
|
+
[databricks]
|
|
30
|
+
databricks-sql-connector>=4.0.0
|
|
31
|
+
|
|
32
|
+
[django]
|
|
33
|
+
opentelemetry-instrumentation-django>=0.41b0
|
|
34
|
+
|
|
35
|
+
[fastapi]
|
|
36
|
+
opentelemetry-instrumentation-fastapi>=0.41b0
|
|
37
|
+
|
|
38
|
+
[flask]
|
|
39
|
+
opentelemetry-instrumentation-flask>=0.41b0
|
|
40
|
+
|
|
41
|
+
[langchain]
|
|
42
|
+
opentelemetry-instrumentation-langchain>=0.30.0
|
|
43
|
+
|
|
44
|
+
[litellm]
|
|
45
|
+
openinference-instrumentation-litellm>=0.1.0
|
|
46
|
+
|
|
47
|
+
[llamaindex]
|
|
48
|
+
opentelemetry-instrumentation-llamaindex>=0.30.0
|
|
49
|
+
|
|
50
|
+
[mistral]
|
|
51
|
+
opentelemetry-instrumentation-mistralai>=0.30.0
|
|
52
|
+
|
|
53
|
+
[openai]
|
|
54
|
+
opentelemetry-instrumentation-openai>=0.30.0
|
|
55
|
+
|
|
56
|
+
[vertexai]
|
|
57
|
+
opentelemetry-instrumentation-vertexai>=0.30.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sigil_telemetry
|