lmnr 0.5.3__py3-none-any.whl → 0.6.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.
Files changed (33) hide show
  1. lmnr/__init__.py +6 -1
  2. lmnr/opentelemetry_lib/__init__.py +23 -36
  3. lmnr/opentelemetry_lib/decorators/__init__.py +219 -0
  4. lmnr/opentelemetry_lib/tracing/__init__.py +158 -1
  5. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +398 -0
  6. lmnr/opentelemetry_lib/tracing/attributes.py +14 -7
  7. lmnr/opentelemetry_lib/tracing/context_properties.py +53 -0
  8. lmnr/opentelemetry_lib/tracing/exporter.py +60 -0
  9. lmnr/opentelemetry_lib/tracing/instruments.py +121 -0
  10. lmnr/opentelemetry_lib/tracing/processor.py +96 -0
  11. lmnr/opentelemetry_lib/tracing/{context_manager.py → tracer.py} +6 -1
  12. lmnr/opentelemetry_lib/utils/package_check.py +3 -1
  13. lmnr/sdk/browser/browser_use_otel.py +1 -1
  14. lmnr/sdk/browser/playwright_otel.py +22 -7
  15. lmnr/sdk/browser/pw_utils.py +25 -9
  16. lmnr/sdk/client/asynchronous/resources/agent.py +3 -1
  17. lmnr/sdk/client/synchronous/resources/agent.py +3 -1
  18. lmnr/sdk/decorators.py +4 -2
  19. lmnr/sdk/evaluations.py +3 -3
  20. lmnr/sdk/laminar.py +28 -31
  21. lmnr/sdk/utils.py +2 -3
  22. lmnr/version.py +1 -1
  23. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/METADATA +65 -62
  24. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/RECORD +27 -28
  25. lmnr/opentelemetry_lib/config/__init__.py +0 -12
  26. lmnr/opentelemetry_lib/decorators/base.py +0 -210
  27. lmnr/opentelemetry_lib/instruments.py +0 -42
  28. lmnr/opentelemetry_lib/tracing/content_allow_list.py +0 -24
  29. lmnr/opentelemetry_lib/tracing/tracing.py +0 -1016
  30. lmnr/opentelemetry_lib/utils/in_memory_span_exporter.py +0 -61
  31. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/LICENSE +0 -0
  32. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/WHEEL +0 -0
  33. {lmnr-0.5.3.dist-info → lmnr-0.6.1.dist-info}/entry_points.txt +0 -0
lmnr/__init__.py CHANGED
@@ -13,8 +13,10 @@ from .sdk.types import (
13
13
  )
14
14
  from .sdk.decorators import observe
15
15
  from .sdk.types import LaminarSpanContext
16
- from .opentelemetry_lib import Instruments
17
16
  from .opentelemetry_lib.tracing.attributes import Attributes
17
+ from .opentelemetry_lib.tracing.instruments import Instruments
18
+ from .opentelemetry_lib.tracing.processor import LaminarSpanProcessor
19
+ from .opentelemetry_lib.tracing.tracer import get_laminar_tracer_provider, get_tracer
18
20
  from opentelemetry.trace import use_span
19
21
 
20
22
  __all__ = [
@@ -29,9 +31,12 @@ __all__ = [
29
31
  "LaminarClient",
30
32
  "LaminarDataset",
31
33
  "LaminarSpanContext",
34
+ "LaminarSpanProcessor",
32
35
  "RunAgentResponseChunk",
33
36
  "StepChunkContent",
34
37
  "TracingLevel",
38
+ "get_laminar_tracer_provider",
39
+ "get_tracer",
35
40
  "evaluate",
36
41
  "observe",
37
42
  "use_span",
@@ -1,19 +1,14 @@
1
+ import logging
1
2
  import sys
2
3
 
3
4
  from typing import Optional, Set
4
- from opentelemetry.sdk.trace import SpanProcessor
5
5
  from opentelemetry.sdk.trace.export import SpanExporter
6
6
  from opentelemetry.sdk.resources import SERVICE_NAME
7
- from opentelemetry.propagators.textmap import TextMapPropagator
8
- from opentelemetry.util.re import parse_env_headers
9
7
 
10
- from lmnr.opentelemetry_lib.instruments import Instruments
11
- from lmnr.opentelemetry_lib.config import (
12
- is_content_tracing_enabled,
13
- is_tracing_enabled,
14
- )
15
- from lmnr.opentelemetry_lib.tracing.tracing import TracerWrapper
16
- from typing import Dict
8
+ from lmnr.opentelemetry_lib.tracing.instruments import Instruments
9
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
10
+
11
+ MAX_MANUAL_SPAN_PAYLOAD_SIZE = 1024 * 1024 # 1MB
17
12
 
18
13
 
19
14
  class TracerManager:
@@ -22,48 +17,40 @@ class TracerManager:
22
17
  @staticmethod
23
18
  def init(
24
19
  app_name: Optional[str] = sys.argv[0],
25
- api_endpoint: str = "https://api.lmnr.ai",
26
- api_key: Optional[str] = None,
27
- headers: Dict[str, str] = {},
28
20
  disable_batch=False,
29
21
  exporter: Optional[SpanExporter] = None,
30
- processor: Optional[SpanProcessor] = None,
31
- propagator: Optional[TextMapPropagator] = None,
32
- should_enrich_metrics: bool = False,
33
22
  resource_attributes: dict = {},
34
23
  instruments: Optional[Set[Instruments]] = None,
35
- base_http_url: Optional[str] = None,
24
+ block_instruments: Optional[Set[Instruments]] = None,
25
+ base_url: str = "https://api.lmnr.ai",
26
+ port: int = 8443,
27
+ http_port: int = 443,
36
28
  project_api_key: Optional[str] = None,
37
29
  max_export_batch_size: Optional[int] = None,
30
+ force_http: bool = False,
31
+ timeout_seconds: int = 30,
32
+ set_global_tracer_provider: bool = True,
33
+ otel_logger_level: int = logging.ERROR,
38
34
  ) -> None:
39
- if not is_tracing_enabled():
40
- return
41
-
42
- enable_content_tracing = is_content_tracing_enabled()
43
-
44
- if isinstance(headers, str):
45
- headers = parse_env_headers(headers)
46
-
47
- if api_key and not exporter and not processor and not headers:
48
- headers = {
49
- "Authorization": f"Bearer {api_key}",
50
- }
35
+ enable_content_tracing = True
51
36
 
52
37
  # Tracer init
53
38
  resource_attributes.update({SERVICE_NAME: app_name})
54
- TracerWrapper.set_static_params(
55
- resource_attributes, enable_content_tracing, api_endpoint, headers
56
- )
39
+ TracerWrapper.set_static_params(resource_attributes, enable_content_tracing)
57
40
  TracerManager.__tracer_wrapper = TracerWrapper(
58
41
  disable_batch=disable_batch,
59
- processor=processor,
60
- propagator=propagator,
61
42
  exporter=exporter,
62
- should_enrich_metrics=should_enrich_metrics,
63
43
  instruments=instruments,
64
- base_http_url=base_http_url,
44
+ block_instruments=block_instruments,
45
+ base_url=base_url,
46
+ port=port,
47
+ http_port=http_port,
65
48
  project_api_key=project_api_key,
66
49
  max_export_batch_size=max_export_batch_size,
50
+ force_http=force_http,
51
+ timeout_seconds=timeout_seconds,
52
+ set_global_tracer_provider=set_global_tracer_provider,
53
+ otel_logger_level=otel_logger_level,
67
54
  )
68
55
 
69
56
  @staticmethod
@@ -0,0 +1,219 @@
1
+ from functools import wraps
2
+ import json
3
+ import logging
4
+ import pydantic
5
+ import types
6
+ from typing import Any, Literal, Optional, Union
7
+
8
+ from opentelemetry import trace
9
+ from opentelemetry import context as context_api
10
+ from opentelemetry.trace import Span
11
+
12
+ from lmnr.sdk.utils import get_input_from_func_args, is_method
13
+ from lmnr.opentelemetry_lib import MAX_MANUAL_SPAN_PAYLOAD_SIZE
14
+ from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
15
+ from lmnr.opentelemetry_lib.tracing.attributes import SPAN_INPUT, SPAN_OUTPUT, SPAN_TYPE
16
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper
17
+ from lmnr.opentelemetry_lib.utils.json_encoder import JSONEncoder
18
+
19
+
20
+ class CustomJSONEncoder(JSONEncoder):
21
+ def default(self, o: Any) -> Any:
22
+ if isinstance(o, pydantic.BaseModel):
23
+ return o.model_dump_json()
24
+ try:
25
+ return super().default(o)
26
+ except TypeError:
27
+ return str(o) # Fallback to string representation for unsupported types
28
+
29
+
30
+ def json_dumps(data: dict) -> str:
31
+ try:
32
+ return json.dumps(data, cls=CustomJSONEncoder)
33
+ except Exception:
34
+ # Log the exception and return a placeholder if serialization completely fails
35
+ logging.warning("Failed to serialize data to JSON, type: %s", type(data))
36
+ return "{}" # Return an empty JSON object as a fallback
37
+
38
+
39
+ def entity_method(
40
+ name: Optional[str] = None,
41
+ ignore_input: bool = False,
42
+ ignore_inputs: Optional[list[str]] = None,
43
+ ignore_output: bool = False,
44
+ span_type: Union[Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]] = "DEFAULT",
45
+ ):
46
+ def decorate(fn):
47
+ @wraps(fn)
48
+ def wrap(*args, **kwargs):
49
+ if not TracerWrapper.verify_initialized():
50
+ return fn(*args, **kwargs)
51
+
52
+ span_name = name or fn.__name__
53
+
54
+ with get_tracer() as tracer:
55
+ span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
56
+
57
+ ctx = trace.set_span_in_context(span, context_api.get_current())
58
+ ctx_token = context_api.attach(ctx)
59
+
60
+ try:
61
+ if not ignore_input:
62
+ inp = json_dumps(
63
+ get_input_from_func_args(
64
+ fn,
65
+ is_method=is_method(fn),
66
+ func_args=args,
67
+ func_kwargs=kwargs,
68
+ ignore_inputs=ignore_inputs,
69
+ )
70
+ )
71
+ if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
72
+ span.set_attribute(
73
+ SPAN_INPUT, "Laminar: input too large to record"
74
+ )
75
+ else:
76
+ span.set_attribute(SPAN_INPUT, inp)
77
+ except TypeError:
78
+ pass
79
+
80
+ try:
81
+ res = fn(*args, **kwargs)
82
+ except Exception as e:
83
+ _process_exception(span, e)
84
+ span.end()
85
+ raise e
86
+
87
+ # span will be ended in the generator
88
+ if isinstance(res, types.GeneratorType):
89
+ return _handle_generator(span, ctx_token, res)
90
+ if isinstance(res, types.AsyncGeneratorType):
91
+ # async def foo() -> AsyncGenerator[int, None]:
92
+ # is not considered async in a classical sense in Python,
93
+ # so we handle this inside the sync wrapper.
94
+ # In particular, CO_COROUTINE is different from CO_ASYNC_GENERATOR.
95
+ # Flags are listed from LSB here:
96
+ # https://docs.python.org/3/library/inspect.html#inspect-module-co-flags
97
+ # See also: https://groups.google.com/g/python-tulip/c/6rWweGXLutU?pli=1
98
+ return _ahandle_generator(span, ctx_token, res)
99
+
100
+ try:
101
+ if not ignore_output:
102
+ output = json_dumps(res)
103
+ if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
104
+ span.set_attribute(
105
+ SPAN_OUTPUT, "Laminar: output too large to record"
106
+ )
107
+ else:
108
+ span.set_attribute(SPAN_OUTPUT, output)
109
+ except TypeError:
110
+ pass
111
+
112
+ span.end()
113
+ context_api.detach(ctx_token)
114
+
115
+ return res
116
+
117
+ return wrap
118
+
119
+ return decorate
120
+
121
+
122
+ # Async Decorators
123
+ def aentity_method(
124
+ name: Optional[str] = None,
125
+ ignore_input: bool = False,
126
+ ignore_inputs: Optional[list[str]] = None,
127
+ ignore_output: bool = False,
128
+ span_type: Union[Literal["DEFAULT"], Literal["LLM"], Literal["TOOL"]] = "DEFAULT",
129
+ ):
130
+ def decorate(fn):
131
+ @wraps(fn)
132
+ async def wrap(*args, **kwargs):
133
+ if not TracerWrapper.verify_initialized():
134
+ return await fn(*args, **kwargs)
135
+
136
+ span_name = name or fn.__name__
137
+
138
+ with get_tracer() as tracer:
139
+ span = tracer.start_span(span_name, attributes={SPAN_TYPE: span_type})
140
+
141
+ ctx = trace.set_span_in_context(span, context_api.get_current())
142
+ ctx_token = context_api.attach(ctx)
143
+
144
+ try:
145
+ if not ignore_input:
146
+ inp = json_dumps(
147
+ get_input_from_func_args(
148
+ fn,
149
+ is_method=is_method(fn),
150
+ func_args=args,
151
+ func_kwargs=kwargs,
152
+ ignore_inputs=ignore_inputs,
153
+ )
154
+ )
155
+ if len(inp) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
156
+ span.set_attribute(
157
+ SPAN_INPUT, "Laminar: input too large to record"
158
+ )
159
+ else:
160
+ span.set_attribute(SPAN_INPUT, inp)
161
+ except TypeError:
162
+ pass
163
+
164
+ try:
165
+ res = await fn(*args, **kwargs)
166
+ except Exception as e:
167
+ _process_exception(span, e)
168
+ span.end()
169
+ raise e
170
+
171
+ # span will be ended in the generator
172
+ if isinstance(res, types.AsyncGeneratorType):
173
+ # probably unreachable, read the comment in the similar
174
+ # part of the sync wrapper.
175
+ return await _ahandle_generator(span, ctx_token, res)
176
+
177
+ try:
178
+ if not ignore_output:
179
+ output = json_dumps(res)
180
+ if len(output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
181
+ span.set_attribute(
182
+ SPAN_OUTPUT, "Laminar: output too large to record"
183
+ )
184
+ else:
185
+ span.set_attribute(SPAN_OUTPUT, output)
186
+ except TypeError:
187
+ pass
188
+
189
+ span.end()
190
+ context_api.detach(ctx_token)
191
+
192
+ return res
193
+
194
+ return wrap
195
+
196
+ return decorate
197
+
198
+
199
+ def _handle_generator(span, ctx_token, res):
200
+ yield from res
201
+
202
+ span.end()
203
+ if ctx_token is not None:
204
+ context_api.detach(ctx_token)
205
+
206
+
207
+ async def _ahandle_generator(span, ctx_token, res):
208
+ # async with contextlib.aclosing(res) as closing_gen:
209
+ async for part in res:
210
+ yield part
211
+
212
+ span.end()
213
+ if ctx_token is not None:
214
+ context_api.detach(ctx_token)
215
+
216
+
217
+ def _process_exception(span: Span, e: Exception):
218
+ # Note that this `escaped` is sent as a StringValue("True"), not a boolean.
219
+ span.record_exception(e, escaped=True)
@@ -1 +1,158 @@
1
- from lmnr.opentelemetry_lib.tracing.context_manager import get_tracer
1
+ import atexit
2
+ import logging
3
+
4
+ from lmnr.opentelemetry_lib.tracing.processor import LaminarSpanProcessor
5
+ from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
6
+ from lmnr.sdk.client.synchronous.sync_client import LaminarClient
7
+ from lmnr.sdk.log import VerboseColorfulFormatter
8
+ from lmnr.opentelemetry_lib.tracing.instruments import (
9
+ Instruments,
10
+ init_instrumentations,
11
+ )
12
+
13
+ from opentelemetry import trace
14
+ from opentelemetry.instrumentation.threading import ThreadingInstrumentor
15
+ from opentelemetry.sdk.resources import Resource
16
+ from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
17
+ from opentelemetry.sdk.trace.export import SpanExporter
18
+ from typing import Optional, Set
19
+
20
+ module_logger = logging.getLogger(__name__)
21
+ console_log_handler = logging.StreamHandler()
22
+ console_log_handler.setFormatter(VerboseColorfulFormatter())
23
+ module_logger.addHandler(console_log_handler)
24
+
25
+
26
+ TRACER_NAME = "lmnr.tracer"
27
+
28
+ MAX_EVENTS_OR_ATTRIBUTES_PER_SPAN = 5000
29
+
30
+
31
+ class TracerWrapper(object):
32
+ resource_attributes: dict = {}
33
+ enable_content_tracing: bool = True
34
+ __tracer_provider: Optional[TracerProvider] = None
35
+ __logger: logging.Logger
36
+ __client: LaminarClient
37
+ __async_client: AsyncLaminarClient
38
+ __resource: Resource
39
+ __span_processor: SpanProcessor
40
+
41
+ def __new__(
42
+ cls,
43
+ disable_batch=False,
44
+ exporter: Optional[SpanExporter] = None,
45
+ instruments: Optional[Set[Instruments]] = None,
46
+ block_instruments: Optional[Set[Instruments]] = None,
47
+ base_url: str = "https://api.lmnr.ai",
48
+ port: int = 8443,
49
+ http_port: int = 443,
50
+ project_api_key: Optional[str] = None,
51
+ max_export_batch_size: Optional[int] = None,
52
+ force_http: bool = False,
53
+ timeout_seconds: int = 10,
54
+ set_global_tracer_provider: bool = True,
55
+ otel_logger_level: int = logging.ERROR,
56
+ ) -> "TracerWrapper":
57
+ # Silence some opentelemetry warnings
58
+ logging.getLogger("opentelemetry.trace").setLevel(otel_logger_level)
59
+
60
+ base_http_url = f"{base_url}:{http_port}"
61
+ cls._initialize_logger(cls)
62
+ if not hasattr(cls, "instance"):
63
+ obj = cls.instance = super(TracerWrapper, cls).__new__(cls)
64
+
65
+ obj.__client = LaminarClient(
66
+ base_url=base_http_url,
67
+ project_api_key=project_api_key,
68
+ )
69
+ obj.__async_client = AsyncLaminarClient(
70
+ base_url=base_http_url,
71
+ project_api_key=project_api_key,
72
+ )
73
+
74
+ obj.__resource = Resource(attributes=TracerWrapper.resource_attributes)
75
+
76
+ obj.__span_processor = LaminarSpanProcessor(
77
+ base_url=base_url,
78
+ api_key=project_api_key,
79
+ port=http_port if force_http else port,
80
+ exporter=exporter,
81
+ max_export_batch_size=max_export_batch_size,
82
+ timeout_seconds=timeout_seconds,
83
+ force_http=force_http,
84
+ disable_batch=disable_batch,
85
+ )
86
+
87
+ lmnr_provider = TracerProvider(resource=obj.__resource)
88
+ global_provider = trace.get_tracer_provider()
89
+ if set_global_tracer_provider and isinstance(
90
+ global_provider, trace.ProxyTracerProvider
91
+ ):
92
+ trace.set_tracer_provider(lmnr_provider)
93
+
94
+ obj.__tracer_provider = lmnr_provider
95
+
96
+ obj.__tracer_provider.add_span_processor(obj.__span_processor)
97
+
98
+ # This is not a real instrumentation and does not generate telemetry
99
+ # data, but it is required to ensure that OpenTelemetry context
100
+ # propagation is enabled.
101
+ # See the README at:
102
+ # https://pypi.org/project/opentelemetry-instrumentation-threading/
103
+ ThreadingInstrumentor().instrument()
104
+
105
+ init_instrumentations(
106
+ tracer_provider=obj.__tracer_provider,
107
+ instruments=instruments,
108
+ block_instruments=block_instruments,
109
+ client=obj.__client,
110
+ async_client=obj.__async_client,
111
+ )
112
+
113
+ # Force flushes for debug environments (e.g. local development)
114
+ atexit.register(obj.exit_handler)
115
+
116
+ return cls.instance
117
+
118
+ def exit_handler(self):
119
+ if isinstance(self.__span_processor, LaminarSpanProcessor):
120
+ self.__span_processor.clear()
121
+ self.flush()
122
+
123
+ def _initialize_logger(self):
124
+ self.__logger = logging.getLogger(__name__)
125
+ console_log_handler = logging.StreamHandler()
126
+ console_log_handler.setFormatter(VerboseColorfulFormatter())
127
+ self.__logger.addHandler(console_log_handler)
128
+
129
+ @staticmethod
130
+ def set_static_params(
131
+ resource_attributes: dict,
132
+ enable_content_tracing: bool,
133
+ ) -> None:
134
+ TracerWrapper.resource_attributes = resource_attributes
135
+ TracerWrapper.enable_content_tracing = enable_content_tracing
136
+
137
+ @classmethod
138
+ def verify_initialized(cls) -> bool:
139
+ return hasattr(cls, "instance")
140
+
141
+ @classmethod
142
+ def clear(cls):
143
+ # Any state cleanup. Now used in between tests
144
+ if isinstance(cls.instance.__span_processor, LaminarSpanProcessor):
145
+ cls.instance.__span_processor.clear()
146
+
147
+ def shutdown(self):
148
+ if self.__tracer_provider is None:
149
+ return
150
+ self.__tracer_provider.shutdown()
151
+
152
+ def flush(self):
153
+ return self.__span_processor.force_flush()
154
+
155
+ def get_tracer(self):
156
+ if self.__tracer_provider is None:
157
+ return trace.get_tracer_provider().get_tracer(TRACER_NAME)
158
+ return self.__tracer_provider.get_tracer(TRACER_NAME)