llmops-observability 8.0.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.
- llmops_observability/__init__.py +27 -0
- llmops_observability/asgi_middleware.py +146 -0
- llmops_observability/config.py +79 -0
- llmops_observability/llm.py +580 -0
- llmops_observability/models.py +32 -0
- llmops_observability/pricing.py +132 -0
- llmops_observability/trace_manager.py +641 -0
- llmops_observability-8.0.0.dist-info/METADATA +263 -0
- llmops_observability-8.0.0.dist-info/RECORD +11 -0
- llmops_observability-8.0.0.dist-info/WHEEL +5 -0
- llmops_observability-8.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLMOps Observability SDK – Public API
|
|
3
|
+
Direct Langfuse integration for LLM tracing without SQS/batching.
|
|
4
|
+
Enhanced with veriskGO-style features: locals capture, nested spans, instant sending.
|
|
5
|
+
"""
|
|
6
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("llmops-observability")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0.0.0"
|
|
12
|
+
|
|
13
|
+
# Core components
|
|
14
|
+
from .trace_manager import TraceManager, track_function
|
|
15
|
+
from .llm import track_llm_call
|
|
16
|
+
from .config import get_langfuse_client, configure
|
|
17
|
+
from .asgi_middleware import LLMOpsASGIMiddleware
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"TraceManager",
|
|
21
|
+
"track_function",
|
|
22
|
+
"track_llm_call",
|
|
23
|
+
"get_langfuse_client",
|
|
24
|
+
"configure",
|
|
25
|
+
"LLMOpsASGIMiddleware",
|
|
26
|
+
"__version__",
|
|
27
|
+
]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ASGI Middleware for LLMOps Observability
|
|
3
|
+
Automatic tracing for FastAPI and other ASGI applications
|
|
4
|
+
Based on veriskGO's asgi_middleware with direct Langfuse integration
|
|
5
|
+
"""
|
|
6
|
+
import uuid
|
|
7
|
+
import time
|
|
8
|
+
import os
|
|
9
|
+
import socket
|
|
10
|
+
from .trace_manager import TraceManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LLMOpsASGIMiddleware:
|
|
14
|
+
"""
|
|
15
|
+
ASGI middleware for automatic tracing of HTTP requests.
|
|
16
|
+
|
|
17
|
+
Usage with FastAPI:
|
|
18
|
+
from fastapi import FastAPI
|
|
19
|
+
from llmops_observability.asgi_middleware import LLMOpsASGIMiddleware
|
|
20
|
+
|
|
21
|
+
app = FastAPI()
|
|
22
|
+
app.add_middleware(LLMOpsASGIMiddleware, service_name="my_api")
|
|
23
|
+
|
|
24
|
+
@app.get("/")
|
|
25
|
+
async def root():
|
|
26
|
+
return {"message": "Hello World"}
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, app, service_name="llmops_service"):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the ASGI middleware.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
app: ASGI application instance
|
|
35
|
+
service_name: Name of the service (used in trace naming)
|
|
36
|
+
"""
|
|
37
|
+
self.app = app
|
|
38
|
+
self.service_name = service_name
|
|
39
|
+
|
|
40
|
+
def get_trace_name(self):
|
|
41
|
+
"""
|
|
42
|
+
Generate a trace name based on project and hostname.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: Trace name in format "project_hostname"
|
|
46
|
+
"""
|
|
47
|
+
project = os.path.basename(os.getcwd())
|
|
48
|
+
hostname = socket.gethostname()
|
|
49
|
+
return f"{project}_{hostname}"
|
|
50
|
+
|
|
51
|
+
async def __call__(self, scope, receive, send):
|
|
52
|
+
"""
|
|
53
|
+
ASGI middleware entry point.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
scope: ASGI scope dictionary
|
|
57
|
+
receive: ASGI receive callable
|
|
58
|
+
send: ASGI send callable
|
|
59
|
+
"""
|
|
60
|
+
# We only trace HTTP traffic
|
|
61
|
+
if scope["type"] != "http":
|
|
62
|
+
await self.app(scope, receive, send)
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
method = scope.get("method", "UNKNOWN")
|
|
66
|
+
path = scope.get("path", "UNKNOWN")
|
|
67
|
+
|
|
68
|
+
# Extract headers (optional user/session identification)
|
|
69
|
+
headers = {k.decode(): v.decode() for k, v in scope.get("headers", [])}
|
|
70
|
+
user_id = headers.get("x-user-id", "anonymous")
|
|
71
|
+
session_id = headers.get("x-session-id", str(uuid.uuid4()))
|
|
72
|
+
|
|
73
|
+
# Generate trace name
|
|
74
|
+
trace_name = self.get_trace_name()
|
|
75
|
+
|
|
76
|
+
# Start trace with metadata
|
|
77
|
+
TraceManager.start_trace(
|
|
78
|
+
trace_name,
|
|
79
|
+
metadata={
|
|
80
|
+
"path": path,
|
|
81
|
+
"method": method,
|
|
82
|
+
"user_id": user_id,
|
|
83
|
+
"session_id": session_id,
|
|
84
|
+
"service": self.service_name,
|
|
85
|
+
},
|
|
86
|
+
user_id=user_id,
|
|
87
|
+
session_id=session_id,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
response_body = None
|
|
92
|
+
status_code = None
|
|
93
|
+
|
|
94
|
+
async def send_wrapper(message):
|
|
95
|
+
"""Wrapper to capture response data"""
|
|
96
|
+
nonlocal response_body, status_code
|
|
97
|
+
|
|
98
|
+
# Capture response status
|
|
99
|
+
if message["type"] == "http.response.start":
|
|
100
|
+
status_code = message.get("status")
|
|
101
|
+
|
|
102
|
+
# Capture response body
|
|
103
|
+
if message["type"] == "http.response.body":
|
|
104
|
+
body = message.get("body")
|
|
105
|
+
try:
|
|
106
|
+
response_body = body.decode() if body else None
|
|
107
|
+
except Exception:
|
|
108
|
+
response_body = str(body) if body else None
|
|
109
|
+
|
|
110
|
+
await send(message)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Execute the application
|
|
114
|
+
await self.app(scope, receive, send_wrapper)
|
|
115
|
+
except Exception as exc:
|
|
116
|
+
# Trace the exception
|
|
117
|
+
TraceManager.finalize_and_send(
|
|
118
|
+
user_id=user_id,
|
|
119
|
+
session_id=session_id,
|
|
120
|
+
trace_name=f"{trace_name}_error",
|
|
121
|
+
trace_input={"path": path, "method": method},
|
|
122
|
+
trace_output={
|
|
123
|
+
"error": str(exc),
|
|
124
|
+
"error_type": type(exc).__name__,
|
|
125
|
+
},
|
|
126
|
+
)
|
|
127
|
+
raise
|
|
128
|
+
|
|
129
|
+
# Normal completion - finalize trace with input/output
|
|
130
|
+
latency_ms = int((time.time() - start_time) * 1000)
|
|
131
|
+
|
|
132
|
+
TraceManager.finalize_and_send(
|
|
133
|
+
user_id=user_id,
|
|
134
|
+
session_id=session_id,
|
|
135
|
+
trace_name=trace_name,
|
|
136
|
+
trace_input={
|
|
137
|
+
"path": path,
|
|
138
|
+
"method": method,
|
|
139
|
+
"headers": dict(headers), # Include all headers
|
|
140
|
+
},
|
|
141
|
+
trace_output={
|
|
142
|
+
"status_code": status_code,
|
|
143
|
+
"response": response_body,
|
|
144
|
+
"latency_ms": latency_ms,
|
|
145
|
+
},
|
|
146
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration management for LLMOps Observability
|
|
3
|
+
Direct Langfuse client configuration
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from langfuse import Langfuse
|
|
8
|
+
import httpx
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
|
|
11
|
+
# Load environment variables
|
|
12
|
+
load_dotenv()
|
|
13
|
+
|
|
14
|
+
# Global Langfuse client
|
|
15
|
+
_langfuse_client: Optional[Langfuse] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_langfuse_client() -> Langfuse:
|
|
19
|
+
"""
|
|
20
|
+
Get or create the global Langfuse client.
|
|
21
|
+
|
|
22
|
+
Environment variables:
|
|
23
|
+
- LANGFUSE_PUBLIC_KEY: Langfuse public key
|
|
24
|
+
- LANGFUSE_SECRET_KEY: Langfuse secret key
|
|
25
|
+
- LANGFUSE_BASE_URL: Langfuse base URL
|
|
26
|
+
- LANGFUSE_VERIFY_SSL: Whether to verify SSL (default: false)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Langfuse: Configured Langfuse client
|
|
30
|
+
"""
|
|
31
|
+
global _langfuse_client
|
|
32
|
+
|
|
33
|
+
if _langfuse_client is None:
|
|
34
|
+
verify_ssl = os.getenv("LANGFUSE_VERIFY_SSL", "false").lower() == "false"
|
|
35
|
+
|
|
36
|
+
# Create custom HTTP client with optional SSL verification
|
|
37
|
+
httpx_client = httpx.Client(verify=verify_ssl) if not verify_ssl else None
|
|
38
|
+
|
|
39
|
+
_langfuse_client = Langfuse(
|
|
40
|
+
public_key=os.getenv("LANGFUSE_PUBLIC_KEY","pk-lf-675b5c31-775e-4cfc-89b7-7aea0219aab1"),
|
|
41
|
+
secret_key=os.getenv("LANGFUSE_SECRET_KEY","sk-lf-bc2e15c2-2dab-44ba-a4f5-66c69087f404"),
|
|
42
|
+
base_url=os.getenv("LANGFUSE_BASE_URL","https://internal-k8s-langfuse-langfuse-6657f5b8d5-481261872.us-east-1.elb.amazonaws.com"),
|
|
43
|
+
httpx_client=httpx_client,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Print the actual base URL being used (including default)
|
|
47
|
+
actual_base_url = os.getenv("LANGFUSE_BASE_URL") or "https://internal-k8s-langfuse-langfuse-6657f5b8d5-481261872.us-east-1.elb.amazonaws.com"
|
|
48
|
+
print(f"[LLMOps-Observability] Langfuse client initialized: {actual_base_url}")
|
|
49
|
+
|
|
50
|
+
return _langfuse_client
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def configure(
|
|
54
|
+
public_key: Optional[str] = None,
|
|
55
|
+
secret_key: Optional[str] = None,
|
|
56
|
+
base_url: Optional[str] = None,
|
|
57
|
+
verify_ssl: bool = False
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Manually configure Langfuse client.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
public_key: Langfuse public key
|
|
64
|
+
secret_key: Langfuse secret key
|
|
65
|
+
base_url: Langfuse base URL
|
|
66
|
+
verify_ssl: Whether to verify SSL certificates
|
|
67
|
+
"""
|
|
68
|
+
global _langfuse_client
|
|
69
|
+
|
|
70
|
+
httpx_client = httpx.Client(verify=verify_ssl) if not verify_ssl else None
|
|
71
|
+
|
|
72
|
+
_langfuse_client = Langfuse(
|
|
73
|
+
public_key=public_key,
|
|
74
|
+
secret_key=secret_key,
|
|
75
|
+
base_url=base_url,
|
|
76
|
+
httpx_client=httpx_client,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
print(f"[LLMOps-Observability] Langfuse client configured: {base_url}")
|