Skip to main content

sigil-telemetry

Plug-and-play telemetry for AI agents. Install it, call init(), and every LLM call your agent makes is automatically tracked.

Quick Start

pip install sigil-telemetry[all]
from sigil_telemetry import init
init()
# Set your agent's ID and collector endpoint
SIGIL_AGENT_ID=your-agent-slug
SIGIL_COLLECTOR_URL=https://your-collector-endpoint/

That's it. Every LLM API call is now captured — tokens, model, latency, errors — and sent to your collector.


What's New in v0.6.2

  • An agent's other outside calls are recorded too. Agents do more than call models: they search the web, fetch prices, read records, send email. Every such call made during a run (while a span is current: a request, a job, a workflow step) is now one HTTP client span, sigil.call.kind = "api", named <METHOD> <host>, in the step that made it: http.request.method, server.address, server.port, url.path, url.query, http.response.status_code, sigil.agent.site (the agent's function that made it), the calling user, request/response sizes and content type, and with content capture on the request and response bodies (sigil.api.request.body, sigil.api.response.body, text only, cut to 8000). Same clients as model calls: httpx, httpx2, requests/urllib3, aiohttp, urllib.
  • Secrets never leave the agent. Headers are never recorded. Values of credential-like keys (api_key, client_secret, access_token, password, Authorization, signature, sig, …) are replaced with [redacted] in query strings and in JSON and form bodies, in requests and responses alike; token-like path segments (webhook and bot tokens, ids) too. With SIGIL_CAPTURE_CONTENT=false no body is kept and every query value is redacted.
  • Plumbing is left out. Sign-in and token endpoints, key vaults and secret stores, telemetry and monitoring endpoints, cloud control planes and instance metadata are never recorded, nor are calls made outside a run (startup), a health probe's checks, a model call's own requests, or the collector. Add your own hosts with SIGIL_API_SKIP_HOSTS.
  • Handoffs from tools to models. An outside call's answer is fingerprinted like a model's: a later model call whose input carries it (search results passed to a model) lists that call in sigil.handoff.from. Works with content capture off.
  • No double counting, bounded volume. If the app already runs an OpenTelemetry HTTP client instrumentor, its spans are kept and no second one is made. Binary responses and multipart uploads are sized, not kept. At most 200 outside calls are recorded per trace; one outside calls not recorded span marks a trace that made more. Turn the feature off with SIGIL_CAPTURE_API=false.
  • AutoGen agents told apart. AutoGen's model clients (OpenAI, Azure OpenAI, Anthropic, Azure AI, Ollama) make each request in a new asyncio task, whose stack holds none of the agent's functions, so every call used to get the app's entry point (__main__.main) as its sigil.agent.site and all of an app's AI tasks looked like one agent. The site is now taken when the client is called and carried into that task, so each call is named after the function that made it, also for streamed and parallel calls.
  • The address of SDK calls. Spans from SDK instrumentors now get server.address / server.port (where the request really went) when the instrumentor left them out, so e.g. a model served from a cloud gateway can be told apart from the provider's own API. Never replaces what the instrumentor recorded.

What's New in v0.6.1

  • List attributes now reach your backend. The Azure Monitor exporter silently drops list-valued attributes, so sigil.input.files, sigil.handoff.from and instrumentors' lists such as gen_ai.response.finish_reasons never arrived. Every list attribute is now exported as a JSON array string, e.g. sigil.input.files = ["image · png · 88 KB"].
  • Prompts of calls an instrumentor skips. When an SDK instrumentor records a call but leaves out its input (the OpenAI instrumentor does for vision calls with images), the prompt the transport read is added — images and files as their type only ({"type": "image_url"}), never their data. What the instrumentor recorded is never replaced, and nothing is added when content capture is off (SIGIL_CAPTURE_CONTENT, TRACELOOP_TRACE_CONTENT, OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT or OPENINFERENCE_HIDE_INPUTS).
  • Launcher scripts aren't the agent. An agent started from a console script (uvicorn, gunicorn in the venv's bin/) no longer gets __main__.<module> as its sigil.agent.site when its own code is installed as a package.

What's New in v0.6.0

  • Every LLM call is captured, however it is made. Before 0.6.0 only calls through an instrumented SDK were seen — an agent calling a model with plain httpx.post(...) sent nothing. init() now also watches the HTTP clients underneath (httpx and its fork httpx2, which the OpenAI SDK uses from 3.x, requests/urllib3, boto3, aiohttp, urllib) plus websockets (OpenAI Realtime) and grpc (older Gemini/Vertex SDKs). A call is recognised from its host, its route, or its request/response shape — Anthropic, OpenAI (Chat and Responses), Azure OpenAI and Foundry, Gemini/Vertex, Bedrock, Ollama, Cohere and any OpenAI-compatible server — streamed (SSE, NDJSON, AWS event-stream) or not, compressed or not.
  • No double counting. When an SDK instrumentor is already recording a call, transport capture records no span of its own; SDK spans keep their richer detail (tool calls, agent names) and get the agent attributes below.
  • Prompts and answers recorded on every LLM span (gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions, tool calls and results included). What Sigil receives is kept as valid JSON under 8000 characters, below the Application Insights property limit of 8192 — the latest turn is protected, older turns are shortened or dropped first. Every other long string on every span, including the instrumentors' own prompt copies (gen_ai.prompt.N.content, llm.input_messages.N…, input.value), is cut to 8000 too, ending in …[truncated]. (If the app also exports to its own backend, that export gets the content unshortened, as the SDK instrumentors always sent it.) Turn off with SIGIL_CAPTURE_CONTENT=false.
  • One attribute vocabulary. Spans from older OpenLLMetry versions (gen_ai.system, gen_ai.usage.prompt_tokens, gen_ai.prompt.N.*) and from OpenInference (llm.*, e.g. LiteLLM) are exported with the current gen_ai.* names too, so a different instrumentor version can't silently change Sigil's numbers.
  • Works alongside the app's own tracing. If the Azure Monitor distro, Logfire or anything else already set a TracerProvider, Sigil's exporter is added to it instead of being silently dropped.
  • Processes too. ProcessPoolExecutor and multiprocessing.Process work keeps the request's trace and user, and is flushed when it ends. Celery tasks are traced from the code that queued them ([celery] extra).
  • Agents told apart, from what they do. Every LLM span says which of the agent's functions made it (sigil.agent.site, e.g. app.services.llm._llm_extract ← …). Calls from one site are compared, and what stays the same across them is the agent's fixed prompt: after 5 calls with at least 3 different inputs (and 2 different users, when calls carry a user) the span gets sigil.prompt.status = split and sigil.prompt.id — one id per AI task, so the same instructions called from two places in the code are one agent, and different instructions are different agents. The fixed prompt is sent once per process per version (sigil.prompt.fixed.0, .1, …, scrubbed of emails, phone numbers, IBANs, card and national id numbers), and from then on each run's gen_ai.input.messages carries only its input, with {"type": "fixed"} where the fixed prompt was.
  • Non-text input described in sigil.input.files: pdf · Report_2026.pdf · 2.1 MB, image · png · 88 KB, audio · wav · 388 KB · 12.4 s (seconds exact for WAV and Realtime audio, ~ estimated for MP3). Files uploaded to start a request (FastAPI UploadFile, Flask request.files, Django request.FILES) are described on the request span — name, type and size only; this follows SIGIL_CAPTURE_USER. sigil.input.chars is the whole prompt's length before any cutting.
  • Handoffs between agents. sigil.handoff.from lists the earlier spans (<trace id>-<span id>) whose answer this call's input carries (at least 30% of it) — one agent's output becoming another's input. sigil.handoff.in / sigil.handoff.out / sigil.handoff.in_max are hashed fingerprints of the input and answer, so your backend can find handoffs across processes and hours (a person reviewing in between). Ids, sites, file descriptions and fingerprints are sent even with SIGIL_CAPTURE_CONTENT=false; no text is.

What's New in v0.3.0

  • Automatic user identity capture — Extracts the calling user from Azure AD / SSO headers or JWT Bearer tokens on every request. No code changes needed — works out of the box with init(). Disable with SIGIL_CAPTURE_USER=false.
  • Worker agent support — Set SIGIL_USER_ID env var for scheduled jobs and queue workers that don't have HTTP requests.
  • Background work stays in its run (0.5.2) — LLM calls made on a ThreadPoolExecutor or Thread after the response has been sent keep the trace and user of the request that started them. Before 0.5.2 each became its own orphan trace with no user.

What's New in v0.2.0

  • 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).
  • 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.
  • Health check exclusion — Routes like /health, /healthz, /ready, /alive, /ping are excluded from tracing entirely.
  • Graceful shutdown — atexit handler flushes all pending spans when the process exits, so you never lose the last batch.
  • Lighter install — Removed unnecessary dependencies from the core install.

Full Example

Here's a real agent that summarizes documents using Claude:

1. The Agent Code

# document_summarizer.py
import anthropic
from sigil_telemetry import init

# Initialize telemetry — call this ONCE at startup
init()

# Your normal agent code — no changes needed
client = anthropic.Anthropic()
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Summarize this document: ..."}
    ]
)
print(response.content[0].text)

2. What Gets Captured (Per LLM Call)

Every time client.messages.create() runs, a span is automatically created with:

Field Example Value Description
operation_Id a1b2c3d4e5f6... Trace ID — groups all LLM calls in a single agent run
sigil.agent.id doc-summarizer Which agent made the call
sigil.agent.version 0.1.0 Agent version
sigil.agent.frameworks Anthropic,FastAPI Which SDKs and frameworks were detected
sigil.user.id jane.doe@example.com Who triggered this run (auto-captured)
gen_ai.system anthropic LLM provider
gen_ai.request.model claude-sonnet-4-20250514 Model used
gen_ai.usage.input_tokens 1250 Tokens sent
gen_ai.usage.output_tokens 340 Tokens received
duration 2.3s How long the call took
status OK or ERROR Whether the call succeeded
sigil.environment production Environment
sigil.agent.division Sales Business division (if set)
sigil.agent.risk_classification low Risk level (if set)

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.

3. How Trace Grouping Works

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.

Worker agents (scheduled jobs, listeners): Wrap your main() function in a custom span. All LLM calls within one job share the same trace_id.

In both cases: COUNT(DISTINCT trace_id) = number of agent runs.

4. Where the Data Goes

Agent makes LLM call
        │
        ▼
sigil-telemetry auto-captures it as an OpenTelemetry span
(noise spans like "http send" are filtered out here)
        │
        ▼
Span is batched and sent via OTLP to:
  → Your configured collector endpoint
        │
        ▼
Collector forwards to:
  → Your observability backend (Jaeger, Zipkin, Datadog, etc.)

User Identity Capture

v0.3.0 automatically captures who triggered each agent run. No code changes needed.

How it works:

Auth Method How Identity Is Captured
Azure AD / SSO (Easy Auth) Reads X-MS-CLIENT-PRINCIPAL-NAME header automatically
Azure AD / SSO (in-app JWT) Decodes Bearer token, extracts preferred_username → upn → email → name → oid
Front end passes the user to a backend Reads X-Sigil-User (recommended), common proxy headers (X-Forwarded-Email, X-Auth-Request-Email, X-User-Email, …), and, as a last resort, any header whose name mentions user/email and whose value is an email (e.g. X-Portal-User-Email)
Unusual header name Set SIGIL_USER_HEADER to the header's name
Worker agents (no HTTP) Reads SIGIL_USER_ID env var

The captured user ID is attached as sigil.user.id on every request span. All LLM calls within that request inherit the user context through the shared trace.

Disable user capture:

SIGIL_CAPTURE_USER=false

Or in code:

init(SigilConfig(capture_user=False))

Supported SDKs

Use [all] to install everything. Only the SDKs your agent actually uses get activated.

SDK Install Extra What It Covers
Anthropic [anthropic] Anthropic API
OpenAI [openai] OpenAI API (including compatible endpoints)
LangChain [langchain] LangChain, LangGraph, any LangChain-wrapped model
CrewAI [crewai] CrewAI multi-agent framework
LlamaIndex [llamaindex] LlamaIndex agents and pipelines
Vertex AI [vertexai] Google Vertex AI, Gemini models
Mistral AI [mistral] Mistral API
AWS Bedrock [bedrock] Claude, Llama, Titan via AWS
LiteLLM [litellm] Unified proxy across 100+ LLM providers

Anything else — raw HTTP, an SDK without an instrumentor, a self-hosted or gateway endpoint — is recorded by transport capture with no extra install: model, tokens, prompt, answer, latency and errors (no SDK-level detail such as tool calls). Those spans carry sigil.capture.source = transport.

Not covered: agents that are not written in Python, low-code platforms, models running inside the process (transformers, llama-cpp), Batch APIs (usage arrives hours later in a file), and gateways whose responses don't carry token usage (the call is recorded without tokens when the host is a known model host).

Web Framework Auto-Instrumentation

These are included in [all] and auto-detected by init():

Framework Install Extra What It Does
FastAPI [fastapi] Creates root span per HTTP request — all LLM calls in that request share one trace_id
Flask [flask] Same trace grouping for Flask apps
Django [django] Same trace grouping for Django apps
Celery [celery] One trace per task, linked to the code that queued it

Health check routes (/health, /healthz, /ready, /alive, /ping, /startup, /liveness, /readiness) are automatically excluded from tracing.

Configuration

Env Variable Default Description
SIGIL_AGENT_ID — Required. Your agent's unique ID
SIGIL_AGENT_VERSION 0.1.0 Track deployments
SIGIL_COLLECTOR_URL — Required. Your collector endpoint URL
SIGIL_ENVIRONMENT production production, staging, development
SIGIL_CONSOLE_EXPORT false Print spans to console for debugging
SIGIL_DIVISION — Business division (e.g., Sales, Engineering)
SIGIL_RISK_CLASSIFICATION — Agent risk level (low, medium, high)
SIGIL_HOURS_SAVED — Estimated hours saved per run
SIGIL_CAPTURE_USER true Set false to disable automatic user identity capture
SIGIL_USER_ID — Static user ID for worker agents with no HTTP context
SIGIL_USER_HEADER — Name of the request header that carries the signed-in user, when it isn't one the package already knows
SIGIL_PROPAGATE_CONTEXT true Set false to stop background threads and processes inheriting the request's trace and user
SIGIL_CAPTURE_CONTENT true Set false to stop recording prompts and answers
SIGIL_CAPTURE_TRANSPORT true Set false to record only calls made through an instrumented SDK
SIGIL_CAPTURE_API true Set false to stop recording the agent's other outside calls (APIs, web search, email)
SIGIL_API_SKIP_HOSTS — Comma-separated hosts never recorded as outside calls; a leading dot matches subdomains (.internal.example)
SIGIL_MAX_CONTENT_CHARS 8000 Longest string attribute sent. Can be lowered, never raised past 8000 (Application Insights cuts or drops a property over 8192)

Or pass config in code:

from sigil_telemetry import init, SigilConfig

init(SigilConfig(
    agent_id="my-agent",
    environment="development",
    console_export=True,
    capture_user=True
))

Custom Spans

Track things beyond LLM calls (document parsing, tool use, etc.):

from sigil_telemetry import get_tracer, record_error

tracer = get_tracer()

with tracer.start_as_current_span("parse-contract") as span:
    span.set_attribute("document.pages", 42)
    try:
        result = parse_pdf(file)
    except Exception as e:
        record_error(span, e)
        raise

Metadata

Release files for sigil-telemetry 0.6.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sigil-telemetry 0.6.2
File Size Uploaded Provenance
sigil_telemetry-0.6.2.tar.gz 78.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for sigil-telemetry 0.6.2
File Interpreter ABI Platform
sigil_telemetry-0.6.2-py3-none-any.whl Python 3 none any Details

Total release size: 156.1 kB

Release files / sigil_telemetry-0.6.2.tar.gz

Download URL sigil_telemetry-0.6.2.tar.gz
Size 78.6 kB
Tags Source
SHA-256 checksum
How to use checksums
1888d56a323441d54ed84b678c34b24d7345d1cc283fd84eae633f04296498cd
BLAKE2b-256 checksum
How to use checksums
e6abaeced1e2e793bb5389a759a4da0bf12fe7f4f432ab69062be19a43466e94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Release files / sigil_telemetry-0.6.2-py3-none-any.whl

Download URL sigil_telemetry-0.6.2-py3-none-any.whl
Size 77.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
28687352621d19303bb9610734749cae964d322b0401340e9241cb8081a86993
BLAKE2b-256 checksum
How to use checksums
2f8a3fe828258b6bfbbb8f8662e511830a4a78b470a598d0b3902191b020293a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.0

Security for sigil-telemetry 0.6.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page