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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,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