lmnr 0.4.53.dev0__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.
- lmnr/__init__.py +32 -11
- lmnr/cli/__init__.py +270 -0
- lmnr/cli/datasets.py +371 -0
- lmnr/cli/evals.py +111 -0
- lmnr/cli/rules.py +42 -0
- lmnr/opentelemetry_lib/__init__.py +70 -0
- lmnr/opentelemetry_lib/decorators/__init__.py +337 -0
- lmnr/opentelemetry_lib/litellm/__init__.py +685 -0
- lmnr/opentelemetry_lib/litellm/utils.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/__init__.py +849 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/config.py +13 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_emitter.py +211 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/span_utils.py +401 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/streaming.py +425 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/utils.py +332 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/anthropic/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/__init__.py +451 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/claude_agent/proxy.py +144 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_agent/__init__.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/__init__.py +476 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/cua_computer/utils.py +12 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/__init__.py +599 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/config.py +9 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/schema_utils.py +26 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/google_genai/utils.py +330 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/__init__.py +488 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/config.py +8 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_emitter.py +143 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/span_utils.py +229 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/utils.py +92 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/groq/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/__init__.py +381 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/kernel/utils.py +36 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/__init__.py +121 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/langgraph/utils.py +60 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/__init__.py +61 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/__init__.py +472 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/chat_wrappers.py +1185 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/completion_wrappers.py +305 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/config.py +16 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/embeddings_wrappers.py +312 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_emitter.py +100 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/event_models.py +41 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/shared/image_gen_wrappers.py +68 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/utils.py +197 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v0/__init__.py +176 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/__init__.py +368 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/assistant_wrappers.py +325 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/event_handler_wrapper.py +135 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/v1/responses_wrappers.py +786 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openai/version.py +1 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/openhands_ai/__init__.py +388 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/opentelemetry/__init__.py +69 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/skyvern/__init__.py +191 -0
- lmnr/opentelemetry_lib/opentelemetry/instrumentation/threading/__init__.py +197 -0
- lmnr/opentelemetry_lib/tracing/__init__.py +263 -0
- lmnr/opentelemetry_lib/tracing/_instrument_initializers.py +516 -0
- lmnr/{openllmetry_sdk → opentelemetry_lib}/tracing/attributes.py +21 -8
- lmnr/opentelemetry_lib/tracing/context.py +200 -0
- lmnr/opentelemetry_lib/tracing/exporter.py +153 -0
- lmnr/opentelemetry_lib/tracing/instruments.py +140 -0
- lmnr/opentelemetry_lib/tracing/processor.py +193 -0
- lmnr/opentelemetry_lib/tracing/span.py +398 -0
- lmnr/opentelemetry_lib/tracing/tracer.py +57 -0
- lmnr/opentelemetry_lib/tracing/utils.py +62 -0
- lmnr/opentelemetry_lib/utils/package_check.py +18 -0
- lmnr/opentelemetry_lib/utils/wrappers.py +11 -0
- lmnr/sdk/browser/__init__.py +0 -0
- lmnr/sdk/browser/background_send_events.py +158 -0
- lmnr/sdk/browser/browser_use_cdp_otel.py +100 -0
- lmnr/sdk/browser/browser_use_otel.py +142 -0
- lmnr/sdk/browser/bubus_otel.py +71 -0
- lmnr/sdk/browser/cdp_utils.py +518 -0
- lmnr/sdk/browser/inject_script.js +514 -0
- lmnr/sdk/browser/patchright_otel.py +151 -0
- lmnr/sdk/browser/playwright_otel.py +322 -0
- lmnr/sdk/browser/pw_utils.py +363 -0
- lmnr/sdk/browser/recorder/record.umd.min.cjs +84 -0
- lmnr/sdk/browser/utils.py +70 -0
- lmnr/sdk/client/asynchronous/async_client.py +180 -0
- lmnr/sdk/client/asynchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/asynchronous/resources/base.py +32 -0
- lmnr/sdk/client/asynchronous/resources/browser_events.py +41 -0
- lmnr/sdk/client/asynchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/asynchronous/resources/evals.py +266 -0
- lmnr/sdk/client/asynchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/asynchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/resources/__init__.py +6 -0
- lmnr/sdk/client/synchronous/resources/base.py +32 -0
- lmnr/sdk/client/synchronous/resources/browser_events.py +40 -0
- lmnr/sdk/client/synchronous/resources/datasets.py +131 -0
- lmnr/sdk/client/synchronous/resources/evals.py +263 -0
- lmnr/sdk/client/synchronous/resources/evaluators.py +85 -0
- lmnr/sdk/client/synchronous/resources/tags.py +83 -0
- lmnr/sdk/client/synchronous/sync_client.py +191 -0
- lmnr/sdk/datasets/__init__.py +94 -0
- lmnr/sdk/datasets/file_utils.py +91 -0
- lmnr/sdk/decorators.py +163 -26
- lmnr/sdk/eval_control.py +3 -2
- lmnr/sdk/evaluations.py +403 -191
- lmnr/sdk/laminar.py +1080 -549
- lmnr/sdk/log.py +7 -2
- lmnr/sdk/types.py +246 -134
- lmnr/sdk/utils.py +151 -7
- lmnr/version.py +46 -0
- {lmnr-0.4.53.dev0.dist-info → lmnr-0.7.26.dist-info}/METADATA +152 -106
- lmnr-0.7.26.dist-info/RECORD +116 -0
- lmnr-0.7.26.dist-info/WHEEL +4 -0
- lmnr-0.7.26.dist-info/entry_points.txt +3 -0
- lmnr/cli.py +0 -101
- lmnr/openllmetry_sdk/.python-version +0 -1
- lmnr/openllmetry_sdk/__init__.py +0 -72
- lmnr/openllmetry_sdk/config/__init__.py +0 -9
- lmnr/openllmetry_sdk/decorators/base.py +0 -185
- lmnr/openllmetry_sdk/instruments.py +0 -38
- lmnr/openllmetry_sdk/tracing/__init__.py +0 -1
- lmnr/openllmetry_sdk/tracing/content_allow_list.py +0 -24
- lmnr/openllmetry_sdk/tracing/context_manager.py +0 -13
- lmnr/openllmetry_sdk/tracing/tracing.py +0 -884
- lmnr/openllmetry_sdk/utils/in_memory_span_exporter.py +0 -61
- lmnr/openllmetry_sdk/utils/package_check.py +0 -7
- lmnr/openllmetry_sdk/version.py +0 -1
- lmnr/sdk/datasets.py +0 -55
- lmnr-0.4.53.dev0.dist-info/LICENSE +0 -75
- lmnr-0.4.53.dev0.dist-info/RECORD +0 -33
- lmnr-0.4.53.dev0.dist-info/WHEEL +0 -4
- lmnr-0.4.53.dev0.dist-info/entry_points.txt +0 -3
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/.flake8 +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/__init__.py +0 -0
- /lmnr/{openllmetry_sdk → opentelemetry_lib}/utils/json_encoder.py +0 -0
- /lmnr/{openllmetry_sdk/decorators/__init__.py → py.typed} +0 -0
lmnr/sdk/laminar.py
CHANGED
|
@@ -1,154 +1,360 @@
|
|
|
1
1
|
from contextlib import contextmanager
|
|
2
2
|
from contextvars import Context
|
|
3
|
-
|
|
4
|
-
from lmnr.
|
|
5
|
-
from lmnr.
|
|
6
|
-
from lmnr.
|
|
3
|
+
import warnings
|
|
4
|
+
from lmnr.opentelemetry_lib import TracerManager
|
|
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
|
|
18
|
+
from lmnr.opentelemetry_lib.tracing.attributes import (
|
|
7
19
|
ASSOCIATION_PROPERTIES,
|
|
20
|
+
PARENT_SPAN_IDS_PATH,
|
|
21
|
+
PARENT_SPAN_PATH,
|
|
22
|
+
USER_ID,
|
|
8
23
|
Attributes,
|
|
9
24
|
SPAN_TYPE,
|
|
10
|
-
OVERRIDE_PARENT_SPAN,
|
|
11
25
|
)
|
|
12
|
-
from lmnr.
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
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
|
|
36
|
+
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
|
|
16
37
|
from opentelemetry.util.types import AttributeValue
|
|
17
38
|
|
|
18
|
-
from
|
|
19
|
-
from
|
|
39
|
+
from typing import Any, Iterator, Literal
|
|
40
|
+
from typing_extensions import TypedDict
|
|
20
41
|
|
|
21
|
-
import aiohttp
|
|
22
|
-
import asyncio
|
|
23
|
-
import copy
|
|
24
42
|
import datetime
|
|
25
|
-
import dotenv
|
|
26
|
-
import json
|
|
27
43
|
import logging
|
|
28
44
|
import os
|
|
29
|
-
import random
|
|
30
|
-
import requests
|
|
31
45
|
import re
|
|
32
|
-
import urllib.parse
|
|
33
46
|
import uuid
|
|
34
47
|
|
|
35
|
-
from lmnr.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
SPAN_OUTPUT,
|
|
39
|
-
TRACE_TYPE,
|
|
40
|
-
)
|
|
41
|
-
from lmnr.openllmetry_sdk.tracing.tracing import (
|
|
42
|
-
remove_association_properties,
|
|
43
|
-
set_association_properties,
|
|
44
|
-
update_association_properties,
|
|
45
|
-
)
|
|
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
|
|
46
51
|
|
|
47
52
|
from .log import VerboseColorfulFormatter
|
|
48
53
|
|
|
49
54
|
from .types import (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
GetDatapointsResponse,
|
|
53
|
-
PipelineRunError,
|
|
54
|
-
PipelineRunResponse,
|
|
55
|
-
NodeInput,
|
|
56
|
-
PipelineRunRequest,
|
|
57
|
-
SemanticSearchRequest,
|
|
58
|
-
SemanticSearchResponse,
|
|
55
|
+
LaminarSpanContext,
|
|
56
|
+
SessionRecordingOptions,
|
|
59
57
|
TraceType,
|
|
60
|
-
TracingLevel,
|
|
61
58
|
)
|
|
62
59
|
|
|
63
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
|
+
|
|
64
164
|
class Laminar:
|
|
65
|
-
|
|
66
|
-
__base_grpc_url: str
|
|
67
|
-
__project_api_key: Optional[str] = None
|
|
68
|
-
__env: dict[str, str] = {}
|
|
165
|
+
__project_api_key: str | None = None
|
|
69
166
|
__initialized: bool = False
|
|
167
|
+
__base_http_url: str | None = None
|
|
168
|
+
__global_metadata: dict[str, AttributeValue] = {}
|
|
70
169
|
|
|
71
170
|
@classmethod
|
|
72
171
|
def initialize(
|
|
73
172
|
cls,
|
|
74
|
-
project_api_key:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
http_port:
|
|
78
|
-
grpc_port:
|
|
79
|
-
instruments:
|
|
173
|
+
project_api_key: str | None = None,
|
|
174
|
+
base_url: str | None = None,
|
|
175
|
+
base_http_url: str | None = None,
|
|
176
|
+
http_port: int | None = None,
|
|
177
|
+
grpc_port: int | 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,
|
|
80
184
|
disable_batch: bool = False,
|
|
185
|
+
max_export_batch_size: int | None = None,
|
|
186
|
+
export_timeout_seconds: int | None = None,
|
|
187
|
+
set_global_tracer_provider: bool = True,
|
|
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,
|
|
81
192
|
):
|
|
82
193
|
"""Initialize Laminar context across the application.
|
|
83
194
|
This method must be called before using any other Laminar methods or
|
|
84
195
|
decorators.
|
|
85
196
|
|
|
86
197
|
Args:
|
|
87
|
-
project_api_key (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
198
|
+
project_api_key (str | None, optional): Laminar project api key.\
|
|
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.
|
|
203
|
+
base_url (str | None, optional): Laminar API url. Do NOT include\
|
|
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.
|
|
109
220
|
disable_batch (bool, optional): If set to True, spans will be sent\
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.
|
|
227
|
+
export_timeout_seconds (int | None, optional): Timeout for the OTLP\
|
|
228
|
+
exporter. Defaults to 30 seconds (unlike the OpenTelemetry\
|
|
229
|
+
default of 10 seconds). Defaults to None.
|
|
230
|
+
set_global_tracer_provider (bool, optional): If set to True, the\
|
|
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
|
"""
|
|
117
|
-
cls.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if not cls.__project_api_key:
|
|
121
|
-
dotenv_path = dotenv.find_dotenv(usecwd=True)
|
|
122
|
-
cls.__project_api_key = dotenv.get_key(
|
|
123
|
-
dotenv_path=dotenv_path, key_to_get="LMNR_PROJECT_API_KEY"
|
|
247
|
+
if cls.is_initialized():
|
|
248
|
+
cls.__logger.info(
|
|
249
|
+
"Laminar is already initialized. Skipping initialization."
|
|
124
250
|
)
|
|
125
|
-
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
cls.__project_api_key = project_api_key or from_env("LMNR_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
|
+
):
|
|
126
260
|
raise ValueError(
|
|
127
261
|
"Please initialize the Laminar object with"
|
|
128
262
|
" your project API key or set the LMNR_PROJECT_API_KEY"
|
|
129
263
|
" environment variable in your environment or .env file"
|
|
130
264
|
)
|
|
131
|
-
url = base_url or "https://api.lmnr.ai"
|
|
132
|
-
if re.search(r":\d{1,5}$", url):
|
|
133
|
-
raise ValueError(
|
|
134
|
-
"Please provide the `base_url` without the port number. "
|
|
135
|
-
"Use the `http_port` and `grpc_port` arguments instead."
|
|
136
|
-
)
|
|
137
|
-
cls.__base_http_url = f"{url}:{http_port or 443}"
|
|
138
|
-
cls.__base_grpc_url = f"{url}:{grpc_port or 8443}"
|
|
139
265
|
|
|
140
|
-
cls.__env = env
|
|
141
|
-
cls.__initialized = True
|
|
142
266
|
cls._initialize_logger()
|
|
143
267
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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))]
|
|
281
|
+
if http_port is None:
|
|
282
|
+
cls.__logger.info(f"Using HTTP port from base URL: {match.group(1)}")
|
|
283
|
+
http_port = int(match.group(1))
|
|
284
|
+
else:
|
|
285
|
+
cls.__logger.info(f"Using HTTP port passed as an argument: {http_port}")
|
|
286
|
+
|
|
287
|
+
cls.__initialized = True
|
|
288
|
+
cls.__base_http_url = f"{http_url}:{http_port or 443}"
|
|
289
|
+
cls.__global_metadata = metadata or {}
|
|
290
|
+
|
|
291
|
+
if not os.getenv("OTEL_ATTRIBUTE_COUNT_LIMIT"):
|
|
292
|
+
# each message is at least 2 attributes: role and content,
|
|
293
|
+
# but the default attribute limit is 128, so raise it
|
|
294
|
+
os.environ["OTEL_ATTRIBUTE_COUNT_LIMIT"] = "10000"
|
|
295
|
+
|
|
296
|
+
TracerManager.init(
|
|
297
|
+
base_url=url,
|
|
298
|
+
http_port=http_port or 443,
|
|
299
|
+
port=grpc_port or 8443,
|
|
300
|
+
project_api_key=cls.__project_api_key,
|
|
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
|
|
148
304
|
),
|
|
149
|
-
instruments=instruments,
|
|
150
305
|
disable_batch=disable_batch,
|
|
306
|
+
max_export_batch_size=max_export_batch_size,
|
|
307
|
+
timeout_seconds=export_timeout_seconds,
|
|
308
|
+
set_global_tracer_provider=set_global_tracer_provider,
|
|
309
|
+
otel_logger_level=otel_logger_level,
|
|
310
|
+
session_recording_options=session_recording_options,
|
|
311
|
+
force_http=force_http,
|
|
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()
|
|
151
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.")
|
|
152
358
|
|
|
153
359
|
@classmethod
|
|
154
360
|
def is_initialized(cls):
|
|
@@ -167,150 +373,52 @@ class Laminar:
|
|
|
167
373
|
console_log_handler.setFormatter(VerboseColorfulFormatter())
|
|
168
374
|
cls.__logger.addHandler(console_log_handler)
|
|
169
375
|
|
|
170
|
-
@classmethod
|
|
171
|
-
def run(
|
|
172
|
-
cls,
|
|
173
|
-
pipeline: str,
|
|
174
|
-
inputs: dict[str, NodeInput],
|
|
175
|
-
env: dict[str, str] = {},
|
|
176
|
-
metadata: dict[str, str] = {},
|
|
177
|
-
parent_span_id: Optional[uuid.UUID] = None,
|
|
178
|
-
trace_id: Optional[uuid.UUID] = None,
|
|
179
|
-
) -> Union[PipelineRunResponse, Awaitable[PipelineRunResponse]]:
|
|
180
|
-
"""Runs the pipeline with the given inputs. If called from an async
|
|
181
|
-
function, must be awaited.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
pipeline (str): name of the Laminar pipeline.\
|
|
185
|
-
The pipeline must have a target version set.
|
|
186
|
-
inputs (dict[str, NodeInput]):
|
|
187
|
-
inputs to the endpoint's target pipeline.\
|
|
188
|
-
Keys in the dictionary must match input node names
|
|
189
|
-
env (dict[str, str], optional):
|
|
190
|
-
Environment variables for the pipeline execution.
|
|
191
|
-
Defaults to {}.
|
|
192
|
-
metadata (dict[str, str], optional):
|
|
193
|
-
any custom metadata to be stored with execution trace.
|
|
194
|
-
Defaults to {}.
|
|
195
|
-
parent_span_id (Optional[uuid.UUID], optional): parent span id for\
|
|
196
|
-
the resulting span.
|
|
197
|
-
Defaults to None.
|
|
198
|
-
trace_id (Optional[uuid.UUID], optional): trace id for the\
|
|
199
|
-
resulting trace.
|
|
200
|
-
Defaults to None.
|
|
201
|
-
|
|
202
|
-
Returns:
|
|
203
|
-
PipelineRunResponse: response object containing the outputs
|
|
204
|
-
|
|
205
|
-
Raises:
|
|
206
|
-
ValueError: if project API key is not set
|
|
207
|
-
PipelineRunError: if the endpoint run fails
|
|
208
|
-
"""
|
|
209
|
-
if cls.__project_api_key is None:
|
|
210
|
-
raise ValueError(
|
|
211
|
-
"Please initialize the Laminar object with your project "
|
|
212
|
-
"API key or set the LMNR_PROJECT_API_KEY environment variable"
|
|
213
|
-
)
|
|
214
|
-
try:
|
|
215
|
-
current_span = trace.get_current_span()
|
|
216
|
-
if current_span != trace.INVALID_SPAN:
|
|
217
|
-
parent_span_id = parent_span_id or uuid.UUID(
|
|
218
|
-
int=current_span.get_span_context().span_id
|
|
219
|
-
)
|
|
220
|
-
trace_id = trace_id or uuid.UUID(
|
|
221
|
-
int=current_span.get_span_context().trace_id
|
|
222
|
-
)
|
|
223
|
-
request = PipelineRunRequest(
|
|
224
|
-
inputs=inputs,
|
|
225
|
-
pipeline=pipeline,
|
|
226
|
-
env=env or cls.__env,
|
|
227
|
-
metadata=metadata,
|
|
228
|
-
parent_span_id=parent_span_id,
|
|
229
|
-
trace_id=trace_id,
|
|
230
|
-
)
|
|
231
|
-
loop = asyncio.get_event_loop()
|
|
232
|
-
if loop.is_running():
|
|
233
|
-
return cls.__run(request)
|
|
234
|
-
else:
|
|
235
|
-
return asyncio.run(cls.__run(request))
|
|
236
|
-
except Exception as e:
|
|
237
|
-
raise ValueError(f"Invalid request: {e}")
|
|
238
|
-
|
|
239
|
-
@classmethod
|
|
240
|
-
def semantic_search(
|
|
241
|
-
cls,
|
|
242
|
-
query: str,
|
|
243
|
-
dataset_id: uuid.UUID,
|
|
244
|
-
limit: Optional[int] = None,
|
|
245
|
-
threshold: Optional[float] = None,
|
|
246
|
-
) -> SemanticSearchResponse:
|
|
247
|
-
"""Perform a semantic search on a dataset. If called from an async
|
|
248
|
-
function, must be awaited.
|
|
249
|
-
|
|
250
|
-
Args:
|
|
251
|
-
query (str): query string to search by
|
|
252
|
-
dataset_id (uuid.UUID): id of the dataset to search in
|
|
253
|
-
limit (Optional[int], optional): maximum number of results to\
|
|
254
|
-
return. Defaults to None.
|
|
255
|
-
threshold (Optional[float], optional): minimum score for a result\
|
|
256
|
-
to be returned. Defaults to None.
|
|
257
|
-
|
|
258
|
-
Returns:
|
|
259
|
-
SemanticSearchResponse: response object containing the search results sorted by score in descending order
|
|
260
|
-
"""
|
|
261
|
-
request = SemanticSearchRequest(
|
|
262
|
-
query=query,
|
|
263
|
-
dataset_id=dataset_id,
|
|
264
|
-
limit=limit,
|
|
265
|
-
threshold=threshold,
|
|
266
|
-
)
|
|
267
|
-
loop = asyncio.get_event_loop()
|
|
268
|
-
if loop.is_running():
|
|
269
|
-
return cls.__semantic_search(request)
|
|
270
|
-
else:
|
|
271
|
-
return asyncio.run(cls.__semantic_search(request))
|
|
272
|
-
|
|
273
376
|
@classmethod
|
|
274
377
|
def event(
|
|
275
378
|
cls,
|
|
276
379
|
name: str,
|
|
277
|
-
|
|
278
|
-
timestamp:
|
|
380
|
+
attributes: dict[str, AttributeValue] | None = None,
|
|
381
|
+
timestamp: datetime.datetime | int | None = None,
|
|
382
|
+
*,
|
|
383
|
+
user_id: str | None = None,
|
|
384
|
+
session_id: str | None = None,
|
|
279
385
|
):
|
|
280
|
-
"""Associate an event with the current span.
|
|
281
|
-
|
|
282
|
-
`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.
|
|
283
388
|
|
|
284
389
|
Args:
|
|
285
390
|
name (str): event name
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
If int, must be epoch nanoseconds. If not\
|
|
292
|
-
specified, relies on the underlying OpenTelemetry\
|
|
293
|
-
implementation. Defaults to None.
|
|
391
|
+
attributes (dict[str, AttributeValue] | None, optional): event attributes.
|
|
392
|
+
Defaults to None.
|
|
393
|
+
timestamp (datetime.datetime | int | None, optional): If int, must\
|
|
394
|
+
be epoch nanoseconds. If not specified, relies on the underlying\
|
|
395
|
+
OpenTelemetry implementation. Defaults to None.
|
|
294
396
|
"""
|
|
397
|
+
if not cls.is_initialized():
|
|
398
|
+
return
|
|
399
|
+
|
|
295
400
|
if timestamp and isinstance(timestamp, datetime.datetime):
|
|
296
401
|
timestamp = int(timestamp.timestamp() * 1e9)
|
|
297
402
|
|
|
298
|
-
|
|
299
|
-
"lmnr.event.type": "default",
|
|
300
|
-
}
|
|
301
|
-
if value is not None:
|
|
302
|
-
event["lmnr.event.value"] = value
|
|
403
|
+
extra_attributes = get_event_attributes_from_context()
|
|
303
404
|
|
|
304
|
-
|
|
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
|
|
411
|
+
|
|
412
|
+
current_span = trace.get_current_span(context=get_current_context())
|
|
305
413
|
if current_span == trace.INVALID_SPAN:
|
|
306
|
-
cls.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
"Make sure to annotate the function with a decorator"
|
|
310
|
-
)
|
|
414
|
+
span = cls.start_span(name)
|
|
415
|
+
span.add_event(name, {**(attributes or {}), **extra_attributes}, timestamp)
|
|
416
|
+
span.end()
|
|
311
417
|
return
|
|
312
418
|
|
|
313
|
-
current_span.add_event(
|
|
419
|
+
current_span.add_event(
|
|
420
|
+
name, {**(attributes or {}), **extra_attributes}, timestamp
|
|
421
|
+
)
|
|
314
422
|
|
|
315
423
|
@classmethod
|
|
316
424
|
@contextmanager
|
|
@@ -318,11 +426,15 @@ class Laminar:
|
|
|
318
426
|
cls,
|
|
319
427
|
name: str,
|
|
320
428
|
input: Any = None,
|
|
321
|
-
span_type:
|
|
322
|
-
context:
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
429
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
430
|
+
context: Context | None = None,
|
|
431
|
+
labels: list[str] | None = None,
|
|
432
|
+
parent_span_context: LaminarSpanContext | None = None,
|
|
433
|
+
tags: list[str] | None = None,
|
|
434
|
+
user_id: str | None = None,
|
|
435
|
+
session_id: str | None = None,
|
|
436
|
+
metadata: dict[str, AttributeValue] | None = None,
|
|
437
|
+
) -> Iterator[LaminarSpan]:
|
|
326
438
|
"""Start a new span as the current span. Useful for manual
|
|
327
439
|
instrumentation. If `span_type` is set to `"LLM"`, you should report
|
|
328
440
|
usage and response attributes manually. See `Laminar.set_span_attributes`
|
|
@@ -339,144 +451,198 @@ class Laminar:
|
|
|
339
451
|
name (str): name of the span
|
|
340
452
|
input (Any, optional): input to the span. Will be sent as an\
|
|
341
453
|
attribute, so must be json serializable. Defaults to None.
|
|
342
|
-
span_type (
|
|
454
|
+
span_type (Literal["DEFAULT", "LLM", "TOOL"], optional):\
|
|
343
455
|
type of the span. If you use `"LLM"`, you should report usage\
|
|
344
456
|
and response attributes manually. Defaults to "DEFAULT".
|
|
345
|
-
context (
|
|
457
|
+
context (Context | None, optional): raw OpenTelemetry context\
|
|
346
458
|
to attach the span to. Defaults to None.
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
459
|
+
parent_span_context (LaminarSpanContext | None, optional): parent\
|
|
460
|
+
span context to use for the span. Useful for continuing traces\
|
|
461
|
+
across services. If parent_span_context is a\
|
|
462
|
+
raw OpenTelemetry span context, or if it is a dictionary or string\
|
|
463
|
+
obtained from `Laminar.get_laminar_span_context_dict()` or\
|
|
464
|
+
`Laminar.get_laminar_span_context_str()` respectively, it will be\
|
|
465
|
+
converted to a `LaminarSpanContext` if possible. See also\
|
|
466
|
+
`Laminar.serialize_span_context` for more information.
|
|
467
|
+
Defaults to None.
|
|
468
|
+
labels (list[str] | None, optional): [DEPRECATED] Use tags\
|
|
469
|
+
instead. Labels to set for the span. Defaults to None.
|
|
470
|
+
tags (list[str] | None, optional): tags to set for the span.
|
|
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.
|
|
352
478
|
"""
|
|
353
479
|
|
|
354
480
|
if not cls.is_initialized():
|
|
355
|
-
yield
|
|
481
|
+
yield trace.NonRecordingSpan(
|
|
482
|
+
trace.SpanContext(
|
|
483
|
+
trace_id=RandomIdGenerator().generate_trace_id(),
|
|
484
|
+
span_id=RandomIdGenerator().generate_span_id(),
|
|
485
|
+
is_remote=False,
|
|
486
|
+
)
|
|
487
|
+
)
|
|
356
488
|
return
|
|
357
489
|
|
|
358
|
-
with
|
|
359
|
-
ctx = context or
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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:
|
|
498
|
+
ctx = trace.set_span_in_context(
|
|
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
|
|
530
|
+
)
|
|
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)
|
|
377
553
|
label_props = {}
|
|
378
554
|
try:
|
|
379
555
|
if labels:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
556
|
+
warnings.warn(
|
|
557
|
+
"`Laminar.start_as_current_span` `labels` is deprecated. Use `tags` instead.",
|
|
558
|
+
DeprecationWarning,
|
|
383
559
|
)
|
|
560
|
+
label_props = {f"{ASSOCIATION_PROPERTIES}.labels": labels}
|
|
384
561
|
except Exception:
|
|
385
562
|
cls.__logger.warning(
|
|
386
563
|
f"`start_as_current_span` Could not set labels: {labels}. "
|
|
387
564
|
"They will be propagated to the next span."
|
|
388
565
|
)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if trace_id is not None and isinstance(trace_id, uuid.UUID):
|
|
398
|
-
span.set_attribute(OVERRIDE_PARENT_SPAN, True)
|
|
399
|
-
if input is not None:
|
|
400
|
-
span.set_attribute(
|
|
401
|
-
SPAN_INPUT,
|
|
402
|
-
json_dumps(input),
|
|
566
|
+
tag_props = {}
|
|
567
|
+
if tags:
|
|
568
|
+
if isinstance(tags, list) and all(isinstance(tag, str) for tag in tags):
|
|
569
|
+
tag_props = {f"{ASSOCIATION_PROPERTIES}.tags": tags}
|
|
570
|
+
else:
|
|
571
|
+
cls.__logger.warning(
|
|
572
|
+
f"`start_as_current_span` Could not set tags: {tags}. Tags must be a list of strings. "
|
|
573
|
+
"Tags will be ignored."
|
|
403
574
|
)
|
|
404
|
-
yield span
|
|
405
|
-
|
|
406
|
-
# TODO: Figure out if this is necessary
|
|
407
|
-
try:
|
|
408
|
-
detach(ctx_token)
|
|
409
|
-
except Exception:
|
|
410
|
-
pass
|
|
411
|
-
|
|
412
|
-
@classmethod
|
|
413
|
-
@contextmanager
|
|
414
|
-
def with_labels(cls, labels: dict[str, str], context: Optional[Context] = None):
|
|
415
|
-
"""Set labels for spans within this `with` context. This is useful for
|
|
416
|
-
adding labels to the spans created in the auto-instrumentations.
|
|
417
|
-
|
|
418
|
-
Requirements:
|
|
419
|
-
- Labels must be created in your project in advance.
|
|
420
|
-
- Keys must be strings from your label names.
|
|
421
|
-
- Values must be strings matching the label's allowed values.
|
|
422
575
|
|
|
423
|
-
Usage example:
|
|
424
|
-
```python
|
|
425
|
-
with Laminar.with_labels({"sentiment": "positive"}):
|
|
426
|
-
openai_client.chat.completions.create()
|
|
427
|
-
```
|
|
428
|
-
"""
|
|
429
|
-
with get_tracer():
|
|
430
|
-
label_props = labels.copy()
|
|
431
|
-
label_props = dict(
|
|
432
|
-
(f"label.{k}", json_dumps(v)) for k, v in label_props.items()
|
|
433
|
-
)
|
|
434
|
-
update_association_properties(
|
|
435
|
-
label_props, set_on_current_span=False, context=context
|
|
436
|
-
)
|
|
437
|
-
yield
|
|
438
576
|
try:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
|
446
600
|
|
|
447
601
|
@classmethod
|
|
448
602
|
def start_span(
|
|
449
603
|
cls,
|
|
450
604
|
name: str,
|
|
451
605
|
input: Any = None,
|
|
452
|
-
span_type:
|
|
453
|
-
context:
|
|
454
|
-
|
|
455
|
-
labels:
|
|
456
|
-
|
|
606
|
+
span_type: Literal["DEFAULT", "LLM", "TOOL"] = "DEFAULT",
|
|
607
|
+
context: Context | None = None,
|
|
608
|
+
parent_span_context: LaminarSpanContext | None = None,
|
|
609
|
+
labels: dict[str, str] | None = None,
|
|
610
|
+
tags: list[str] | None = None,
|
|
611
|
+
user_id: str | None = None,
|
|
612
|
+
session_id: str | None = None,
|
|
613
|
+
metadata: dict[str, AttributeValue] | None = None,
|
|
614
|
+
) -> LaminarSpan | Span:
|
|
457
615
|
"""Start a new span. Useful for manual instrumentation.
|
|
458
616
|
If `span_type` is set to `"LLM"`, you should report usage and response
|
|
459
617
|
attributes manually. See `Laminar.set_span_attributes` for more
|
|
460
618
|
information.
|
|
461
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
|
+
|
|
462
628
|
Usage example:
|
|
463
629
|
```python
|
|
464
|
-
from src.lmnr import Laminar
|
|
630
|
+
from src.lmnr import Laminar
|
|
465
631
|
def foo(span):
|
|
466
|
-
with use_span(span):
|
|
632
|
+
with Laminar.use_span(span):
|
|
467
633
|
with Laminar.start_as_current_span("foo_inner"):
|
|
468
634
|
some_function()
|
|
469
|
-
|
|
635
|
+
|
|
470
636
|
def bar():
|
|
471
|
-
with use_span(span):
|
|
637
|
+
with Laminar.use_span(span):
|
|
472
638
|
openai_client.chat.completions.create()
|
|
473
|
-
|
|
639
|
+
|
|
474
640
|
span = Laminar.start_span("outer")
|
|
475
641
|
foo(span)
|
|
476
642
|
bar(span)
|
|
477
643
|
# IMPORTANT: End the span manually
|
|
478
644
|
span.end()
|
|
479
|
-
|
|
645
|
+
|
|
480
646
|
# Results in:
|
|
481
647
|
# | outer
|
|
482
648
|
# | | foo
|
|
@@ -489,125 +655,388 @@ class Laminar:
|
|
|
489
655
|
name (str): name of the span
|
|
490
656
|
input (Any, optional): input to the span. Will be sent as an\
|
|
491
657
|
attribute, so must be json serializable. Defaults to None.
|
|
492
|
-
span_type (
|
|
658
|
+
span_type (Literal["DEFAULT", "LLM", "TOOL"], optional):\
|
|
493
659
|
type of the span. If you use `"LLM"`, you should report usage\
|
|
494
660
|
and response attributes manually. Defaults to "DEFAULT".
|
|
495
|
-
context (
|
|
661
|
+
context (Context | None, optional): raw OpenTelemetry context\
|
|
496
662
|
to attach the span to. Defaults to None.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
663
|
+
parent_span_context (LaminarSpanContext | None, optional): parent\
|
|
664
|
+
span context to use for the span. Useful for continuing traces\
|
|
665
|
+
across services. If parent_span_context is a\
|
|
666
|
+
raw OpenTelemetry span context, or if it is a dictionary or string\
|
|
667
|
+
obtained from `Laminar.get_laminar_span_context_dict()` or\
|
|
668
|
+
`Laminar.get_laminar_span_context_str()` respectively, it will be\
|
|
669
|
+
converted to a `LaminarSpanContext` if possible. See also\
|
|
670
|
+
`Laminar.get_span_context`, `Laminar.get_span_context_dict` and\
|
|
671
|
+
`Laminar.get_span_context_str` for more information.
|
|
672
|
+
Defaults to None.
|
|
673
|
+
tags (list[str] | None, optional): tags to set for the span.
|
|
674
|
+
Defaults to None.
|
|
675
|
+
labels (dict[str, str] | None, optional): [DEPRECATED] Use tags\
|
|
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.
|
|
502
683
|
"""
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
684
|
+
if not cls.is_initialized():
|
|
685
|
+
return trace.NonRecordingSpan(
|
|
686
|
+
trace.SpanContext(
|
|
687
|
+
trace_id=RandomIdGenerator().generate_trace_id(),
|
|
688
|
+
span_id=RandomIdGenerator().generate_span_id(),
|
|
689
|
+
is_remote=False,
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
|
|
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:
|
|
701
|
+
ctx = trace.set_span_in_context(
|
|
702
|
+
trace.NonRecordingSpan(parsed["otel_span_context"]), ctx
|
|
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
|
+
|
|
521
710
|
label_props = {}
|
|
522
711
|
try:
|
|
523
712
|
if labels:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
713
|
+
warnings.warn(
|
|
714
|
+
"`Laminar.start_span` `labels` is deprecated. Use `tags` instead.",
|
|
715
|
+
DeprecationWarning,
|
|
527
716
|
)
|
|
717
|
+
label_props = {
|
|
718
|
+
f"{ASSOCIATION_PROPERTIES}.labels": json_dumps(labels)
|
|
719
|
+
}
|
|
528
720
|
except Exception:
|
|
529
721
|
cls.__logger.warning(
|
|
530
722
|
f"`start_span` Could not set labels: {labels}. They will be "
|
|
531
723
|
"propagated to the next span."
|
|
532
724
|
)
|
|
725
|
+
tag_props = {}
|
|
726
|
+
if tags:
|
|
727
|
+
if isinstance(tags, list) and all(isinstance(tag, str) for tag in tags):
|
|
728
|
+
tag_props = {f"{ASSOCIATION_PROPERTIES}.tags": tags}
|
|
729
|
+
else:
|
|
730
|
+
cls.__logger.warning(
|
|
731
|
+
f"`start_span` Could not set tags: {tags}. Tags must be a list of strings. "
|
|
732
|
+
+ "Tags will be ignored."
|
|
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
|
+
|
|
533
784
|
span = tracer.start_span(
|
|
534
785
|
name,
|
|
535
786
|
context=ctx,
|
|
536
787
|
attributes={
|
|
537
788
|
SPAN_TYPE: span_type,
|
|
789
|
+
PARENT_SPAN_PATH: parsed["path"],
|
|
790
|
+
PARENT_SPAN_IDS_PATH: parsed["span_ids_path"],
|
|
538
791
|
**(label_props),
|
|
792
|
+
**(tag_props),
|
|
793
|
+
**(association_props),
|
|
539
794
|
},
|
|
540
795
|
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
SPAN_INPUT,
|
|
546
|
-
json_dumps(input),
|
|
547
|
-
)
|
|
796
|
+
|
|
797
|
+
if not isinstance(span, LaminarSpan):
|
|
798
|
+
span = LaminarSpan(span)
|
|
799
|
+
span.set_input(input)
|
|
548
800
|
return span
|
|
549
801
|
|
|
550
802
|
@classmethod
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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.
|
|
554
815
|
|
|
555
816
|
Args:
|
|
556
|
-
|
|
557
|
-
|
|
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.
|
|
558
827
|
"""
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
|
853
|
+
else:
|
|
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()
|
|
562
890
|
|
|
563
891
|
@classmethod
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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.
|
|
576
910
|
|
|
577
|
-
|
|
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.
|
|
924
|
+
|
|
925
|
+
Usage example:
|
|
578
926
|
```python
|
|
579
|
-
from lmnr import Laminar,
|
|
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()
|
|
580
933
|
|
|
581
|
-
|
|
934
|
+
@observe()
|
|
935
|
+
def bar():
|
|
582
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
|
|
583
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.
|
|
584
979
|
"""
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
587
1008
|
else:
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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)
|
|
595
1024
|
|
|
596
1025
|
@classmethod
|
|
597
1026
|
def set_span_attributes(
|
|
598
1027
|
cls,
|
|
599
|
-
attributes: dict[Attributes, Any],
|
|
1028
|
+
attributes: dict[Attributes | str, Any],
|
|
600
1029
|
):
|
|
601
1030
|
"""Set attributes for the current span. Useful for manual
|
|
602
1031
|
instrumentation.
|
|
603
1032
|
Example:
|
|
604
1033
|
```python
|
|
605
|
-
with
|
|
1034
|
+
with Laminar.start_as_current_span(
|
|
606
1035
|
name="my_span_name", input=input["messages"], span_type="LLM"
|
|
607
1036
|
):
|
|
608
1037
|
response = await my_custom_call_to_openai(input)
|
|
609
|
-
|
|
610
|
-
|
|
1038
|
+
Laminar.set_span_output(response["choices"][0]["message"]["content"])
|
|
1039
|
+
Laminar.set_span_attributes({
|
|
611
1040
|
Attributes.PROVIDER: 'openai',
|
|
612
1041
|
Attributes.REQUEST_MODEL: input["model"],
|
|
613
1042
|
Attributes.RESPONSE_MODEL: response["model"],
|
|
@@ -618,132 +1047,245 @@ class Laminar:
|
|
|
618
1047
|
```
|
|
619
1048
|
|
|
620
1049
|
Args:
|
|
621
|
-
attributes (dict[
|
|
1050
|
+
attributes (dict[Attributes | str, Any]): attributes to set for the span
|
|
622
1051
|
"""
|
|
623
|
-
span =
|
|
624
|
-
if span == trace.INVALID_SPAN:
|
|
1052
|
+
span = cls.get_current_span()
|
|
1053
|
+
if span == trace.INVALID_SPAN or span is None:
|
|
625
1054
|
return
|
|
626
1055
|
|
|
627
1056
|
for key, value in attributes.items():
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
cls.__logger.warning(
|
|
633
|
-
f"Attribute {key} is not a valid Laminar attribute."
|
|
634
|
-
)
|
|
635
|
-
continue
|
|
636
|
-
if not isinstance(value, (str, int, float, bool)):
|
|
637
|
-
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))
|
|
638
1061
|
else:
|
|
639
|
-
span.set_attribute(key
|
|
1062
|
+
span.set_attribute(key, value)
|
|
640
1063
|
|
|
641
1064
|
@classmethod
|
|
642
|
-
def
|
|
643
|
-
cls,
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
1065
|
+
def get_laminar_span_context(
|
|
1066
|
+
cls, span: trace.Span | None = None
|
|
1067
|
+
) -> LaminarSpanContext | None:
|
|
1068
|
+
"""Get the laminar span context for a given span.
|
|
1069
|
+
If no span is provided, the current active span will be used.
|
|
1070
|
+
"""
|
|
1071
|
+
if not cls.is_initialized():
|
|
1072
|
+
return None
|
|
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()
|
|
1080
|
+
|
|
1081
|
+
@classmethod
|
|
1082
|
+
def get_laminar_span_context_dict(
|
|
1083
|
+
cls, span: trace.Span | None = None
|
|
1084
|
+
) -> dict | None:
|
|
1085
|
+
span_context = cls.get_laminar_span_context(span)
|
|
1086
|
+
if span_context is None:
|
|
1087
|
+
return None
|
|
1088
|
+
return span_context.model_dump()
|
|
1089
|
+
|
|
1090
|
+
@classmethod
|
|
1091
|
+
def serialize_span_context(cls, span: trace.Span | None = None) -> str | None:
|
|
1092
|
+
"""Get the laminar span context for a given span as a string.
|
|
1093
|
+
If no span is provided, the current active span will be used.
|
|
1094
|
+
|
|
1095
|
+
This is useful for continuing a trace across services.
|
|
1096
|
+
|
|
1097
|
+
Example:
|
|
1098
|
+
```python
|
|
1099
|
+
# service A:
|
|
1100
|
+
with Laminar.start_as_current_span("service_a"):
|
|
1101
|
+
span_context = Laminar.serialize_span_context()
|
|
1102
|
+
# send span_context to service B
|
|
1103
|
+
call_service_b(request, headers={"laminar-span-context": span_context})
|
|
1104
|
+
|
|
1105
|
+
# service B:
|
|
1106
|
+
def call_service_b(request, headers):
|
|
1107
|
+
span_context = Laminar.deserialize_span_context(headers["laminar-span-context"])
|
|
1108
|
+
with Laminar.start_as_current_span("service_b", parent_span_context=span_context):
|
|
1109
|
+
# rest of the function
|
|
1110
|
+
pass
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
This will result in a trace like:
|
|
1114
|
+
```
|
|
1115
|
+
service_a
|
|
1116
|
+
service_b
|
|
1117
|
+
```
|
|
1118
|
+
"""
|
|
1119
|
+
span_context = cls.get_laminar_span_context(span)
|
|
1120
|
+
if span_context is None:
|
|
1121
|
+
return None
|
|
1122
|
+
return str(span_context)
|
|
1123
|
+
|
|
1124
|
+
@classmethod
|
|
1125
|
+
def deserialize_span_context(cls, span_context: dict | str) -> LaminarSpanContext:
|
|
1126
|
+
return LaminarSpanContext.deserialize(span_context)
|
|
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.
|
|
649
1132
|
|
|
650
1133
|
Args:
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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.
|
|
655
1141
|
"""
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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)
|
|
660
1150
|
|
|
661
1151
|
@classmethod
|
|
662
|
-
def
|
|
663
|
-
"""
|
|
1152
|
+
def flush(cls) -> bool:
|
|
1153
|
+
"""Flush the internal tracer.
|
|
1154
|
+
|
|
1155
|
+
Returns:
|
|
1156
|
+
bool: True if the tracer was flushed, False otherwise
|
|
1157
|
+
(e.g. no tracer or timeout).
|
|
1158
|
+
"""
|
|
1159
|
+
if not cls.is_initialized():
|
|
1160
|
+
return False
|
|
1161
|
+
return TracerManager.flush()
|
|
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
|
+
|
|
1179
|
+
@classmethod
|
|
1180
|
+
def shutdown(cls):
|
|
1181
|
+
if cls.is_initialized():
|
|
1182
|
+
TracerManager.shutdown()
|
|
1183
|
+
cls.__initialized = False
|
|
1184
|
+
|
|
1185
|
+
@classmethod
|
|
1186
|
+
def set_span_tags(cls, tags: list[str]):
|
|
1187
|
+
"""Set the tags for the current span.
|
|
664
1188
|
|
|
665
1189
|
Args:
|
|
666
|
-
|
|
667
|
-
sent as attributes, so must be json serializable.
|
|
1190
|
+
tags (list[str]): Tags to set for the span.
|
|
668
1191
|
"""
|
|
669
|
-
|
|
670
|
-
|
|
1192
|
+
if not cls.is_initialized():
|
|
1193
|
+
return
|
|
1194
|
+
|
|
1195
|
+
span = cls.get_current_span()
|
|
1196
|
+
if span is None:
|
|
1197
|
+
return
|
|
1198
|
+
span.set_tags(tags)
|
|
671
1199
|
|
|
672
1200
|
@classmethod
|
|
673
|
-
def
|
|
674
|
-
"""
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
set_association_properties(props)
|
|
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:
|
|
1205
|
+
return
|
|
1206
|
+
span.add_tags(tags)
|
|
680
1207
|
|
|
681
1208
|
@classmethod
|
|
682
|
-
def
|
|
683
|
-
"""
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1209
|
+
def set_trace_session_id(cls, session_id: str | None = None):
|
|
1210
|
+
"""Set the session id for the current trace.
|
|
1211
|
+
Overrides any existing session id.
|
|
1212
|
+
|
|
1213
|
+
Args:
|
|
1214
|
+
session_id (str | None, optional): Custom session id. Defaults to None.
|
|
1215
|
+
"""
|
|
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:
|
|
1223
|
+
cls.__logger.warning("No active span to set session id on")
|
|
1224
|
+
return
|
|
1225
|
+
span.set_trace_session_id(session_id)
|
|
688
1226
|
|
|
689
1227
|
@classmethod
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
try:
|
|
708
|
-
resp_json = await response.json()
|
|
709
|
-
raise ValueError(
|
|
710
|
-
f"Error creating evaluation {json.dumps(resp_json)}"
|
|
711
|
-
)
|
|
712
|
-
except aiohttp.ClientError:
|
|
713
|
-
text = await response.text()
|
|
714
|
-
raise ValueError(f"Error creating evaluation {text}")
|
|
715
|
-
resp_json = await response.json()
|
|
716
|
-
return CreateEvaluationResponse.model_validate(resp_json)
|
|
1228
|
+
def set_trace_user_id(cls, user_id: str | None = None):
|
|
1229
|
+
"""Set the user id for the current trace.
|
|
1230
|
+
Overrides any existing user id.
|
|
1231
|
+
|
|
1232
|
+
Args:
|
|
1233
|
+
user_id (str | None, optional): Custom user id. Defaults to None.
|
|
1234
|
+
"""
|
|
1235
|
+
if not cls.is_initialized():
|
|
1236
|
+
return
|
|
1237
|
+
|
|
1238
|
+
context = set_association_prop_context(user_id=user_id, attach=True)
|
|
1239
|
+
|
|
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")
|
|
1243
|
+
return
|
|
1244
|
+
span.set_trace_user_id(user_id)
|
|
717
1245
|
|
|
718
1246
|
@classmethod
|
|
719
|
-
def
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1247
|
+
def set_trace_metadata(cls, metadata: dict[str, AttributeValue]):
|
|
1248
|
+
"""Set the metadata for the current trace.
|
|
1249
|
+
|
|
1250
|
+
Args:
|
|
1251
|
+
metadata (dict[str, AttributeValue]): Metadata to set for the trace.
|
|
1252
|
+
"""
|
|
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:
|
|
1260
|
+
cls.__logger.warning("No active span to set metadata on")
|
|
1261
|
+
return
|
|
1262
|
+
span.set_trace_metadata(merged_metadata)
|
|
1263
|
+
|
|
1264
|
+
@classmethod
|
|
1265
|
+
def get_base_http_url(cls):
|
|
1266
|
+
return cls.__base_http_url
|
|
1267
|
+
|
|
1268
|
+
@classmethod
|
|
1269
|
+
def get_project_api_key(cls):
|
|
1270
|
+
return cls.__project_api_key
|
|
1271
|
+
|
|
1272
|
+
@classmethod
|
|
1273
|
+
def get_trace_id(cls) -> uuid.UUID | None:
|
|
1274
|
+
"""Get the trace id for the current active span represented as a UUID.
|
|
1275
|
+
Returns None if there is no active span.
|
|
1276
|
+
|
|
1277
|
+
Returns:
|
|
1278
|
+
uuid.UUID | None: The trace id for the current span, or None if\
|
|
1279
|
+
there is no active span.
|
|
1280
|
+
"""
|
|
1281
|
+
trace_id = (
|
|
1282
|
+
trace.get_current_span(context=get_current_context())
|
|
1283
|
+
.get_span_context()
|
|
1284
|
+
.trace_id
|
|
734
1285
|
)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
resp_json = response.json()
|
|
739
|
-
raise ValueError(
|
|
740
|
-
f"Error fetching datapoints: [{response.status_code}] {json.dumps(resp_json)}"
|
|
741
|
-
)
|
|
742
|
-
except requests.exceptions.RequestException:
|
|
743
|
-
raise ValueError(
|
|
744
|
-
f"Error fetching datapoints: [{response.status_code}] {response.text}"
|
|
745
|
-
)
|
|
746
|
-
return GetDatapointsResponse.model_validate(response.json())
|
|
1286
|
+
if trace_id == INVALID_TRACE_ID:
|
|
1287
|
+
return None
|
|
1288
|
+
return uuid.UUID(int=trace_id)
|
|
747
1289
|
|
|
748
1290
|
@classmethod
|
|
749
1291
|
def _headers(cls):
|
|
@@ -762,56 +1304,45 @@ class Laminar:
|
|
|
762
1304
|
Args:
|
|
763
1305
|
trace_type (TraceType): Type of the trace
|
|
764
1306
|
"""
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
update_association_properties(association_properties)
|
|
1307
|
+
if not cls.is_initialized():
|
|
1308
|
+
return
|
|
769
1309
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
async with aiohttp.ClientSession() as session:
|
|
776
|
-
async with session.post(
|
|
777
|
-
cls.__base_http_url + "/v1/pipeline/run",
|
|
778
|
-
data=json.dumps(request.to_dict()),
|
|
779
|
-
headers=cls._headers(),
|
|
780
|
-
) as response:
|
|
781
|
-
if response.status != 200:
|
|
782
|
-
raise PipelineRunError(response)
|
|
783
|
-
try:
|
|
784
|
-
resp_json = await response.json()
|
|
785
|
-
keys = list(resp_json.keys())
|
|
786
|
-
for key in keys:
|
|
787
|
-
value = resp_json[key]
|
|
788
|
-
del resp_json[key]
|
|
789
|
-
resp_json[to_snake(key)] = value
|
|
790
|
-
return PipelineRunResponse(**resp_json)
|
|
791
|
-
except Exception:
|
|
792
|
-
raise PipelineRunError(response)
|
|
1310
|
+
span = trace.get_current_span(context=get_current_context())
|
|
1311
|
+
if span == trace.INVALID_SPAN:
|
|
1312
|
+
cls.__logger.warning("No active span to set trace type on")
|
|
1313
|
+
return
|
|
1314
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}", trace_type.value)
|
|
793
1315
|
|
|
794
1316
|
@classmethod
|
|
795
|
-
|
|
1317
|
+
def _get_association_prop_attributes(
|
|
796
1318
|
cls,
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|