ignis-observability 0.2.0__py3-none-any.whl
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.
- ignis_observability/__init__.py +34 -0
- ignis_observability/config.py +99 -0
- ignis_observability/database.py +135 -0
- ignis_observability/engine.py +787 -0
- ignis_observability/providers/__init__.py +1 -0
- ignis_observability/providers/anthropic.py +138 -0
- ignis_observability/providers/gemini.py +138 -0
- ignis_observability/providers/ingestion/__init__.py +23 -0
- ignis_observability/providers/ingestion/arize.py +375 -0
- ignis_observability/providers/ingestion/base.py +14 -0
- ignis_observability/providers/ingestion/field_mapping.py +27 -0
- ignis_observability/providers/ingestion/laminar.py +47 -0
- ignis_observability/providers/ingestion/langfuse.py +46 -0
- ignis_observability/providers/ingestion/langsmith.py +641 -0
- ignis_observability/providers/ingestion/langwatch.py +294 -0
- ignis_observability/providers/ingestion/otel.py +232 -0
- ignis_observability/providers/openai.py +139 -0
- ignis_observability/providers/sync/__init__.py +1 -0
- ignis_observability/providers/sync/__main__.py +4 -0
- ignis_observability/providers/sync/arize_adapter.py +149 -0
- ignis_observability/providers/sync/base_adapter.py +130 -0
- ignis_observability/providers/sync/db_writer.py +319 -0
- ignis_observability/providers/sync/field_mapping.py +116 -0
- ignis_observability/providers/sync/langsmith_adapter.py +197 -0
- ignis_observability/providers/sync/langsmith_sync.py +304 -0
- ignis_observability/providers/sync/langwatch_adapter.py +259 -0
- ignis_observability/providers/sync/pull_from_tools.py +1186 -0
- ignis_observability/providers/sync/run_pull_scheduler.py +28 -0
- ignis_observability/py.typed +0 -0
- ignis_observability/trace_builder.py +240 -0
- ignis_observability-0.2.0.dist-info/METADATA +549 -0
- ignis_observability-0.2.0.dist-info/RECORD +35 -0
- ignis_observability-0.2.0.dist-info/WHEEL +5 -0
- ignis_observability-0.2.0.dist-info/licenses/LICENSE +21 -0
- ignis_observability-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ignis_observability — Non-Blocking AI Observability Hub for multi-model LLM applications.
|
|
3
|
+
|
|
4
|
+
Quick start::
|
|
5
|
+
|
|
6
|
+
from ignis_observability import ObsHub, DBManager, settings
|
|
7
|
+
|
|
8
|
+
db = DBManager(settings.DATABASE_URL)
|
|
9
|
+
await db.init_db()
|
|
10
|
+
await db.seed_prices()
|
|
11
|
+
|
|
12
|
+
hub = ObsHub(langsmith_api_key=settings.LANGCHAIN_API_KEY, db_manager=db)
|
|
13
|
+
|
|
14
|
+
@hub.observe(name="my_llm_call")
|
|
15
|
+
async def call_llm(prompt: str):
|
|
16
|
+
...
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .config import settings
|
|
20
|
+
from .database import DBManager, UnifiedTrace, ModelPrice
|
|
21
|
+
from .engine import ObsHub
|
|
22
|
+
from .trace_builder import TraceBuilder
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.0"
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"ObsHub",
|
|
28
|
+
"DBManager",
|
|
29
|
+
"UnifiedTrace",
|
|
30
|
+
"ModelPrice",
|
|
31
|
+
"TraceBuilder",
|
|
32
|
+
"settings",
|
|
33
|
+
"__version__",
|
|
34
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
#APPLICATION NAME
|
|
6
|
+
APPLICATION_NAME: str = "ignis"
|
|
7
|
+
USER_ID: str = ""
|
|
8
|
+
|
|
9
|
+
# OpenAI Configuration
|
|
10
|
+
OPENAI_API_KEY: str = ""
|
|
11
|
+
# Other LLM providers (optional)
|
|
12
|
+
ANTHROPIC_API_KEY: str = ""
|
|
13
|
+
GEMINI_API_KEY: str = ""
|
|
14
|
+
|
|
15
|
+
# LangSmith Configuration
|
|
16
|
+
LANGCHAIN_API_KEY: str = ""
|
|
17
|
+
LANGCHAIN_ENDPOINT: str = "https://api.smith.langchain.com"
|
|
18
|
+
LANGCHAIN_PROJECT: str = "default"
|
|
19
|
+
LANGCHAIN_PROJECT_ID: str = ""
|
|
20
|
+
LANGCHAIN_TRACING_V2: bool = True
|
|
21
|
+
|
|
22
|
+
# Database Configuration
|
|
23
|
+
DATABASE_URL: str = "sqlite+aiosqlite:///./obs_hub.db"
|
|
24
|
+
|
|
25
|
+
# Sync Configuration
|
|
26
|
+
SYNC_INTERVAL: int = 60 # Sync every 60 seconds
|
|
27
|
+
|
|
28
|
+
# Logging
|
|
29
|
+
LOG_LEVEL: str = "info"
|
|
30
|
+
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
# Environment & Request Context
|
|
33
|
+
# ------------------------------------------------------------------
|
|
34
|
+
ENVIRONMENT: str = "development" # development, staging, production
|
|
35
|
+
SERVICE_NAME: str = "llm-app"
|
|
36
|
+
SERVICE_VERSION: str = "1.0.0"
|
|
37
|
+
DEFAULT_USER_ID: str = "" # Can be overridden per request
|
|
38
|
+
DEFAULT_SESSION_ID: str = "" # Can be overridden per request
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------
|
|
41
|
+
# Advanced Tracing Configuration (Optional)
|
|
42
|
+
# ------------------------------------------------------------------
|
|
43
|
+
# Master toggle for advanced tracing features
|
|
44
|
+
ADVANCED_TRACING_ENABLED: bool = True
|
|
45
|
+
|
|
46
|
+
# Individual feature toggles (only apply when ADVANCED_TRACING_ENABLED=True)
|
|
47
|
+
CAPTURE_REQUEST_ID: bool = True # Unique trace ID per request
|
|
48
|
+
CAPTURE_USER_SESSION: bool = True # User ID & Session ID tracking
|
|
49
|
+
CAPTURE_LLM_PARAMS: bool = True # temperature, top_p, max_tokens, etc.
|
|
50
|
+
CAPTURE_STATUS_ERROR: bool = True # Status & error information
|
|
51
|
+
CAPTURE_TAGS: bool = True # Custom tags for organization
|
|
52
|
+
CAPTURE_ENVIRONMENT: bool = True # Environment context
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
# Additional Observability Platform Configuration
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# LangWatch
|
|
58
|
+
LANGWATCH_API_KEY: str = ""
|
|
59
|
+
LANGWATCH_PROJECT: str = ""
|
|
60
|
+
LANGWATCH_ENDPOINT: str = ""
|
|
61
|
+
LANGWATCH_ENABLED: bool = False
|
|
62
|
+
|
|
63
|
+
# LangFuse
|
|
64
|
+
LANGFUSE_API_KEY: str = ""
|
|
65
|
+
LANGFUSE_DATASET: str = ""
|
|
66
|
+
LANGFUSE_ENDPOINT: str = ""
|
|
67
|
+
LANGFUSE_ENABLED: bool = False
|
|
68
|
+
|
|
69
|
+
# Arize Cloud
|
|
70
|
+
ARIZE_API_KEY: str = ""
|
|
71
|
+
ARIZE_SPACE_KEY: str = ""
|
|
72
|
+
ARIZE_MODEL_NAME: str = ""
|
|
73
|
+
ARIZE_ENDPOINT: str = ""
|
|
74
|
+
ARIZE_COLLECTOR_ENDPOINT: str = "https://otlp.arize.com"
|
|
75
|
+
ARIZE_PROJECT: str = ""
|
|
76
|
+
ARIZE_ENABLED: bool = False
|
|
77
|
+
|
|
78
|
+
# Arize Phoenix (open-source observability)
|
|
79
|
+
PHOENIX_API_KEY: str = ""
|
|
80
|
+
PHOENIX_COLLECTOR_ENDPOINT: str = "https://app.phoenix.arize.com"
|
|
81
|
+
|
|
82
|
+
# Laminar
|
|
83
|
+
LAMINAR_API_KEY: str = ""
|
|
84
|
+
LAMINAR_PROJECT: str = ""
|
|
85
|
+
LAMINAR_ENDPOINT: str = ""
|
|
86
|
+
LAMINAR_ENABLED: bool = False
|
|
87
|
+
|
|
88
|
+
# OpenTelemetry (OTel)
|
|
89
|
+
OTEL_ENABLED: bool = False
|
|
90
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: str = "http://localhost:4317"
|
|
91
|
+
OTEL_SERVICE_NAME: str = "llm-app"
|
|
92
|
+
OTEL_ENVIRONMENT: str = "development"
|
|
93
|
+
|
|
94
|
+
model_config = SettingsConfigDict(
|
|
95
|
+
env_file=".env", env_file_encoding="utf-8", case_sensitive=True, extra="ignore"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
settings = Settings()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from sqlalchemy import JSON, DateTime, Float, Integer, String, func, select
|
|
5
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
6
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Base(DeclarativeBase):
|
|
10
|
+
"""Base class for all database models"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelPrice(Base):
|
|
14
|
+
__tablename__ = "model_prices"
|
|
15
|
+
model: Mapped[str] = mapped_column(String, primary_key=True)
|
|
16
|
+
input_1k_price: Mapped[float] = mapped_column(Float) # Price per 1k tokens
|
|
17
|
+
output_1k_price: Mapped[float] = mapped_column(Float)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UnifiedTrace(Base):
|
|
21
|
+
__tablename__ = "llm_traces"
|
|
22
|
+
|
|
23
|
+
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
24
|
+
timestamp: Mapped[datetime] = mapped_column(DateTime, default=func.now())
|
|
25
|
+
name: Mapped[str] = mapped_column(String)
|
|
26
|
+
model: Mapped[str] = mapped_column(String)
|
|
27
|
+
latency_ms: Mapped[float] = mapped_column(Float)
|
|
28
|
+
prompt_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
|
29
|
+
completion_tokens: Mapped[int] = mapped_column(Integer, default=0)
|
|
30
|
+
total_cost: Mapped[float] = mapped_column(Float, default=0.0)
|
|
31
|
+
prompt: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
32
|
+
completion: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
33
|
+
|
|
34
|
+
# Advanced tracing fields
|
|
35
|
+
request_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
36
|
+
session_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
37
|
+
user_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
38
|
+
status: Mapped[str] = mapped_column(
|
|
39
|
+
String, default="success"
|
|
40
|
+
) # success, error, timeout
|
|
41
|
+
error: Mapped[Optional[str]] = mapped_column(String, nullable=True)
|
|
42
|
+
tags: Mapped[Optional[str]] = mapped_column(
|
|
43
|
+
String, nullable=True
|
|
44
|
+
) # comma-separated
|
|
45
|
+
environment: Mapped[str] = mapped_column(String, default="development")
|
|
46
|
+
llm_params_json: Mapped[Optional[dict]] = mapped_column(
|
|
47
|
+
JSON, nullable=True
|
|
48
|
+
) # temperature, max_tokens, etc.
|
|
49
|
+
|
|
50
|
+
metadata_json: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DBManager:
|
|
54
|
+
def __init__(self, db_url: str = "sqlite+aiosqlite:///./obs_hub.db"):
|
|
55
|
+
self.engine = create_async_engine(db_url)
|
|
56
|
+
self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False)
|
|
57
|
+
|
|
58
|
+
async def init_db(self):
|
|
59
|
+
async with self.engine.begin() as conn:
|
|
60
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
61
|
+
|
|
62
|
+
async def save_trace(self, data: dict):
|
|
63
|
+
"""Save a trace, filtering to only database fields and converting types"""
|
|
64
|
+
async with self.session_factory() as session:
|
|
65
|
+
# Extract only fields that exist in UnifiedTrace model
|
|
66
|
+
db_fields = {
|
|
67
|
+
"id",
|
|
68
|
+
"timestamp",
|
|
69
|
+
"name",
|
|
70
|
+
"model",
|
|
71
|
+
"latency_ms",
|
|
72
|
+
"prompt_tokens",
|
|
73
|
+
"completion_tokens",
|
|
74
|
+
"total_cost",
|
|
75
|
+
"prompt",
|
|
76
|
+
"completion",
|
|
77
|
+
"request_id",
|
|
78
|
+
"session_id",
|
|
79
|
+
"user_id",
|
|
80
|
+
"status",
|
|
81
|
+
"error",
|
|
82
|
+
"tags",
|
|
83
|
+
"environment",
|
|
84
|
+
"llm_params_json",
|
|
85
|
+
"metadata_json",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
filtered_data = {k: v for k, v in data.items() if k in db_fields}
|
|
89
|
+
|
|
90
|
+
# Convert tags list to comma-separated string if needed
|
|
91
|
+
if "tags" in filtered_data and isinstance(filtered_data["tags"], list):
|
|
92
|
+
filtered_data["tags"] = ",".join(filtered_data["tags"])
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
# Create ORM object and add to session (don't await ORM methods)
|
|
96
|
+
trace = UnifiedTrace(**filtered_data)
|
|
97
|
+
session.add(trace)
|
|
98
|
+
await session.commit()
|
|
99
|
+
except Exception as e:
|
|
100
|
+
await session.rollback()
|
|
101
|
+
print(f"Error saving trace: {e}")
|
|
102
|
+
|
|
103
|
+
async def seed_prices(self):
|
|
104
|
+
async with self.session_factory() as session:
|
|
105
|
+
# 2026 Estimated Pricing (based on current trends)
|
|
106
|
+
prices = [
|
|
107
|
+
ModelPrice(
|
|
108
|
+
model="gpt-3.5-turbo", input_1k_price=0.0005, output_1k_price=0.0015
|
|
109
|
+
),
|
|
110
|
+
ModelPrice(
|
|
111
|
+
model="gpt-4o", input_1k_price=0.0025, output_1k_price=0.010
|
|
112
|
+
),
|
|
113
|
+
ModelPrice(
|
|
114
|
+
model="gpt-4o-mini", input_1k_price=0.00015, output_1k_price=0.0006
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
for p in prices:
|
|
118
|
+
await session.merge(p)
|
|
119
|
+
await session.commit()
|
|
120
|
+
|
|
121
|
+
async def calculate_and_save(self, data: dict):
|
|
122
|
+
async with self.session_factory() as session:
|
|
123
|
+
# Fetch rates for the model
|
|
124
|
+
res = await session.execute(
|
|
125
|
+
select(ModelPrice).where(ModelPrice.model == data["model"])
|
|
126
|
+
)
|
|
127
|
+
price = res.scalar_one_or_none()
|
|
128
|
+
|
|
129
|
+
if price:
|
|
130
|
+
input_cost = (data["prompt_tokens"] / 1000) * price.input_1k_price
|
|
131
|
+
output_cost = (data["completion_tokens"] / 1000) * price.output_1k_price
|
|
132
|
+
data["total_cost"] = input_cost + output_cost
|
|
133
|
+
|
|
134
|
+
await session.merge(UnifiedTrace(**data))
|
|
135
|
+
await session.commit()
|