lmnr 0.7.0__py3-none-any.whl → 0.7.1__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.
- lmnr/opentelemetry_lib/decorators/__init__.py +43 -4
- lmnr/opentelemetry_lib/litellm/__init__.py +5 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +8 -3
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +6 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +139 -10
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +8 -3
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +6 -2
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +6 -3
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +4 -1
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +14 -5
- lmnr/opentelemetry_lib/tracing/context.py +18 -1
- lmnr/sdk/browser/pw_utils.py +43 -108
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/laminar.py +51 -26
- lmnr/sdk/types.py +17 -5
- lmnr/version.py +1 -1
- {lmnr-0.7.0.dist-info → lmnr-0.7.1.dist-info}/METADATA +1 -1
- {lmnr-0.7.0.dist-info → lmnr-0.7.1.dist-info}/RECORD +20 -20
- lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
- {lmnr-0.7.0.dist-info → lmnr-0.7.1.dist-info}/WHEEL +0 -0
- {lmnr-0.7.0.dist-info → lmnr-0.7.1.dist-info}/entry_points.txt +0 -0
@@ -6,8 +6,15 @@ import types
|
|
6
6
|
from typing import Any, AsyncGenerator, Callable, Generator, Literal
|
7
7
|
|
8
8
|
from opentelemetry import context as context_api
|
9
|
-
from opentelemetry.trace import Span
|
10
|
-
|
9
|
+
from opentelemetry.trace import Span, Status, StatusCode
|
10
|
+
|
11
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
12
|
+
CONTEXT_SESSION_ID_KEY,
|
13
|
+
CONTEXT_USER_ID_KEY,
|
14
|
+
attach_context,
|
15
|
+
detach_context,
|
16
|
+
get_event_attributes_from_context,
|
17
|
+
)
|
11
18
|
from lmnr.sdk.utils import get_input_from_func_args, is_method
|
12
19
|
from lmnr.opentelemetry_lib import MAX_MANUAL_SPAN_PAYLOAD_SIZE
|
13
20
|
from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
|
@@ -180,7 +187,21 @@ def observe_base(
|
|
180
187
|
|
181
188
|
span = _setup_span(span_name, span_type, association_properties)
|
182
189
|
new_context = wrapper.push_span_context(span)
|
190
|
+
if session_id := association_properties.get("session_id"):
|
191
|
+
new_context = context_api.set_value(
|
192
|
+
CONTEXT_SESSION_ID_KEY, session_id, new_context
|
193
|
+
)
|
194
|
+
if user_id := association_properties.get("user_id"):
|
195
|
+
new_context = context_api.set_value(
|
196
|
+
CONTEXT_USER_ID_KEY, user_id, new_context
|
197
|
+
)
|
198
|
+
# Some auto-instrumentations are not under our control, so they
|
199
|
+
# don't have access to our isolated context. We attach the context
|
200
|
+
# to the OTEL global context, so that spans know their parent
|
201
|
+
# span and trace_id.
|
183
202
|
ctx_token = context_api.attach(new_context)
|
203
|
+
# update our isolated context too
|
204
|
+
isolated_ctx_token = attach_context(new_context)
|
184
205
|
|
185
206
|
_process_input(
|
186
207
|
span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
|
@@ -195,7 +216,7 @@ def observe_base(
|
|
195
216
|
finally:
|
196
217
|
# Always restore global context
|
197
218
|
context_api.detach(ctx_token)
|
198
|
-
|
219
|
+
detach_context(isolated_ctx_token)
|
199
220
|
# span will be ended in the generator
|
200
221
|
if isinstance(res, types.GeneratorType):
|
201
222
|
return _handle_generator(span, ctx_token, res)
|
@@ -240,7 +261,21 @@ def async_observe_base(
|
|
240
261
|
|
241
262
|
span = _setup_span(span_name, span_type, association_properties)
|
242
263
|
new_context = wrapper.push_span_context(span)
|
264
|
+
if session_id := association_properties.get("session_id"):
|
265
|
+
new_context = context_api.set_value(
|
266
|
+
CONTEXT_SESSION_ID_KEY, session_id, new_context
|
267
|
+
)
|
268
|
+
if user_id := association_properties.get("user_id"):
|
269
|
+
new_context = context_api.set_value(
|
270
|
+
CONTEXT_USER_ID_KEY, user_id, new_context
|
271
|
+
)
|
272
|
+
# Some auto-instrumentations are not under our control, so they
|
273
|
+
# don't have access to our isolated context. We attach the context
|
274
|
+
# to the OTEL global context, so that spans know their parent
|
275
|
+
# span and trace_id.
|
243
276
|
ctx_token = context_api.attach(new_context)
|
277
|
+
# update our isolated context too
|
278
|
+
isolated_ctx_token = attach_context(new_context)
|
244
279
|
|
245
280
|
_process_input(
|
246
281
|
span, fn, args, kwargs, ignore_input, ignore_inputs, input_formatter
|
@@ -255,6 +290,7 @@ def async_observe_base(
|
|
255
290
|
finally:
|
256
291
|
# Always restore global context
|
257
292
|
context_api.detach(ctx_token)
|
293
|
+
detach_context(isolated_ctx_token)
|
258
294
|
|
259
295
|
# span will be ended in the generator
|
260
296
|
if isinstance(res, types.AsyncGeneratorType):
|
@@ -288,4 +324,7 @@ async def _ahandle_generator(span: Span, wrapper: TracerWrapper, res: AsyncGener
|
|
288
324
|
|
289
325
|
def _process_exception(span: Span, e: Exception):
|
290
326
|
# Note that this `escaped` is sent as a StringValue("True"), not a boolean.
|
291
|
-
span.record_exception(
|
327
|
+
span.record_exception(
|
328
|
+
e, attributes=get_event_attributes_from_context(), escaped=True
|
329
|
+
)
|
330
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
@@ -7,6 +7,7 @@ from opentelemetry.trace import SpanKind, Status, StatusCode, Tracer
|
|
7
7
|
from lmnr.opentelemetry_lib.litellm.utils import model_as_dict, set_span_attribute
|
8
8
|
from lmnr.opentelemetry_lib.tracing import TracerWrapper
|
9
9
|
|
10
|
+
from lmnr.opentelemetry_lib.tracing.context import get_event_attributes_from_context
|
10
11
|
from lmnr.opentelemetry_lib.utils.package_check import is_package_installed
|
11
12
|
from lmnr.sdk.log import get_default_logger
|
12
13
|
|
@@ -141,10 +142,12 @@ try:
|
|
141
142
|
else:
|
142
143
|
span.set_status(Status(StatusCode.ERROR))
|
143
144
|
if isinstance(response_obj, Exception):
|
144
|
-
|
145
|
+
attributes = get_event_attributes_from_context()
|
146
|
+
span.record_exception(response_obj, attributes=attributes)
|
145
147
|
|
146
148
|
except Exception as e:
|
147
|
-
|
149
|
+
attributes = get_event_attributes_from_context()
|
150
|
+
span.record_exception(e, attributes=attributes)
|
148
151
|
logger.error(f"Error in Laminar LiteLLM instrumentation: {e}")
|
149
152
|
finally:
|
150
153
|
span.end(int(end_time.timestamp() * 1e9))
|
@@ -8,7 +8,10 @@ from typing import AsyncGenerator, Callable, Collection, Generator
|
|
8
8
|
|
9
9
|
from google.genai import types
|
10
10
|
|
11
|
-
from lmnr.opentelemetry_lib.tracing.context import
|
11
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
12
|
+
get_current_context,
|
13
|
+
get_event_attributes_from_context,
|
14
|
+
)
|
12
15
|
|
13
16
|
from .config import (
|
14
17
|
Config,
|
@@ -491,8 +494,9 @@ def _wrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
491
494
|
span.end()
|
492
495
|
return response
|
493
496
|
except Exception as e:
|
497
|
+
attributes = get_event_attributes_from_context()
|
494
498
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
495
|
-
span.record_exception(e)
|
499
|
+
span.record_exception(e, attributes=attributes)
|
496
500
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
497
501
|
span.end()
|
498
502
|
raise e
|
@@ -529,8 +533,9 @@ async def _awrap(tracer: Tracer, to_wrap, wrapped, instance, args, kwargs):
|
|
529
533
|
span.end()
|
530
534
|
return response
|
531
535
|
except Exception as e:
|
536
|
+
attributes = get_event_attributes_from_context()
|
532
537
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
533
|
-
span.record_exception(e)
|
538
|
+
span.record_exception(e, attributes=attributes)
|
534
539
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
535
540
|
span.end()
|
536
541
|
raise e
|
@@ -395,6 +395,12 @@ def get_token_count_from_string(string: str, model_name: str):
|
|
395
395
|
f"Failed to get tiktoken encoding for model_name {model_name}, error: {str(ex)}"
|
396
396
|
)
|
397
397
|
return None
|
398
|
+
except Exception as ex:
|
399
|
+
# Other exceptions in tiktoken
|
400
|
+
logger.warning(
|
401
|
+
f"Failed to get tiktoken encoding for model_name {model_name}, error: {str(ex)}"
|
402
|
+
)
|
403
|
+
return None
|
398
404
|
|
399
405
|
tiktoken_encodings[model_name] = encoding
|
400
406
|
else:
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import copy
|
2
2
|
import json
|
3
3
|
import logging
|
4
|
+
import threading
|
4
5
|
import time
|
5
6
|
from functools import singledispatch
|
6
7
|
from typing import List, Optional, Union
|
@@ -39,7 +40,10 @@ from ..utils import (
|
|
39
40
|
should_emit_events,
|
40
41
|
should_send_prompts,
|
41
42
|
)
|
42
|
-
from lmnr.opentelemetry_lib.tracing.context import
|
43
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
44
|
+
get_current_context,
|
45
|
+
get_event_attributes_from_context,
|
46
|
+
)
|
43
47
|
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
44
48
|
from opentelemetry.metrics import Counter, Histogram
|
45
49
|
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
@@ -111,7 +115,8 @@ def chat_wrapper(
|
|
111
115
|
exception_counter.add(1, attributes=attributes)
|
112
116
|
|
113
117
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
114
|
-
|
118
|
+
attributes = get_event_attributes_from_context()
|
119
|
+
span.record_exception(e, attributes=attributes)
|
115
120
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
116
121
|
span.end()
|
117
122
|
|
@@ -211,7 +216,8 @@ async def achat_wrapper(
|
|
211
216
|
exception_counter.add(1, attributes=attributes)
|
212
217
|
|
213
218
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
214
|
-
|
219
|
+
attributes = get_event_attributes_from_context()
|
220
|
+
span.record_exception(e, attributes=attributes)
|
215
221
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
216
222
|
span.end()
|
217
223
|
|
@@ -296,6 +302,7 @@ def _handle_response(
|
|
296
302
|
choice_counter=None,
|
297
303
|
duration_histogram=None,
|
298
304
|
duration=None,
|
305
|
+
is_streaming: bool = False,
|
299
306
|
):
|
300
307
|
if is_openai_v1():
|
301
308
|
response_dict = model_as_dict(response)
|
@@ -310,6 +317,7 @@ def _handle_response(
|
|
310
317
|
duration_histogram,
|
311
318
|
response_dict,
|
312
319
|
duration,
|
320
|
+
is_streaming,
|
313
321
|
)
|
314
322
|
|
315
323
|
# span attributes
|
@@ -327,13 +335,19 @@ def _handle_response(
|
|
327
335
|
|
328
336
|
|
329
337
|
def _set_chat_metrics(
|
330
|
-
instance,
|
338
|
+
instance,
|
339
|
+
token_counter,
|
340
|
+
choice_counter,
|
341
|
+
duration_histogram,
|
342
|
+
response_dict,
|
343
|
+
duration,
|
344
|
+
is_streaming: bool = False,
|
331
345
|
):
|
332
346
|
shared_attributes = metric_shared_attributes(
|
333
347
|
response_model=response_dict.get("model") or None,
|
334
348
|
operation="chat",
|
335
349
|
server_address=_get_openai_base_url(instance),
|
336
|
-
is_streaming=
|
350
|
+
is_streaming=is_streaming,
|
337
351
|
)
|
338
352
|
|
339
353
|
# token metrics
|
@@ -520,11 +534,9 @@ def _set_completions(span, choices):
|
|
520
534
|
def _set_streaming_token_metrics(
|
521
535
|
request_kwargs, complete_response, span, token_counter, shared_attributes
|
522
536
|
):
|
523
|
-
# use tiktoken calculate token usage
|
524
537
|
if not should_record_stream_token_usage():
|
525
538
|
return
|
526
539
|
|
527
|
-
# kwargs={'model': 'gpt-3.5', 'messages': [{'role': 'user', 'content': '...'}], 'stream': True}
|
528
540
|
prompt_usage = -1
|
529
541
|
completion_usage = -1
|
530
542
|
|
@@ -621,11 +633,35 @@ class ChatStream(ObjectProxy):
|
|
621
633
|
self._time_of_first_token = self._start_time
|
622
634
|
self._complete_response = {"choices": [], "model": ""}
|
623
635
|
|
636
|
+
# Cleanup state tracking to prevent duplicate operations
|
637
|
+
self._cleanup_completed = False
|
638
|
+
self._cleanup_lock = threading.Lock()
|
639
|
+
|
640
|
+
def __del__(self):
|
641
|
+
"""Cleanup when object is garbage collected"""
|
642
|
+
if hasattr(self, "_cleanup_completed") and not self._cleanup_completed:
|
643
|
+
self._ensure_cleanup()
|
644
|
+
|
624
645
|
def __enter__(self):
|
625
646
|
return self
|
626
647
|
|
627
648
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
628
|
-
|
649
|
+
cleanup_exception = None
|
650
|
+
try:
|
651
|
+
self._ensure_cleanup()
|
652
|
+
except Exception as e:
|
653
|
+
cleanup_exception = e
|
654
|
+
# Don't re-raise to avoid masking original exception
|
655
|
+
|
656
|
+
result = self.__wrapped__.__exit__(exc_type, exc_val, exc_tb)
|
657
|
+
|
658
|
+
if cleanup_exception:
|
659
|
+
# Log cleanup exception but don't affect context manager behavior
|
660
|
+
logger.debug(
|
661
|
+
"Error during ChatStream cleanup in __exit__: %s", cleanup_exception
|
662
|
+
)
|
663
|
+
|
664
|
+
return result
|
629
665
|
|
630
666
|
async def __aenter__(self):
|
631
667
|
return self
|
@@ -645,7 +681,12 @@ class ChatStream(ObjectProxy):
|
|
645
681
|
except Exception as e:
|
646
682
|
if isinstance(e, StopIteration):
|
647
683
|
self._process_complete_response()
|
648
|
-
|
684
|
+
else:
|
685
|
+
# Handle cleanup for other exceptions during stream iteration
|
686
|
+
self._ensure_cleanup()
|
687
|
+
if self._span and self._span.is_recording():
|
688
|
+
self._span.set_status(Status(StatusCode.ERROR, str(e)))
|
689
|
+
raise
|
649
690
|
else:
|
650
691
|
self._process_item(chunk)
|
651
692
|
return chunk
|
@@ -656,7 +697,12 @@ class ChatStream(ObjectProxy):
|
|
656
697
|
except Exception as e:
|
657
698
|
if isinstance(e, StopAsyncIteration):
|
658
699
|
self._process_complete_response()
|
659
|
-
|
700
|
+
else:
|
701
|
+
# Handle cleanup for other exceptions during stream iteration
|
702
|
+
self._ensure_cleanup()
|
703
|
+
if self._span and self._span.is_recording():
|
704
|
+
self._span.set_status(Status(StatusCode.ERROR, str(e)))
|
705
|
+
raise
|
660
706
|
else:
|
661
707
|
self._process_item(chunk)
|
662
708
|
return chunk
|
@@ -727,6 +773,82 @@ class ChatStream(ObjectProxy):
|
|
727
773
|
|
728
774
|
self._span.set_status(Status(StatusCode.OK))
|
729
775
|
self._span.end()
|
776
|
+
self._cleanup_completed = True
|
777
|
+
|
778
|
+
@dont_throw
|
779
|
+
def _ensure_cleanup(self):
|
780
|
+
"""Thread-safe cleanup method that handles different cleanup scenarios"""
|
781
|
+
with self._cleanup_lock:
|
782
|
+
if self._cleanup_completed:
|
783
|
+
logger.debug("ChatStream cleanup already completed, skipping")
|
784
|
+
return
|
785
|
+
|
786
|
+
try:
|
787
|
+
logger.debug("Starting ChatStream cleanup")
|
788
|
+
|
789
|
+
# Set span status and close it
|
790
|
+
if self._span and self._span.is_recording():
|
791
|
+
self._span.set_status(Status(StatusCode.OK))
|
792
|
+
self._span.end()
|
793
|
+
logger.debug("ChatStream span closed successfully")
|
794
|
+
|
795
|
+
# Calculate partial metrics based on available data
|
796
|
+
self._record_partial_metrics()
|
797
|
+
|
798
|
+
self._cleanup_completed = True
|
799
|
+
logger.debug("ChatStream cleanup completed successfully")
|
800
|
+
|
801
|
+
except Exception as e:
|
802
|
+
# Log cleanup errors but don't propagate to avoid masking original issues
|
803
|
+
logger.debug("Error during ChatStream cleanup: %s", str(e))
|
804
|
+
|
805
|
+
# Still try to close the span even if metrics recording failed
|
806
|
+
try:
|
807
|
+
if self._span and self._span.is_recording():
|
808
|
+
self._span.set_status(
|
809
|
+
Status(StatusCode.ERROR, "Cleanup failed")
|
810
|
+
)
|
811
|
+
self._span.end()
|
812
|
+
self._cleanup_completed = True
|
813
|
+
except Exception:
|
814
|
+
# Final fallback - just mark as completed to prevent infinite loops
|
815
|
+
self._cleanup_completed = True
|
816
|
+
|
817
|
+
@dont_throw
|
818
|
+
def _record_partial_metrics(self):
|
819
|
+
"""Record metrics based on available partial data"""
|
820
|
+
# Always record duration if we have start time
|
821
|
+
if (
|
822
|
+
self._start_time
|
823
|
+
and isinstance(self._start_time, (float, int))
|
824
|
+
and self._duration_histogram
|
825
|
+
):
|
826
|
+
duration = time.time() - self._start_time
|
827
|
+
self._duration_histogram.record(
|
828
|
+
duration, attributes=self._shared_attributes()
|
829
|
+
)
|
830
|
+
|
831
|
+
# Record basic span attributes even without complete response
|
832
|
+
if self._span and self._span.is_recording():
|
833
|
+
_set_response_attributes(self._span, self._complete_response)
|
834
|
+
|
835
|
+
# Record partial token metrics if we have any data
|
836
|
+
if self._complete_response.get("choices") or self._request_kwargs:
|
837
|
+
_set_streaming_token_metrics(
|
838
|
+
self._request_kwargs,
|
839
|
+
self._complete_response,
|
840
|
+
self._span,
|
841
|
+
self._token_counter,
|
842
|
+
self._shared_attributes(),
|
843
|
+
)
|
844
|
+
|
845
|
+
# Record choice metrics if we have any choices processed
|
846
|
+
if self._choice_counter and self._complete_response.get("choices"):
|
847
|
+
_set_choice_counter_metrics(
|
848
|
+
self._choice_counter,
|
849
|
+
self._complete_response.get("choices"),
|
850
|
+
self._shared_attributes(),
|
851
|
+
)
|
730
852
|
|
731
853
|
|
732
854
|
# Backward compatibility with OpenAI v0
|
@@ -975,6 +1097,13 @@ def _accumulate_stream_items(item, complete_response):
|
|
975
1097
|
complete_response["model"] = item.get("model")
|
976
1098
|
complete_response["id"] = item.get("id")
|
977
1099
|
|
1100
|
+
# capture usage information from the last stream chunks
|
1101
|
+
if item.get("usage"):
|
1102
|
+
complete_response["usage"] = item.get("usage")
|
1103
|
+
elif item.get("choices") and item["choices"][0].get("usage"):
|
1104
|
+
# Some LLM providers like moonshot mistakenly place token usage information within choices[0], handle this.
|
1105
|
+
complete_response["usage"] = item["choices"][0].get("usage")
|
1106
|
+
|
978
1107
|
# prompt filter results
|
979
1108
|
if item.get("prompt_filter_results"):
|
980
1109
|
complete_response["prompt_filter_results"] = item.get("prompt_filter_results")
|
@@ -27,7 +27,10 @@ from ..utils import (
|
|
27
27
|
should_emit_events,
|
28
28
|
should_send_prompts,
|
29
29
|
)
|
30
|
-
from lmnr.opentelemetry_lib.tracing.context import
|
30
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
31
|
+
get_current_context,
|
32
|
+
get_event_attributes_from_context,
|
33
|
+
)
|
31
34
|
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
32
35
|
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
33
36
|
from opentelemetry.semconv_ai import (
|
@@ -65,7 +68,8 @@ def completion_wrapper(tracer, wrapped, instance, args, kwargs):
|
|
65
68
|
response = wrapped(*args, **kwargs)
|
66
69
|
except Exception as e:
|
67
70
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
68
|
-
|
71
|
+
attributes = get_event_attributes_from_context()
|
72
|
+
span.record_exception(e, attributes=attributes)
|
69
73
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
70
74
|
span.end()
|
71
75
|
raise
|
@@ -100,7 +104,8 @@ async def acompletion_wrapper(tracer, wrapped, instance, args, kwargs):
|
|
100
104
|
response = await wrapped(*args, **kwargs)
|
101
105
|
except Exception as e:
|
102
106
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
103
|
-
|
107
|
+
attributes = get_event_attributes_from_context()
|
108
|
+
span.record_exception(e, attributes=attributes)
|
104
109
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
105
110
|
span.end()
|
106
111
|
raise
|
@@ -3,6 +3,8 @@ import time
|
|
3
3
|
from collections.abc import Iterable
|
4
4
|
|
5
5
|
from opentelemetry import context as context_api
|
6
|
+
|
7
|
+
from lmnr.opentelemetry_lib.tracing.context import get_event_attributes_from_context
|
6
8
|
from ..shared import (
|
7
9
|
OPENAI_LLM_USAGE_TOKEN_TYPES,
|
8
10
|
_get_openai_base_url,
|
@@ -91,7 +93,8 @@ def embeddings_wrapper(
|
|
91
93
|
exception_counter.add(1, attributes=attributes)
|
92
94
|
|
93
95
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
94
|
-
|
96
|
+
attributes = get_event_attributes_from_context()
|
97
|
+
span.record_exception(e, attributes=attributes)
|
95
98
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
96
99
|
span.end()
|
97
100
|
|
@@ -156,7 +159,8 @@ async def aembeddings_wrapper(
|
|
156
159
|
exception_counter.add(1, attributes=attributes)
|
157
160
|
|
158
161
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
159
|
-
|
162
|
+
attributes = get_event_attributes_from_context()
|
163
|
+
span.record_exception(e, attributes=attributes)
|
160
164
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
161
165
|
span.end()
|
162
166
|
|
@@ -17,7 +17,10 @@ from ..utils import (
|
|
17
17
|
dont_throw,
|
18
18
|
should_emit_events,
|
19
19
|
)
|
20
|
-
from lmnr.opentelemetry_lib.tracing.context import
|
20
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
21
|
+
get_current_context,
|
22
|
+
get_event_attributes_from_context,
|
23
|
+
)
|
21
24
|
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
22
25
|
from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
|
23
26
|
from opentelemetry.semconv_ai import LLMRequestTypeValues, SpanAttributes
|
@@ -132,7 +135,7 @@ def messages_list_wrapper(tracer, wrapped, instance, args, kwargs):
|
|
132
135
|
|
133
136
|
if exception := run.get("exception"):
|
134
137
|
span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
|
135
|
-
span.record_exception(exception)
|
138
|
+
span.record_exception(exception, attributes=get_event_attributes_from_context())
|
136
139
|
span.set_status(Status(StatusCode.ERROR, str(exception)))
|
137
140
|
span.end(run.get("end_time"))
|
138
141
|
|
@@ -316,7 +319,7 @@ def runs_create_and_stream_wrapper(tracer, wrapped, instance, args, kwargs):
|
|
316
319
|
return response
|
317
320
|
except Exception as e:
|
318
321
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
319
|
-
span.record_exception(e)
|
322
|
+
span.record_exception(e, attributes=get_event_attributes_from_context())
|
320
323
|
span.set_status(Status(StatusCode.ERROR, str(e)))
|
321
324
|
span.end()
|
322
325
|
raise
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from lmnr.opentelemetry_lib.tracing.context import get_event_attributes_from_context
|
1
2
|
from ..shared import _set_span_attribute
|
2
3
|
from ..shared.event_emitter import emit_event
|
3
4
|
from ..shared.event_models import ChoiceEvent
|
@@ -69,7 +70,9 @@ class EventHandlerWrapper(AssistantEventHandler):
|
|
69
70
|
@override
|
70
71
|
def on_exception(self, exception: Exception):
|
71
72
|
self._span.set_attribute(ERROR_TYPE, exception.__class__.__name__)
|
72
|
-
self._span.record_exception(
|
73
|
+
self._span.record_exception(
|
74
|
+
exception, attributes=get_event_attributes_from_context()
|
75
|
+
)
|
73
76
|
self._span.set_status(Status(StatusCode.ERROR, str(exception)))
|
74
77
|
self._original_handler.on_exception(exception)
|
75
78
|
|
@@ -36,7 +36,10 @@ except ImportError:
|
|
36
36
|
ResponseOutputMessageParam = Dict[str, Any]
|
37
37
|
RESPONSES_AVAILABLE = False
|
38
38
|
|
39
|
-
from lmnr.opentelemetry_lib.tracing.context import
|
39
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
40
|
+
get_current_context,
|
41
|
+
get_event_attributes_from_context,
|
42
|
+
)
|
40
43
|
from openai._legacy_response import LegacyAPIResponse
|
41
44
|
from opentelemetry import context as context_api
|
42
45
|
from opentelemetry.instrumentation.utils import _SUPPRESS_INSTRUMENTATION_KEY
|
@@ -433,7 +436,7 @@ def responses_get_or_create_wrapper(tracer: Tracer, wrapped, instance, args, kwa
|
|
433
436
|
context=get_current_context(),
|
434
437
|
)
|
435
438
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
436
|
-
span.record_exception(e)
|
439
|
+
span.record_exception(e, attributes=get_event_attributes_from_context())
|
437
440
|
span.set_status(StatusCode.ERROR, str(e))
|
438
441
|
if traced_data:
|
439
442
|
set_data_attributes(traced_data, span)
|
@@ -529,7 +532,7 @@ async def async_responses_get_or_create_wrapper(
|
|
529
532
|
context=get_current_context(),
|
530
533
|
)
|
531
534
|
span.set_attribute(ERROR_TYPE, e.__class__.__name__)
|
532
|
-
span.record_exception(e)
|
535
|
+
span.record_exception(e, attributes=get_event_attributes_from_context())
|
533
536
|
span.set_status(StatusCode.ERROR, str(e))
|
534
537
|
if traced_data:
|
535
538
|
set_data_attributes(traced_data, span)
|
@@ -597,7 +600,10 @@ def responses_cancel_wrapper(tracer: Tracer, wrapped, instance, args, kwargs):
|
|
597
600
|
record_exception=True,
|
598
601
|
context=get_current_context(),
|
599
602
|
)
|
600
|
-
span.record_exception(
|
603
|
+
span.record_exception(
|
604
|
+
Exception("Response cancelled"),
|
605
|
+
attributes=get_event_attributes_from_context(),
|
606
|
+
)
|
601
607
|
set_data_attributes(existing_data, span)
|
602
608
|
span.end()
|
603
609
|
return response
|
@@ -624,7 +630,10 @@ async def async_responses_cancel_wrapper(
|
|
624
630
|
record_exception=True,
|
625
631
|
context=get_current_context(),
|
626
632
|
)
|
627
|
-
span.record_exception(
|
633
|
+
span.record_exception(
|
634
|
+
Exception("Response cancelled"),
|
635
|
+
attributes=get_event_attributes_from_context(),
|
636
|
+
)
|
628
637
|
set_data_attributes(existing_data, span)
|
629
638
|
span.end()
|
630
639
|
return response
|
@@ -2,7 +2,9 @@ import threading
|
|
2
2
|
|
3
3
|
from abc import ABC, abstractmethod
|
4
4
|
from contextvars import ContextVar
|
5
|
-
from opentelemetry.context import Context, Token
|
5
|
+
from opentelemetry.context import Context, Token, create_key, get_value
|
6
|
+
|
7
|
+
from lmnr.opentelemetry_lib.tracing.attributes import SESSION_ID, USER_ID
|
6
8
|
|
7
9
|
|
8
10
|
class _IsolatedRuntimeContext(ABC):
|
@@ -107,3 +109,18 @@ def attach_context(context: Context) -> Token[Context]:
|
|
107
109
|
def detach_context(token: Token[Context]) -> None:
|
108
110
|
"""Detach a context from the isolated runtime context."""
|
109
111
|
_ISOLATED_RUNTIME_CONTEXT.detach(token)
|
112
|
+
|
113
|
+
|
114
|
+
CONTEXT_USER_ID_KEY = create_key(f"lmnr.{USER_ID}")
|
115
|
+
CONTEXT_SESSION_ID_KEY = create_key(f"lmnr.{SESSION_ID}")
|
116
|
+
|
117
|
+
|
118
|
+
def get_event_attributes_from_context(context: Context | None = None) -> dict[str, str]:
|
119
|
+
"""Get the event attributes from the context."""
|
120
|
+
context = context or get_current_context()
|
121
|
+
attributes = {}
|
122
|
+
if session_id := get_value(CONTEXT_SESSION_ID_KEY, context):
|
123
|
+
attributes["lmnr.event.session_id"] = session_id
|
124
|
+
if user_id := get_value(CONTEXT_USER_ID_KEY, context):
|
125
|
+
attributes["lmnr.event.user_id"] = user_id
|
126
|
+
return attributes
|