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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from typing import Any
|
|
6
|
+
from opentelemetry.context import Context, Token, create_key, get_value, set_value
|
|
7
|
+
|
|
8
|
+
from lmnr.opentelemetry_lib.tracing.attributes import (
|
|
9
|
+
METADATA,
|
|
10
|
+
SESSION_ID,
|
|
11
|
+
TRACE_TYPE,
|
|
12
|
+
USER_ID,
|
|
13
|
+
)
|
|
14
|
+
from lmnr.sdk.types import TraceType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _IsolatedRuntimeContext(ABC):
|
|
18
|
+
"""The isolated RuntimeContext interface, identical to OpenTelemetry's _RuntimeContext
|
|
19
|
+
but isolated from the global context.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def attach(self, context: Context) -> Token[Context]:
|
|
24
|
+
"""Sets the current `Context` object. Returns a
|
|
25
|
+
token that can be used to reset to the previous `Context`.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
context: The Context to set.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def get_current(self) -> Context:
|
|
33
|
+
"""Returns the current `Context` object."""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def detach(self, token: Token[Context]) -> None:
|
|
37
|
+
"""Resets Context to a previous value
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
token: A reference to a previous Context.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class IsolatedContextVarsRuntimeContext(_IsolatedRuntimeContext):
|
|
45
|
+
"""An isolated implementation of the RuntimeContext interface which wraps ContextVar
|
|
46
|
+
but uses its own ContextVar instead of the global one.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._current_context = ContextVar(
|
|
51
|
+
"isolated_current_context", default=Context()
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def attach(self, context: Context) -> Token[Context]:
|
|
55
|
+
"""Sets the current `Context` object. Returns a
|
|
56
|
+
token that can be used to reset to the previous `Context`.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
context: The Context to set.
|
|
60
|
+
"""
|
|
61
|
+
return self._current_context.set(context)
|
|
62
|
+
|
|
63
|
+
def get_current(self) -> Context:
|
|
64
|
+
"""Returns the current `Context` object."""
|
|
65
|
+
return self._current_context.get()
|
|
66
|
+
|
|
67
|
+
def detach(self, token: Token[Context]) -> None:
|
|
68
|
+
"""Resets Context to a previous value
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
token: A reference to a previous Context.
|
|
72
|
+
"""
|
|
73
|
+
self._current_context.reset(token)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Create the isolated runtime context
|
|
77
|
+
_ISOLATED_RUNTIME_CONTEXT = IsolatedContextVarsRuntimeContext()
|
|
78
|
+
|
|
79
|
+
# Token stack for push/pop API compatibility - much lighter than copying contexts
|
|
80
|
+
_isolated_token_stack: ContextVar[list[Token[Context]]] = ContextVar(
|
|
81
|
+
"isolated_token_stack", default=[]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Thread-local storage for threading support
|
|
85
|
+
_isolated_token_stack_storage = threading.local()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_token_stack() -> list[Token[Context]]:
|
|
89
|
+
"""Get the token stack, supporting both asyncio and threading."""
|
|
90
|
+
try:
|
|
91
|
+
return _isolated_token_stack.get()
|
|
92
|
+
except LookupError:
|
|
93
|
+
if not hasattr(_isolated_token_stack_storage, "token_stack"):
|
|
94
|
+
_isolated_token_stack_storage.token_stack = []
|
|
95
|
+
return _isolated_token_stack_storage.token_stack
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def set_token_stack(stack: list[Token[Context]]) -> None:
|
|
99
|
+
"""Set the token stack, supporting both asyncio and threading."""
|
|
100
|
+
try:
|
|
101
|
+
_isolated_token_stack.set(stack)
|
|
102
|
+
except LookupError:
|
|
103
|
+
_isolated_token_stack_storage.token_stack = stack
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_current_context() -> Context:
|
|
107
|
+
"""Get the current isolated context."""
|
|
108
|
+
return _ISOLATED_RUNTIME_CONTEXT.get_current()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def attach_context(context: Context) -> Token[Context]:
|
|
112
|
+
"""Attach a context to the isolated runtime context."""
|
|
113
|
+
return _ISOLATED_RUNTIME_CONTEXT.attach(context)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def detach_context(token: Token[Context]) -> None:
|
|
117
|
+
"""Detach a context from the isolated runtime context."""
|
|
118
|
+
_ISOLATED_RUNTIME_CONTEXT.detach(token)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
CONTEXT_USER_ID_KEY = create_key(f"lmnr.{USER_ID}")
|
|
122
|
+
CONTEXT_SESSION_ID_KEY = create_key(f"lmnr.{SESSION_ID}")
|
|
123
|
+
CONTEXT_METADATA_KEY = create_key(f"lmnr.{METADATA}")
|
|
124
|
+
CONTEXT_TRACE_TYPE_KEY = create_key(f"lmnr.{TRACE_TYPE}")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_event_attributes_from_context(context: Context | None = None) -> dict[str, str]:
|
|
128
|
+
"""Get the event attributes from the context."""
|
|
129
|
+
context = context or get_current_context()
|
|
130
|
+
attributes = {}
|
|
131
|
+
if session_id := get_value(CONTEXT_SESSION_ID_KEY, context):
|
|
132
|
+
attributes["lmnr.event.session_id"] = session_id
|
|
133
|
+
if user_id := get_value(CONTEXT_USER_ID_KEY, context):
|
|
134
|
+
attributes["lmnr.event.user_id"] = user_id
|
|
135
|
+
return attributes
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def set_association_prop_context(
|
|
139
|
+
user_id: str | None = None,
|
|
140
|
+
session_id: str | None = None,
|
|
141
|
+
trace_type: TraceType | None = None,
|
|
142
|
+
context: Context | None = None,
|
|
143
|
+
metadata: dict[str, Any] | None = None,
|
|
144
|
+
attach: bool = True,
|
|
145
|
+
) -> Context:
|
|
146
|
+
context = context or get_current_context()
|
|
147
|
+
if user_id is not None:
|
|
148
|
+
context = set_value(CONTEXT_USER_ID_KEY, user_id, context)
|
|
149
|
+
if session_id is not None:
|
|
150
|
+
context = set_value(CONTEXT_SESSION_ID_KEY, session_id, context)
|
|
151
|
+
if trace_type is not None:
|
|
152
|
+
context = set_value(CONTEXT_TRACE_TYPE_KEY, trace_type.value, context)
|
|
153
|
+
if metadata is not None:
|
|
154
|
+
context = set_value(CONTEXT_METADATA_KEY, metadata, context)
|
|
155
|
+
if attach:
|
|
156
|
+
attach_context(context)
|
|
157
|
+
return context
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def pop_span_context() -> None:
|
|
161
|
+
"""Pop the current span context from the stack."""
|
|
162
|
+
current_stack = get_token_stack().copy()
|
|
163
|
+
if current_stack:
|
|
164
|
+
token = current_stack.pop()
|
|
165
|
+
set_token_stack(current_stack)
|
|
166
|
+
detach_context(token)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def push_span_context(context: Context) -> None:
|
|
170
|
+
"""Push a new span context onto the stack."""
|
|
171
|
+
token = attach_context(context)
|
|
172
|
+
token_stack = get_token_stack().copy()
|
|
173
|
+
token_stack.append(token)
|
|
174
|
+
set_token_stack(token_stack)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def clear_context() -> None:
|
|
178
|
+
"""Clear the isolated context and token stack.
|
|
179
|
+
|
|
180
|
+
This is primarily used during force_flush operations in Lambda-like
|
|
181
|
+
environments to ensure subsequent invocations don't continue traces
|
|
182
|
+
from previous invocations.
|
|
183
|
+
|
|
184
|
+
Warning: This should only be called when you're certain no spans are
|
|
185
|
+
actively being processed, as it will reset all context state.
|
|
186
|
+
"""
|
|
187
|
+
# Clear the token stack first
|
|
188
|
+
try:
|
|
189
|
+
_isolated_token_stack.set([])
|
|
190
|
+
except LookupError:
|
|
191
|
+
pass
|
|
192
|
+
|
|
193
|
+
# Clear thread-local storage if it exists
|
|
194
|
+
if hasattr(_isolated_token_stack_storage, "token_stack"):
|
|
195
|
+
_isolated_token_stack_storage.token_stack = []
|
|
196
|
+
|
|
197
|
+
# Reset the context to a fresh empty context
|
|
198
|
+
# This doesn't require manually detaching tokens since we're
|
|
199
|
+
# intentionally resetting everything to a clean state
|
|
200
|
+
_ISOLATED_RUNTIME_CONTEXT._current_context.set(Context())
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import grpc
|
|
2
|
+
import re
|
|
3
|
+
import threading
|
|
4
|
+
from urllib.parse import urlparse, urlunparse
|
|
5
|
+
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
6
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
7
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
8
|
+
OTLPSpanExporter,
|
|
9
|
+
)
|
|
10
|
+
from opentelemetry.exporter.otlp.proto.http import Compression as HTTPCompression
|
|
11
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
12
|
+
OTLPSpanExporter as HTTPOTLPSpanExporter,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from lmnr.sdk.log import get_default_logger
|
|
16
|
+
from lmnr.sdk.utils import from_env, get_otel_env_var, parse_otel_headers
|
|
17
|
+
|
|
18
|
+
logger = get_default_logger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LaminarSpanExporter(SpanExporter):
|
|
22
|
+
instance: OTLPSpanExporter | HTTPOTLPSpanExporter
|
|
23
|
+
endpoint: str
|
|
24
|
+
headers: dict[str, str]
|
|
25
|
+
timeout: float
|
|
26
|
+
force_http: bool
|
|
27
|
+
_instance_lock: threading.RLock
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
base_url: str | None = None,
|
|
32
|
+
port: int | None = None,
|
|
33
|
+
api_key: str | None = None,
|
|
34
|
+
timeout_seconds: int = 30,
|
|
35
|
+
force_http: bool = False,
|
|
36
|
+
):
|
|
37
|
+
self._instance_lock = threading.RLock()
|
|
38
|
+
url = base_url or from_env("LMNR_BASE_URL") or "https://api.lmnr.ai"
|
|
39
|
+
url = url.rstrip("/")
|
|
40
|
+
if match := re.search(r":(\d{1,5})$", url):
|
|
41
|
+
url = url[: -len(match.group(0))]
|
|
42
|
+
if port is None:
|
|
43
|
+
port = int(match.group(1))
|
|
44
|
+
if port is None:
|
|
45
|
+
port = 443 if force_http else 8443
|
|
46
|
+
final_url = f"{url}:{port or 443}"
|
|
47
|
+
api_key = api_key or from_env("LMNR_PROJECT_API_KEY")
|
|
48
|
+
self.endpoint = final_url
|
|
49
|
+
if api_key:
|
|
50
|
+
self.headers = (
|
|
51
|
+
{"Authorization": f"Bearer {api_key}"}
|
|
52
|
+
if force_http
|
|
53
|
+
else {"authorization": f"Bearer {api_key}"}
|
|
54
|
+
)
|
|
55
|
+
elif get_otel_env_var("HEADERS"):
|
|
56
|
+
self.headers = parse_otel_headers(get_otel_env_var("HEADERS"))
|
|
57
|
+
else:
|
|
58
|
+
self.headers = {}
|
|
59
|
+
self.timeout = timeout_seconds
|
|
60
|
+
self.force_http = force_http
|
|
61
|
+
if get_otel_env_var("ENDPOINT"):
|
|
62
|
+
if not base_url:
|
|
63
|
+
self.endpoint = get_otel_env_var("ENDPOINT")
|
|
64
|
+
else:
|
|
65
|
+
logger.warning(
|
|
66
|
+
"OTEL_ENDPOINT is set, but Laminar base URL is also set. Ignoring OTEL_ENDPOINT."
|
|
67
|
+
)
|
|
68
|
+
protocol = get_otel_env_var("PROTOCOL") or "grpc/protobuf"
|
|
69
|
+
exporter_type = from_env("OTEL_EXPORTER") or "otlp_grpc"
|
|
70
|
+
self.force_http = (
|
|
71
|
+
protocol in ("http/protobuf", "http/json")
|
|
72
|
+
or exporter_type == "otlp_http"
|
|
73
|
+
)
|
|
74
|
+
if not self.endpoint:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"Laminar base URL is not set and OTEL_ENDPOINT is not set. Please either\n"
|
|
77
|
+
"- set the LMNR_BASE_URL environment variable\n"
|
|
78
|
+
"- set the OTEL_ENDPOINT environment variable\n"
|
|
79
|
+
"- pass the base_url parameter to Laminar.initialize"
|
|
80
|
+
)
|
|
81
|
+
self._init_instance()
|
|
82
|
+
|
|
83
|
+
def _normalize_http_endpoint(self, endpoint: str) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Normalize HTTP endpoint URL by adding /v1/traces path if no path is present.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
endpoint: The endpoint URL to normalize
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The normalized endpoint URL with /v1/traces path if needed
|
|
92
|
+
"""
|
|
93
|
+
try:
|
|
94
|
+
parsed = urlparse(endpoint)
|
|
95
|
+
# Check if there's no path or only a trailing slash
|
|
96
|
+
if not parsed.path or parsed.path == "/":
|
|
97
|
+
# Add /v1/traces to the endpoint
|
|
98
|
+
new_parsed = parsed._replace(path="/v1/traces")
|
|
99
|
+
normalized_url = urlunparse(new_parsed)
|
|
100
|
+
logger.info(
|
|
101
|
+
f"No path found in HTTP endpoint URL. "
|
|
102
|
+
f"Adding default path /v1/traces: {endpoint} -> {normalized_url}"
|
|
103
|
+
)
|
|
104
|
+
return normalized_url
|
|
105
|
+
return endpoint
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.warning(
|
|
108
|
+
f"Failed to parse endpoint URL '{endpoint}': {e}. Using as-is."
|
|
109
|
+
)
|
|
110
|
+
return endpoint
|
|
111
|
+
|
|
112
|
+
def _init_instance(self):
|
|
113
|
+
# Create new instance first (outside critical section for performance)
|
|
114
|
+
if self.force_http:
|
|
115
|
+
# Normalize HTTP endpoint to ensure it has a path
|
|
116
|
+
http_endpoint = self._normalize_http_endpoint(self.endpoint)
|
|
117
|
+
new_instance = HTTPOTLPSpanExporter(
|
|
118
|
+
endpoint=http_endpoint,
|
|
119
|
+
headers=self.headers,
|
|
120
|
+
compression=HTTPCompression.Gzip,
|
|
121
|
+
timeout=self.timeout,
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
new_instance = OTLPSpanExporter(
|
|
125
|
+
endpoint=self.endpoint,
|
|
126
|
+
headers=self.headers,
|
|
127
|
+
timeout=self.timeout,
|
|
128
|
+
compression=grpc.Compression.Gzip,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Atomic swap with proper cleanup
|
|
132
|
+
with self._instance_lock:
|
|
133
|
+
old_instance: OTLPSpanExporter | HTTPOTLPSpanExporter | None = getattr(
|
|
134
|
+
self, "instance", None
|
|
135
|
+
)
|
|
136
|
+
if old_instance is not None:
|
|
137
|
+
try:
|
|
138
|
+
old_instance.shutdown()
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning(f"Error shutting down old exporter instance: {e}")
|
|
141
|
+
self.instance = new_instance
|
|
142
|
+
|
|
143
|
+
def export(self, spans: list[ReadableSpan]) -> SpanExportResult:
|
|
144
|
+
with self._instance_lock:
|
|
145
|
+
return self.instance.export(spans)
|
|
146
|
+
|
|
147
|
+
def shutdown(self) -> None:
|
|
148
|
+
with self._instance_lock:
|
|
149
|
+
return self.instance.shutdown()
|
|
150
|
+
|
|
151
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
152
|
+
with self._instance_lock:
|
|
153
|
+
return self.instance.force_flush(timeout_millis)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from opentelemetry.trace import TracerProvider
|
|
6
|
+
import lmnr.opentelemetry_lib.tracing._instrument_initializers as initializers
|
|
7
|
+
from lmnr.sdk.client.asynchronous.async_client import AsyncLaminarClient
|
|
8
|
+
|
|
9
|
+
module_logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Instruments(Enum):
|
|
13
|
+
# The list of libraries which will be autoinstrumented
|
|
14
|
+
# if no specific instruments are provided to initialize()
|
|
15
|
+
ALEPHALPHA = "alephalpha"
|
|
16
|
+
ANTHROPIC = "anthropic"
|
|
17
|
+
BEDROCK = "bedrock"
|
|
18
|
+
BROWSER_USE = "browser_use"
|
|
19
|
+
BROWSER_USE_SESSION = "browser_use_session"
|
|
20
|
+
BUBUS = "bubus"
|
|
21
|
+
CHROMA = "chroma"
|
|
22
|
+
CLAUDE_AGENT = "claude_agent"
|
|
23
|
+
COHERE = "cohere"
|
|
24
|
+
CREWAI = "crewai"
|
|
25
|
+
CUA_AGENT = "cua_agent"
|
|
26
|
+
CUA_COMPUTER = "cua_computer"
|
|
27
|
+
GOOGLE_GENAI = "google_genai"
|
|
28
|
+
GROQ = "groq"
|
|
29
|
+
HAYSTACK = "haystack"
|
|
30
|
+
KERNEL = "kernel"
|
|
31
|
+
LANCEDB = "lancedb"
|
|
32
|
+
LANGCHAIN = "langchain"
|
|
33
|
+
LANGGRAPH = "langgraph"
|
|
34
|
+
LLAMA_INDEX = "llama_index"
|
|
35
|
+
MARQO = "marqo"
|
|
36
|
+
MCP = "mcp"
|
|
37
|
+
MILVUS = "milvus"
|
|
38
|
+
MISTRAL = "mistral"
|
|
39
|
+
OLLAMA = "ollama"
|
|
40
|
+
OPENAI = "openai"
|
|
41
|
+
OPENHANDS = "openhands"
|
|
42
|
+
# Patch OpenTelemetry to fix DataDog's broken Span context
|
|
43
|
+
# See lmnr.opentelemetry_lib.opentelemetry.instrumentation.opentelemetry
|
|
44
|
+
# for more details.
|
|
45
|
+
OPENTELEMETRY = "opentelemetry"
|
|
46
|
+
###
|
|
47
|
+
PATCHRIGHT = "patchright"
|
|
48
|
+
PINECONE = "pinecone"
|
|
49
|
+
PLAYWRIGHT = "playwright"
|
|
50
|
+
QDRANT = "qdrant"
|
|
51
|
+
REPLICATE = "replicate"
|
|
52
|
+
SAGEMAKER = "sagemaker"
|
|
53
|
+
SKYVERN = "skyvern"
|
|
54
|
+
TOGETHER = "together"
|
|
55
|
+
TRANSFORMERS = "transformers"
|
|
56
|
+
VERTEXAI = "vertexai"
|
|
57
|
+
WATSONX = "watsonx"
|
|
58
|
+
WEAVIATE = "weaviate"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
INSTRUMENTATION_INITIALIZERS: dict[
|
|
62
|
+
Instruments, initializers.InstrumentorInitializer
|
|
63
|
+
] = {
|
|
64
|
+
Instruments.ALEPHALPHA: initializers.AlephAlphaInstrumentorInitializer(),
|
|
65
|
+
Instruments.ANTHROPIC: initializers.AnthropicInstrumentorInitializer(),
|
|
66
|
+
Instruments.BEDROCK: initializers.BedrockInstrumentorInitializer(),
|
|
67
|
+
Instruments.BROWSER_USE: initializers.BrowserUseInstrumentorInitializer(),
|
|
68
|
+
Instruments.BROWSER_USE_SESSION: initializers.BrowserUseSessionInstrumentorInitializer(),
|
|
69
|
+
Instruments.BUBUS: initializers.BubusInstrumentorInitializer(),
|
|
70
|
+
Instruments.CHROMA: initializers.ChromaInstrumentorInitializer(),
|
|
71
|
+
Instruments.CLAUDE_AGENT: initializers.ClaudeAgentInstrumentorInitializer(),
|
|
72
|
+
Instruments.COHERE: initializers.CohereInstrumentorInitializer(),
|
|
73
|
+
Instruments.CREWAI: initializers.CrewAIInstrumentorInitializer(),
|
|
74
|
+
Instruments.CUA_AGENT: initializers.CuaAgentInstrumentorInitializer(),
|
|
75
|
+
Instruments.CUA_COMPUTER: initializers.CuaComputerInstrumentorInitializer(),
|
|
76
|
+
Instruments.GOOGLE_GENAI: initializers.GoogleGenAIInstrumentorInitializer(),
|
|
77
|
+
Instruments.GROQ: initializers.GroqInstrumentorInitializer(),
|
|
78
|
+
Instruments.HAYSTACK: initializers.HaystackInstrumentorInitializer(),
|
|
79
|
+
Instruments.KERNEL: initializers.KernelInstrumentorInitializer(),
|
|
80
|
+
Instruments.LANCEDB: initializers.LanceDBInstrumentorInitializer(),
|
|
81
|
+
Instruments.LANGCHAIN: initializers.LangchainInstrumentorInitializer(),
|
|
82
|
+
Instruments.LANGGRAPH: initializers.LanggraphInstrumentorInitializer(),
|
|
83
|
+
Instruments.LLAMA_INDEX: initializers.LlamaIndexInstrumentorInitializer(),
|
|
84
|
+
Instruments.MARQO: initializers.MarqoInstrumentorInitializer(),
|
|
85
|
+
Instruments.MCP: initializers.MCPInstrumentorInitializer(),
|
|
86
|
+
Instruments.MILVUS: initializers.MilvusInstrumentorInitializer(),
|
|
87
|
+
Instruments.MISTRAL: initializers.MistralInstrumentorInitializer(),
|
|
88
|
+
Instruments.OLLAMA: initializers.OllamaInstrumentorInitializer(),
|
|
89
|
+
Instruments.OPENAI: initializers.OpenAIInstrumentorInitializer(),
|
|
90
|
+
Instruments.OPENHANDS: initializers.OpenHandsAIInstrumentorInitializer(),
|
|
91
|
+
Instruments.OPENTELEMETRY: initializers.OpenTelemetryInstrumentorInitializer(),
|
|
92
|
+
Instruments.PATCHRIGHT: initializers.PatchrightInstrumentorInitializer(),
|
|
93
|
+
Instruments.PINECONE: initializers.PineconeInstrumentorInitializer(),
|
|
94
|
+
Instruments.PLAYWRIGHT: initializers.PlaywrightInstrumentorInitializer(),
|
|
95
|
+
Instruments.QDRANT: initializers.QdrantInstrumentorInitializer(),
|
|
96
|
+
Instruments.REPLICATE: initializers.ReplicateInstrumentorInitializer(),
|
|
97
|
+
Instruments.SAGEMAKER: initializers.SageMakerInstrumentorInitializer(),
|
|
98
|
+
Instruments.SKYVERN: initializers.SkyvernInstrumentorInitializer(),
|
|
99
|
+
Instruments.TOGETHER: initializers.TogetherInstrumentorInitializer(),
|
|
100
|
+
Instruments.TRANSFORMERS: initializers.TransformersInstrumentorInitializer(),
|
|
101
|
+
Instruments.VERTEXAI: initializers.VertexAIInstrumentorInitializer(),
|
|
102
|
+
Instruments.WATSONX: initializers.WatsonxInstrumentorInitializer(),
|
|
103
|
+
Instruments.WEAVIATE: initializers.WeaviateInstrumentorInitializer(),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def init_instrumentations(
|
|
108
|
+
tracer_provider: TracerProvider,
|
|
109
|
+
instruments: set[Instruments] | None = None,
|
|
110
|
+
block_instruments: set[Instruments] | None = None,
|
|
111
|
+
async_client: AsyncLaminarClient | None = None,
|
|
112
|
+
):
|
|
113
|
+
block_instruments = block_instruments or set()
|
|
114
|
+
if instruments is None:
|
|
115
|
+
instruments = set(Instruments)
|
|
116
|
+
if not isinstance(instruments, set):
|
|
117
|
+
instruments = set(instruments)
|
|
118
|
+
|
|
119
|
+
# Remove any instruments that were explicitly blocked
|
|
120
|
+
instruments = instruments - block_instruments
|
|
121
|
+
|
|
122
|
+
for instrument in instruments:
|
|
123
|
+
initializer = INSTRUMENTATION_INITIALIZERS.get(instrument)
|
|
124
|
+
if initializer is None:
|
|
125
|
+
module_logger.error(f"Invalid instrument: {instrument}")
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
instrumentor = initializer.init_instrumentor(async_client)
|
|
130
|
+
if instrumentor is None:
|
|
131
|
+
continue
|
|
132
|
+
if not instrumentor.is_instrumented_by_opentelemetry:
|
|
133
|
+
instrumentor.instrument(tracer_provider=tracer_provider)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
if "No module named 'langchain_community'" in str(e):
|
|
136
|
+
# LangChain instrumentor does not require langchain_community,
|
|
137
|
+
# but throws this error if it's not installed.
|
|
138
|
+
continue
|
|
139
|
+
module_logger.error(f"Error initializing instrumentor: {e}")
|
|
140
|
+
continue
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import threading
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
from opentelemetry.sdk.trace.export import (
|
|
6
|
+
SpanProcessor,
|
|
7
|
+
SpanExporter,
|
|
8
|
+
BatchSpanProcessor,
|
|
9
|
+
SimpleSpanProcessor,
|
|
10
|
+
)
|
|
11
|
+
from opentelemetry.sdk.trace import Span
|
|
12
|
+
from opentelemetry.context import Context, get_value
|
|
13
|
+
|
|
14
|
+
from lmnr.opentelemetry_lib.tracing.attributes import (
|
|
15
|
+
ASSOCIATION_PROPERTIES,
|
|
16
|
+
PARENT_SPAN_IDS_PATH,
|
|
17
|
+
PARENT_SPAN_PATH,
|
|
18
|
+
SESSION_ID,
|
|
19
|
+
SPAN_IDS_PATH,
|
|
20
|
+
SPAN_INSTRUMENTATION_SOURCE,
|
|
21
|
+
SPAN_LANGUAGE_VERSION,
|
|
22
|
+
SPAN_PATH,
|
|
23
|
+
SPAN_SDK_VERSION,
|
|
24
|
+
TRACE_TYPE,
|
|
25
|
+
USER_ID,
|
|
26
|
+
)
|
|
27
|
+
from lmnr.opentelemetry_lib.tracing.context import (
|
|
28
|
+
CONTEXT_METADATA_KEY,
|
|
29
|
+
CONTEXT_SESSION_ID_KEY,
|
|
30
|
+
CONTEXT_TRACE_TYPE_KEY,
|
|
31
|
+
CONTEXT_USER_ID_KEY,
|
|
32
|
+
)
|
|
33
|
+
from lmnr.opentelemetry_lib.tracing.exporter import LaminarSpanExporter
|
|
34
|
+
from lmnr.sdk.log import get_default_logger
|
|
35
|
+
from lmnr.sdk.utils import is_otel_attribute_value_type, json_dumps
|
|
36
|
+
from lmnr.version import PYTHON_VERSION, __version__
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LaminarSpanProcessor(SpanProcessor):
|
|
40
|
+
instance: BatchSpanProcessor | SimpleSpanProcessor
|
|
41
|
+
logger: logging.Logger
|
|
42
|
+
__span_id_to_path: dict[int, list[str]] = {}
|
|
43
|
+
__span_id_lists: dict[int, list[str]] = {}
|
|
44
|
+
max_export_batch_size: int
|
|
45
|
+
_instance_lock: threading.RLock
|
|
46
|
+
_paths_lock: threading.RLock
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
base_url: str | None = None,
|
|
51
|
+
port: int | None = None,
|
|
52
|
+
api_key: str | None = None,
|
|
53
|
+
timeout_seconds: int = 30,
|
|
54
|
+
force_http: bool = False,
|
|
55
|
+
max_export_batch_size: int = 64,
|
|
56
|
+
disable_batch: bool = False,
|
|
57
|
+
exporter: SpanExporter | None = None,
|
|
58
|
+
):
|
|
59
|
+
self._instance_lock = threading.RLock()
|
|
60
|
+
self._paths_lock = threading.RLock()
|
|
61
|
+
self.logger = get_default_logger(__name__)
|
|
62
|
+
self.max_export_batch_size = max_export_batch_size
|
|
63
|
+
self.exporter = exporter or LaminarSpanExporter(
|
|
64
|
+
base_url=base_url,
|
|
65
|
+
port=port,
|
|
66
|
+
api_key=api_key,
|
|
67
|
+
timeout_seconds=timeout_seconds,
|
|
68
|
+
force_http=force_http,
|
|
69
|
+
)
|
|
70
|
+
self.instance = (
|
|
71
|
+
SimpleSpanProcessor(self.exporter)
|
|
72
|
+
if disable_batch
|
|
73
|
+
else BatchSpanProcessor(
|
|
74
|
+
self.exporter, max_export_batch_size=max_export_batch_size
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def on_start(self, span: Span, parent_context: Context | None = None):
|
|
79
|
+
with self._paths_lock:
|
|
80
|
+
parent_span_path = list(span.attributes.get(PARENT_SPAN_PATH, tuple())) or (
|
|
81
|
+
self.__span_id_to_path.get(span.parent.span_id) if span.parent else None
|
|
82
|
+
)
|
|
83
|
+
parent_span_ids_path = list(
|
|
84
|
+
span.attributes.get(PARENT_SPAN_IDS_PATH, tuple())
|
|
85
|
+
) or (
|
|
86
|
+
self.__span_id_lists.get(span.parent.span_id, []) if span.parent else []
|
|
87
|
+
)
|
|
88
|
+
span_path = (
|
|
89
|
+
parent_span_path + [span.name] if parent_span_path else [span.name]
|
|
90
|
+
)
|
|
91
|
+
span_ids_path = parent_span_ids_path + [
|
|
92
|
+
str(uuid.UUID(int=span.get_span_context().span_id))
|
|
93
|
+
]
|
|
94
|
+
span.set_attribute(SPAN_PATH, span_path)
|
|
95
|
+
span.set_attribute(SPAN_IDS_PATH, span_ids_path)
|
|
96
|
+
self.__span_id_to_path[span.get_span_context().span_id] = span_path
|
|
97
|
+
self.__span_id_lists[span.get_span_context().span_id] = span_ids_path
|
|
98
|
+
|
|
99
|
+
span.set_attribute(SPAN_INSTRUMENTATION_SOURCE, "python")
|
|
100
|
+
span.set_attribute(SPAN_SDK_VERSION, __version__)
|
|
101
|
+
span.set_attribute(SPAN_LANGUAGE_VERSION, f"python@{PYTHON_VERSION}")
|
|
102
|
+
|
|
103
|
+
if parent_context:
|
|
104
|
+
trace_type = get_value(CONTEXT_TRACE_TYPE_KEY, parent_context)
|
|
105
|
+
if trace_type:
|
|
106
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{TRACE_TYPE}", trace_type)
|
|
107
|
+
user_id = get_value(CONTEXT_USER_ID_KEY, parent_context)
|
|
108
|
+
if user_id:
|
|
109
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{USER_ID}", user_id)
|
|
110
|
+
session_id = get_value(CONTEXT_SESSION_ID_KEY, parent_context)
|
|
111
|
+
if session_id:
|
|
112
|
+
span.set_attribute(f"{ASSOCIATION_PROPERTIES}.{SESSION_ID}", session_id)
|
|
113
|
+
ctx_metadata = get_value(CONTEXT_METADATA_KEY, parent_context)
|
|
114
|
+
if ctx_metadata and isinstance(ctx_metadata, dict):
|
|
115
|
+
span_metadata = {}
|
|
116
|
+
if hasattr(span, "attributes") and hasattr(span.attributes, "items"):
|
|
117
|
+
for key, value in span.attributes.items():
|
|
118
|
+
if key.startswith(f"{ASSOCIATION_PROPERTIES}.metadata."):
|
|
119
|
+
span_metadata[
|
|
120
|
+
key.replace(f"{ASSOCIATION_PROPERTIES}.metadata.", "")
|
|
121
|
+
] = value
|
|
122
|
+
|
|
123
|
+
for key, value in {**ctx_metadata, **span_metadata}.items():
|
|
124
|
+
span.set_attribute(
|
|
125
|
+
f"{ASSOCIATION_PROPERTIES}.metadata.{key}",
|
|
126
|
+
(
|
|
127
|
+
value
|
|
128
|
+
if is_otel_attribute_value_type(value)
|
|
129
|
+
else json_dumps(value)
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if span.name == "LangGraph.workflow":
|
|
134
|
+
graph_context = get_value("lmnr.langgraph.graph") or {}
|
|
135
|
+
for key, value in graph_context.items():
|
|
136
|
+
span.set_attribute(f"lmnr.association.properties.{key}", value)
|
|
137
|
+
|
|
138
|
+
with self._instance_lock:
|
|
139
|
+
self.instance.on_start(span, parent_context)
|
|
140
|
+
|
|
141
|
+
def on_end(self, span: Span):
|
|
142
|
+
with self._instance_lock:
|
|
143
|
+
self.instance.on_end(span)
|
|
144
|
+
|
|
145
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
146
|
+
with self._instance_lock:
|
|
147
|
+
return self.instance.force_flush(timeout_millis)
|
|
148
|
+
|
|
149
|
+
def force_reinit(self):
|
|
150
|
+
if not isinstance(self.exporter, LaminarSpanExporter):
|
|
151
|
+
self.logger.warning(
|
|
152
|
+
"LaminarSpanProcessor is not using LaminarSpanExporter, cannot force reinit"
|
|
153
|
+
)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# Reinitialize exporter (thread-safe, handles its own locking)
|
|
157
|
+
self.exporter._init_instance()
|
|
158
|
+
|
|
159
|
+
with self._instance_lock:
|
|
160
|
+
old_instance = self.instance
|
|
161
|
+
disable_batch = isinstance(old_instance, SimpleSpanProcessor)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
old_instance.shutdown()
|
|
165
|
+
except Exception as e:
|
|
166
|
+
self.logger.debug(f"Error shutting down old processor instance: {e}")
|
|
167
|
+
|
|
168
|
+
self.instance = (
|
|
169
|
+
SimpleSpanProcessor(self.exporter)
|
|
170
|
+
if disable_batch
|
|
171
|
+
else BatchSpanProcessor(
|
|
172
|
+
self.exporter, max_export_batch_size=self.max_export_batch_size
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def shutdown(self):
|
|
177
|
+
with self._instance_lock:
|
|
178
|
+
self.instance.shutdown()
|
|
179
|
+
|
|
180
|
+
def clear(self):
|
|
181
|
+
with self._paths_lock:
|
|
182
|
+
self.__span_id_to_path = {}
|
|
183
|
+
self.__span_id_lists = {}
|
|
184
|
+
|
|
185
|
+
def set_parent_path_info(
|
|
186
|
+
self,
|
|
187
|
+
parent_span_id: int,
|
|
188
|
+
span_path: list[str],
|
|
189
|
+
span_ids_path: list[str],
|
|
190
|
+
):
|
|
191
|
+
with self._paths_lock:
|
|
192
|
+
self.__span_id_to_path[parent_span_id] = span_path
|
|
193
|
+
self.__span_id_lists[parent_span_id] = span_ids_path
|