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.
@@ -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}")