lmnr 0.6.16__py3-none-any.whl → 0.7.26__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 (113) hide show
  1. lmnr/__init__.py +6 -15
  2. lmnr/cli/__init__.py +270 -0
  3. lmnr/cli/datasets.py +371 -0
  4. lmnr/{cli.py → cli/evals.py} +20 -102
  5. lmnr/cli/rules.py +42 -0
  6. lmnr/opentelemetry_lib/__init__.py +9 -2
  7. lmnr/opentelemetry_lib/decorators/__init__.py +274 -168
  8. lmnr/opentelemetry_lib/litellm/__init__.py +352 -38
  9. lmnr/opentelemetry_lib/litellm/utils.py +82 -0
  10. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
  11. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
  12. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
  13. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
  14. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
  15. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
  16. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
  17. lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
  18. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
  19. lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
  20. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
  21. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
  22. lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
  23. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +191 -129
  24. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
  25. lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +126 -41
  26. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
  27. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
  28. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
  29. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
  30. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
  31. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
  32. lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
  33. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
  34. lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
  35. lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +16 -16
  36. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
  37. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
  38. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
  39. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
  40. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
  41. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
  42. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
  43. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
  44. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
  45. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
  46. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
  47. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
  48. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
  49. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
  50. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
  51. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
  52. lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
  53. lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
  54. lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +59 -61
  55. lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
  56. lmnr/opentelemetry_lib/tracing/__init__.py +119 -18
  57. lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +124 -25
  58. lmnr/opentelemetry_lib/tracing/attributes.py +4 -0
  59. lmnr/opentelemetry_lib/tracing/context.py +200 -0
  60. lmnr/opentelemetry_lib/tracing/exporter.py +109 -15
  61. lmnr/opentelemetry_lib/tracing/instruments.py +22 -5
  62. lmnr/opentelemetry_lib/tracing/processor.py +128 -30
  63. lmnr/opentelemetry_lib/tracing/span.py +398 -0
  64. lmnr/opentelemetry_lib/tracing/tracer.py +40 -1
  65. lmnr/opentelemetry_lib/tracing/utils.py +62 -0
  66. lmnr/opentelemetry_lib/utils/package_check.py +9 -0
  67. lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
  68. lmnr/sdk/browser/background_send_events.py +158 -0
  69. lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
  70. lmnr/sdk/browser/browser_use_otel.py +12 -12
  71. lmnr/sdk/browser/bubus_otel.py +71 -0
  72. lmnr/sdk/browser/cdp_utils.py +518 -0
  73. lmnr/sdk/browser/inject_script.js +514 -0
  74. lmnr/sdk/browser/patchright_otel.py +18 -44
  75. lmnr/sdk/browser/playwright_otel.py +104 -187
  76. lmnr/sdk/browser/pw_utils.py +249 -210
  77. lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
  78. lmnr/sdk/browser/utils.py +1 -1
  79. lmnr/sdk/client/asynchronous/async_client.py +47 -15
  80. lmnr/sdk/client/asynchronous/resources/__init__.py +2 -7
  81. lmnr/sdk/client/asynchronous/resources/browser_events.py +1 -0
  82. lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
  83. lmnr/sdk/client/asynchronous/resources/evals.py +122 -18
  84. lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
  85. lmnr/sdk/client/asynchronous/resources/tags.py +4 -10
  86. lmnr/sdk/client/synchronous/resources/__init__.py +2 -2
  87. lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
  88. lmnr/sdk/client/synchronous/resources/evals.py +83 -17
  89. lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
  90. lmnr/sdk/client/synchronous/resources/tags.py +4 -10
  91. lmnr/sdk/client/synchronous/sync_client.py +47 -15
  92. lmnr/sdk/datasets/__init__.py +94 -0
  93. lmnr/sdk/datasets/file_utils.py +91 -0
  94. lmnr/sdk/decorators.py +103 -23
  95. lmnr/sdk/evaluations.py +122 -33
  96. lmnr/sdk/laminar.py +816 -333
  97. lmnr/sdk/log.py +7 -2
  98. lmnr/sdk/types.py +124 -143
  99. lmnr/sdk/utils.py +115 -2
  100. lmnr/version.py +1 -1
  101. {lmnr-0.6.16.dist-info → lmnr-0.7.26.dist-info}/METADATA +71 -78
  102. lmnr-0.7.26.dist-info/RECORD +116 -0
  103. lmnr-0.7.26.dist-info/WHEEL +4 -0
  104. lmnr-0.7.26.dist-info/entry_points.txt +3 -0
  105. lmnr/opentelemetry_lib/tracing/context_properties.py +0 -65
  106. lmnr/sdk/browser/rrweb/rrweb.umd.min.cjs +0 -98
  107. lmnr/sdk/client/asynchronous/resources/agent.py +0 -329
  108. lmnr/sdk/client/synchronous/resources/agent.py +0 -323
  109. lmnr/sdk/datasets.py +0 -60
  110. lmnr-0.6.16.dist-info/LICENSE +0 -75
  111. lmnr-0.6.16.dist-info/RECORD +0 -61
  112. lmnr-0.6.16.dist-info/WHEEL +0 -4
  113. lmnr-0.6.16.dist-info/entry_points.txt +0 -3
lmnr/sdk/laminar.py CHANGED
@@ -1,25 +1,43 @@
1
1
  from contextlib import contextmanager
2
2
  from contextvars import Context
3
3
  import warnings
4
- from typing_extensions import deprecated
5
4
  from lmnr.opentelemetry_lib import TracerManager
6
- from lmnr.opentelemetry_lib.tracing.instruments import Instruments
7
- from lmnr.opentelemetry_lib.tracing.tracer import get_tracer
5
+ from lmnr.opentelemetry_lib.tracing import TracerWrapper, get_current_context
6
+ from lmnr.opentelemetry_lib.tracing.context import (
7
+ CONTEXT_METADATA_KEY,
8
+ CONTEXT_SESSION_ID_KEY,
9
+ CONTEXT_TRACE_TYPE_KEY,
10
+ CONTEXT_USER_ID_KEY,
11
+ attach_context,
12
+ detach_context,
13
+ get_event_attributes_from_context,
14
+ push_span_context,
15
+ set_association_prop_context,
16
+ )
17
+ from opentelemetry.context import get_value
8
18
  from lmnr.opentelemetry_lib.tracing.attributes import (
9
19
  ASSOCIATION_PROPERTIES,
20
+ PARENT_SPAN_IDS_PATH,
21
+ PARENT_SPAN_PATH,
10
22
  USER_ID,
11
23
  Attributes,
12
24
  SPAN_TYPE,
13
25
  )
14
- from lmnr.opentelemetry_lib import MAX_MANUAL_SPAN_PAYLOAD_SIZE
15
- from lmnr.opentelemetry_lib.decorators import json_dumps
16
- from opentelemetry import context as context_api, trace
17
- from opentelemetry.context import attach, detach
18
- from opentelemetry.trace import INVALID_TRACE_ID
26
+ from lmnr.opentelemetry_lib.tracing.instruments import Instruments
27
+ from lmnr.opentelemetry_lib.tracing.processor import LaminarSpanProcessor
28
+ from lmnr.opentelemetry_lib.tracing.span import LaminarSpan
29
+ from lmnr.opentelemetry_lib.tracing.tracer import get_tracer_with_context
30
+ from lmnr.opentelemetry_lib.tracing.utils import set_association_props_in_context
31
+ from lmnr.sdk.utils import get_otel_env_var
32
+
33
+ from opentelemetry import trace
34
+ from opentelemetry import context as context_api
35
+ from opentelemetry.trace import INVALID_TRACE_ID, Span, Status, StatusCode, use_span
19
36
  from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
20
37
  from opentelemetry.util.types import AttributeValue
21
38
 
22
- from typing import Any, Literal
39
+ from typing import Any, Iterator, Literal
40
+ from typing_extensions import TypedDict
23
41
 
24
42
  import datetime
25
43
  import logging
@@ -27,48 +45,150 @@ import os
27
45
  import re
28
46
  import uuid
29
47
 
30
- from lmnr.opentelemetry_lib.tracing.attributes import (
31
- SESSION_ID,
32
- SPAN_INPUT,
33
- SPAN_OUTPUT,
34
- TRACE_TYPE,
35
- )
36
- from lmnr.opentelemetry_lib.tracing.context_properties import (
37
- get_association_properties,
38
- remove_association_properties,
39
- set_association_properties,
40
- update_association_properties,
41
- )
42
- from lmnr.sdk.utils import from_env, is_otel_attribute_value_type
48
+ from lmnr.opentelemetry_lib.tracing.attributes import SESSION_ID, TRACE_TYPE
49
+
50
+ from lmnr.sdk.utils import from_env, is_otel_attribute_value_type, json_dumps
43
51
 
44
52
  from .log import VerboseColorfulFormatter
45
53
 
46
54
  from .types import (
47
55
  LaminarSpanContext,
56
+ SessionRecordingOptions,
48
57
  TraceType,
49
- TracingLevel,
50
58
  )
51
59
 
52
60
 
61
+ class ParsedParentSpanContext(TypedDict):
62
+ """Parsed information from a parent span context."""
63
+
64
+ otel_span_context: trace.SpanContext | None
65
+ path: list[str]
66
+ span_ids_path: list[str]
67
+ user_id: str | None
68
+ session_id: str | None
69
+ trace_type: TraceType | None
70
+ metadata: dict[str, Any] | None
71
+
72
+
73
+ def _parse_parent_span_context(
74
+ parent_span_context: LaminarSpanContext | dict | str | None,
75
+ logger: logging.Logger,
76
+ ) -> ParsedParentSpanContext:
77
+ """Parse parent_span_context and extract all relevant information.
78
+
79
+ Args:
80
+ parent_span_context: Parent span context to parse
81
+ logger: Logger for warnings
82
+
83
+ Returns:
84
+ ParsedParentSpanContext with otel_span_context, path, span_ids_path,
85
+ user_id, session_id, trace_type, and metadata
86
+ """
87
+ if parent_span_context is None:
88
+ return ParsedParentSpanContext(
89
+ otel_span_context=None,
90
+ path=[],
91
+ span_ids_path=[],
92
+ user_id=None,
93
+ session_id=None,
94
+ trace_type=None,
95
+ metadata=None,
96
+ )
97
+
98
+ path = []
99
+ span_ids_path = []
100
+ user_id = None
101
+ session_id = None
102
+ trace_type = None
103
+ metadata = None
104
+ laminar_span_context = None
105
+
106
+ # Try to deserialize if dict or str
107
+ if isinstance(parent_span_context, (dict, str)):
108
+ try:
109
+ laminar_span_context = LaminarSpanContext.deserialize(parent_span_context)
110
+ except Exception:
111
+ logger.warning(
112
+ f"Could not deserialize parent_span_context: {parent_span_context}. "
113
+ "Will use it as is."
114
+ )
115
+ laminar_span_context = parent_span_context
116
+ else:
117
+ laminar_span_context = parent_span_context
118
+
119
+ # Extract path and association props from LaminarSpanContext
120
+ if isinstance(laminar_span_context, LaminarSpanContext):
121
+ path = laminar_span_context.span_path
122
+ span_ids_path = laminar_span_context.span_ids_path
123
+ user_id = laminar_span_context.user_id
124
+ session_id = laminar_span_context.session_id
125
+ if laminar_span_context.trace_type is not None:
126
+ try:
127
+ trace_type = (
128
+ TraceType(laminar_span_context.trace_type)
129
+ if isinstance(laminar_span_context.trace_type, str)
130
+ else laminar_span_context.trace_type
131
+ )
132
+ except (ValueError, TypeError):
133
+ pass
134
+ metadata = laminar_span_context.metadata
135
+
136
+ # Convert to OTEL span context
137
+ try:
138
+ otel_span_context = LaminarSpanContext.try_to_otel_span_context(
139
+ laminar_span_context, logger
140
+ )
141
+ except ValueError as exc:
142
+ logger.warning(f"Invalid span context provided: {exc}")
143
+ return ParsedParentSpanContext(
144
+ otel_span_context=None,
145
+ path=path,
146
+ span_ids_path=span_ids_path,
147
+ user_id=user_id,
148
+ session_id=session_id,
149
+ trace_type=trace_type,
150
+ metadata=metadata,
151
+ )
152
+
153
+ return ParsedParentSpanContext(
154
+ otel_span_context=otel_span_context,
155
+ path=path,
156
+ span_ids_path=span_ids_path,
157
+ user_id=user_id,
158
+ session_id=session_id,
159
+ trace_type=trace_type,
160
+ metadata=metadata,
161
+ )
162
+
163
+
53
164
  class Laminar:
54
165
  __project_api_key: str | None = None
55
166
  __initialized: bool = False
56
167
  __base_http_url: str | None = None
168
+ __global_metadata: dict[str, AttributeValue] = {}
57
169
 
58
170
  @classmethod
59
171
  def initialize(
60
172
  cls,
61
173
  project_api_key: str | None = None,
62
174
  base_url: str | None = None,
175
+ base_http_url: str | None = None,
63
176
  http_port: int | None = None,
64
177
  grpc_port: int | None = None,
65
- instruments: set[Instruments] | None = None,
66
- disabled_instruments: set[Instruments] | None = None,
178
+ instruments: (
179
+ list[Instruments] | set[Instruments] | tuple[Instruments] | None
180
+ ) = None,
181
+ disabled_instruments: (
182
+ list[Instruments] | set[Instruments] | tuple[Instruments] | None
183
+ ) = None,
67
184
  disable_batch: bool = False,
68
185
  max_export_batch_size: int | None = None,
69
186
  export_timeout_seconds: int | None = None,
70
187
  set_global_tracer_provider: bool = True,
71
188
  otel_logger_level: int = logging.ERROR,
189
+ session_recording_options: SessionRecordingOptions | None = None,
190
+ force_http: bool = False,
191
+ metadata: dict[str, AttributeValue] | None = None,
72
192
  ):
73
193
  """Initialize Laminar context across the application.
74
194
  This method must be called before using any other Laminar methods or
@@ -76,41 +196,51 @@ class Laminar:
76
196
 
77
197
  Args:
78
198
  project_api_key (str | None, optional): Laminar project api key.\
79
- You can generate one by going to the projects\
80
- settings page on the Laminar dashboard.\
81
- If not specified, it will try to read from the\
82
- LMNR_PROJECT_API_KEY environment variable\
83
- in os.environ or in .env file.
84
- Defaults to None.
199
+ You can generate one by going to the projects settings page on\
200
+ the Laminar dashboard. If not specified, we will try to read\
201
+ from the LMNR_PROJECT_API_KEY environment variable in os.environ\
202
+ or in .env file. Defaults to None.
85
203
  base_url (str | None, optional): Laminar API url. Do NOT include\
86
- the port number, use `http_port` and `grpc_port`.\
87
- If not specified, defaults to https://api.lmnr.ai.
88
- http_port (int | None, optional): Laminar API http port.\
89
- If not specified, defaults to 443.
90
- grpc_port (int | None, optional): Laminar API grpc port.\
91
- If not specified, defaults to 8443.
92
- instruments (set[Instruments] | None, optional): Instruments to\
93
- enable. Defaults to all instruments. You can pass\
94
- an empty set to disable all instruments. Read more:\
95
- https://docs.lmnr.ai/tracing/automatic-instrumentation
96
- disabled_instruments (set[Instruments] | None, optional): Instruments to\
97
- disable. Defaults to None.
204
+ the port number, use `http_port` and `grpc_port`. If not\
205
+ specified, defaults to https://api.lmnr.ai.
206
+ base_http_url (str | None, optional): Laminar API http url. Only\
207
+ set this if your Laminar backend HTTP is proxied through a\
208
+ different host. If not specified, defaults to\
209
+ https://api.lmnr.ai.
210
+ http_port (int | None, optional): Laminar API http port. If not\
211
+ specified, defaults to 443.
212
+ grpc_port (int | None, optional): Laminar API grpc port. If not\
213
+ specified, defaults to 8443.
214
+ instruments (set[Instruments] | list[Instruments] | tuple[Instruments] | None, optional):
215
+ Instruments to enable. Defaults to all instruments. You can pass\
216
+ an empty set to disable all instruments. Read more:\
217
+ https://docs.lmnr.ai/tracing/automatic-instrumentation
218
+ disabled_instruments (set[Instruments] | list[Instruments] | tuple[Instruments] | None, optional):
219
+ Instruments to disable. Defaults to None.
98
220
  disable_batch (bool, optional): If set to True, spans will be sent\
99
- immediately to the backend. Useful for debugging, but\
100
- may cause performance overhead in production.
101
- Defaults to False.
221
+ immediately to the backend. Useful for debugging, but may cause\
222
+ performance overhead in production. Defaults to False.
223
+ max_export_batch_size (int | None, optional): Maximum number of spans\
224
+ to export in a single batch. If not specified, defaults to 64\
225
+ (lower than the OpenTelemetry default of 512). If you see\
226
+ `DEADLINE_EXCEEDED` errors, try reducing this value.
102
227
  export_timeout_seconds (int | None, optional): Timeout for the OTLP\
103
- exporter. Defaults to 30 seconds (unlike the\
104
- OpenTelemetry default of 10 seconds).
105
- Defaults to None.
228
+ exporter. Defaults to 30 seconds (unlike the OpenTelemetry\
229
+ default of 10 seconds). Defaults to None.
106
230
  set_global_tracer_provider (bool, optional): If set to True, the\
107
- Laminar tracer provider will be set as the global\
108
- tracer provider. OpenTelemetry allows only one tracer\
109
- provider per app, so set this to False, if you are using\
110
- another tracing library. Setting this to False may break\
111
- some external instrumentations, e.g. LiteLLM.
112
- Defaults to True.
113
-
231
+ Laminar tracer provider will be set as the global tracer provider.\
232
+ OpenTelemetry allows only one tracer provider per app, so set this\
233
+ to False, if you are using another tracing library. Setting this to\
234
+ False may break some external instrumentations, e.g. LiteLLM.\
235
+ Defaults to True.
236
+ otel_logger_level (int, optional): OpenTelemetry logger level. Defaults\
237
+ to logging.ERROR.
238
+ session_recording_options (SessionRecordingOptions | None, optional): Options\
239
+ for browser session recording. Currently supports 'mask_input'\
240
+ (bool) to control whether input fields are masked during recording.\
241
+ Defaults to None (uses default masking behavior).
242
+ force_http (bool, optional): If set to True, the HTTP OTEL exporter will be\
243
+ used instead of the gRPC OTEL exporter. Defaults to False.
114
244
  Raises:
115
245
  ValueError: If project API key is not set
116
246
  """
@@ -121,7 +251,12 @@ class Laminar:
121
251
  return
122
252
 
123
253
  cls.__project_api_key = project_api_key or from_env("LMNR_PROJECT_API_KEY")
124
- if not cls.__project_api_key:
254
+
255
+ if (
256
+ not cls.__project_api_key
257
+ and not get_otel_env_var("ENDPOINT")
258
+ and not get_otel_env_var("HEADERS")
259
+ ):
125
260
  raise ValueError(
126
261
  "Please initialize the Laminar object with"
127
262
  " your project API key or set the LMNR_PROJECT_API_KEY"
@@ -130,12 +265,19 @@ class Laminar:
130
265
 
131
266
  cls._initialize_logger()
132
267
 
133
- url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
134
- url = url.rstrip("/")
135
- if not url.startswith("http"):
136
- url = f"https://{url}"
137
- if match := re.search(r":(\d{1,5})$", url):
138
- url = url[: -len(match.group(0))]
268
+ url = base_url or from_env("LMNR_BASE_URL")
269
+ if url:
270
+ url = url.rstrip("/")
271
+ if not url.startswith("http:") and not url.startswith("https:"):
272
+ url = f"https://{url}"
273
+ if match := re.search(r":(\d{1,5})$", url):
274
+ url = url[: -len(match.group(0))]
275
+ cls.__logger.info(f"Ignoring port in base URL: {match.group(1)}")
276
+ http_url = base_http_url or url or "https://api.lmnr.ai"
277
+ if not http_url.startswith("http:") and not http_url.startswith("https:"):
278
+ http_url = f"https://{http_url}"
279
+ if match := re.search(r":(\d{1,5})$", http_url):
280
+ http_url = http_url[: -len(match.group(0))]
139
281
  if http_port is None:
140
282
  cls.__logger.info(f"Using HTTP port from base URL: {match.group(1)}")
141
283
  http_port = int(match.group(1))
@@ -143,7 +285,8 @@ class Laminar:
143
285
  cls.__logger.info(f"Using HTTP port passed as an argument: {http_port}")
144
286
 
145
287
  cls.__initialized = True
146
- cls.__base_http_url = f"{url}:{http_port or 443}"
288
+ cls.__base_http_url = f"{http_url}:{http_port or 443}"
289
+ cls.__global_metadata = metadata or {}
147
290
 
148
291
  if not os.getenv("OTEL_ATTRIBUTE_COUNT_LIMIT"):
149
292
  # each message is at least 2 attributes: role and content,
@@ -155,14 +298,63 @@ class Laminar:
155
298
  http_port=http_port or 443,
156
299
  port=grpc_port or 8443,
157
300
  project_api_key=cls.__project_api_key,
158
- instruments=instruments,
159
- block_instruments=disabled_instruments,
301
+ instruments=set(instruments) if instruments is not None else None,
302
+ block_instruments=(
303
+ set(disabled_instruments) if disabled_instruments is not None else None
304
+ ),
160
305
  disable_batch=disable_batch,
161
306
  max_export_batch_size=max_export_batch_size,
162
307
  timeout_seconds=export_timeout_seconds,
163
308
  set_global_tracer_provider=set_global_tracer_provider,
164
309
  otel_logger_level=otel_logger_level,
310
+ session_recording_options=session_recording_options,
311
+ force_http=force_http,
165
312
  )
313
+ with get_tracer_with_context() as (tracer, isolated_context):
314
+ new_ctx = context_api.set_value(
315
+ CONTEXT_METADATA_KEY, cls.__global_metadata, isolated_context
316
+ )
317
+ attach_context(new_ctx)
318
+
319
+ cls._initialize_context_from_env()
320
+
321
+ @classmethod
322
+ def _initialize_context_from_env(cls) -> None:
323
+ """Attach upstream Laminar context from the environment, if provided."""
324
+ env_context = os.getenv("LMNR_SPAN_CONTEXT")
325
+ if not env_context:
326
+ return
327
+
328
+ try:
329
+ laminar_context = LaminarSpanContext.deserialize(env_context)
330
+ except Exception as exc: # pylint: disable=broad-exception-caught
331
+ cls.__logger.warning(
332
+ "LMNR_SPAN_CONTEXT is set but could not be deserialized: %s", exc
333
+ )
334
+ return
335
+
336
+ try:
337
+ otel_span_context = LaminarSpanContext.try_to_otel_span_context(
338
+ laminar_context, cls.__logger
339
+ )
340
+ except ValueError as exc:
341
+ cls.__logger.warning(
342
+ "LMNR_SPAN_CONTEXT is set but invalid span context provided: %s", exc
343
+ )
344
+ return
345
+
346
+ base_context = trace.set_span_in_context(
347
+ trace.NonRecordingSpan(otel_span_context), get_current_context()
348
+ )
349
+ processor = TracerWrapper.instance._span_processor
350
+ if isinstance(processor, LaminarSpanProcessor):
351
+ processor.set_parent_path_info(
352
+ otel_span_context.span_id,
353
+ laminar_context.span_path,
354
+ laminar_context.span_ids_path,
355
+ )
356
+ push_span_context(base_context)
357
+ cls.__logger.debug("Initialized Laminar parent context from LMNR_SPAN_CONTEXT.")
166
358
 
167
359
  @classmethod
168
360
  def is_initialized(cls):
@@ -185,42 +377,48 @@ class Laminar:
185
377
  def event(
186
378
  cls,
187
379
  name: str,
188
- value: AttributeValue | None = None,
380
+ attributes: dict[str, AttributeValue] | None = None,
189
381
  timestamp: datetime.datetime | int | None = None,
382
+ *,
383
+ user_id: str | None = None,
384
+ session_id: str | None = None,
190
385
  ):
191
- """Associate an event with the current span. If using manual\
192
- instrumentation, use raw OpenTelemetry `span.add_event()` instead.\
193
- `value` will be saved as a `lmnr.event.value` attribute.
386
+ """Associate an event with the current span. This is a wrapper around
387
+ `span.add_event()` that adds the event to the current span.
194
388
 
195
389
  Args:
196
390
  name (str): event name
197
- value (AttributeValue | None, optional): event value. Must be a\
198
- primitive type. Boolean `True` is assumed in the backend if\
199
- `value` is None.
391
+ attributes (dict[str, AttributeValue] | None, optional): event attributes.
200
392
  Defaults to None.
201
393
  timestamp (datetime.datetime | int | None, optional): If int, must\
202
394
  be epoch nanoseconds. If not specified, relies on the underlying\
203
395
  OpenTelemetry implementation. Defaults to None.
204
396
  """
397
+ if not cls.is_initialized():
398
+ return
399
+
205
400
  if timestamp and isinstance(timestamp, datetime.datetime):
206
401
  timestamp = int(timestamp.timestamp() * 1e9)
207
402
 
208
- event = {
209
- "lmnr.event.type": "default",
210
- }
211
- if value is not None:
212
- event["lmnr.event.value"] = value
403
+ extra_attributes = get_event_attributes_from_context()
404
+
405
+ # override the user_id and session_id from the context with the ones
406
+ # passed as arguments
407
+ if user_id is not None:
408
+ extra_attributes["lmnr.event.user_id"] = user_id
409
+ if session_id is not None:
410
+ extra_attributes["lmnr.event.session_id"] = session_id
213
411
 
214
- current_span = trace.get_current_span()
412
+ current_span = trace.get_current_span(context=get_current_context())
215
413
  if current_span == trace.INVALID_SPAN:
216
- cls.__logger.warning(
217
- "`Laminar().event()` called outside of span context. "
218
- f"Event '{name}' will not be recorded in the trace. "
219
- "Make sure to annotate the function with a decorator"
220
- )
414
+ span = cls.start_span(name)
415
+ span.add_event(name, {**(attributes or {}), **extra_attributes}, timestamp)
416
+ span.end()
221
417
  return
222
418
 
223
- current_span.add_event(name, event, timestamp)
419
+ current_span.add_event(
420
+ name, {**(attributes or {}), **extra_attributes}, timestamp
421
+ )
224
422
 
225
423
  @classmethod
226
424
  @contextmanager
@@ -233,7 +431,10 @@ class Laminar:
233
431
  labels: list[str] | None = None,
234
432
  parent_span_context: LaminarSpanContext | None = None,
235
433
  tags: list[str] | None = None,
236
- ):
434
+ user_id: str | None = None,
435
+ session_id: str | None = None,
436
+ metadata: dict[str, AttributeValue] | None = None,
437
+ ) -> Iterator[LaminarSpan]:
237
438
  """Start a new span as the current span. Useful for manual
238
439
  instrumentation. If `span_type` is set to `"LLM"`, you should report
239
440
  usage and response attributes manually. See `Laminar.set_span_attributes`
@@ -262,13 +463,18 @@ class Laminar:
262
463
  obtained from `Laminar.get_laminar_span_context_dict()` or\
263
464
  `Laminar.get_laminar_span_context_str()` respectively, it will be\
264
465
  converted to a `LaminarSpanContext` if possible. See also\
265
- `Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
266
- `Laminar.get_span_context_str` for more information.
466
+ `Laminar.serialize_span_context` for more information.
267
467
  Defaults to None.
268
468
  labels (list[str] | None, optional): [DEPRECATED] Use tags\
269
469
  instead. Labels to set for the span. Defaults to None.
270
470
  tags (list[str] | None, optional): tags to set for the span.
271
471
  Defaults to None.
472
+ user_id (str | None, optional): user id to set for the trace.
473
+ Defaults to None.
474
+ session_id (str | None, optional): session id to set for the trace.
475
+ Defaults to None.
476
+ metadata (dict[str, AttributeValue] | None, optional): metadata to\
477
+ set for the trace. Defaults to None.
272
478
  """
273
479
 
274
480
  if not cls.is_initialized():
@@ -281,16 +487,69 @@ class Laminar:
281
487
  )
282
488
  return
283
489
 
284
- with get_tracer() as tracer:
285
- ctx = context or context_api.get_current()
286
- if parent_span_context is not None:
287
- span_context = LaminarSpanContext.try_to_otel_span_context(
288
- parent_span_context, cls.__logger
289
- )
490
+ with get_tracer_with_context() as (tracer, isolated_context):
491
+ ctx = context or isolated_context
492
+
493
+ # Parse parent_span_context and extract all info
494
+ parsed = _parse_parent_span_context(parent_span_context, cls.__logger)
495
+
496
+ # Set parent span in context if present
497
+ if parsed["otel_span_context"] is not None:
290
498
  ctx = trace.set_span_in_context(
291
- trace.NonRecordingSpan(span_context), ctx
499
+ trace.NonRecordingSpan(parsed["otel_span_context"]), ctx
500
+ )
501
+
502
+ # Determine trace_type with proper priority
503
+ trace_type = None
504
+ if span_type in ["EVALUATION", "EXECUTOR", "EVALUATOR"]:
505
+ trace_type = TraceType.EVALUATION
506
+ elif parsed["trace_type"] is not None:
507
+ trace_type = parsed["trace_type"]
508
+
509
+ # Merge metadata: context (inherited) + global + parent + explicit (explicit wins)
510
+ # Get metadata from context if it exists
511
+ ctx_metadata = get_value(CONTEXT_METADATA_KEY, ctx) or {}
512
+ # Merge with priority: global < context < parent < explicit
513
+ merged_metadata = {
514
+ **(cls.__global_metadata or {}),
515
+ **(ctx_metadata or {}),
516
+ **(parsed["metadata"] or {}),
517
+ **(metadata or {}),
518
+ }
519
+
520
+ # Get association props from context (fallback values)
521
+ ctx_user_id = get_value(CONTEXT_USER_ID_KEY, ctx)
522
+ ctx_session_id = get_value(CONTEXT_SESSION_ID_KEY, ctx)
523
+
524
+ # Merge user_id and session_id with priority: context < parent < explicit
525
+ final_user_id = (
526
+ user_id
527
+ if user_id is not None
528
+ else (
529
+ parsed["user_id"] if parsed["user_id"] is not None else ctx_user_id
292
530
  )
293
- ctx_token = attach(ctx)
531
+ )
532
+ final_session_id = (
533
+ session_id
534
+ if session_id is not None
535
+ else (
536
+ parsed["session_id"]
537
+ if parsed["session_id"] is not None
538
+ else ctx_session_id
539
+ )
540
+ )
541
+
542
+ ctx = set_association_prop_context(
543
+ trace_type=trace_type,
544
+ user_id=final_user_id,
545
+ session_id=final_session_id,
546
+ metadata=merged_metadata if merged_metadata else None,
547
+ context=ctx,
548
+ # we need a token separately, so we manually attach the context
549
+ attach=False,
550
+ )
551
+ ctx_token = context_api.attach(ctx)
552
+ isolated_context_token = attach_context(ctx)
294
553
  label_props = {}
295
554
  try:
296
555
  if labels:
@@ -313,82 +572,31 @@ class Laminar:
313
572
  f"`start_as_current_span` Could not set tags: {tags}. Tags must be a list of strings. "
314
573
  "Tags will be ignored."
315
574
  )
316
- with tracer.start_as_current_span(
317
- name,
318
- context=ctx,
319
- attributes={
320
- SPAN_TYPE: span_type,
321
- **(label_props),
322
- **(tag_props),
323
- },
324
- ) as span:
325
- if input is not None:
326
- serialized_input = json_dumps(input)
327
- if len(serialized_input) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
328
- span.set_attribute(
329
- SPAN_INPUT,
330
- "Laminar: input too large to record",
331
- )
332
- else:
333
- span.set_attribute(
334
- SPAN_INPUT,
335
- serialized_input,
336
- )
337
- yield span
338
-
339
- # TODO: Figure out if this is necessary
340
- try:
341
- detach(ctx_token)
342
- except Exception:
343
- pass
344
-
345
- @classmethod
346
- @contextmanager
347
- @deprecated(
348
- "Use `Laminar.set_span_tags` or the `tags` argument of "
349
- "`Laminar.start_as_current_span` or `Laminar.start_span` instead"
350
- )
351
- def with_labels(cls, labels: list[str], context: Context | None = None):
352
- """Set labels for spans within this `with` context. This is useful for
353
- adding labels to the spans created in the auto-instrumentations.
354
-
355
- Requirements:
356
- - Labels must be created in your project in advance.
357
- - Keys must be strings from your label names.
358
- - Values must be strings matching the label's allowed values.
359
-
360
- Usage example:
361
- ```python
362
- with Laminar.with_labels({"sentiment": "positive"}):
363
- openai_client.chat.completions.create()
364
- ```
365
- """
366
- warnings.warn(
367
- "`Laminar.with_labels` is deprecated. Use `Laminar.set_span_tags` or the `tags` argument of "
368
- "`Laminar.start_as_current_span` or `Laminar.start_span` instead",
369
- DeprecationWarning,
370
- )
371
- if not cls.is_initialized():
372
- yield
373
- return
374
575
 
375
- with get_tracer():
376
- label_props = labels.copy()
377
- prev_labels = get_association_properties(context).get("labels", [])
378
- update_association_properties(
379
- {"labels": prev_labels + label_props},
380
- set_on_current_span=False,
381
- context=context,
382
- )
383
- yield
384
576
  try:
385
- set_association_properties({"labels": prev_labels})
386
- except Exception:
387
- cls.__logger.warning(
388
- f"`with_labels` Could not remove labels: {labels}. They will be "
389
- "propagated to the next span."
390
- )
391
- pass
577
+ with tracer.start_as_current_span(
578
+ name,
579
+ context=ctx,
580
+ attributes={
581
+ SPAN_TYPE: span_type,
582
+ PARENT_SPAN_PATH: parsed["path"],
583
+ PARENT_SPAN_IDS_PATH: parsed["span_ids_path"],
584
+ **(label_props),
585
+ **(tag_props),
586
+ # Association properties are attached to context above
587
+ # and the relevant attributes are populated in the processor
588
+ },
589
+ ) as span:
590
+ if not isinstance(span, LaminarSpan):
591
+ span = LaminarSpan(span)
592
+ span.set_input(input)
593
+ yield span
594
+ finally:
595
+ try:
596
+ detach_context(isolated_context_token)
597
+ context_api.detach(ctx_token)
598
+ except Exception:
599
+ pass
392
600
 
393
601
  @classmethod
394
602
  def start_span(
@@ -400,30 +608,41 @@ class Laminar:
400
608
  parent_span_context: LaminarSpanContext | None = None,
401
609
  labels: dict[str, str] | None = None,
402
610
  tags: list[str] | None = None,
403
- ):
611
+ user_id: str | None = None,
612
+ session_id: str | None = None,
613
+ metadata: dict[str, AttributeValue] | None = None,
614
+ ) -> LaminarSpan | Span:
404
615
  """Start a new span. Useful for manual instrumentation.
405
616
  If `span_type` is set to `"LLM"`, you should report usage and response
406
617
  attributes manually. See `Laminar.set_span_attributes` for more
407
618
  information.
408
619
 
620
+ Note that spans started with this method must be ended manually.
621
+ In addition, they must be ended in LIFO order, e.g.
622
+ span1 = Laminar.start_span("span1")
623
+ span2 = Laminar.start_span("span2")
624
+ span2.end()
625
+ span1.end()
626
+ Otherwise, the behavior is undefined.
627
+
409
628
  Usage example:
410
629
  ```python
411
- from src.lmnr import Laminar, use_span
630
+ from src.lmnr import Laminar
412
631
  def foo(span):
413
- with use_span(span):
632
+ with Laminar.use_span(span):
414
633
  with Laminar.start_as_current_span("foo_inner"):
415
634
  some_function()
416
-
635
+
417
636
  def bar():
418
- with use_span(span):
637
+ with Laminar.use_span(span):
419
638
  openai_client.chat.completions.create()
420
-
639
+
421
640
  span = Laminar.start_span("outer")
422
641
  foo(span)
423
642
  bar(span)
424
643
  # IMPORTANT: End the span manually
425
644
  span.end()
426
-
645
+
427
646
  # Results in:
428
647
  # | outer
429
648
  # | | foo
@@ -455,6 +674,12 @@ class Laminar:
455
674
  Defaults to None.
456
675
  labels (dict[str, str] | None, optional): [DEPRECATED] Use tags\
457
676
  instead. Labels to set for the span. Defaults to None.
677
+ user_id (str | None, optional): user id to set for the trace.
678
+ Defaults to None.
679
+ session_id (str | None, optional): session id to set for the trace.
680
+ Defaults to None.
681
+ metadata (dict[str, AttributeValue] | None, optional): metadata to\
682
+ set for the trace. Defaults to None.
458
683
  """
459
684
  if not cls.is_initialized():
460
685
  return trace.NonRecordingSpan(
@@ -465,15 +690,23 @@ class Laminar:
465
690
  )
466
691
  )
467
692
 
468
- with get_tracer() as tracer:
469
- ctx = context or context_api.get_current()
470
- if parent_span_context is not None:
471
- span_context = LaminarSpanContext.try_to_otel_span_context(
472
- parent_span_context, cls.__logger
473
- )
693
+ with get_tracer_with_context() as (tracer, isolated_context):
694
+ ctx = context or isolated_context
695
+
696
+ # Parse parent_span_context and extract all info
697
+ parsed = _parse_parent_span_context(parent_span_context, cls.__logger)
698
+
699
+ # Set parent span in context if present
700
+ if parsed["otel_span_context"] is not None:
474
701
  ctx = trace.set_span_in_context(
475
- trace.NonRecordingSpan(span_context), ctx
702
+ trace.NonRecordingSpan(parsed["otel_span_context"]), ctx
476
703
  )
704
+
705
+ # Get association props from context (fallback values)
706
+ ctx_user_id = get_value(CONTEXT_USER_ID_KEY, ctx)
707
+ ctx_session_id = get_value(CONTEXT_SESSION_ID_KEY, ctx)
708
+ ctx_metadata = get_value(CONTEXT_METADATA_KEY, ctx)
709
+
477
710
  label_props = {}
478
711
  try:
479
712
  if labels:
@@ -498,97 +731,312 @@ class Laminar:
498
731
  f"`start_span` Could not set tags: {tags}. Tags must be a list of strings. "
499
732
  + "Tags will be ignored."
500
733
  )
734
+
735
+ # Determine trace_type with proper priority: explicit > parent > context
736
+ trace_type = None
737
+ if span_type in ["EVALUATION", "EXECUTOR", "EVALUATOR"]:
738
+ trace_type = TraceType.EVALUATION
739
+ elif parsed["trace_type"] is not None:
740
+ trace_type = parsed["trace_type"]
741
+ else:
742
+ # Get trace_type from context if not set explicitly or from parent
743
+ ctx_trace_type = get_value(CONTEXT_TRACE_TYPE_KEY, ctx)
744
+ if ctx_trace_type:
745
+ try:
746
+ trace_type = TraceType(ctx_trace_type)
747
+ except (ValueError, TypeError):
748
+ pass
749
+
750
+ # Merge with priority: global < context < parent < explicit
751
+ merged_metadata = {
752
+ **(cls.__global_metadata or {}),
753
+ **(ctx_metadata or {}),
754
+ **(parsed["metadata"] or {}),
755
+ **(metadata or {}),
756
+ }
757
+
758
+ # Merge user_id and session_id with priority: context < parent < explicit
759
+ final_user_id = (
760
+ user_id
761
+ if user_id is not None
762
+ else (
763
+ parsed["user_id"] if parsed["user_id"] is not None else ctx_user_id
764
+ )
765
+ )
766
+ final_session_id = (
767
+ session_id
768
+ if session_id is not None
769
+ else (
770
+ parsed["session_id"]
771
+ if parsed["session_id"] is not None
772
+ else ctx_session_id
773
+ )
774
+ )
775
+
776
+ # Build association_props using merged values
777
+ association_props = cls._get_association_prop_attributes(
778
+ user_id=final_user_id,
779
+ session_id=final_session_id,
780
+ metadata=merged_metadata if merged_metadata else None,
781
+ trace_type=trace_type,
782
+ )
783
+
501
784
  span = tracer.start_span(
502
785
  name,
503
786
  context=ctx,
504
787
  attributes={
505
788
  SPAN_TYPE: span_type,
789
+ PARENT_SPAN_PATH: parsed["path"],
790
+ PARENT_SPAN_IDS_PATH: parsed["span_ids_path"],
506
791
  **(label_props),
507
792
  **(tag_props),
793
+ **(association_props),
508
794
  },
509
795
  )
510
- if input is not None:
511
- serialized_input = json_dumps(input)
512
- if len(serialized_input) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
513
- span.set_attribute(
514
- SPAN_INPUT,
515
- "Laminar: input too large to record",
516
- )
517
- else:
518
- span.set_attribute(
519
- SPAN_INPUT,
520
- serialized_input,
521
- )
796
+
797
+ if not isinstance(span, LaminarSpan):
798
+ span = LaminarSpan(span)
799
+ span.set_input(input)
522
800
  return span
523
801
 
524
802
  @classmethod
525
- def set_span_output(cls, output: Any = None):
526
- """Set the output of the current span. Useful for manual
527
- instrumentation.
803
+ @contextmanager
804
+ def use_span(
805
+ cls,
806
+ span: Span,
807
+ end_on_exit: bool = False,
808
+ record_exception: bool = True,
809
+ set_status_on_exception: bool = True,
810
+ ) -> Iterator[LaminarSpan | Span]:
811
+ """Use a span as the current span. Useful for manual instrumentation.
812
+
813
+ Fully copies the implementation of `use_span` from opentelemetry.trace
814
+ and replaces the context API with Laminar's isolated context.
528
815
 
529
816
  Args:
530
- output (Any, optional): output of the span. Will be sent as an\
531
- attribute, so must be json serializable. Defaults to None.
817
+ span: The span that should be activated in the current context.
818
+ end_on_exit: Whether to end the span automatically when leaving the
819
+ context manager scope.
820
+ record_exception: Whether to record any exceptions raised within the
821
+ context as error event on the span.
822
+ set_status_on_exception: Only relevant if the returned span is used
823
+ in a with/context manager. Defines whether the span status will
824
+ be automatically set to ERROR when an uncaught exception is
825
+ raised in the span with block. The span status won't be set by
826
+ this mechanism if it was previously set manually.
532
827
  """
533
- span = trace.get_current_span()
534
- if output is not None and span != trace.INVALID_SPAN:
535
- serialized_output = json_dumps(output)
536
- if len(serialized_output) > MAX_MANUAL_SPAN_PAYLOAD_SIZE:
537
- span.set_attribute(
538
- SPAN_OUTPUT,
539
- "Laminar: output too large to record",
540
- )
828
+ if not cls.is_initialized():
829
+ with use_span(
830
+ span, end_on_exit, record_exception, set_status_on_exception
831
+ ) as s:
832
+ yield s
833
+ return
834
+
835
+ wrapper = TracerWrapper()
836
+
837
+ try:
838
+ # Set association props in context before push_span_context
839
+ # so child spans inherit them
840
+ assoc_props_token = set_association_props_in_context(span)
841
+ if assoc_props_token and isinstance(span, LaminarSpan):
842
+ span._lmnr_assoc_props_token = assoc_props_token
843
+
844
+ context = wrapper.push_span_context(span)
845
+ # Some auto-instrumentations are not under our control, so they
846
+ # don't have access to our isolated context. We attach the context
847
+ # to the OTEL global context, so that spans know their parent
848
+ # span and trace_id.
849
+ isolated_context_token = attach_context(context)
850
+ context_token = context_api.attach(context)
851
+ if isinstance(span, LaminarSpan):
852
+ yield span
541
853
  else:
542
- span.set_attribute(SPAN_OUTPUT, serialized_output)
854
+ yield LaminarSpan(span)
855
+
856
+ # Record only exceptions that inherit Exception class but not BaseException, because
857
+ # classes that directly inherit BaseException are not technically errors, e.g. GeneratorExit.
858
+ # See https://github.com/open-telemetry/opentelemetry-python/issues/4484
859
+ except Exception as exc: # pylint: disable=broad-exception-caught
860
+ if isinstance(span, Span) and span.is_recording():
861
+ # Record the exception as an event
862
+ if record_exception:
863
+ span.record_exception(
864
+ exc, attributes=get_event_attributes_from_context()
865
+ )
866
+
867
+ # Set status in case exception was raised
868
+ if set_status_on_exception:
869
+ span.set_status(
870
+ Status(
871
+ status_code=StatusCode.ERROR,
872
+ description=f"{type(exc).__name__}: {exc}",
873
+ )
874
+ )
875
+
876
+ # This causes parent spans to set their status to ERROR and to record
877
+ # an exception as an event if a child span raises an exception even if
878
+ # such child span was started with both record_exception and
879
+ # set_status_on_exception attributes set to False.
880
+ raise
881
+
882
+ finally:
883
+ try:
884
+ context_api.detach(context_token)
885
+ detach_context(isolated_context_token)
886
+ wrapper.pop_span_context()
887
+ finally:
888
+ if end_on_exit:
889
+ span.end()
543
890
 
544
891
  @classmethod
545
- @contextmanager
546
- def set_tracing_level(self, level: TracingLevel):
547
- """Set the tracing level for the current span and the context
548
- (i.e. any children spans created from the current span in the current
549
- thread).
550
-
551
- Tracing level can be one of:
552
- - `TracingLevel.ALL`: Enable tracing for the current span and all
553
- children spans.
554
- - `TracingLevel.META_ONLY`: Enable tracing for the current span and all
555
- children spans, but only record metadata, e.g. tokens, costs.
556
- - `TracingLevel.OFF`: Disable recording any spans.
892
+ def start_active_span(
893
+ cls,
894
+ name: str,
895
+ input: Any = None,
896
+ span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
897
+ context: Context | None = None,
898
+ parent_span_context: LaminarSpanContext | None = None,
899
+ tags: list[str] | None = None,
900
+ user_id: str | None = None,
901
+ session_id: str | None = None,
902
+ metadata: dict[str, AttributeValue] | None = None,
903
+ ) -> LaminarSpan | Span:
904
+ """Start a span and mark it as active within the current context.
905
+ All spans started after this one will be children of this span.
906
+ Useful for manual instrumentation. Must be ended manually.
907
+ If `span_type` is set to `"LLM"`, you should report usage and response
908
+ attributes manually. See `Laminar.set_span_attributes` for more
909
+ information. Returns the span object.
910
+
911
+ Note that ending the started span in a different async context yields
912
+ unexpected results. When propagating spans across different async or
913
+ threading contexts, it is recommended to either:
914
+ - Make sure to start and end the span in the same async context or thread, or
915
+ - Use `Laminar.start_span` + `Laminar.use_span` where possible.
916
+
917
+ Note that spans started with this method must be ended manually.
918
+ In addition, they must be ended in LIFO order, e.g.
919
+ span1 = Laminar.start_active_span("span1")
920
+ span2 = Laminar.start_active_span("span2")
921
+ span2.end()
922
+ span1.end()
923
+ Otherwise, the behavior is undefined.
557
924
 
558
- Example:
925
+ Usage example:
559
926
  ```python
560
- from lmnr import Laminar, TracingLevel
927
+ from src.lmnr import Laminar, observe
928
+
929
+ @observe()
930
+ def foo():
931
+ with Laminar.start_as_current_span("foo_inner"):
932
+ some_function()
561
933
 
562
- with Laminar.set_tracing_level(TracingLevel.META_ONLY):
934
+ @observe()
935
+ def bar():
563
936
  openai_client.chat.completions.create()
937
+
938
+ span = Laminar.start_active_span("outer")
939
+ foo()
940
+ bar()
941
+ # IMPORTANT: End the span manually
942
+ span.end()
943
+
944
+ # Results in:
945
+ # | outer
946
+ # | | foo
947
+ # | | | foo_inner
948
+ # | | bar
949
+ # | | | openai.chat
564
950
  ```
951
+
952
+ Args:
953
+ name (str): name of the span
954
+ input (Any, optional): input to the span. Will be sent as an\
955
+ attribute, so must be json serializable. Defaults to None.
956
+ span_type (Literal["DEFAULT", "LLM", "TOOL"], optional):\
957
+ type of the span. If you use `"LLM"`, you should report usage\
958
+ and response attributes manually. Defaults to "DEFAULT".
959
+ context (Context | None, optional): raw OpenTelemetry context\
960
+ to attach the span to. Defaults to None.
961
+ parent_span_context (LaminarSpanContext | None, optional): parent\
962
+ span context to use for the span. Useful for continuing traces\
963
+ across services. If parent_span_context is a\
964
+ raw OpenTelemetry span context, or if it is a dictionary or string\
965
+ obtained from `Laminar.get_laminar_span_context_dict()` or\
966
+ `Laminar.get_laminar_span_context_str()` respectively, it will be\
967
+ converted to a `LaminarSpanContext` if possible. See also\
968
+ `Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
969
+ `Laminar.get_span_context_str` for more information.
970
+ Defaults to None.
971
+ tags (list[str] | None, optional): tags to set for the span.
972
+ Defaults to None.
973
+ user_id (str | None, optional): user id to set for the trace.
974
+ Defaults to None.
975
+ session_id (str | None, optional): session id to set for the trace.
976
+ Defaults to None.
977
+ metadata (dict[str, AttributeValue] | None, optional): metadata to\
978
+ set for the trace. Defaults to None.
565
979
  """
566
- if level == TracingLevel.ALL:
567
- yield
980
+ span = cls.start_span(
981
+ name=name,
982
+ input=input,
983
+ span_type=span_type,
984
+ context=context,
985
+ parent_span_context=parent_span_context,
986
+ tags=tags,
987
+ user_id=user_id,
988
+ session_id=session_id,
989
+ metadata=metadata,
990
+ )
991
+ if not cls.is_initialized():
992
+ return span
993
+ wrapper = TracerWrapper()
994
+
995
+ # Set association props in context before push_span_context
996
+ # so child spans inherit them
997
+ assoc_props_token = set_association_props_in_context(span)
998
+ if assoc_props_token and isinstance(span, LaminarSpan):
999
+ span._lmnr_assoc_props_token = assoc_props_token
1000
+
1001
+ context = wrapper.push_span_context(span)
1002
+ context_token = context_api.attach(context)
1003
+ isolated_context_token = attach_context(context)
1004
+ span._lmnr_ctx_token = context_token
1005
+ span._lmnr_isolated_ctx_token = isolated_context_token
1006
+ if isinstance(span, LaminarSpan):
1007
+ return span
568
1008
  else:
569
- level = "meta_only" if level == TracingLevel.META_ONLY else "off"
570
- update_association_properties({"tracing_level": level})
571
- yield
572
- try:
573
- remove_association_properties({"tracing_level": level})
574
- except Exception:
575
- pass
1009
+ return LaminarSpan(span)
1010
+
1011
+ @classmethod
1012
+ def set_span_output(cls, output: Any = None):
1013
+ """Set the output of the current span. Useful for manual
1014
+ instrumentation.
1015
+
1016
+ Args:
1017
+ output (Any, optional): output of the span. Will be sent as an\
1018
+ attribute, so must be json serializable. Defaults to None.
1019
+ """
1020
+ span = cls.get_current_span()
1021
+ if span is None:
1022
+ return
1023
+ span.set_output(output)
576
1024
 
577
1025
  @classmethod
578
1026
  def set_span_attributes(
579
1027
  cls,
580
- attributes: dict[Attributes, Any],
1028
+ attributes: dict[Attributes | str, Any],
581
1029
  ):
582
1030
  """Set attributes for the current span. Useful for manual
583
1031
  instrumentation.
584
1032
  Example:
585
1033
  ```python
586
- with L.start_as_current_span(
1034
+ with Laminar.start_as_current_span(
587
1035
  name="my_span_name", input=input["messages"], span_type="LLM"
588
1036
  ):
589
1037
  response = await my_custom_call_to_openai(input)
590
- L.set_span_output(response["choices"][0]["message"]["content"])
591
- L.set_span_attributes({
1038
+ Laminar.set_span_output(response["choices"][0]["message"]["content"])
1039
+ Laminar.set_span_attributes({
592
1040
  Attributes.PROVIDER: 'openai',
593
1041
  Attributes.REQUEST_MODEL: input["model"],
594
1042
  Attributes.RESPONSE_MODEL: response["model"],
@@ -599,24 +1047,19 @@ class Laminar:
599
1047
  ```
600
1048
 
601
1049
  Args:
602
- attributes (dict[ATTRIBUTES, Any]): attributes to set for the span
1050
+ attributes (dict[Attributes | str, Any]): attributes to set for the span
603
1051
  """
604
- span = trace.get_current_span()
605
- if span == trace.INVALID_SPAN:
1052
+ span = cls.get_current_span()
1053
+ if span == trace.INVALID_SPAN or span is None:
606
1054
  return
607
1055
 
608
1056
  for key, value in attributes.items():
609
- # Python 3.12+ should do: if key not in Attributes:
610
- try:
611
- Attributes(key.value)
612
- except (TypeError, AttributeError):
613
- cls.__logger.warning(
614
- f"Attribute {key} is not a valid Laminar attribute."
615
- )
616
- if not isinstance(value, (str, int, float, bool)):
617
- span.set_attribute(key.value, json_dumps(value))
1057
+ if isinstance(key, Attributes):
1058
+ key = key.value
1059
+ if not is_otel_attribute_value_type(value):
1060
+ span.set_attribute(key, json_dumps(value))
618
1061
  else:
619
- span.set_attribute(key.value, value)
1062
+ span.set_attribute(key, value)
620
1063
 
621
1064
  @classmethod
622
1065
  def get_laminar_span_context(
@@ -625,14 +1068,15 @@ class Laminar:
625
1068
  """Get the laminar span context for a given span.
626
1069
  If no span is provided, the current active span will be used.
627
1070
  """
628
- span = span or trace.get_current_span()
629
- if span == trace.INVALID_SPAN:
1071
+ if not cls.is_initialized():
630
1072
  return None
631
- return LaminarSpanContext(
632
- trace_id=uuid.UUID(int=span.get_span_context().trace_id),
633
- span_id=uuid.UUID(int=span.get_span_context().span_id),
634
- is_remote=span.get_span_context().is_remote,
635
- )
1073
+
1074
+ span = span or cls.get_current_span()
1075
+ if span == trace.INVALID_SPAN or span is None:
1076
+ return None
1077
+ if not isinstance(span, LaminarSpan):
1078
+ span = LaminarSpan(span)
1079
+ return span.get_laminar_span_context()
636
1080
 
637
1081
  @classmethod
638
1082
  def get_laminar_span_context_dict(
@@ -681,6 +1125,29 @@ class Laminar:
681
1125
  def deserialize_span_context(cls, span_context: dict | str) -> LaminarSpanContext:
682
1126
  return LaminarSpanContext.deserialize(span_context)
683
1127
 
1128
+ @classmethod
1129
+ def get_current_span(cls, context: Context | None = None) -> LaminarSpan | None:
1130
+ """Get the current active span. If a context is provided, the span will
1131
+ be retrieved from that context.
1132
+
1133
+ Args:
1134
+ context (Context | None, optional): The context to get the span\
1135
+ from. If not provided, the current context will be used.
1136
+ Defaults to None.
1137
+
1138
+ Returns:
1139
+ LaminarSpan | None: The current active span, or None if there is no\
1140
+ active span.
1141
+ """
1142
+ context = context or get_current_context()
1143
+ span = trace.get_current_span(context=context)
1144
+ if span == trace.INVALID_SPAN:
1145
+ return None
1146
+ if isinstance(span, LaminarSpan):
1147
+ return span
1148
+ else:
1149
+ return LaminarSpan(span)
1150
+
684
1151
  @classmethod
685
1152
  def flush(cls) -> bool:
686
1153
  """Flush the internal tracer.
@@ -693,6 +1160,22 @@ class Laminar:
693
1160
  return False
694
1161
  return TracerManager.flush()
695
1162
 
1163
+ @classmethod
1164
+ def force_flush(cls):
1165
+ """Force flush the internal tracer. WARNING: Any active spans are
1166
+ removed from context; that is, spans started afterwards will start
1167
+ a new trace.
1168
+
1169
+ Actually shuts down the span processor and re-initializes it as long
1170
+ as it is a LaminarSpanProcessor. This is not recommended in production
1171
+ workflows, but is useful at the end of Lambda functions, where a regular
1172
+ flush might be killed by the Lambda runtime, because the actual export
1173
+ inside it runs in a background thread.
1174
+ """
1175
+ if not cls.is_initialized():
1176
+ return
1177
+ TracerManager.force_reinit_processor()
1178
+
696
1179
  @classmethod
697
1180
  def shutdown(cls):
698
1181
  if cls.is_initialized():
@@ -706,47 +1189,21 @@ class Laminar:
706
1189
  Args:
707
1190
  tags (list[str]): Tags to set for the span.
708
1191
  """
709
- span = trace.get_current_span()
710
- if span == trace.INVALID_SPAN:
711
- cls.__logger.warning("No active span to set tags on")
1192
+ if not cls.is_initialized():
712
1193
  return
713
- if not isinstance(tags, list) or not all(isinstance(tag, str) for tag in tags):
714
- cls.__logger.warning(
715
- "Tags must be a list of strings. Tags will be ignored."
716
- )
1194
+
1195
+ span = cls.get_current_span()
1196
+ if span is None:
717
1197
  return
718
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.tags", tags)
1198
+ span.set_tags(tags)
719
1199
 
720
1200
  @classmethod
721
- @deprecated("Use `Laminar.set_trace_session_id` instead")
722
- def set_session(
723
- cls,
724
- session_id: str | None = None,
725
- ):
726
- """Set the session id for the current span and the context
727
- (i.e. any children spans created from the current span in the current
728
- thread).
729
-
730
- Args:
731
- session_id (str | None, optional): Custom session id.\
732
- Useful to debug and group long-running\
733
- sessions/conversations.
734
- Defaults to None.
735
- """
736
- warnings.warn(
737
- "`Laminar.set_session` is deprecated. Use `Laminar.set_trace_session_id` instead",
738
- DeprecationWarning,
739
- )
740
- association_properties = {}
741
- if session_id is not None:
742
- association_properties[SESSION_ID] = session_id
743
- # update_association_properties(association_properties)
744
- span = trace.get_current_span()
745
- if span == trace.INVALID_SPAN:
746
- cls.__logger.warning("No active span to set session id on")
1201
+ def add_span_tags(cls, tags: list[str]):
1202
+ """Add tags to the current span."""
1203
+ span = cls.get_current_span()
1204
+ if span is None:
747
1205
  return
748
- if session_id is not None:
749
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", session_id)
1206
+ span.add_tags(tags)
750
1207
 
751
1208
  @classmethod
752
1209
  def set_trace_session_id(cls, session_id: str | None = None):
@@ -756,12 +1213,16 @@ class Laminar:
756
1213
  Args:
757
1214
  session_id (str | None, optional): Custom session id. Defaults to None.
758
1215
  """
759
- span = trace.get_current_span()
760
- if span == trace.INVALID_SPAN:
1216
+ if not cls.is_initialized():
1217
+ return
1218
+
1219
+ context = set_association_prop_context(session_id=session_id, attach=True)
1220
+
1221
+ span = cls.get_current_span(context=context)
1222
+ if span is None:
761
1223
  cls.__logger.warning("No active span to set session id on")
762
1224
  return
763
- if session_id is not None:
764
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", session_id)
1225
+ span.set_trace_session_id(session_id)
765
1226
 
766
1227
  @classmethod
767
1228
  def set_trace_user_id(cls, user_id: str | None = None):
@@ -771,34 +1232,16 @@ class Laminar:
771
1232
  Args:
772
1233
  user_id (str | None, optional): Custom user id. Defaults to None.
773
1234
  """
774
- span = trace.get_current_span()
775
- if span == trace.INVALID_SPAN:
776
- cls.__logger.warning("No active span to set user id on")
1235
+ if not cls.is_initialized():
777
1236
  return
778
- if user_id is not None:
779
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{USER_ID}", user_id)
780
1237
 
781
- @classmethod
782
- @deprecated("Use `Laminar.set_trace_metadata` instead")
783
- def set_metadata(cls, metadata: dict[str, str]):
784
- """Set the metadata for the current trace.
1238
+ context = set_association_prop_context(user_id=user_id, attach=True)
785
1239
 
786
- Args:
787
- metadata (dict[str, str]): Metadata to set for the trace. Will be\
788
- sent as attributes, so must be json serializable.
789
- """
790
- warnings.warn(
791
- "`Laminar.set_metadata` is deprecated. Use `Laminar.set_trace_metadata` instead",
792
- DeprecationWarning,
793
- )
794
- props = {f"metadata.{k}": json_dumps(v) for k, v in metadata.items()}
795
- # update_association_properties(props)
796
- span = trace.get_current_span()
797
- if span == trace.INVALID_SPAN:
798
- cls.__logger.warning("No active span to set metadata on")
1240
+ span = cls.get_current_span(context=context)
1241
+ if span is None:
1242
+ cls.__logger.warning("No active span to set user id on")
799
1243
  return
800
- for key, value in props.items():
801
- span.set_attribute(key, value)
1244
+ span.set_trace_user_id(user_id)
802
1245
 
803
1246
  @classmethod
804
1247
  def set_trace_metadata(cls, metadata: dict[str, AttributeValue]):
@@ -807,17 +1250,16 @@ class Laminar:
807
1250
  Args:
808
1251
  metadata (dict[str, AttributeValue]): Metadata to set for the trace.
809
1252
  """
810
- span = trace.get_current_span()
811
- if span == trace.INVALID_SPAN:
1253
+ if not cls.is_initialized():
1254
+ return
1255
+
1256
+ merged_metadata = {**cls.__global_metadata, **(metadata or {})}
1257
+
1258
+ span = cls.get_current_span()
1259
+ if span is None:
812
1260
  cls.__logger.warning("No active span to set metadata on")
813
1261
  return
814
- for key, value in metadata.items():
815
- if is_otel_attribute_value_type(value):
816
- span.set_attribute(f"{ASSOCIATION_PROPERTIES}.metadata.{key}", value)
817
- else:
818
- span.set_attribute(
819
- f"{ASSOCIATION_PROPERTIES}.metadata.{key}", json_dumps(value)
820
- )
1262
+ span.set_trace_metadata(merged_metadata)
821
1263
 
822
1264
  @classmethod
823
1265
  def get_base_http_url(cls):
@@ -836,7 +1278,11 @@ class Laminar:
836
1278
  uuid.UUID | None: The trace id for the current span, or None if\
837
1279
  there is no active span.
838
1280
  """
839
- trace_id = trace.get_current_span().get_span_context().trace_id
1281
+ trace_id = (
1282
+ trace.get_current_span(context=get_current_context())
1283
+ .get_span_context()
1284
+ .trace_id
1285
+ )
840
1286
  if trace_id == INVALID_TRACE_ID:
841
1287
  return None
842
1288
  return uuid.UUID(int=trace_id)
@@ -858,8 +1304,45 @@ class Laminar:
858
1304
  Args:
859
1305
  trace_type (TraceType): Type of the trace
860
1306
  """
861
- span = trace.get_current_span()
1307
+ if not cls.is_initialized():
1308
+ return
1309
+
1310
+ span = trace.get_current_span(context=get_current_context())
862
1311
  if span == trace.INVALID_SPAN:
863
1312
  cls.__logger.warning("No active span to set trace type on")
864
1313
  return
865
1314
  span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}", trace_type.value)
1315
+
1316
+ @classmethod
1317
+ def _get_association_prop_attributes(
1318
+ cls,
1319
+ user_id: str | None = None,
1320
+ session_id: str | None = None,
1321
+ trace_type: TraceType | None = None,
1322
+ metadata: dict[str, AttributeValue] | None = None,
1323
+ ) -> dict[str, AttributeValue]:
1324
+ association_properties = {}
1325
+ if user_id is not None:
1326
+ association_properties[f"{ASSOCIATION_PROPERTIES}.{USER_ID}"] = user_id
1327
+ if session_id is not None:
1328
+ association_properties[f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}"] = (
1329
+ session_id
1330
+ )
1331
+ if trace_type is not None:
1332
+ trace_type_val = (
1333
+ trace_type.value if isinstance(trace_type, TraceType) else trace_type
1334
+ )
1335
+ association_properties[f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}"] = (
1336
+ trace_type_val
1337
+ )
1338
+
1339
+ merged_metadata = {**cls.__global_metadata, **(metadata or {})}
1340
+ association_properties.update(
1341
+ {
1342
+ f"{ASSOCIATION_PROPERTIES}.metadata.{k}": (
1343
+ v if is_otel_attribute_value_type(v) else json_dumps(v)
1344
+ )
1345
+ for k, v in merged_metadata.items()
1346
+ }
1347
+ )
1348
+ return association_properties