kalibr 1.0.28__tar.gz → 1.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kalibr-1.1.0/LICENSE +21 -0
- kalibr-1.1.0/PKG-INFO +97 -0
- kalibr-1.1.0/README.md +54 -0
- kalibr-1.1.0/kalibr/__init__.py +172 -0
- kalibr-1.1.0/kalibr/__main__.py +6 -0
- kalibr-1.1.0/kalibr/capsule_middleware.py +108 -0
- kalibr-1.1.0/kalibr/cli/__init__.py +5 -0
- kalibr-1.1.0/kalibr/cli/capsule_cmd.py +174 -0
- kalibr-1.1.0/kalibr/cli/deploy_cmd.py +114 -0
- kalibr-1.1.0/kalibr/cli/main.py +67 -0
- kalibr-1.1.0/kalibr/cli/run.py +200 -0
- kalibr-1.1.0/kalibr/cli/serve.py +59 -0
- kalibr-1.1.0/kalibr/client.py +293 -0
- kalibr-1.1.0/kalibr/collector.py +173 -0
- kalibr-1.1.0/kalibr/context.py +132 -0
- kalibr-1.1.0/kalibr/cost_adapter.py +222 -0
- kalibr-1.1.0/kalibr/decorators.py +140 -0
- kalibr-1.1.0/kalibr/instrumentation/__init__.py +13 -0
- kalibr-1.1.0/kalibr/instrumentation/anthropic_instr.py +282 -0
- kalibr-1.1.0/kalibr/instrumentation/base.py +108 -0
- kalibr-1.1.0/kalibr/instrumentation/google_instr.py +281 -0
- kalibr-1.1.0/kalibr/instrumentation/openai_instr.py +265 -0
- kalibr-1.1.0/kalibr/instrumentation/registry.py +153 -0
- kalibr-1.1.0/kalibr/kalibr.py +173 -0
- kalibr-1.1.0/kalibr/kalibr_app.py +82 -0
- kalibr-1.1.0/kalibr/middleware/__init__.py +5 -0
- kalibr-1.1.0/kalibr/middleware/auto_tracer.py +356 -0
- kalibr-1.1.0/kalibr/models.py +41 -0
- kalibr-1.1.0/kalibr/redaction.py +44 -0
- kalibr-1.1.0/kalibr/schemas.py +116 -0
- kalibr-1.1.0/kalibr/simple_tracer.py +255 -0
- kalibr-1.1.0/kalibr/tokens.py +52 -0
- kalibr-1.1.0/kalibr/trace_capsule.py +296 -0
- kalibr-1.1.0/kalibr/trace_models.py +201 -0
- kalibr-1.1.0/kalibr/tracer.py +354 -0
- kalibr-1.1.0/kalibr/types.py +38 -0
- kalibr-1.1.0/kalibr/utils.py +198 -0
- kalibr-1.1.0/kalibr.egg-info/PKG-INFO +97 -0
- kalibr-1.1.0/kalibr.egg-info/SOURCES.txt +45 -0
- kalibr-1.1.0/kalibr.egg-info/entry_points.txt +2 -0
- kalibr-1.1.0/kalibr.egg-info/requires.txt +17 -0
- kalibr-1.1.0/pyproject.toml +75 -0
- kalibr-1.1.0/tests/test_capsule_builder.py +326 -0
- kalibr-1.1.0/tests/test_instrumentation.py +235 -0
- kalibr-1.0.28/LICENSE +0 -11
- kalibr-1.0.28/MANIFEST.in +0 -3
- kalibr-1.0.28/PKG-INFO +0 -175
- kalibr-1.0.28/README.md +0 -152
- kalibr-1.0.28/examples/README.md +0 -173
- kalibr-1.0.28/examples/__init__.py +0 -1
- kalibr-1.0.28/examples/basic_kalibr_example.py +0 -66
- kalibr-1.0.28/examples/enhanced_kalibr_example.py +0 -347
- kalibr-1.0.28/kalibr/__init__.py +0 -5
- kalibr-1.0.28/kalibr/__main__.py +0 -206
- kalibr-1.0.28/kalibr/deployment.py +0 -41
- kalibr-1.0.28/kalibr/kalibr.py +0 -259
- kalibr-1.0.28/kalibr/kalibr_app.py +0 -343
- kalibr-1.0.28/kalibr/packager.py +0 -43
- kalibr-1.0.28/kalibr/runtime_router.py +0 -138
- kalibr-1.0.28/kalibr/schema_generators.py +0 -159
- kalibr-1.0.28/kalibr/types.py +0 -106
- kalibr-1.0.28/kalibr/validator.py +0 -70
- kalibr-1.0.28/kalibr.egg-info/PKG-INFO +0 -175
- kalibr-1.0.28/kalibr.egg-info/SOURCES.txt +0 -25
- kalibr-1.0.28/kalibr.egg-info/entry_points.txt +0 -2
- kalibr-1.0.28/kalibr.egg-info/requires.txt +0 -7
- kalibr-1.0.28/pyproject.toml +0 -22
- kalibr-1.0.28/setup.py +0 -42
- {kalibr-1.0.28 → kalibr-1.1.0}/kalibr.egg-info/dependency_links.txt +0 -0
- {kalibr-1.0.28 → kalibr-1.1.0}/kalibr.egg-info/top_level.txt +0 -0
- {kalibr-1.0.28 → kalibr-1.1.0}/setup.cfg +0 -0
kalibr-1.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Kalibr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
kalibr-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kalibr
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Unified LLM Observability & Multi-Model AI Integration Framework - Deploy to GPT, Claude, Gemini, Copilot with full telemetry
|
|
5
|
+
Author-email: Kalibr Team <team@kalibr.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/devonakelley/kalibr-sdk
|
|
8
|
+
Project-URL: Documentation, https://github.com/devonakelley/kalibr-sdk#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/devonakelley/kalibr-sdk
|
|
10
|
+
Project-URL: Issues, https://github.com/devonakelley/kalibr-sdk/issues
|
|
11
|
+
Keywords: ai,mcp,gpt,claude,gemini,copilot,openai,anthropic,google,microsoft,observability,telemetry,tracing,llm,schema-generation,api,multi-model
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Requires-Dist: tiktoken>=0.8.0
|
|
28
|
+
Requires-Dist: fastapi>=0.110.1
|
|
29
|
+
Requires-Dist: uvicorn>=0.25.0
|
|
30
|
+
Requires-Dist: pydantic>=2.6.4
|
|
31
|
+
Requires-Dist: typer>=0.9.0
|
|
32
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
33
|
+
Requires-Dist: rich>=10.0.0
|
|
34
|
+
Requires-Dist: requests>=2.31.0
|
|
35
|
+
Requires-Dist: opentelemetry-api>=1.20.0
|
|
36
|
+
Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
37
|
+
Requires-Dist: opentelemetry-exporter-otlp>=1.20.0
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
40
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
41
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# Kalibr Python SDK
|
|
45
|
+
|
|
46
|
+
Production-grade observability for LLM applications.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
```bash
|
|
50
|
+
pip install kalibr
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quickstart
|
|
54
|
+
```python
|
|
55
|
+
from kalibr import trace
|
|
56
|
+
import openai
|
|
57
|
+
|
|
58
|
+
@trace(api_key="your-kalibr-api-key")
|
|
59
|
+
def my_agent():
|
|
60
|
+
response = openai.chat.completions.create(
|
|
61
|
+
model="gpt-4",
|
|
62
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
63
|
+
)
|
|
64
|
+
return response
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Features
|
|
68
|
+
|
|
69
|
+
- ✅ Zero-code instrumentation for OpenAI, Anthropic, Google AI
|
|
70
|
+
- ✅ Automatic parent-child trace relationships
|
|
71
|
+
- ✅ Real-time cost tracking
|
|
72
|
+
- ✅ Token usage monitoring
|
|
73
|
+
- ✅ Performance metrics
|
|
74
|
+
|
|
75
|
+
## CLI Tools
|
|
76
|
+
```bash
|
|
77
|
+
# Run your app locally
|
|
78
|
+
kalibr serve myapp.py
|
|
79
|
+
|
|
80
|
+
# Deploy to Fly.io
|
|
81
|
+
kalibr deploy myapp.py
|
|
82
|
+
|
|
83
|
+
# Fetch trace data
|
|
84
|
+
kalibr capsule <trace-id>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Examples
|
|
88
|
+
|
|
89
|
+
See `examples/` directory for complete examples.
|
|
90
|
+
|
|
91
|
+
## Documentation
|
|
92
|
+
|
|
93
|
+
Full docs at https://docs.kalibr.systems
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
kalibr-1.1.0/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Kalibr Python SDK
|
|
2
|
+
|
|
3
|
+
Production-grade observability for LLM applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
```bash
|
|
7
|
+
pip install kalibr
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quickstart
|
|
11
|
+
```python
|
|
12
|
+
from kalibr import trace
|
|
13
|
+
import openai
|
|
14
|
+
|
|
15
|
+
@trace(api_key="your-kalibr-api-key")
|
|
16
|
+
def my_agent():
|
|
17
|
+
response = openai.chat.completions.create(
|
|
18
|
+
model="gpt-4",
|
|
19
|
+
messages=[{"role": "user", "content": "Hello!"}]
|
|
20
|
+
)
|
|
21
|
+
return response
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- ✅ Zero-code instrumentation for OpenAI, Anthropic, Google AI
|
|
27
|
+
- ✅ Automatic parent-child trace relationships
|
|
28
|
+
- ✅ Real-time cost tracking
|
|
29
|
+
- ✅ Token usage monitoring
|
|
30
|
+
- ✅ Performance metrics
|
|
31
|
+
|
|
32
|
+
## CLI Tools
|
|
33
|
+
```bash
|
|
34
|
+
# Run your app locally
|
|
35
|
+
kalibr serve myapp.py
|
|
36
|
+
|
|
37
|
+
# Deploy to Fly.io
|
|
38
|
+
kalibr deploy myapp.py
|
|
39
|
+
|
|
40
|
+
# Fetch trace data
|
|
41
|
+
kalibr capsule <trace-id>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Examples
|
|
45
|
+
|
|
46
|
+
See `examples/` directory for complete examples.
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
Full docs at https://docs.kalibr.systems
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Kalibr SDK v1.1.0 - Unified LLM Observability & Multi-Model AI Integration Framework
|
|
2
|
+
|
|
3
|
+
This SDK combines:
|
|
4
|
+
1. Full LLM Observability with tracing, cost tracking, and analytics
|
|
5
|
+
2. Multi-Model AI Integration (GPT, Claude, Gemini, Copilot)
|
|
6
|
+
3. One-line deployment with Docker and runtime router
|
|
7
|
+
4. Schema generation for all major AI platforms
|
|
8
|
+
5. **NEW in 1.1.0**: Auto-instrumentation of LLM SDKs (OpenAI, Anthropic, Google)
|
|
9
|
+
|
|
10
|
+
Features:
|
|
11
|
+
- **Auto-Instrumentation**: Zero-config tracing of OpenAI, Anthropic, Google SDK calls
|
|
12
|
+
- **OpenTelemetry**: OTel-compatible spans with OTLP export
|
|
13
|
+
- **Tracing**: Complete telemetry with @trace decorator
|
|
14
|
+
- **Cost Tracking**: Multi-vendor cost calculation (OpenAI, Anthropic, etc.)
|
|
15
|
+
- **Deployment**: One-command deployment to Fly.io, Render, or local
|
|
16
|
+
- **Schema Generation**: Auto-generate schemas for GPT Actions, Claude MCP, Gemini, Copilot
|
|
17
|
+
- **Error Handling**: Automatic error capture with stack traces
|
|
18
|
+
- **Analytics**: ClickHouse-backed analytics and alerting
|
|
19
|
+
|
|
20
|
+
Usage - Auto-Instrumentation (NEW):
|
|
21
|
+
from kalibr import Kalibr
|
|
22
|
+
import openai # Automatically instrumented!
|
|
23
|
+
|
|
24
|
+
app = Kalibr(title="My API")
|
|
25
|
+
|
|
26
|
+
@app.action("chat", "Chat with GPT")
|
|
27
|
+
def chat(message: str):
|
|
28
|
+
# This OpenAI call is automatically traced!
|
|
29
|
+
response = openai.chat.completions.create(
|
|
30
|
+
model="gpt-4",
|
|
31
|
+
messages=[{"role": "user", "content": message}]
|
|
32
|
+
)
|
|
33
|
+
return response.choices[0].message.content
|
|
34
|
+
|
|
35
|
+
Usage - Manual Tracing:
|
|
36
|
+
from kalibr import trace
|
|
37
|
+
|
|
38
|
+
@trace(operation="chat_completion", vendor="openai", model="gpt-4")
|
|
39
|
+
def call_openai(prompt):
|
|
40
|
+
response = openai.chat.completions.create(
|
|
41
|
+
model="gpt-4",
|
|
42
|
+
messages=[{"role": "user", "content": prompt}]
|
|
43
|
+
)
|
|
44
|
+
return response
|
|
45
|
+
|
|
46
|
+
CLI Usage:
|
|
47
|
+
kalibr serve my_app.py # Run locally
|
|
48
|
+
kalibr deploy my_app.py --runtime fly # Deploy to Fly.io
|
|
49
|
+
kalibr run my_app.py # Run with auto-tracing
|
|
50
|
+
kalibr version # Show version
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
__version__ = "1.1.0"
|
|
54
|
+
|
|
55
|
+
# Auto-instrument LLM SDKs on import (can be disabled via env var)
|
|
56
|
+
import os
|
|
57
|
+
|
|
58
|
+
# ============================================================================
|
|
59
|
+
# OBSERVABILITY & TRACING (from 1023)
|
|
60
|
+
# ============================================================================
|
|
61
|
+
from .client import KalibrClient
|
|
62
|
+
|
|
63
|
+
# ============================================================================
|
|
64
|
+
# PHASE 1: SDK INSTRUMENTATION & OPENTELEMETRY (v1.1.0)
|
|
65
|
+
# ============================================================================
|
|
66
|
+
from .collector import (
|
|
67
|
+
get_tracer_provider,
|
|
68
|
+
)
|
|
69
|
+
from .collector import is_configured as is_collector_configured
|
|
70
|
+
from .collector import (
|
|
71
|
+
setup_collector,
|
|
72
|
+
)
|
|
73
|
+
from .context import get_parent_span_id, get_trace_id, new_trace_id, trace_context
|
|
74
|
+
from .cost_adapter import (
|
|
75
|
+
AnthropicCostAdapter,
|
|
76
|
+
BaseCostAdapter,
|
|
77
|
+
CostAdapterFactory,
|
|
78
|
+
OpenAICostAdapter,
|
|
79
|
+
)
|
|
80
|
+
from .instrumentation import auto_instrument, get_instrumented_providers
|
|
81
|
+
|
|
82
|
+
# ============================================================================
|
|
83
|
+
# SDK & DEPLOYMENT (from 1.0.30)
|
|
84
|
+
# ============================================================================
|
|
85
|
+
from .kalibr import Kalibr
|
|
86
|
+
from .kalibr_app import KalibrApp
|
|
87
|
+
from .models import EventData, TraceConfig
|
|
88
|
+
from .schemas import (
|
|
89
|
+
generate_copilot_schema,
|
|
90
|
+
generate_gemini_schema,
|
|
91
|
+
generate_mcp_schema,
|
|
92
|
+
get_base_url,
|
|
93
|
+
get_supported_models,
|
|
94
|
+
)
|
|
95
|
+
from .simple_tracer import trace
|
|
96
|
+
from .trace_capsule import TraceCapsule, get_or_create_capsule
|
|
97
|
+
from .tracer import SpanContext, Tracer
|
|
98
|
+
from .types import FileUpload, Session
|
|
99
|
+
from .utils import load_config_from_env
|
|
100
|
+
|
|
101
|
+
if os.getenv("KALIBR_AUTO_INSTRUMENT", "true").lower() == "true":
|
|
102
|
+
# Setup OpenTelemetry collector
|
|
103
|
+
try:
|
|
104
|
+
setup_collector(
|
|
105
|
+
service_name=os.getenv("KALIBR_SERVICE_NAME", "kalibr"),
|
|
106
|
+
file_export=True,
|
|
107
|
+
console_export=os.getenv("KALIBR_CONSOLE_EXPORT", "false").lower() == "true",
|
|
108
|
+
)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
print(f"⚠️ Failed to setup OpenTelemetry collector: {e}")
|
|
111
|
+
|
|
112
|
+
# Auto-instrument available SDKs
|
|
113
|
+
try:
|
|
114
|
+
auto_instrument(["openai", "anthropic", "google"])
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(f"⚠️ Failed to auto-instrument SDKs: {e}")
|
|
117
|
+
|
|
118
|
+
__all__ = [
|
|
119
|
+
# ========================================================================
|
|
120
|
+
# OBSERVABILITY & TRACING
|
|
121
|
+
# ========================================================================
|
|
122
|
+
# Simple tracing API (recommended)
|
|
123
|
+
"trace",
|
|
124
|
+
# Capsule propagation (Phase 6)
|
|
125
|
+
"TraceCapsule",
|
|
126
|
+
"get_or_create_capsule",
|
|
127
|
+
# Client
|
|
128
|
+
"KalibrClient",
|
|
129
|
+
# Context
|
|
130
|
+
"trace_context",
|
|
131
|
+
"get_trace_id",
|
|
132
|
+
"get_parent_span_id",
|
|
133
|
+
"new_trace_id",
|
|
134
|
+
# Tracer
|
|
135
|
+
"Tracer",
|
|
136
|
+
"SpanContext",
|
|
137
|
+
# Cost Adapters
|
|
138
|
+
"BaseCostAdapter",
|
|
139
|
+
"OpenAICostAdapter",
|
|
140
|
+
"AnthropicCostAdapter",
|
|
141
|
+
"CostAdapterFactory",
|
|
142
|
+
# Models
|
|
143
|
+
"TraceConfig",
|
|
144
|
+
"EventData",
|
|
145
|
+
# Utils
|
|
146
|
+
"load_config_from_env",
|
|
147
|
+
# ========================================================================
|
|
148
|
+
# SDK & DEPLOYMENT
|
|
149
|
+
# ========================================================================
|
|
150
|
+
# SDK Classes
|
|
151
|
+
"Kalibr",
|
|
152
|
+
"KalibrApp",
|
|
153
|
+
# Types
|
|
154
|
+
"FileUpload",
|
|
155
|
+
"Session",
|
|
156
|
+
# Schema Generation
|
|
157
|
+
"get_base_url",
|
|
158
|
+
"generate_mcp_schema",
|
|
159
|
+
"generate_gemini_schema",
|
|
160
|
+
"generate_copilot_schema",
|
|
161
|
+
"get_supported_models",
|
|
162
|
+
# ========================================================================
|
|
163
|
+
# PHASE 1: SDK INSTRUMENTATION & OPENTELEMETRY (v1.1.0)
|
|
164
|
+
# ========================================================================
|
|
165
|
+
# Auto-instrumentation
|
|
166
|
+
"auto_instrument",
|
|
167
|
+
"get_instrumented_providers",
|
|
168
|
+
# OpenTelemetry collector
|
|
169
|
+
"setup_collector",
|
|
170
|
+
"get_tracer_provider",
|
|
171
|
+
"is_collector_configured",
|
|
172
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kalibr Capsule Middleware for FastAPI.
|
|
3
|
+
|
|
4
|
+
Automatically extracts, propagates, and injects trace capsules in HTTP requests.
|
|
5
|
+
|
|
6
|
+
Usage in FastAPI app:
|
|
7
|
+
from kalibr.capsule_middleware import add_capsule_middleware
|
|
8
|
+
|
|
9
|
+
app = FastAPI()
|
|
10
|
+
add_capsule_middleware(app)
|
|
11
|
+
|
|
12
|
+
# Now all requests have request.state.capsule available
|
|
13
|
+
@app.get("/")
|
|
14
|
+
def endpoint(request: Request):
|
|
15
|
+
capsule = request.state.capsule
|
|
16
|
+
# Use capsule...
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI, Request
|
|
22
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
23
|
+
from starlette.responses import Response
|
|
24
|
+
|
|
25
|
+
from .trace_capsule import TraceCapsule, get_or_create_capsule
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
CAPSULE_HEADER = "X-Kalibr-Capsule"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CapsuleMiddleware(BaseHTTPMiddleware):
|
|
33
|
+
"""Middleware that extracts and propagates Kalibr trace capsules.
|
|
34
|
+
|
|
35
|
+
This middleware:
|
|
36
|
+
1. Extracts capsule from incoming X-Kalibr-Capsule header
|
|
37
|
+
2. Attaches capsule to request.state for access in endpoints
|
|
38
|
+
3. Automatically injects updated capsule in response headers
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
async def dispatch(self, request: Request, call_next):
|
|
42
|
+
"""Process request and response with capsule handling.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
request: Incoming HTTP request
|
|
46
|
+
call_next: Next middleware/endpoint in chain
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Response with X-Kalibr-Capsule header attached
|
|
50
|
+
"""
|
|
51
|
+
# Extract capsule from header (or create new one)
|
|
52
|
+
capsule_header = request.headers.get(CAPSULE_HEADER)
|
|
53
|
+
|
|
54
|
+
if capsule_header:
|
|
55
|
+
capsule = TraceCapsule.from_json(capsule_header)
|
|
56
|
+
logger.debug(f"📦 Received capsule: {capsule}")
|
|
57
|
+
else:
|
|
58
|
+
capsule = TraceCapsule()
|
|
59
|
+
logger.debug(f"📦 Created new capsule: {capsule.trace_id}")
|
|
60
|
+
|
|
61
|
+
# Attach capsule to request state
|
|
62
|
+
request.state.capsule = capsule
|
|
63
|
+
|
|
64
|
+
# Process request
|
|
65
|
+
response = await call_next(request)
|
|
66
|
+
|
|
67
|
+
# Inject updated capsule in response headers
|
|
68
|
+
try:
|
|
69
|
+
capsule_json = capsule.to_json()
|
|
70
|
+
response.headers[CAPSULE_HEADER] = capsule_json
|
|
71
|
+
logger.debug(f"📦 Sending capsule: {capsule}")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
logger.warning(f"⚠️ Failed to serialize capsule: {e}")
|
|
74
|
+
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def add_capsule_middleware(app: FastAPI) -> None:
|
|
79
|
+
"""Add capsule middleware to FastAPI application.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
app: FastAPI application instance
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
app = FastAPI()
|
|
86
|
+
add_capsule_middleware(app)
|
|
87
|
+
"""
|
|
88
|
+
app.add_middleware(CapsuleMiddleware)
|
|
89
|
+
logger.info("✅ Kalibr Capsule middleware added")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_capsule(request: Request) -> TraceCapsule:
|
|
93
|
+
"""Get capsule from request state (convenience function).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
request: FastAPI Request object
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
TraceCapsule attached to request
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
AttributeError: If middleware not installed
|
|
103
|
+
"""
|
|
104
|
+
if not hasattr(request.state, "capsule"):
|
|
105
|
+
logger.warning("⚠️ Capsule middleware not installed, creating new capsule")
|
|
106
|
+
request.state.capsule = TraceCapsule()
|
|
107
|
+
|
|
108
|
+
return request.state.capsule
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Capsule Reconstruction CLI Command
|
|
3
|
+
Fetch and export trace capsules from Kalibr Platform
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import typer
|
|
13
|
+
from rich import print as rprint
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def capsule(
|
|
21
|
+
trace_id: str = typer.Argument(..., help="Trace ID to reconstruct capsule for"),
|
|
22
|
+
api_url: Optional[str] = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--api-url",
|
|
25
|
+
"-u",
|
|
26
|
+
help="Kalibr API base URL (default: from env KALIBR_API_URL or http://localhost:8001)",
|
|
27
|
+
envvar="KALIBR_API_URL",
|
|
28
|
+
),
|
|
29
|
+
output: Optional[Path] = typer.Option(
|
|
30
|
+
None,
|
|
31
|
+
"--output",
|
|
32
|
+
"-o",
|
|
33
|
+
help="Output file path (JSON format). If not specified, prints to stdout.",
|
|
34
|
+
),
|
|
35
|
+
export: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--export",
|
|
38
|
+
"-e",
|
|
39
|
+
help="Use export endpoint to download as file",
|
|
40
|
+
),
|
|
41
|
+
pretty: bool = typer.Option(
|
|
42
|
+
True,
|
|
43
|
+
"--pretty/--no-pretty",
|
|
44
|
+
"-p/-np",
|
|
45
|
+
help="Pretty print JSON output",
|
|
46
|
+
),
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Reconstruct and fetch a trace capsule by trace_id.
|
|
50
|
+
|
|
51
|
+
The capsule contains all linked trace events, aggregated metrics,
|
|
52
|
+
and metadata for a complete execution chain.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
|
|
56
|
+
# Fetch capsule and display in terminal
|
|
57
|
+
kalibr capsule abc-123-def
|
|
58
|
+
|
|
59
|
+
# Save capsule to file
|
|
60
|
+
kalibr capsule abc-123-def --output capsule.json
|
|
61
|
+
|
|
62
|
+
# Use export endpoint
|
|
63
|
+
kalibr capsule abc-123-def --export --output capsule.json
|
|
64
|
+
|
|
65
|
+
# Specify custom API URL
|
|
66
|
+
kalibr capsule abc-123-def -u https://api.kalibr.io
|
|
67
|
+
"""
|
|
68
|
+
# Determine API base URL
|
|
69
|
+
base_url = api_url or "http://localhost:8001"
|
|
70
|
+
base_url = base_url.rstrip("/")
|
|
71
|
+
|
|
72
|
+
# Build endpoint URL
|
|
73
|
+
if export:
|
|
74
|
+
endpoint = f"{base_url}/api/capsule/{trace_id}/export"
|
|
75
|
+
else:
|
|
76
|
+
endpoint = f"{base_url}/api/capsule/{trace_id}"
|
|
77
|
+
|
|
78
|
+
console.print(f"[cyan]Fetching capsule for trace_id:[/cyan] [bold]{trace_id}[/bold]")
|
|
79
|
+
console.print(f"[dim]Endpoint: {endpoint}[/dim]")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Make API request
|
|
83
|
+
response = requests.get(endpoint, timeout=30)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
|
|
86
|
+
# Parse response
|
|
87
|
+
capsule_data = response.json()
|
|
88
|
+
|
|
89
|
+
# Display summary
|
|
90
|
+
console.print("\n[green]✓ Capsule reconstructed successfully[/green]\n")
|
|
91
|
+
|
|
92
|
+
# Create summary table
|
|
93
|
+
table = Table(title="Capsule Summary", show_header=False)
|
|
94
|
+
table.add_column("Field", style="cyan", no_wrap=True)
|
|
95
|
+
table.add_column("Value", style="white")
|
|
96
|
+
|
|
97
|
+
table.add_row("Capsule ID", capsule_data.get("capsule_id", "N/A"))
|
|
98
|
+
table.add_row("Total Cost (USD)", f"${capsule_data.get('total_cost_usd', 0):.6f}")
|
|
99
|
+
table.add_row("Total Latency (ms)", str(capsule_data.get("total_latency_ms", 0)))
|
|
100
|
+
table.add_row("Hop Count", str(capsule_data.get("hop_count", 0)))
|
|
101
|
+
table.add_row("Providers", ", ".join(capsule_data.get("providers", [])))
|
|
102
|
+
table.add_row("Reconstructed At", capsule_data.get("reconstructed_at", "N/A"))
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
|
|
106
|
+
# Output to file or stdout
|
|
107
|
+
if output:
|
|
108
|
+
output_path = Path(output)
|
|
109
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
with open(output_path, "w") as f:
|
|
112
|
+
if pretty:
|
|
113
|
+
json.dump(capsule_data, f, indent=2, sort_keys=True)
|
|
114
|
+
else:
|
|
115
|
+
json.dump(capsule_data, f)
|
|
116
|
+
|
|
117
|
+
console.print(f"\n[green]✓ Capsule saved to:[/green] {output_path}")
|
|
118
|
+
else:
|
|
119
|
+
# Print to stdout
|
|
120
|
+
console.print("\n[bold]Capsule Data:[/bold]")
|
|
121
|
+
if pretty:
|
|
122
|
+
rprint(json.dumps(capsule_data, indent=2, sort_keys=True))
|
|
123
|
+
else:
|
|
124
|
+
print(json.dumps(capsule_data))
|
|
125
|
+
|
|
126
|
+
# Display event details
|
|
127
|
+
events = capsule_data.get("events", [])
|
|
128
|
+
if events:
|
|
129
|
+
console.print(f"\n[bold]Events ({len(events)}):[/bold]")
|
|
130
|
+
event_table = Table()
|
|
131
|
+
event_table.add_column("Trace ID", style="cyan")
|
|
132
|
+
event_table.add_column("Provider", style="yellow")
|
|
133
|
+
event_table.add_column("Model", style="magenta")
|
|
134
|
+
event_table.add_column("Operation", style="blue")
|
|
135
|
+
event_table.add_column("Duration (ms)", justify="right", style="green")
|
|
136
|
+
event_table.add_column("Cost (USD)", justify="right", style="green")
|
|
137
|
+
event_table.add_column("Status", style="white")
|
|
138
|
+
|
|
139
|
+
for event in events:
|
|
140
|
+
event_table.add_row(
|
|
141
|
+
event.get("trace_id", "")[:12] + "...",
|
|
142
|
+
event.get("provider", "N/A"),
|
|
143
|
+
event.get("model_id", "N/A")[:20],
|
|
144
|
+
event.get("operation", "N/A"),
|
|
145
|
+
str(event.get("duration_ms", 0)),
|
|
146
|
+
f"${event.get('cost_usd', 0):.6f}",
|
|
147
|
+
event.get("status", "N/A"),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
console.print(event_table)
|
|
151
|
+
|
|
152
|
+
return 0
|
|
153
|
+
|
|
154
|
+
except requests.exceptions.HTTPError as e:
|
|
155
|
+
if e.response.status_code == 404:
|
|
156
|
+
console.print(f"[red]✗ Capsule not found for trace_id: {trace_id}[/red]")
|
|
157
|
+
console.print("[yellow]Make sure the trace_id exists and has been ingested.[/yellow]")
|
|
158
|
+
else:
|
|
159
|
+
console.print(f"[red]✗ API Error ({e.response.status_code}):[/red] {e.response.text}")
|
|
160
|
+
return 1
|
|
161
|
+
|
|
162
|
+
except requests.exceptions.ConnectionError:
|
|
163
|
+
console.print(f"[red]✗ Connection Error:[/red] Unable to connect to {base_url}")
|
|
164
|
+
console.print("[yellow]Make sure the Kalibr backend is running and accessible.[/yellow]")
|
|
165
|
+
return 1
|
|
166
|
+
|
|
167
|
+
except requests.exceptions.Timeout:
|
|
168
|
+
console.print("[red]✗ Timeout:[/red] Request took too long to complete")
|
|
169
|
+
return 1
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
console.print(f"[red]✗ Unexpected Error:[/red] {str(e)}")
|
|
173
|
+
console.print("[yellow]Run with --help for usage information[/yellow]")
|
|
174
|
+
return 1
|