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,641 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trace Manager for LLMOps Observability
|
|
3
|
+
Handles tracing and tracking of LLM operations with direct Langfuse integration
|
|
4
|
+
Inspired by veriskGO's trace_manager with instant span sending to Langfuse
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
import uuid
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
import functools
|
|
12
|
+
import inspect
|
|
13
|
+
import sys
|
|
14
|
+
import json
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Optional, Dict, Any, List, Union
|
|
17
|
+
from .models import SpanContext
|
|
18
|
+
from .config import get_langfuse_client
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ============================================================
|
|
22
|
+
# Safe Serialization Helpers (from veriskGO)
|
|
23
|
+
# ============================================================
|
|
24
|
+
|
|
25
|
+
# Maximum size for serialized output (200 KB)
|
|
26
|
+
MAX_OUTPUT_SIZE = 200 * 1024 # 200 KB
|
|
27
|
+
|
|
28
|
+
def serialize_value(value: Any, max_size: int = MAX_OUTPUT_SIZE) -> Any:
|
|
29
|
+
"""Serialize value with size limit to prevent large data issues.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
value: Value to serialize
|
|
33
|
+
max_size: Maximum size in bytes (default 200 KB)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Serialized value, truncated if necessary
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# First, serialize to JSON
|
|
40
|
+
serialized_str = json.dumps(value, default=str)
|
|
41
|
+
serialized_bytes = serialized_str.encode('utf-8')
|
|
42
|
+
|
|
43
|
+
# Check size
|
|
44
|
+
if len(serialized_bytes) <= max_size:
|
|
45
|
+
return json.loads(serialized_str)
|
|
46
|
+
|
|
47
|
+
# Too large - return truncation info instead
|
|
48
|
+
preview_size = min(1000, max_size // 2) # Show first 1KB as preview
|
|
49
|
+
preview = serialized_str[:preview_size]
|
|
50
|
+
|
|
51
|
+
print(f"[LLMOps-Observability] Output truncated: {len(serialized_bytes)} bytes → {max_size} bytes limit")
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"_truncated": True,
|
|
55
|
+
"_original_size_bytes": len(serialized_bytes),
|
|
56
|
+
"_original_size_mb": round(len(serialized_bytes) / (1024 * 1024), 2),
|
|
57
|
+
"_preview": preview + "...",
|
|
58
|
+
"_message": f"Output truncated (original: {round(len(serialized_bytes) / (1024 * 1024), 2)} MB, limit: {round(max_size / 1024, 0)} KB)"
|
|
59
|
+
}
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return str(value)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def safe_locals(d: Dict[str, Any]) -> Dict[str, Any]:
|
|
65
|
+
"""Safely serialize local variables"""
|
|
66
|
+
return {k: serialize_value(v) for k, v in d.items() if not k.startswith("_")}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ============================================================
|
|
70
|
+
# Core Trace Manager
|
|
71
|
+
# ============================================================
|
|
72
|
+
|
|
73
|
+
class TraceManager:
|
|
74
|
+
_lock = threading.Lock()
|
|
75
|
+
_active: Dict[str, Any] = {
|
|
76
|
+
"trace_id": None,
|
|
77
|
+
"trace_obj": None, # Langfuse trace object reference
|
|
78
|
+
"spans": [],
|
|
79
|
+
"stack": [], # Stack of active spans for nesting
|
|
80
|
+
"metadata": {},
|
|
81
|
+
"observation_stack": [], # Stack of Langfuse observation contexts
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def has_active_trace(cls) -> bool:
|
|
86
|
+
"""Check if there's an active trace"""
|
|
87
|
+
return cls._active["trace_id"] is not None
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def _id(cls) -> str:
|
|
91
|
+
"""Generate a unique ID compatible with Langfuse (32 lowercase hex chars)"""
|
|
92
|
+
return uuid.uuid4().hex
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def _now(cls) -> str:
|
|
96
|
+
"""Get current timestamp in ISO format"""
|
|
97
|
+
return datetime.now(timezone.utc).isoformat()
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def start_trace(
|
|
101
|
+
cls,
|
|
102
|
+
name: str,
|
|
103
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
104
|
+
user_id: Optional[str] = None,
|
|
105
|
+
session_id: Optional[str] = None,
|
|
106
|
+
tags: Optional[List[str]] = None,
|
|
107
|
+
) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Start a new trace (similar to veriskGO API).
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
name: Name of the trace
|
|
113
|
+
metadata: Optional metadata dictionary
|
|
114
|
+
user_id: Optional user ID
|
|
115
|
+
session_id: Optional session ID
|
|
116
|
+
tags: Optional list of tags
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
str: Trace ID
|
|
120
|
+
"""
|
|
121
|
+
with cls._lock:
|
|
122
|
+
if cls._active["trace_id"]:
|
|
123
|
+
print("[LLMOps-Observability] WARNING: Trace already active. Ending previous trace.")
|
|
124
|
+
cls._end_trace_internal()
|
|
125
|
+
|
|
126
|
+
trace_id = cls._id()
|
|
127
|
+
|
|
128
|
+
# Store trace information
|
|
129
|
+
cls._active["trace_id"] = trace_id
|
|
130
|
+
cls._active["trace_obj"] = {
|
|
131
|
+
"name": name,
|
|
132
|
+
"user_id": user_id,
|
|
133
|
+
"session_id": session_id,
|
|
134
|
+
"metadata": metadata or {},
|
|
135
|
+
"tags": tags or [],
|
|
136
|
+
"trace_input": None,
|
|
137
|
+
"trace_output": None,
|
|
138
|
+
}
|
|
139
|
+
cls._active["spans"] = []
|
|
140
|
+
cls._active["stack"] = [] # Initialize stack for nested spans
|
|
141
|
+
cls._active["metadata"] = metadata or {}
|
|
142
|
+
|
|
143
|
+
print(f"[LLMOps-Observability] Trace started: {name} (ID: {trace_id})")
|
|
144
|
+
return trace_id
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def _end_trace_internal(cls):
|
|
148
|
+
"""Internal method to end trace without lock (already within lock context)"""
|
|
149
|
+
trace_id = cls._active["trace_id"]
|
|
150
|
+
|
|
151
|
+
if trace_id:
|
|
152
|
+
# Flush to Langfuse immediately
|
|
153
|
+
langfuse = get_langfuse_client()
|
|
154
|
+
langfuse.flush()
|
|
155
|
+
|
|
156
|
+
# Clear the active trace
|
|
157
|
+
cls._active["trace_id"] = None
|
|
158
|
+
cls._active["trace_obj"] = None
|
|
159
|
+
cls._active["spans"] = []
|
|
160
|
+
cls._active["stack"] = []
|
|
161
|
+
cls._active["metadata"] = {}
|
|
162
|
+
cls._active["observation_stack"] = []
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def end_trace(cls, final_output: Optional[Any] = None) -> Optional[str]:
|
|
166
|
+
"""
|
|
167
|
+
End the current trace and flush to Langfuse.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
final_output: Optional final output for the trace
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Optional[str]: Trace ID if successful
|
|
174
|
+
"""
|
|
175
|
+
with cls._lock:
|
|
176
|
+
trace_id = cls._active["trace_id"]
|
|
177
|
+
|
|
178
|
+
if not trace_id:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
if final_output and cls._active["trace_obj"]:
|
|
182
|
+
cls._active["trace_obj"]["trace_output"] = final_output
|
|
183
|
+
|
|
184
|
+
cls._end_trace_internal()
|
|
185
|
+
|
|
186
|
+
print(f"[LLMOps-Observability] Trace ended and flushed: {trace_id}")
|
|
187
|
+
return trace_id
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def finalize_and_send(
|
|
191
|
+
cls,
|
|
192
|
+
*,
|
|
193
|
+
user_id: str,
|
|
194
|
+
session_id: str,
|
|
195
|
+
trace_name: str,
|
|
196
|
+
trace_input: dict,
|
|
197
|
+
trace_output: dict,
|
|
198
|
+
extra_spans: list = [],
|
|
199
|
+
):
|
|
200
|
+
"""
|
|
201
|
+
Finalize and send the trace with input/output (compatible with veriskGO API).
|
|
202
|
+
|
|
203
|
+
This method provides compatibility with veriskGO's API by accepting
|
|
204
|
+
trace_input and trace_output, then ending the trace.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
user_id: User ID for the trace
|
|
208
|
+
session_id: Session ID for the trace
|
|
209
|
+
trace_name: Name of the trace
|
|
210
|
+
trace_input: Input data for the trace
|
|
211
|
+
trace_output: Output data for the trace
|
|
212
|
+
extra_spans: Additional spans (not used in this implementation)
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
bool: True if successful, False otherwise
|
|
216
|
+
"""
|
|
217
|
+
with cls._lock:
|
|
218
|
+
trace_id = cls._active.get("trace_id")
|
|
219
|
+
trace_obj = cls._active.get("trace_obj")
|
|
220
|
+
|
|
221
|
+
if not trace_id or not trace_obj:
|
|
222
|
+
print("[LLMOps-Observability] ERROR: No active trace to finalize.")
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
# Update trace object with input/output
|
|
226
|
+
trace_obj["trace_input"] = serialize_value(trace_input)
|
|
227
|
+
trace_obj["trace_output"] = serialize_value(trace_output)
|
|
228
|
+
trace_obj["user_id"] = user_id
|
|
229
|
+
trace_obj["session_id"] = session_id
|
|
230
|
+
trace_obj["name"] = trace_name
|
|
231
|
+
|
|
232
|
+
# Store updated trace_obj
|
|
233
|
+
cls._active["trace_obj"] = trace_obj
|
|
234
|
+
|
|
235
|
+
# Send trace-level data to Langfuse using the correct API
|
|
236
|
+
try:
|
|
237
|
+
langfuse = get_langfuse_client()
|
|
238
|
+
|
|
239
|
+
# Create trace-level observation using start_as_current_observation
|
|
240
|
+
with langfuse.start_as_current_observation(
|
|
241
|
+
as_type="span",
|
|
242
|
+
name=trace_name,
|
|
243
|
+
trace_context={"trace_id": trace_id},
|
|
244
|
+
input=trace_obj["trace_input"],
|
|
245
|
+
output=trace_obj["trace_output"],
|
|
246
|
+
) as root:
|
|
247
|
+
# Update trace with user_id, session_id, and metadata
|
|
248
|
+
root.update_trace(
|
|
249
|
+
name=trace_name,
|
|
250
|
+
user_id=user_id,
|
|
251
|
+
session_id=session_id,
|
|
252
|
+
metadata=trace_obj.get("metadata", {}),
|
|
253
|
+
input=trace_obj["trace_input"],
|
|
254
|
+
output=trace_obj["trace_output"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Flush immediately (no batching)
|
|
258
|
+
langfuse.flush()
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
print(f"[LLMOps-Observability] Error sending trace to Langfuse: {e}")
|
|
262
|
+
traceback.print_exc()
|
|
263
|
+
|
|
264
|
+
# End the trace
|
|
265
|
+
with cls._lock:
|
|
266
|
+
cls._end_trace_internal()
|
|
267
|
+
|
|
268
|
+
print(f"[LLMOps-Observability] Trace finalized and sent: {trace_name} (ID: {trace_id})")
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def start_observation_context(cls, span_name: str, span_type: str, input_data: Any):
|
|
274
|
+
"""
|
|
275
|
+
Start a Langfuse observation context that will be the parent for nested calls.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
span_name: Name of the span
|
|
279
|
+
span_type: Type ("span" or "generation")
|
|
280
|
+
input_data: Input data for the span
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
observation context manager
|
|
284
|
+
"""
|
|
285
|
+
with cls._lock:
|
|
286
|
+
trace_id = cls._active.get("trace_id")
|
|
287
|
+
trace_obj = cls._active.get("trace_obj")
|
|
288
|
+
current_obs_stack = cls._active.get("observation_stack", [])
|
|
289
|
+
|
|
290
|
+
if not trace_id or not trace_obj:
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
langfuse = get_langfuse_client()
|
|
294
|
+
|
|
295
|
+
# Serialize input
|
|
296
|
+
serialized_input = serialize_value(input_data)
|
|
297
|
+
|
|
298
|
+
# Check if we have a parent observation
|
|
299
|
+
parent_obs = current_obs_stack[-1] if current_obs_stack else None
|
|
300
|
+
|
|
301
|
+
# Start observation context from parent or from langfuse root
|
|
302
|
+
if parent_obs:
|
|
303
|
+
# Create child observation from parent
|
|
304
|
+
if span_type == "generation":
|
|
305
|
+
obs_ctx = parent_obs.start_as_current_observation(
|
|
306
|
+
as_type="generation",
|
|
307
|
+
name=span_name,
|
|
308
|
+
input=serialized_input,
|
|
309
|
+
)
|
|
310
|
+
else:
|
|
311
|
+
obs_ctx = parent_obs.start_as_current_observation(
|
|
312
|
+
as_type="span",
|
|
313
|
+
name=span_name,
|
|
314
|
+
input=serialized_input,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
# Create root observation from langfuse client
|
|
318
|
+
# Build trace context
|
|
319
|
+
trace_context = {"trace_id": trace_id}
|
|
320
|
+
if trace_obj.get("user_id"):
|
|
321
|
+
trace_context["user_id"] = trace_obj.get("user_id")
|
|
322
|
+
if trace_obj.get("session_id"):
|
|
323
|
+
trace_context["session_id"] = trace_obj.get("session_id")
|
|
324
|
+
|
|
325
|
+
if span_type == "generation":
|
|
326
|
+
obs_ctx = langfuse.start_as_current_observation(
|
|
327
|
+
as_type="generation",
|
|
328
|
+
name=span_name,
|
|
329
|
+
trace_context=trace_context,
|
|
330
|
+
input=serialized_input,
|
|
331
|
+
)
|
|
332
|
+
else:
|
|
333
|
+
obs_ctx = langfuse.start_as_current_observation(
|
|
334
|
+
as_type="span",
|
|
335
|
+
name=span_name,
|
|
336
|
+
trace_context=trace_context,
|
|
337
|
+
input=serialized_input,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
return obs_ctx
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def push_observation(cls, obs):
|
|
344
|
+
"""Push an observation onto the stack when entering its context"""
|
|
345
|
+
with cls._lock:
|
|
346
|
+
cls._active["observation_stack"].append(obs)
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def pop_observation(cls):
|
|
350
|
+
"""Pop an observation from the stack when exiting its context"""
|
|
351
|
+
with cls._lock:
|
|
352
|
+
if cls._active["observation_stack"]:
|
|
353
|
+
cls._active["observation_stack"].pop()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ============================================================
|
|
357
|
+
# Local Capture Utilities (from veriskGO)
|
|
358
|
+
# ============================================================
|
|
359
|
+
|
|
360
|
+
def capture_function_locals(func, capture_locals=True, capture_self=True):
|
|
361
|
+
"""
|
|
362
|
+
Capture local variables before and after function execution.
|
|
363
|
+
Uses sys.settrace for frame inspection (similar to veriskGO).
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
func: Function to capture locals from
|
|
367
|
+
capture_locals: Whether to capture locals (True, False, or list of var names)
|
|
368
|
+
capture_self: Whether to capture 'self' variable
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Tuple of (tracer, locals_before, locals_after)
|
|
372
|
+
"""
|
|
373
|
+
locals_before = {}
|
|
374
|
+
locals_after = {}
|
|
375
|
+
|
|
376
|
+
if not capture_locals:
|
|
377
|
+
return None, locals_before, locals_after
|
|
378
|
+
|
|
379
|
+
target_code = func.__code__
|
|
380
|
+
target_name = func.__name__
|
|
381
|
+
target_module = func.__module__
|
|
382
|
+
|
|
383
|
+
entered = False
|
|
384
|
+
|
|
385
|
+
def tracer(frame, event, arg):
|
|
386
|
+
nonlocal entered
|
|
387
|
+
|
|
388
|
+
if frame.f_code is target_code and frame.f_globals.get("__name__") == target_module:
|
|
389
|
+
try:
|
|
390
|
+
if not entered:
|
|
391
|
+
entered = True
|
|
392
|
+
f_locals = frame.f_locals
|
|
393
|
+
|
|
394
|
+
# Filter specific variables if capture_locals is a list
|
|
395
|
+
if isinstance(capture_locals, list):
|
|
396
|
+
f_locals = {k: v for k, v in f_locals.items() if k in capture_locals}
|
|
397
|
+
|
|
398
|
+
if not capture_self and "self" in f_locals:
|
|
399
|
+
f_locals = {k: v for k, v in f_locals.items() if k != "self"}
|
|
400
|
+
locals_before.update(safe_locals(f_locals))
|
|
401
|
+
|
|
402
|
+
if event == "return":
|
|
403
|
+
f_locals = frame.f_locals
|
|
404
|
+
|
|
405
|
+
# Filter specific variables if capture_locals is a list
|
|
406
|
+
if isinstance(capture_locals, list):
|
|
407
|
+
f_locals = {k: v for k, v in f_locals.items() if k in capture_locals}
|
|
408
|
+
|
|
409
|
+
if not capture_self and "self" in f_locals:
|
|
410
|
+
f_locals = {k: v for k, v in f_locals.items() if k != "self"}
|
|
411
|
+
locals_after.update(safe_locals(f_locals))
|
|
412
|
+
locals_after["_return"] = serialize_value(arg)
|
|
413
|
+
except Exception as e:
|
|
414
|
+
print(f"[LLMOps-Observability] TRACER ERROR: {e}")
|
|
415
|
+
traceback.print_exc()
|
|
416
|
+
|
|
417
|
+
return tracer
|
|
418
|
+
|
|
419
|
+
return tracer, locals_before, locals_after
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
# ============================================================
|
|
423
|
+
# Decorator: track_function (Enhanced with veriskGO features)
|
|
424
|
+
# ============================================================
|
|
425
|
+
|
|
426
|
+
def track_function(
|
|
427
|
+
name: Optional[str] = None,
|
|
428
|
+
*,
|
|
429
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
430
|
+
capture_locals: Union[bool, List[str]] = False,
|
|
431
|
+
capture_self: bool = False,
|
|
432
|
+
):
|
|
433
|
+
"""
|
|
434
|
+
Decorator to track function execution with Langfuse (enhanced version from veriskGO).
|
|
435
|
+
|
|
436
|
+
Features:
|
|
437
|
+
- Captures input arguments and output
|
|
438
|
+
- Optionally captures local variables before/after execution
|
|
439
|
+
- Manages span stack for proper nesting
|
|
440
|
+
- Sends directly to Langfuse (no batching)
|
|
441
|
+
|
|
442
|
+
Usage:
|
|
443
|
+
@track_function()
|
|
444
|
+
def process_data(input_data):
|
|
445
|
+
# Your code here
|
|
446
|
+
return result
|
|
447
|
+
|
|
448
|
+
@track_function(name="custom_name", tags={"version": "1.0"}, capture_locals=True)
|
|
449
|
+
def detailed_function(x, y):
|
|
450
|
+
result = x + y
|
|
451
|
+
return result
|
|
452
|
+
|
|
453
|
+
@track_function(capture_locals=["important_var"])
|
|
454
|
+
async def async_function():
|
|
455
|
+
important_var = "tracked"
|
|
456
|
+
return result
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
name: Optional custom name for the span
|
|
460
|
+
tags: Optional metadata tags
|
|
461
|
+
capture_locals: Capture local variables (True/False or list of var names)
|
|
462
|
+
capture_self: Whether to capture 'self' variable (default False)
|
|
463
|
+
"""
|
|
464
|
+
def decorator(func):
|
|
465
|
+
span_name = name or func.__name__
|
|
466
|
+
is_async = inspect.iscoroutinefunction(func)
|
|
467
|
+
|
|
468
|
+
if is_async:
|
|
469
|
+
@functools.wraps(func)
|
|
470
|
+
async def async_wrapper(*args, **kwargs):
|
|
471
|
+
if not TraceManager.has_active_trace():
|
|
472
|
+
return await func(*args, **kwargs)
|
|
473
|
+
|
|
474
|
+
# Build input data
|
|
475
|
+
input_data = {
|
|
476
|
+
"args": serialize_value(args),
|
|
477
|
+
"kwargs": serialize_value(kwargs),
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
# Start observation context (this will be parent for nested calls)
|
|
481
|
+
obs_ctx = TraceManager.start_observation_context(span_name, "span", input_data)
|
|
482
|
+
|
|
483
|
+
if not obs_ctx:
|
|
484
|
+
return await func(*args, **kwargs)
|
|
485
|
+
|
|
486
|
+
# Setup local variable capture
|
|
487
|
+
tracer, locals_before, locals_after = capture_function_locals(
|
|
488
|
+
func, capture_locals=capture_locals, capture_self=capture_self
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
error = None
|
|
492
|
+
result = None
|
|
493
|
+
start_time = time.time()
|
|
494
|
+
|
|
495
|
+
# Use the observation context properly with 'with' statement
|
|
496
|
+
# This keeps the context active during function execution
|
|
497
|
+
with obs_ctx as obs:
|
|
498
|
+
# Push observation onto stack so nested calls become children
|
|
499
|
+
TraceManager.push_observation(obs)
|
|
500
|
+
|
|
501
|
+
if tracer:
|
|
502
|
+
sys.settrace(tracer)
|
|
503
|
+
|
|
504
|
+
try:
|
|
505
|
+
result = await func(*args, **kwargs)
|
|
506
|
+
except Exception as e:
|
|
507
|
+
error = e
|
|
508
|
+
raise
|
|
509
|
+
finally:
|
|
510
|
+
if tracer:
|
|
511
|
+
sys.settrace(None)
|
|
512
|
+
|
|
513
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
514
|
+
|
|
515
|
+
# Build output
|
|
516
|
+
if error:
|
|
517
|
+
output_data = {
|
|
518
|
+
"status": "error",
|
|
519
|
+
"error": str(error),
|
|
520
|
+
"stacktrace": traceback.format_exc(),
|
|
521
|
+
"locals_before": locals_before,
|
|
522
|
+
"locals_after": locals_after,
|
|
523
|
+
}
|
|
524
|
+
else:
|
|
525
|
+
output_data = {
|
|
526
|
+
"status": "success",
|
|
527
|
+
"latency_ms": duration_ms,
|
|
528
|
+
"locals_before": locals_before,
|
|
529
|
+
"locals_after": locals_after,
|
|
530
|
+
"output": serialize_value(result),
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
# Update observation with output
|
|
534
|
+
obs.update(
|
|
535
|
+
output=serialize_value(output_data),
|
|
536
|
+
metadata=tags or {},
|
|
537
|
+
level="ERROR" if error else "DEFAULT",
|
|
538
|
+
status_message=str(error) if error else None,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Note: flush happens after context exit
|
|
542
|
+
|
|
543
|
+
# Flush after exiting context
|
|
544
|
+
langfuse = get_langfuse_client()
|
|
545
|
+
langfuse.flush()
|
|
546
|
+
|
|
547
|
+
status_str = " (error)" if error else ""
|
|
548
|
+
print(f"[LLMOps-Observability] Span sent{status_str}: {span_name} ({duration_ms}ms)")
|
|
549
|
+
|
|
550
|
+
return result
|
|
551
|
+
|
|
552
|
+
return async_wrapper
|
|
553
|
+
else:
|
|
554
|
+
@functools.wraps(func)
|
|
555
|
+
def sync_wrapper(*args, **kwargs):
|
|
556
|
+
if not TraceManager.has_active_trace():
|
|
557
|
+
return func(*args, **kwargs)
|
|
558
|
+
|
|
559
|
+
# Build input data
|
|
560
|
+
input_data = {
|
|
561
|
+
"args": serialize_value(args),
|
|
562
|
+
"kwargs": serialize_value(kwargs),
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
# Start observation context (this will be parent for nested calls)
|
|
566
|
+
obs_ctx = TraceManager.start_observation_context(span_name, "span", input_data)
|
|
567
|
+
|
|
568
|
+
if not obs_ctx:
|
|
569
|
+
return func(*args, **kwargs)
|
|
570
|
+
|
|
571
|
+
# Setup local variable capture
|
|
572
|
+
tracer, locals_before, locals_after = capture_function_locals(
|
|
573
|
+
func, capture_locals=capture_locals, capture_self=capture_self
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
error = None
|
|
577
|
+
result = None
|
|
578
|
+
start_time = time.time()
|
|
579
|
+
|
|
580
|
+
# Use the observation context properly with 'with' statement
|
|
581
|
+
# This keeps the context active during function execution
|
|
582
|
+
with obs_ctx as obs:
|
|
583
|
+
# Push observation onto stack so nested calls become children
|
|
584
|
+
TraceManager.push_observation(obs)
|
|
585
|
+
|
|
586
|
+
if tracer:
|
|
587
|
+
sys.settrace(tracer)
|
|
588
|
+
|
|
589
|
+
try:
|
|
590
|
+
result = func(*args, **kwargs)
|
|
591
|
+
except Exception as e:
|
|
592
|
+
error = e
|
|
593
|
+
raise
|
|
594
|
+
finally:
|
|
595
|
+
# Pop observation from stack
|
|
596
|
+
TraceManager.pop_observation()
|
|
597
|
+
|
|
598
|
+
if tracer:
|
|
599
|
+
sys.settrace(None)
|
|
600
|
+
|
|
601
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
602
|
+
|
|
603
|
+
# Build output
|
|
604
|
+
if error:
|
|
605
|
+
output_data = {
|
|
606
|
+
"status": "error",
|
|
607
|
+
"error": str(error),
|
|
608
|
+
"stacktrace": traceback.format_exc(),
|
|
609
|
+
"locals_before": locals_before,
|
|
610
|
+
"locals_after": locals_after,
|
|
611
|
+
}
|
|
612
|
+
else:
|
|
613
|
+
output_data = {
|
|
614
|
+
"status": "success",
|
|
615
|
+
"latency_ms": duration_ms,
|
|
616
|
+
"locals_before": locals_before,
|
|
617
|
+
"locals_after": locals_after,
|
|
618
|
+
"output": serialize_value(result),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# Update observation with output
|
|
622
|
+
obs.update(
|
|
623
|
+
output=serialize_value(output_data),
|
|
624
|
+
metadata=tags or {},
|
|
625
|
+
level="ERROR" if error else "DEFAULT",
|
|
626
|
+
status_message=str(error) if error else None,
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Flush after exiting context
|
|
630
|
+
langfuse = get_langfuse_client()
|
|
631
|
+
langfuse.flush()
|
|
632
|
+
|
|
633
|
+
status_str = " (error)" if error else ""
|
|
634
|
+
print(f"[LLMOps-Observability] Span sent{status_str}: {span_name} ({duration_ms}ms)")
|
|
635
|
+
|
|
636
|
+
return result
|
|
637
|
+
|
|
638
|
+
return sync_wrapper
|
|
639
|
+
|
|
640
|
+
return decorator
|
|
641
|
+
|