langwatch 0.2.16__py3-none-any.whl → 0.2.18__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.
- langwatch/__version__.py +1 -1
- langwatch/client.py +366 -114
- langwatch/evaluations.py +2 -2
- langwatch/litellm.py +1 -5
- langwatch/openai.py +6 -19
- langwatch/telemetry/__tests__/test_tracing.py +48 -0
- langwatch/telemetry/context.py +28 -25
- langwatch/telemetry/tracing.py +18 -0
- langwatch/types.py +17 -1
- langwatch/utils/initialization.py +31 -16
- {langwatch-0.2.16.dist-info → langwatch-0.2.18.dist-info}/METADATA +2 -1
- {langwatch-0.2.16.dist-info → langwatch-0.2.18.dist-info}/RECORD +13 -12
- {langwatch-0.2.16.dist-info → langwatch-0.2.18.dist-info}/WHEEL +0 -0
langwatch/__version__.py
CHANGED
langwatch/client.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import atexit
|
|
2
2
|
import os
|
|
3
3
|
import logging
|
|
4
|
-
from typing import List, Optional, Sequence
|
|
4
|
+
from typing import List, Optional, Sequence, ClassVar
|
|
5
5
|
|
|
6
6
|
from langwatch.__version__ import __version__
|
|
7
7
|
from langwatch.attributes import AttributeKey
|
|
@@ -14,7 +14,7 @@ from opentelemetry.sdk.resources import Resource
|
|
|
14
14
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
15
15
|
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ALWAYS_OFF
|
|
16
16
|
from opentelemetry.sdk.trace.export import SpanExporter, SpanExportResult
|
|
17
|
-
from opentelemetry.trace import
|
|
17
|
+
from opentelemetry.sdk.trace import ReadableSpan
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
from .exporters.filterable_batch_span_exporter import FilterableBatchSpanProcessor
|
|
@@ -25,7 +25,7 @@ from .generated.langwatch_rest_api_client import Client as LangWatchApiClient
|
|
|
25
25
|
import opentelemetry.trace
|
|
26
26
|
from opentelemetry.util._once import Once
|
|
27
27
|
|
|
28
|
-
logger = logging.getLogger(__name__)
|
|
28
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class Client(LangWatchClientProtocol):
|
|
@@ -33,17 +33,47 @@ class Client(LangWatchClientProtocol):
|
|
|
33
33
|
Client for the LangWatch tracing SDK.
|
|
34
34
|
"""
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
# Class variables - shared across all instances
|
|
37
|
+
_instance: ClassVar[Optional["Client"]] = None
|
|
38
|
+
_debug: ClassVar[bool] = False
|
|
39
|
+
_api_key: ClassVar[str] = ""
|
|
40
|
+
_endpoint_url: ClassVar[str] = ""
|
|
41
|
+
_base_attributes: ClassVar[BaseAttributes] = {}
|
|
42
|
+
_instrumentors: ClassVar[Sequence[BaseInstrumentor]] = ()
|
|
43
|
+
_disable_sending: ClassVar[bool] = False
|
|
44
|
+
_flush_on_exit: ClassVar[bool] = True
|
|
45
|
+
_span_exclude_rules: ClassVar[List[SpanProcessingExcludeRule]] = [] # type: ignore[misc]
|
|
46
|
+
_ignore_global_tracer_provider_override_warning: ClassVar[bool] = False
|
|
47
|
+
_skip_open_telemetry_setup: ClassVar[bool] = False
|
|
48
|
+
_tracer_provider: ClassVar[Optional[TracerProvider]] = None
|
|
49
|
+
_rest_api_client: ClassVar[Optional[LangWatchApiClient]] = None
|
|
50
|
+
_registered_instrumentors: ClassVar[
|
|
51
|
+
dict[opentelemetry.trace.TracerProvider, set[BaseInstrumentor]]
|
|
52
|
+
] = {}
|
|
53
|
+
|
|
54
|
+
# Regular attributes for protocol compatibility
|
|
55
|
+
base_attributes: BaseAttributes
|
|
56
|
+
tracer_provider: Optional[TracerProvider]
|
|
57
|
+
instrumentors: Sequence[BaseInstrumentor]
|
|
58
|
+
|
|
59
|
+
def __new__(
|
|
60
|
+
cls,
|
|
61
|
+
api_key: Optional[str] = None,
|
|
62
|
+
endpoint_url: Optional[str] = None,
|
|
63
|
+
base_attributes: Optional[BaseAttributes] = None,
|
|
64
|
+
instrumentors: Optional[Sequence[BaseInstrumentor]] = None,
|
|
65
|
+
tracer_provider: Optional[TracerProvider] = None,
|
|
66
|
+
debug: Optional[bool] = None,
|
|
67
|
+
disable_sending: Optional[bool] = None,
|
|
68
|
+
flush_on_exit: Optional[bool] = None,
|
|
69
|
+
span_exclude_rules: Optional[List[SpanProcessingExcludeRule]] = None,
|
|
70
|
+
ignore_global_tracer_provider_override_warning: Optional[bool] = None,
|
|
71
|
+
skip_open_telemetry_setup: Optional[bool] = None,
|
|
72
|
+
) -> "Client":
|
|
73
|
+
"""Ensure only one instance of Client exists (singleton pattern)."""
|
|
74
|
+
if cls._instance is None:
|
|
75
|
+
cls._instance = super().__new__(cls)
|
|
76
|
+
return cls._instance
|
|
47
77
|
|
|
48
78
|
def __init__(
|
|
49
79
|
self,
|
|
@@ -52,12 +82,12 @@ class Client(LangWatchClientProtocol):
|
|
|
52
82
|
base_attributes: Optional[BaseAttributes] = None,
|
|
53
83
|
instrumentors: Optional[Sequence[BaseInstrumentor]] = None,
|
|
54
84
|
tracer_provider: Optional[TracerProvider] = None,
|
|
55
|
-
debug: bool =
|
|
56
|
-
disable_sending: bool =
|
|
57
|
-
flush_on_exit: bool =
|
|
85
|
+
debug: Optional[bool] = None,
|
|
86
|
+
disable_sending: Optional[bool] = None,
|
|
87
|
+
flush_on_exit: Optional[bool] = None,
|
|
58
88
|
span_exclude_rules: Optional[List[SpanProcessingExcludeRule]] = None,
|
|
59
|
-
ignore_global_tracer_provider_override_warning: bool =
|
|
60
|
-
skip_open_telemetry_setup: bool =
|
|
89
|
+
ignore_global_tracer_provider_override_warning: Optional[bool] = None,
|
|
90
|
+
skip_open_telemetry_setup: Optional[bool] = None,
|
|
61
91
|
):
|
|
62
92
|
"""
|
|
63
93
|
Initialize the LangWatch tracing client.
|
|
@@ -75,87 +105,294 @@ class Client(LangWatchClientProtocol):
|
|
|
75
105
|
skip_open_telemetry_setup: Optional. If True, OpenTelemetry setup will be skipped entirely. This is useful when you want to handle OpenTelemetry setup yourself.
|
|
76
106
|
"""
|
|
77
107
|
|
|
78
|
-
|
|
79
|
-
self
|
|
80
|
-
|
|
81
|
-
or os.getenv("
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
# Check if this instance has already been initialized
|
|
109
|
+
if hasattr(self, "_initialized"):
|
|
110
|
+
# Instance already exists, update it with new parameters
|
|
111
|
+
debug_flag = debug or os.getenv("LANGWATCH_DEBUG") == "true"
|
|
112
|
+
if debug_flag:
|
|
113
|
+
logger.debug("Updating existing LangWatch client instance")
|
|
114
|
+
|
|
115
|
+
# Update via public setters so side-effects run
|
|
116
|
+
if api_key is not None and api_key != self.api_key:
|
|
117
|
+
self.api_key = api_key
|
|
118
|
+
if endpoint_url is not None and endpoint_url != Client._endpoint_url:
|
|
119
|
+
Client._endpoint_url = endpoint_url
|
|
120
|
+
if (
|
|
121
|
+
not Client._skip_open_telemetry_setup
|
|
122
|
+
and not Client._disable_sending
|
|
123
|
+
):
|
|
124
|
+
self.__shutdown_tracer_provider()
|
|
125
|
+
self.__setup_tracer_provider()
|
|
126
|
+
if debug is not None:
|
|
127
|
+
Client._debug = debug
|
|
128
|
+
if (
|
|
129
|
+
disable_sending is not None
|
|
130
|
+
and disable_sending != Client._disable_sending
|
|
131
|
+
):
|
|
132
|
+
self.disable_sending = disable_sending
|
|
133
|
+
if flush_on_exit is not None:
|
|
134
|
+
Client._flush_on_exit = flush_on_exit
|
|
135
|
+
if span_exclude_rules is not None:
|
|
136
|
+
Client._span_exclude_rules = span_exclude_rules
|
|
137
|
+
if ignore_global_tracer_provider_override_warning is not None:
|
|
138
|
+
Client._ignore_global_tracer_provider_override_warning = (
|
|
139
|
+
ignore_global_tracer_provider_override_warning
|
|
140
|
+
)
|
|
141
|
+
if skip_open_telemetry_setup is not None:
|
|
142
|
+
Client._skip_open_telemetry_setup = skip_open_telemetry_setup
|
|
143
|
+
if base_attributes is not None:
|
|
144
|
+
Client._base_attributes = base_attributes
|
|
145
|
+
# Ensure required SDK attributes remain present after reconfiguration
|
|
146
|
+
Client._base_attributes[AttributeKey.LangWatchSDKName] = (
|
|
147
|
+
"langwatch-observability-sdk"
|
|
148
|
+
)
|
|
149
|
+
Client._base_attributes[AttributeKey.LangWatchSDKVersion] = str(
|
|
150
|
+
__version__
|
|
151
|
+
)
|
|
152
|
+
Client._base_attributes[AttributeKey.LangWatchSDKLanguage] = "python"
|
|
153
|
+
if instrumentors is not None:
|
|
154
|
+
Client._instrumentors = instrumentors
|
|
155
|
+
if tracer_provider is not None:
|
|
156
|
+
Client._tracer_provider = tracer_provider
|
|
157
|
+
# Ensure OTEL is configured and instrumentors are registered for the active provider
|
|
158
|
+
if not Client._skip_open_telemetry_setup:
|
|
159
|
+
Client._tracer_provider = self.__ensure_otel_setup(
|
|
160
|
+
Client._tracer_provider
|
|
161
|
+
)
|
|
162
|
+
current_tracer_provider = (
|
|
163
|
+
Client._tracer_provider or trace.get_tracer_provider()
|
|
164
|
+
)
|
|
165
|
+
if current_tracer_provider not in Client._registered_instrumentors:
|
|
166
|
+
Client._registered_instrumentors[current_tracer_provider] = set()
|
|
167
|
+
for instrumentor in Client._instrumentors:
|
|
168
|
+
if (
|
|
169
|
+
instrumentor
|
|
170
|
+
not in Client._registered_instrumentors[current_tracer_provider]
|
|
171
|
+
):
|
|
172
|
+
instrumentor.instrument(tracer_provider=current_tracer_provider)
|
|
173
|
+
Client._registered_instrumentors[current_tracer_provider].add(
|
|
174
|
+
instrumentor
|
|
175
|
+
)
|
|
176
|
+
# Refresh REST client for endpoint/api-key updates
|
|
177
|
+
self._setup_rest_api_client()
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Mark this instance as initialized
|
|
181
|
+
self._initialized = True
|
|
182
|
+
|
|
183
|
+
# Update class variables with provided values or environment defaults
|
|
184
|
+
if api_key is not None:
|
|
185
|
+
Client._api_key = api_key
|
|
186
|
+
elif not Client._api_key:
|
|
187
|
+
Client._api_key = os.getenv("LANGWATCH_API_KEY", "")
|
|
188
|
+
|
|
189
|
+
if endpoint_url is not None:
|
|
190
|
+
Client._endpoint_url = endpoint_url
|
|
191
|
+
elif not Client._endpoint_url:
|
|
192
|
+
Client._endpoint_url = (
|
|
193
|
+
os.getenv("LANGWATCH_ENDPOINT") or "https://app.langwatch.ai"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if debug is not None:
|
|
197
|
+
Client._debug = debug
|
|
198
|
+
elif not Client._debug:
|
|
199
|
+
Client._debug = os.getenv("LANGWATCH_DEBUG") == "true"
|
|
200
|
+
|
|
201
|
+
if disable_sending is not None:
|
|
202
|
+
Client._disable_sending = disable_sending
|
|
203
|
+
|
|
204
|
+
if flush_on_exit is not None:
|
|
205
|
+
Client._flush_on_exit = flush_on_exit
|
|
206
|
+
|
|
207
|
+
if span_exclude_rules is not None:
|
|
208
|
+
Client._span_exclude_rules = span_exclude_rules
|
|
209
|
+
|
|
210
|
+
if ignore_global_tracer_provider_override_warning is not None:
|
|
211
|
+
Client._ignore_global_tracer_provider_override_warning = (
|
|
212
|
+
ignore_global_tracer_provider_override_warning
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if skip_open_telemetry_setup is not None:
|
|
216
|
+
Client._skip_open_telemetry_setup = skip_open_telemetry_setup
|
|
217
|
+
|
|
218
|
+
if base_attributes is not None:
|
|
219
|
+
Client._base_attributes = base_attributes
|
|
220
|
+
elif not Client._base_attributes:
|
|
221
|
+
Client._base_attributes = {}
|
|
222
|
+
|
|
223
|
+
if instrumentors is not None:
|
|
224
|
+
Client._instrumentors = instrumentors
|
|
225
|
+
elif not Client._instrumentors:
|
|
226
|
+
Client._instrumentors = ()
|
|
227
|
+
|
|
228
|
+
if tracer_provider is not None:
|
|
229
|
+
Client._tracer_provider = tracer_provider
|
|
230
|
+
|
|
231
|
+
# Set up base attributes with SDK info
|
|
232
|
+
Client._base_attributes[AttributeKey.LangWatchSDKName] = (
|
|
94
233
|
"langwatch-observability-sdk"
|
|
95
234
|
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if not
|
|
100
|
-
|
|
235
|
+
Client._base_attributes[AttributeKey.LangWatchSDKVersion] = str(__version__)
|
|
236
|
+
Client._base_attributes[AttributeKey.LangWatchSDKLanguage] = "python"
|
|
237
|
+
|
|
238
|
+
# Set up OpenTelemetry if not skipped
|
|
239
|
+
if not Client._skip_open_telemetry_setup:
|
|
240
|
+
Client._tracer_provider = self.__ensure_otel_setup(Client._tracer_provider)
|
|
241
|
+
elif Client._debug:
|
|
242
|
+
logger.debug("Skipping OpenTelemetry setup as requested")
|
|
243
|
+
|
|
244
|
+
# Run instrumentors only if they haven't been registered with the current tracer provider
|
|
245
|
+
current_tracer_provider = Client._tracer_provider or trace.get_tracer_provider()
|
|
246
|
+
if current_tracer_provider not in Client._registered_instrumentors:
|
|
247
|
+
Client._registered_instrumentors[current_tracer_provider] = set()
|
|
248
|
+
|
|
249
|
+
for instrumentor in Client._instrumentors:
|
|
250
|
+
if (
|
|
251
|
+
instrumentor
|
|
252
|
+
not in Client._registered_instrumentors[current_tracer_provider]
|
|
253
|
+
):
|
|
254
|
+
instrumentor.instrument(tracer_provider=current_tracer_provider)
|
|
255
|
+
Client._registered_instrumentors[current_tracer_provider].add(
|
|
256
|
+
instrumentor
|
|
257
|
+
)
|
|
101
258
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if self._debug:
|
|
109
|
-
logger.debug("Skipping OpenTelemetry setup as requested")
|
|
259
|
+
# Initialize instance attributes for protocol compatibility
|
|
260
|
+
self.base_attributes = (
|
|
261
|
+
self._base_attributes.copy() if self._base_attributes else {}
|
|
262
|
+
)
|
|
263
|
+
self.tracer_provider = self._tracer_provider
|
|
264
|
+
self.instrumentors = list(self._instrumentors) if self._instrumentors else []
|
|
110
265
|
|
|
111
266
|
self._setup_rest_api_client()
|
|
112
267
|
|
|
268
|
+
@classmethod
|
|
269
|
+
def _get_instance(cls) -> Optional["Client"]:
|
|
270
|
+
"""Get the singleton instance of the LangWatch client. Internal use only."""
|
|
271
|
+
return cls._instance
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def _create_instance(
|
|
275
|
+
cls,
|
|
276
|
+
api_key: Optional[str] = None,
|
|
277
|
+
endpoint_url: Optional[str] = None,
|
|
278
|
+
base_attributes: Optional[BaseAttributes] = None,
|
|
279
|
+
instrumentors: Optional[Sequence[BaseInstrumentor]] = None,
|
|
280
|
+
tracer_provider: Optional[TracerProvider] = None,
|
|
281
|
+
debug: Optional[bool] = None,
|
|
282
|
+
disable_sending: Optional[bool] = None,
|
|
283
|
+
flush_on_exit: Optional[bool] = True,
|
|
284
|
+
span_exclude_rules: Optional[List[SpanProcessingExcludeRule]] = None,
|
|
285
|
+
ignore_global_tracer_provider_override_warning: Optional[bool] = None,
|
|
286
|
+
skip_open_telemetry_setup: Optional[bool] = None,
|
|
287
|
+
) -> "Client":
|
|
288
|
+
"""Create or get the singleton instance of the LangWatch client. Internal use only."""
|
|
289
|
+
if cls._instance is None:
|
|
290
|
+
cls._instance = cls(
|
|
291
|
+
api_key=api_key,
|
|
292
|
+
endpoint_url=endpoint_url,
|
|
293
|
+
base_attributes=base_attributes,
|
|
294
|
+
instrumentors=instrumentors,
|
|
295
|
+
tracer_provider=tracer_provider,
|
|
296
|
+
debug=debug,
|
|
297
|
+
disable_sending=disable_sending,
|
|
298
|
+
flush_on_exit=flush_on_exit,
|
|
299
|
+
span_exclude_rules=span_exclude_rules,
|
|
300
|
+
ignore_global_tracer_provider_override_warning=ignore_global_tracer_provider_override_warning,
|
|
301
|
+
skip_open_telemetry_setup=skip_open_telemetry_setup,
|
|
302
|
+
)
|
|
303
|
+
return cls._instance
|
|
304
|
+
|
|
305
|
+
@classmethod
|
|
306
|
+
def _reset_instance(cls) -> None:
|
|
307
|
+
"""Reset the singleton instance. Internal use only, primarily for testing."""
|
|
308
|
+
if cls._instance is not None:
|
|
309
|
+
# Shutdown the existing instance if it has a tracer provider
|
|
310
|
+
if (
|
|
311
|
+
hasattr(cls._instance, "tracer_provider")
|
|
312
|
+
and cls._instance.tracer_provider
|
|
313
|
+
):
|
|
314
|
+
cls._instance.__shutdown_tracer_provider()
|
|
315
|
+
cls._instance = None
|
|
316
|
+
|
|
317
|
+
# Reset all class variables to their default values
|
|
318
|
+
cls._debug = False
|
|
319
|
+
cls._api_key = ""
|
|
320
|
+
cls._endpoint_url = ""
|
|
321
|
+
cls._base_attributes = {}
|
|
322
|
+
cls._instrumentors = ()
|
|
323
|
+
cls._disable_sending = False
|
|
324
|
+
cls._flush_on_exit = True
|
|
325
|
+
cls._span_exclude_rules = []
|
|
326
|
+
cls._ignore_global_tracer_provider_override_warning = False
|
|
327
|
+
cls._skip_open_telemetry_setup = False
|
|
328
|
+
cls._tracer_provider = None
|
|
329
|
+
cls._rest_api_client = None
|
|
330
|
+
cls._registered_instrumentors.clear()
|
|
331
|
+
|
|
332
|
+
@classmethod
|
|
333
|
+
def reset_for_testing(cls) -> None:
|
|
334
|
+
"""Reset the singleton instance for testing purposes."""
|
|
335
|
+
cls._reset_instance()
|
|
336
|
+
|
|
337
|
+
@classmethod
|
|
338
|
+
def get_singleton_instance(cls) -> Optional["Client"]:
|
|
339
|
+
"""Get the singleton instance for testing purposes."""
|
|
340
|
+
return cls._instance
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def is_initialized(self) -> bool:
|
|
344
|
+
"""Check if this instance has been initialized for testing purposes."""
|
|
345
|
+
return hasattr(self, "_initialized") and self._initialized
|
|
346
|
+
|
|
113
347
|
@property
|
|
114
348
|
def debug(self) -> bool:
|
|
115
349
|
"""Get the debug flag for the client."""
|
|
116
|
-
return
|
|
350
|
+
return Client._debug
|
|
117
351
|
|
|
118
352
|
@debug.setter
|
|
119
353
|
def debug(self, value: bool) -> None:
|
|
120
354
|
"""Set the debug flag for the client."""
|
|
121
|
-
|
|
355
|
+
Client._debug = value
|
|
122
356
|
|
|
123
357
|
@property
|
|
124
358
|
def endpoint_url(self) -> str:
|
|
125
359
|
"""Get the endpoint URL for the client."""
|
|
126
|
-
return
|
|
360
|
+
return Client._endpoint_url
|
|
127
361
|
|
|
128
362
|
@property
|
|
129
363
|
def flush_on_exit(self) -> bool:
|
|
130
364
|
"""Get the flush on exit flag for the client."""
|
|
131
|
-
return
|
|
365
|
+
return Client._flush_on_exit
|
|
132
366
|
|
|
133
367
|
@property
|
|
134
368
|
def api_key(self) -> str:
|
|
135
369
|
"""Get the API key for the client."""
|
|
136
|
-
return
|
|
370
|
+
return Client._api_key
|
|
137
371
|
|
|
138
372
|
@api_key.setter
|
|
139
373
|
def api_key(self, value: str) -> None:
|
|
140
374
|
"""Set the API key for the client."""
|
|
141
|
-
if value ==
|
|
375
|
+
if value == Client._api_key:
|
|
142
376
|
return
|
|
143
377
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
self._api_key = value
|
|
378
|
+
previous_key = Client._api_key
|
|
379
|
+
Client._api_key = value
|
|
147
380
|
|
|
148
|
-
if
|
|
381
|
+
if previous_key and not Client._skip_open_telemetry_setup:
|
|
149
382
|
# Shut down any existing tracer provider, as API key change requires re-initialization.
|
|
150
383
|
self.__shutdown_tracer_provider()
|
|
151
384
|
|
|
152
385
|
# HACK: set global tracer provider to a proxy tracer provider back
|
|
153
|
-
opentelemetry.trace._TRACER_PROVIDER = None
|
|
154
|
-
opentelemetry.trace._TRACER_PROVIDER_SET_ONCE = Once()
|
|
386
|
+
opentelemetry.trace._TRACER_PROVIDER = None # type: ignore
|
|
387
|
+
opentelemetry.trace._TRACER_PROVIDER_SET_ONCE = Once() # type: ignore
|
|
155
388
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
389
|
+
# Ensure provider/exporter exist after setting the key
|
|
390
|
+
if (
|
|
391
|
+
value
|
|
392
|
+
and not Client._disable_sending
|
|
393
|
+
and not Client._skip_open_telemetry_setup
|
|
394
|
+
):
|
|
395
|
+
self.__setup_tracer_provider()
|
|
159
396
|
|
|
160
397
|
if value:
|
|
161
398
|
self._setup_rest_api_client()
|
|
@@ -163,67 +400,71 @@ class Client(LangWatchClientProtocol):
|
|
|
163
400
|
@property
|
|
164
401
|
def disable_sending(self) -> bool:
|
|
165
402
|
"""Get whether sending is disabled."""
|
|
166
|
-
return
|
|
403
|
+
return Client._disable_sending
|
|
167
404
|
|
|
168
405
|
@property
|
|
169
406
|
def rest_api_client(self) -> LangWatchApiClient:
|
|
170
407
|
"""Get the REST API client for the client."""
|
|
171
|
-
|
|
408
|
+
if Client._rest_api_client is None:
|
|
409
|
+
raise RuntimeError(
|
|
410
|
+
"REST API client not initialized. Call _setup_rest_api_client() first."
|
|
411
|
+
)
|
|
412
|
+
return Client._rest_api_client
|
|
172
413
|
|
|
173
414
|
@property
|
|
174
415
|
def skip_open_telemetry_setup(self) -> bool:
|
|
175
416
|
"""Get whether OpenTelemetry setup is skipped."""
|
|
176
|
-
return
|
|
417
|
+
return Client._skip_open_telemetry_setup
|
|
177
418
|
|
|
178
419
|
@disable_sending.setter
|
|
179
420
|
def disable_sending(self, value: bool) -> None:
|
|
180
|
-
"""Set whether sending is disabled.
|
|
181
|
-
if
|
|
421
|
+
"""Set whether sending is disabled. Spans are still created; the exporter conditionally drops them."""
|
|
422
|
+
if Client._disable_sending == value:
|
|
182
423
|
return
|
|
183
424
|
|
|
184
425
|
# force flush the tracer provider before changing the disable_sending flag
|
|
185
|
-
if
|
|
186
|
-
|
|
426
|
+
if Client._tracer_provider and not Client._skip_open_telemetry_setup:
|
|
427
|
+
Client._tracer_provider.force_flush()
|
|
187
428
|
|
|
188
|
-
|
|
429
|
+
Client._disable_sending = value
|
|
189
430
|
|
|
190
431
|
def __shutdown_tracer_provider(self) -> None:
|
|
191
432
|
"""Shuts down the current tracer provider, including flushing."""
|
|
192
|
-
if self.
|
|
433
|
+
if self._tracer_provider:
|
|
193
434
|
if self._flush_on_exit:
|
|
194
435
|
try:
|
|
195
436
|
# Unregister the atexit hook if it was registered.
|
|
196
|
-
atexit.unregister(self.
|
|
437
|
+
atexit.unregister(self._tracer_provider.force_flush)
|
|
197
438
|
except ValueError:
|
|
198
439
|
pass # Handler was never registered or already unregistered.
|
|
199
440
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
):
|
|
441
|
+
force_flush = getattr(self._tracer_provider, "force_flush", None)
|
|
442
|
+
if callable(force_flush):
|
|
203
443
|
if self._debug:
|
|
204
444
|
logger.debug("Forcing flush of tracer provider before shutdown.")
|
|
205
|
-
|
|
445
|
+
force_flush()
|
|
206
446
|
|
|
207
|
-
if
|
|
447
|
+
if Client._debug:
|
|
208
448
|
logger.debug("Shutting down tracer provider.")
|
|
209
|
-
|
|
210
|
-
|
|
449
|
+
if Client._tracer_provider is not None:
|
|
450
|
+
Client._tracer_provider.shutdown()
|
|
451
|
+
Client._tracer_provider = None
|
|
211
452
|
|
|
212
453
|
def __setup_tracer_provider(self) -> None:
|
|
213
454
|
"""Sets up the tracer provider if not already active."""
|
|
214
|
-
if
|
|
215
|
-
if
|
|
455
|
+
if Client._skip_open_telemetry_setup:
|
|
456
|
+
if Client._debug:
|
|
216
457
|
logger.debug("Skipping tracer provider setup as requested.")
|
|
217
458
|
return
|
|
218
459
|
|
|
219
|
-
if not
|
|
220
|
-
if
|
|
460
|
+
if not Client._tracer_provider:
|
|
461
|
+
if Client._debug:
|
|
221
462
|
logger.debug("Setting up new tracer provider.")
|
|
222
|
-
|
|
463
|
+
Client._tracer_provider = self.__ensure_otel_setup()
|
|
223
464
|
|
|
224
465
|
return
|
|
225
466
|
|
|
226
|
-
if
|
|
467
|
+
if Client._debug:
|
|
227
468
|
logger.debug("Tracer provider already active, not setting up again.")
|
|
228
469
|
|
|
229
470
|
def __ensure_otel_setup(
|
|
@@ -239,34 +480,43 @@ class Client(LangWatchClientProtocol):
|
|
|
239
480
|
trace.set_tracer_provider(settable_tracer_provider)
|
|
240
481
|
return settable_tracer_provider
|
|
241
482
|
|
|
242
|
-
if
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
483
|
+
if isinstance(global_provider, TracerProvider):
|
|
484
|
+
if not self._ignore_global_tracer_provider_override_warning:
|
|
485
|
+
logger.warning(
|
|
486
|
+
"An existing global tracer provider was found. Attaching LangWatch exporter to the existing provider. Set `ignore_global_tracer_provider_override_warning=True` to suppress this warning."
|
|
487
|
+
)
|
|
488
|
+
self.__set_langwatch_exporter(global_provider)
|
|
489
|
+
return global_provider
|
|
490
|
+
else:
|
|
491
|
+
if Client._debug:
|
|
492
|
+
logger.debug(
|
|
493
|
+
"Global tracer provider is not an SDK TracerProvider; creating/using a new provider for LangWatch exporter."
|
|
494
|
+
)
|
|
495
|
+
self.__set_langwatch_exporter(settable_tracer_provider)
|
|
496
|
+
return settable_tracer_provider
|
|
249
497
|
|
|
250
498
|
except Exception as e:
|
|
251
499
|
raise RuntimeError(
|
|
252
|
-
f"Failed to setup OpenTelemetry tracer provider: {
|
|
500
|
+
f"Failed to setup OpenTelemetry tracer provider: {e}"
|
|
253
501
|
) from e
|
|
254
502
|
|
|
255
503
|
def __create_new_tracer_provider(self) -> TracerProvider:
|
|
256
504
|
try:
|
|
257
|
-
resource = Resource.create(
|
|
258
|
-
sampler = ALWAYS_OFF if
|
|
505
|
+
resource = Resource.create(Client._base_attributes)
|
|
506
|
+
sampler = ALWAYS_OFF if Client._disable_sending else TraceIdRatioBased(1.0)
|
|
259
507
|
provider = TracerProvider(resource=resource, sampler=sampler)
|
|
260
508
|
|
|
261
|
-
|
|
509
|
+
# Only set up LangWatch exporter if sending is not disabled
|
|
510
|
+
if not Client._disable_sending:
|
|
511
|
+
self.__set_langwatch_exporter(provider)
|
|
262
512
|
|
|
263
|
-
if
|
|
513
|
+
if Client._flush_on_exit:
|
|
264
514
|
logger.info(
|
|
265
515
|
"Registering atexit handler to flush tracer provider on exit"
|
|
266
516
|
)
|
|
267
517
|
atexit.register(provider.force_flush)
|
|
268
518
|
|
|
269
|
-
if
|
|
519
|
+
if Client._debug:
|
|
270
520
|
logger.info(
|
|
271
521
|
"Successfully configured tracer provider with OTLP exporter"
|
|
272
522
|
)
|
|
@@ -274,25 +524,25 @@ class Client(LangWatchClientProtocol):
|
|
|
274
524
|
return provider
|
|
275
525
|
except Exception as e:
|
|
276
526
|
raise RuntimeError(
|
|
277
|
-
f"Failed to create and configure tracer provider: {
|
|
527
|
+
f"Failed to create and configure tracer provider: {e}"
|
|
278
528
|
) from e
|
|
279
529
|
|
|
280
530
|
def __set_langwatch_exporter(self, provider: TracerProvider) -> None:
|
|
281
|
-
if not
|
|
531
|
+
if not Client._api_key:
|
|
282
532
|
raise ValueError("LangWatch API key is required but not provided")
|
|
283
533
|
|
|
284
534
|
headers = {
|
|
285
|
-
"Authorization": f"Bearer {
|
|
535
|
+
"Authorization": f"Bearer {Client._api_key}",
|
|
286
536
|
"X-LangWatch-SDK-Version": str(__version__),
|
|
287
537
|
}
|
|
288
538
|
|
|
289
|
-
if
|
|
539
|
+
if Client._debug:
|
|
290
540
|
logger.info(
|
|
291
|
-
f"Configuring OTLP exporter with endpoint: {
|
|
541
|
+
f"Configuring OTLP exporter with endpoint: {Client._endpoint_url}/api/otel/v1/traces"
|
|
292
542
|
)
|
|
293
543
|
|
|
294
544
|
otlp_exporter = OTLPSpanExporter(
|
|
295
|
-
endpoint=f"{
|
|
545
|
+
endpoint=f"{Client._endpoint_url}/api/otel/v1/traces",
|
|
296
546
|
headers=headers,
|
|
297
547
|
timeout=int(os.getenv("OTEL_EXPORTER_OTLP_TRACES_TIMEOUT", 30)),
|
|
298
548
|
)
|
|
@@ -304,7 +554,7 @@ class Client(LangWatchClientProtocol):
|
|
|
304
554
|
|
|
305
555
|
processor = FilterableBatchSpanProcessor(
|
|
306
556
|
span_exporter=conditional_exporter,
|
|
307
|
-
exclude_rules=
|
|
557
|
+
exclude_rules=Client._span_exclude_rules,
|
|
308
558
|
max_export_batch_size=int(os.getenv("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", 100)),
|
|
309
559
|
max_queue_size=int(os.getenv("OTEL_BSP_MAX_QUEUE_SIZE", 512)),
|
|
310
560
|
schedule_delay_millis=float(os.getenv("OTEL_BSP_SCHEDULE_DELAY", 1000)),
|
|
@@ -316,20 +566,20 @@ class Client(LangWatchClientProtocol):
|
|
|
316
566
|
"""
|
|
317
567
|
Sets up the REST API client for the client.
|
|
318
568
|
"""
|
|
319
|
-
|
|
320
|
-
base_url=
|
|
321
|
-
headers={"X-Auth-Token":
|
|
569
|
+
Client._rest_api_client = LangWatchApiClient(
|
|
570
|
+
base_url=Client._endpoint_url,
|
|
571
|
+
headers={"X-Auth-Token": Client._api_key},
|
|
322
572
|
raise_on_unexpected_status=True,
|
|
323
573
|
)
|
|
324
574
|
|
|
325
|
-
return
|
|
575
|
+
return Client._rest_api_client
|
|
326
576
|
|
|
327
577
|
|
|
328
578
|
class ConditionalSpanExporter(SpanExporter):
|
|
329
579
|
def __init__(self, wrapped_exporter: SpanExporter):
|
|
330
|
-
self.wrapped_exporter = wrapped_exporter
|
|
580
|
+
self.wrapped_exporter: SpanExporter = wrapped_exporter
|
|
331
581
|
|
|
332
|
-
def export(self, spans: Sequence[
|
|
582
|
+
def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult:
|
|
333
583
|
# Check your singleton's disable flag
|
|
334
584
|
client = get_instance()
|
|
335
585
|
if client and client.disable_sending:
|
|
@@ -342,8 +592,10 @@ class ConditionalSpanExporter(SpanExporter):
|
|
|
342
592
|
def shutdown(self) -> None:
|
|
343
593
|
return self.wrapped_exporter.shutdown()
|
|
344
594
|
|
|
345
|
-
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
595
|
+
def force_flush(self, timeout_millis: Optional[int] = 30000) -> bool:
|
|
346
596
|
client = get_instance()
|
|
347
597
|
if client and client.disable_sending:
|
|
348
598
|
return True # Nothing to flush
|
|
349
|
-
|
|
599
|
+
# Handle None case by providing default value
|
|
600
|
+
actual_timeout = timeout_millis if timeout_millis is not None else 30000
|
|
601
|
+
return self.wrapped_exporter.force_flush(actual_timeout)
|
langwatch/evaluations.py
CHANGED
|
@@ -7,7 +7,7 @@ from deprecated import deprecated
|
|
|
7
7
|
import httpx
|
|
8
8
|
import langwatch
|
|
9
9
|
from langwatch.domain import SpanTimestamps
|
|
10
|
-
import
|
|
10
|
+
from pksuid import PKSUID
|
|
11
11
|
from langwatch.telemetry.span import LangWatchSpan
|
|
12
12
|
from langwatch.telemetry.context import get_current_span
|
|
13
13
|
from langwatch.state import get_api_key, get_endpoint, get_instance
|
|
@@ -424,7 +424,7 @@ def _add_evaluation( # type: ignore
|
|
|
424
424
|
span_id = format(span_ctx.span_id, "x")
|
|
425
425
|
|
|
426
426
|
evaluation = Evaluation(
|
|
427
|
-
evaluation_id=evaluation_id or
|
|
427
|
+
evaluation_id=evaluation_id or str(PKSUID("eval")),
|
|
428
428
|
span_id=span_id,
|
|
429
429
|
name=name,
|
|
430
430
|
type=type,
|
langwatch/litellm.py
CHANGED
|
@@ -14,7 +14,6 @@ from typing import (
|
|
|
14
14
|
from langwatch.tracer import (
|
|
15
15
|
ContextSpan,
|
|
16
16
|
ContextTrace,
|
|
17
|
-
get_current_span,
|
|
18
17
|
get_current_trace,
|
|
19
18
|
)
|
|
20
19
|
|
|
@@ -122,10 +121,7 @@ class LiteLLMPatch:
|
|
|
122
121
|
if not trace or trace not in self.tracked_traces:
|
|
123
122
|
return await cast(Any, self.client)._original_acompletion(*args, **kwargs)
|
|
124
123
|
|
|
125
|
-
span = trace.span(
|
|
126
|
-
type="llm",
|
|
127
|
-
parent=get_current_span(),
|
|
128
|
-
).__enter__()
|
|
124
|
+
span = trace.span(type="llm").__enter__()
|
|
129
125
|
|
|
130
126
|
started_at = milliseconds_timestamp()
|
|
131
127
|
|
langwatch/openai.py
CHANGED
|
@@ -18,7 +18,6 @@ from langwatch.utils.capture import (
|
|
|
18
18
|
capture_chunks_with_timings_and_reyield,
|
|
19
19
|
)
|
|
20
20
|
from langwatch.utils.utils import milliseconds_timestamp, safe_get
|
|
21
|
-
import nanoid
|
|
22
21
|
from langwatch.telemetry.span import LangWatchSpan
|
|
23
22
|
from langwatch.telemetry.tracing import LangWatchTrace
|
|
24
23
|
|
|
@@ -65,9 +64,7 @@ class OpenAITracer:
|
|
|
65
64
|
if trace:
|
|
66
65
|
self.trace = trace
|
|
67
66
|
else:
|
|
68
|
-
self.trace = ContextTrace(
|
|
69
|
-
trace_id=trace_id or nanoid.generate(), metadata=metadata
|
|
70
|
-
)
|
|
67
|
+
self.trace = ContextTrace()
|
|
71
68
|
self.completion_tracer = OpenAICompletionTracer(client=client, trace=self.trace)
|
|
72
69
|
self.chat_completion_tracer = OpenAIChatCompletionTracer(
|
|
73
70
|
client=client, trace=self.trace
|
|
@@ -123,9 +120,7 @@ class OpenAICompletionTracer:
|
|
|
123
120
|
if trace:
|
|
124
121
|
self.trace = trace
|
|
125
122
|
else:
|
|
126
|
-
self.trace = ContextTrace(
|
|
127
|
-
trace_id=trace_id or nanoid.generate(), metadata=metadata
|
|
128
|
-
)
|
|
123
|
+
self.trace = ContextTrace()
|
|
129
124
|
self.tracked_traces.add(self.trace)
|
|
130
125
|
|
|
131
126
|
if not hasattr(self.client.completions, "_original_create"):
|
|
@@ -155,10 +150,7 @@ class OpenAICompletionTracer:
|
|
|
155
150
|
if not trace or trace not in self.tracked_traces:
|
|
156
151
|
return cast(Any, self.client.completions)._original_create(*args, **kwargs)
|
|
157
152
|
|
|
158
|
-
span = trace.span(
|
|
159
|
-
type="llm",
|
|
160
|
-
parent=trace.get_current_span(),
|
|
161
|
-
).__enter__()
|
|
153
|
+
span = trace.span(type="llm").__enter__()
|
|
162
154
|
|
|
163
155
|
started_at = milliseconds_timestamp()
|
|
164
156
|
try:
|
|
@@ -214,10 +206,7 @@ class OpenAICompletionTracer:
|
|
|
214
206
|
*args, **kwargs
|
|
215
207
|
)
|
|
216
208
|
|
|
217
|
-
span = trace.span(
|
|
218
|
-
type="llm",
|
|
219
|
-
parent=trace.get_current_span(),
|
|
220
|
-
).__enter__()
|
|
209
|
+
span = trace.span(type="llm").__enter__()
|
|
221
210
|
|
|
222
211
|
started_at = milliseconds_timestamp()
|
|
223
212
|
response: Union[Completion, AsyncStream[Completion]] = await cast(
|
|
@@ -403,9 +392,7 @@ class OpenAIChatCompletionTracer:
|
|
|
403
392
|
if trace:
|
|
404
393
|
self.trace = trace
|
|
405
394
|
else:
|
|
406
|
-
self.trace = ContextTrace(
|
|
407
|
-
trace_id=trace_id or nanoid.generate(), metadata=metadata
|
|
408
|
-
)
|
|
395
|
+
self.trace = ContextTrace()
|
|
409
396
|
self.tracked_traces.add(self.trace)
|
|
410
397
|
|
|
411
398
|
if not hasattr(self.client.chat.completions, "_original_create"):
|
|
@@ -560,7 +547,7 @@ class OpenAIChatCompletionTracer:
|
|
|
560
547
|
delta = choice.delta
|
|
561
548
|
if delta.role:
|
|
562
549
|
chat_message: ChatMessage = {
|
|
563
|
-
"role": delta.role,
|
|
550
|
+
"role": delta.role, # type: ignore
|
|
564
551
|
"content": delta.content,
|
|
565
552
|
}
|
|
566
553
|
if delta.function_call:
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from langwatch.telemetry.tracing import LangWatchTrace
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_metadata_initialization_and_update_merge():
|
|
6
|
+
# Initial metadata
|
|
7
|
+
trace = LangWatchTrace(metadata={"foo": 1, "bar": 2})
|
|
8
|
+
assert trace.metadata == {"foo": 1, "bar": 2}
|
|
9
|
+
|
|
10
|
+
# Update with new metadata (should merge, not replace)
|
|
11
|
+
trace.update(metadata={"baz": 3, "foo": 42})
|
|
12
|
+
# 'foo' should be overwritten, 'bar' should remain, 'baz' should be added
|
|
13
|
+
assert trace.metadata == {"foo": 42, "bar": 2, "baz": 3}
|
|
14
|
+
|
|
15
|
+
# Update with metadata=None (should not clear metadata)
|
|
16
|
+
trace.update(metadata=None)
|
|
17
|
+
assert trace.metadata == {"foo": 42, "bar": 2, "baz": 3}
|
|
18
|
+
|
|
19
|
+
# Update with another dict
|
|
20
|
+
trace.update(metadata={"new": "val"})
|
|
21
|
+
assert trace.metadata == {"foo": 42, "bar": 2, "baz": 3, "new": "val"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_metadata_on_root_span():
|
|
25
|
+
# Create trace with metadata and root span
|
|
26
|
+
trace = LangWatchTrace(metadata={"a": 1, "b": 2})
|
|
27
|
+
with trace:
|
|
28
|
+
trace.update(metadata={"b": 3, "c": 4})
|
|
29
|
+
# The root span's metadata attribute should match the merged metadata
|
|
30
|
+
root_metadata = trace.root_span._span.attributes.get("metadata")
|
|
31
|
+
import json
|
|
32
|
+
|
|
33
|
+
assert json.loads(root_metadata) == {"a": 1, "b": 3, "c": 4}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_metadata_not_lost_on_multiple_updates():
|
|
37
|
+
trace = LangWatchTrace(metadata={"x": 1})
|
|
38
|
+
trace.update(metadata={"y": 2})
|
|
39
|
+
trace.update(metadata={"z": 3})
|
|
40
|
+
assert trace.metadata == {"x": 1, "y": 2, "z": 3}
|
|
41
|
+
|
|
42
|
+
# Overwrite a key
|
|
43
|
+
trace.update(metadata={"y": 99})
|
|
44
|
+
assert trace.metadata == {"x": 1, "y": 99, "z": 3}
|
|
45
|
+
|
|
46
|
+
# None update should not clear
|
|
47
|
+
trace.update(metadata=None)
|
|
48
|
+
assert trace.metadata == {"x": 1, "y": 99, "z": 3}
|
langwatch/telemetry/context.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import threading
|
|
2
|
-
from typing import TYPE_CHECKING, List
|
|
2
|
+
from typing import TYPE_CHECKING, Dict, List
|
|
3
3
|
import contextvars
|
|
4
4
|
import warnings
|
|
5
5
|
from opentelemetry import trace as trace_api
|
|
6
|
+
from opentelemetry import context
|
|
6
7
|
|
|
7
8
|
from langwatch.utils.initialization import ensure_setup
|
|
8
9
|
|
|
@@ -44,7 +45,19 @@ def get_current_trace(
|
|
|
44
45
|
|
|
45
46
|
trace = LangWatchTrace()
|
|
46
47
|
if start_if_none:
|
|
47
|
-
|
|
48
|
+
otel_span = trace_api.get_current_span()
|
|
49
|
+
otel_span_id = otel_span.get_span_context().span_id
|
|
50
|
+
trace.__enter__()
|
|
51
|
+
|
|
52
|
+
# Keep the previous span in the context if we are starting a new trace not at root level
|
|
53
|
+
if otel_span_id != 0:
|
|
54
|
+
ctx = trace_api.set_span_in_context(otel_span)
|
|
55
|
+
context.attach(ctx)
|
|
56
|
+
otel_span = trace_api.get_current_span()
|
|
57
|
+
|
|
58
|
+
trace.__exit__(None, None, None)
|
|
59
|
+
|
|
60
|
+
return trace
|
|
48
61
|
return trace
|
|
49
62
|
|
|
50
63
|
|
|
@@ -57,20 +70,20 @@ def get_current_span() -> "LangWatchSpan":
|
|
|
57
70
|
"""
|
|
58
71
|
ensure_setup()
|
|
59
72
|
|
|
60
|
-
# First try getting from LangWatch context
|
|
61
|
-
span = stored_langwatch_span.get(None)
|
|
62
|
-
if span is not None:
|
|
63
|
-
return span
|
|
64
|
-
|
|
65
|
-
if _is_on_child_thread() and len(main_thread_langwatch_span) > 0:
|
|
66
|
-
return main_thread_langwatch_span[-1]
|
|
67
|
-
|
|
68
|
-
# Fall back to OpenTelemetry context
|
|
69
73
|
otel_span = trace_api.get_current_span()
|
|
70
|
-
|
|
74
|
+
otel_span_id = otel_span.get_span_context().span_id
|
|
75
|
+
|
|
76
|
+
# If on a child thread and there is no parent, try to find a parent from the main thread
|
|
77
|
+
if (
|
|
78
|
+
_is_on_child_thread()
|
|
79
|
+
and len(main_thread_langwatch_span) > 0
|
|
80
|
+
and otel_span_id == 0
|
|
81
|
+
):
|
|
82
|
+
return main_thread_langwatch_span[-1]
|
|
71
83
|
|
|
72
84
|
from langwatch.telemetry.span import LangWatchSpan
|
|
73
85
|
|
|
86
|
+
trace = get_current_trace()
|
|
74
87
|
return LangWatchSpan.wrap_otel_span(otel_span, trace)
|
|
75
88
|
|
|
76
89
|
|
|
@@ -91,11 +104,8 @@ def _set_current_span(span: "LangWatchSpan"):
|
|
|
91
104
|
if not _is_on_child_thread():
|
|
92
105
|
main_thread_langwatch_span.append(span)
|
|
93
106
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
except Exception as e:
|
|
97
|
-
warnings.warn(f"Failed to set LangWatch span context: {e}")
|
|
98
|
-
return None
|
|
107
|
+
# Dummy token, just for the main thread span list
|
|
108
|
+
return "token"
|
|
99
109
|
|
|
100
110
|
|
|
101
111
|
def _reset_current_trace(token: contextvars.Token):
|
|
@@ -112,19 +122,12 @@ def _reset_current_trace(token: contextvars.Token):
|
|
|
112
122
|
warnings.warn(f"Failed to reset LangWatch trace context: {e}")
|
|
113
123
|
|
|
114
124
|
|
|
115
|
-
def _reset_current_span(
|
|
125
|
+
def _reset_current_span(_token: str):
|
|
116
126
|
global main_thread_langwatch_span
|
|
117
127
|
if not _is_on_child_thread():
|
|
118
128
|
if len(main_thread_langwatch_span) > 0:
|
|
119
129
|
main_thread_langwatch_span.pop()
|
|
120
130
|
|
|
121
|
-
try:
|
|
122
|
-
stored_langwatch_span.reset(token)
|
|
123
|
-
except Exception as e:
|
|
124
|
-
# Only warn if it's not a context error
|
|
125
|
-
if "different Context" not in str(e):
|
|
126
|
-
warnings.warn(f"Failed to reset LangWatch span context: {e}")
|
|
127
|
-
|
|
128
131
|
|
|
129
132
|
def _is_on_child_thread() -> bool:
|
|
130
133
|
return threading.current_thread() != threading.main_thread()
|
langwatch/telemetry/tracing.py
CHANGED
|
@@ -12,6 +12,7 @@ from langwatch.utils.transformation import (
|
|
|
12
12
|
)
|
|
13
13
|
from opentelemetry import trace as trace_api
|
|
14
14
|
from opentelemetry.sdk.trace import TracerProvider
|
|
15
|
+
from opentelemetry.trace import Link
|
|
15
16
|
from typing import (
|
|
16
17
|
Dict,
|
|
17
18
|
List,
|
|
@@ -214,6 +215,19 @@ class LangWatchTrace:
|
|
|
214
215
|
root_span_params["timestamps"], cls=SerializableWithStringFallback
|
|
215
216
|
)
|
|
216
217
|
|
|
218
|
+
# Check if we're creating this trace within an existing OpenTelemetry trace context
|
|
219
|
+
current_span = trace_api.get_current_span()
|
|
220
|
+
if current_span.get_span_context().is_valid:
|
|
221
|
+
# Add link to current span
|
|
222
|
+
links = root_span_params.get("links", [])
|
|
223
|
+
links.append(
|
|
224
|
+
Link(
|
|
225
|
+
current_span.get_span_context(),
|
|
226
|
+
{"relationship": "parent_trace"},
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
root_span_params["links"] = links
|
|
230
|
+
|
|
217
231
|
self.root_span = LangWatchSpan(trace=self, **root_span_params)
|
|
218
232
|
self.root_span.__enter__()
|
|
219
233
|
context = self.root_span.get_span_context()
|
|
@@ -477,6 +491,7 @@ class LangWatchTrace:
|
|
|
477
491
|
|
|
478
492
|
@functools.wraps(func)
|
|
479
493
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
494
|
+
# Always create a new trace, but it will be nested if there's already a current trace
|
|
480
495
|
async with self._clone() as trace:
|
|
481
496
|
trace._set_callee_input_information(func, *args, **kwargs)
|
|
482
497
|
items = []
|
|
@@ -496,6 +511,7 @@ class LangWatchTrace:
|
|
|
496
511
|
|
|
497
512
|
@functools.wraps(func)
|
|
498
513
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
514
|
+
# Always create a new trace, but it will be nested if there's already a current trace
|
|
499
515
|
with self._clone() as trace:
|
|
500
516
|
trace._set_callee_input_information(func, *args, **kwargs)
|
|
501
517
|
items = []
|
|
@@ -515,6 +531,7 @@ class LangWatchTrace:
|
|
|
515
531
|
|
|
516
532
|
@functools.wraps(func)
|
|
517
533
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
534
|
+
# Always create a new trace, but it will be nested if there's already a current trace
|
|
518
535
|
async with self._clone() as trace:
|
|
519
536
|
trace._set_callee_input_information(func, *args, **kwargs)
|
|
520
537
|
output = await func(*args, **kwargs)
|
|
@@ -526,6 +543,7 @@ class LangWatchTrace:
|
|
|
526
543
|
|
|
527
544
|
@functools.wraps(func)
|
|
528
545
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
546
|
+
# Create new trace
|
|
529
547
|
with self._clone() as trace:
|
|
530
548
|
trace._set_callee_input_information(func, *args, **kwargs)
|
|
531
549
|
output = func(*args, **kwargs)
|
langwatch/types.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Common types and protocols for LangWatch."""
|
|
2
2
|
|
|
3
|
-
from typing import Protocol
|
|
3
|
+
from typing import Protocol, Optional, Sequence, List
|
|
4
4
|
from langwatch.domain import (
|
|
5
5
|
BaseAttributes,
|
|
6
6
|
AttributeValue,
|
|
@@ -28,11 +28,16 @@ from langwatch.domain import (
|
|
|
28
28
|
SpanInputOutput,
|
|
29
29
|
SpanTimestamps,
|
|
30
30
|
TraceMetadata,
|
|
31
|
+
SpanProcessingExcludeRule,
|
|
31
32
|
)
|
|
32
33
|
from .generated.langwatch_rest_api_client import Client as LangWatchRestApiClient
|
|
34
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
35
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
36
|
+
|
|
33
37
|
|
|
34
38
|
class LangWatchClientProtocol(Protocol):
|
|
35
39
|
"""Protocol defining the required interface for LangWatch client instances."""
|
|
40
|
+
|
|
36
41
|
@property
|
|
37
42
|
def endpoint_url(self) -> str:
|
|
38
43
|
"""Get the endpoint URL for the client."""
|
|
@@ -79,6 +84,17 @@ class LangWatchClientProtocol(Protocol):
|
|
|
79
84
|
"""Get the REST API client for the client."""
|
|
80
85
|
...
|
|
81
86
|
|
|
87
|
+
@property
|
|
88
|
+
def skip_open_telemetry_setup(self) -> bool:
|
|
89
|
+
"""Get whether OpenTelemetry setup is skipped."""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
# Regular attributes (not properties)
|
|
93
|
+
base_attributes: BaseAttributes
|
|
94
|
+
tracer_provider: Optional[TracerProvider]
|
|
95
|
+
instrumentors: Sequence[BaseInstrumentor]
|
|
96
|
+
_span_exclude_rules: List[SpanProcessingExcludeRule]
|
|
97
|
+
|
|
82
98
|
|
|
83
99
|
__all__ = [
|
|
84
100
|
"BaseAttributes",
|
|
@@ -11,14 +11,17 @@ from langwatch.state import get_instance, set_instance
|
|
|
11
11
|
from langwatch.client import Client
|
|
12
12
|
from langwatch.domain import BaseAttributes, SpanProcessingExcludeRule
|
|
13
13
|
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
14
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
15
16
|
|
|
16
17
|
def _setup_logging(debug: bool = False) -> None:
|
|
17
18
|
"""Configure logging for LangWatch."""
|
|
18
19
|
root_logger = logging.getLogger("langwatch")
|
|
19
20
|
if not root_logger.handlers:
|
|
20
21
|
handler = logging.StreamHandler(sys.stdout)
|
|
21
|
-
formatter = logging.Formatter(
|
|
22
|
+
formatter = logging.Formatter(
|
|
23
|
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
24
|
+
)
|
|
22
25
|
handler.setFormatter(formatter)
|
|
23
26
|
root_logger.addHandler(handler)
|
|
24
27
|
|
|
@@ -27,14 +30,16 @@ def _setup_logging(debug: bool = False) -> None:
|
|
|
27
30
|
else:
|
|
28
31
|
root_logger.setLevel(logging.INFO)
|
|
29
32
|
|
|
33
|
+
|
|
30
34
|
def setup(
|
|
31
35
|
api_key: Optional[str] = None,
|
|
32
36
|
endpoint_url: Optional[str] = None,
|
|
33
37
|
base_attributes: Optional[BaseAttributes] = None,
|
|
34
38
|
tracer_provider: Optional[TracerProvider] = None,
|
|
35
39
|
instrumentors: Optional[Sequence[BaseInstrumentor]] = None,
|
|
36
|
-
span_exclude_rules: Optional[List[SpanProcessingExcludeRule]] =
|
|
37
|
-
debug: bool =
|
|
40
|
+
span_exclude_rules: Optional[List[SpanProcessingExcludeRule]] = None,
|
|
41
|
+
debug: Optional[bool] = None,
|
|
42
|
+
skip_open_telemetry_setup: Optional[bool] = None,
|
|
38
43
|
) -> Client:
|
|
39
44
|
"""
|
|
40
45
|
Initialize the LangWatch client.
|
|
@@ -47,23 +52,30 @@ def setup(
|
|
|
47
52
|
instrumentors: The instrumentors for the LangWatch client.
|
|
48
53
|
span_exclude_rules: Optional. A list of rules that will be applied to spans processed by the exporter.
|
|
49
54
|
debug: Whether to enable debug logging for the LangWatch client.
|
|
50
|
-
|
|
55
|
+
skip_open_telemetry_setup: Whether to skip setting up the OpenTelemetry tracer provider. If this is skipped, instrumentors will be added to the global tracer provider.
|
|
51
56
|
Returns:
|
|
52
57
|
The LangWatch client.
|
|
53
58
|
"""
|
|
54
|
-
_setup_logging(debug)
|
|
59
|
+
_setup_logging(debug or False)
|
|
55
60
|
|
|
56
61
|
if debug:
|
|
57
62
|
logger.info("Setting up LangWatch client...")
|
|
58
63
|
|
|
59
|
-
#
|
|
64
|
+
# Get existing client to check if we're changing the API key
|
|
65
|
+
existing_client = get_instance()
|
|
60
66
|
changed_api_key = False
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
existing_client is not None
|
|
70
|
+
and api_key is not None
|
|
71
|
+
and api_key != existing_client.api_key
|
|
72
|
+
):
|
|
73
|
+
logger.warning(
|
|
74
|
+
"LangWatch was already setup before, and now it is being setup with a new API key. This will nuke the previous tracing providers. This is not recommended."
|
|
75
|
+
)
|
|
65
76
|
changed_api_key = True
|
|
66
77
|
|
|
78
|
+
# Create or update the client (singleton pattern handles the rest)
|
|
67
79
|
client = Client(
|
|
68
80
|
api_key=api_key,
|
|
69
81
|
endpoint_url=endpoint_url,
|
|
@@ -73,14 +85,17 @@ def setup(
|
|
|
73
85
|
debug=debug,
|
|
74
86
|
span_exclude_rules=span_exclude_rules,
|
|
75
87
|
ignore_global_tracer_provider_override_warning=changed_api_key,
|
|
88
|
+
skip_open_telemetry_setup=skip_open_telemetry_setup,
|
|
76
89
|
)
|
|
77
90
|
|
|
78
91
|
if debug:
|
|
79
92
|
logger.info("LangWatch client setup complete")
|
|
80
93
|
|
|
94
|
+
# Update the state module to track the instance
|
|
81
95
|
set_instance(client)
|
|
82
96
|
return client
|
|
83
97
|
|
|
98
|
+
|
|
84
99
|
def ensure_setup(api_key: Optional[str] = None) -> None:
|
|
85
100
|
"""Ensure LangWatch client is setup.
|
|
86
101
|
|
|
@@ -90,13 +105,13 @@ def ensure_setup(api_key: Optional[str] = None) -> None:
|
|
|
90
105
|
client = get_instance()
|
|
91
106
|
if client is None:
|
|
92
107
|
logger.debug("No LangWatch client found, creating default client")
|
|
93
|
-
client = setup(
|
|
108
|
+
client = setup(
|
|
109
|
+
debug=True,
|
|
110
|
+
api_key=api_key,
|
|
111
|
+
) # Enable debug logging for auto-created clients
|
|
94
112
|
|
|
95
113
|
# Verify we have a valid tracer provider
|
|
96
114
|
tracer_provider = trace.get_tracer_provider()
|
|
97
|
-
if tracer_provider
|
|
98
|
-
logger.warning("No tracer provider found, creating new one")
|
|
99
|
-
client = setup(debug=True, api_key=api_key)
|
|
100
|
-
elif isinstance(tracer_provider, trace.ProxyTracerProvider):
|
|
115
|
+
if isinstance(tracer_provider, trace.ProxyTracerProvider):
|
|
101
116
|
logger.debug("Found proxy tracer provider, will be replaced with real provider")
|
|
102
117
|
# This is fine - the client will replace it with a real provider
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langwatch
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.18
|
|
4
4
|
Summary: LangWatch Python SDK, for monitoring your LLMs
|
|
5
5
|
Author-email: Langwatch Engineers <engineering@langwatch.ai>
|
|
6
6
|
License: MIT
|
|
@@ -28,6 +28,7 @@ Requires-Dist: opentelemetry-api>=1.32.1
|
|
|
28
28
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.32.1
|
|
29
29
|
Requires-Dist: opentelemetry-instrumentation-crewai>=0.45.0
|
|
30
30
|
Requires-Dist: opentelemetry-sdk>=1.32.1
|
|
31
|
+
Requires-Dist: pksuid>=1.1.2
|
|
31
32
|
Requires-Dist: pydantic>=2
|
|
32
33
|
Requires-Dist: python-liquid>=2.0.2
|
|
33
34
|
Requires-Dist: retry>=0.9.2
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
langwatch/__init__.py,sha256=OX8vN2-VFNUqRo9mJC8LUjHGMKYSRTwmzUQj_sKAVXQ,4210
|
|
2
|
-
langwatch/__version__.py,sha256=
|
|
2
|
+
langwatch/__version__.py,sha256=HHZMUDcvD8rFyW1uyw1zA9mutRi3pSZ0g7D2KwclJ6A,65
|
|
3
3
|
langwatch/attributes.py,sha256=nXdI_G85wQQCAdAcwjCiLYdEYj3wATmfgCmhlf6dVIk,3910
|
|
4
4
|
langwatch/batch_evaluation.py,sha256=piez7TYqUZPb9NlIShTuTPmSzrZqX-vm2Grz_NGXe04,16078
|
|
5
|
-
langwatch/client.py,sha256=
|
|
6
|
-
langwatch/evaluations.py,sha256=
|
|
5
|
+
langwatch/client.py,sha256=WTNcYSik7kZ2kH-qGDnhbMTosc8e_Xhab_lZlfh5TC8,25559
|
|
6
|
+
langwatch/evaluations.py,sha256=6JYaChMoG2oVvHRtZEVzn2xNxQIZr1nUFfHffm6Rfp0,16832
|
|
7
7
|
langwatch/guardrails.py,sha256=4d320HyklXPUVszF34aWsDKGzuvPggcDM_f45_eJTnc,1352
|
|
8
8
|
langwatch/langchain.py,sha256=nUuV5puASRs66kSW05i9tCRy09st5m_6uy9FYR672Zg,18658
|
|
9
|
-
langwatch/litellm.py,sha256=
|
|
9
|
+
langwatch/litellm.py,sha256=mPcw5cLykt0SQf9bTNSoT7elMx4gj-wZ_K2PC14Bw50,11998
|
|
10
10
|
langwatch/login.py,sha256=Wm8Vs7zxJNeGEPO7s_wSQN2DYObteDg56HcyluhTobw,896
|
|
11
|
-
langwatch/openai.py,sha256=
|
|
11
|
+
langwatch/openai.py,sha256=h_NCIwJ0qs57PS-R7gQZsnf2_EBAahlYQMuqS9-Cj3Q,25139
|
|
12
12
|
langwatch/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
langwatch/state.py,sha256=qXvPAjO90jdokCU6tPSwjHIac4QU_5N0pSd9dfmc9kY,1204
|
|
14
14
|
langwatch/tracer.py,sha256=t5FOdP1es9H_pPGqGUBLXCyEln0tTi4m4M9b6WxCrPU,975
|
|
15
|
-
langwatch/types.py,sha256=
|
|
15
|
+
langwatch/types.py,sha256=h6r3tNTzWqENx-9j_JPmOMZfFoKq9SNpEtxpAACk2G0,3114
|
|
16
16
|
langwatch/dataset/__init__.py,sha256=hZBcbjXuBO2qE5osJtd9wIE9f45F6-jpNTrne5nk4eE,2606
|
|
17
17
|
langwatch/domain/__init__.py,sha256=luuin3-6mmMqDaOJE_1wb7M1ccjhhZL80UN0TBA_TXM,6040
|
|
18
18
|
langwatch/dspy/__init__.py,sha256=E9rqyhOyIO3E-GxJSZlHtsvjapFjKQGF85QRcpKSvKE,34280
|
|
@@ -396,18 +396,19 @@ langwatch/prompts/service.py,sha256=CiVuW1y8sYti05jaYSNk40miCg2Q0bA2FSC3iVoM5_o,
|
|
|
396
396
|
langwatch/prompts/types.py,sha256=0SPb-jfcndvay53PMpuHvtCrIptT-h-eo-9EHM-grl4,654
|
|
397
397
|
langwatch/prompts/decorators/prompt_service_tracing.py,sha256=ELrQ9bd3nPurbUXX2K_CN-2zWE00nqjXObs9Gkp4eVg,2309
|
|
398
398
|
langwatch/prompts/decorators/prompt_tracing.py,sha256=Q3-RM8G-PHkBjVKntSg3c8H2o0P1bpO3UbR3uRgbke0,3182
|
|
399
|
-
langwatch/telemetry/context.py,sha256=
|
|
399
|
+
langwatch/telemetry/context.py,sha256=q0hUG9PM3aifIr6ZRuuNNbsGtcAImu9Pv2XTKUp3CGc,4029
|
|
400
400
|
langwatch/telemetry/sampling.py,sha256=XDf6ZoXiwpHaHDYd_dDszSqH8_9-CHFNsGAZWOW1VYk,1327
|
|
401
401
|
langwatch/telemetry/span.py,sha256=g-RGWfQk4Q3b2TpipiHqjEV7rwmidaUHp54q51UxQ6s,32801
|
|
402
|
-
langwatch/telemetry/tracing.py,sha256=
|
|
402
|
+
langwatch/telemetry/tracing.py,sha256=oyCAqW-9sFFRYPWy9epZVN0aNvqToRY4_PGxQAtS-dI,27622
|
|
403
403
|
langwatch/telemetry/types.py,sha256=Q9H7nT3GMK1aluRB7CCX8BR7VFKrQY_vdFdyF4Yc98U,501
|
|
404
|
+
langwatch/telemetry/__tests__/test_tracing.py,sha256=mD_SAO-dD5m81EVQ909AwGROnnofxuL3DXCoaUTrS3M,1710
|
|
404
405
|
langwatch/utils/__init__.py,sha256=3rqQTgzEtmICJW_KSPuLa5q8p5udxt5SRi28Z2vZB10,138
|
|
405
406
|
langwatch/utils/capture.py,sha256=uVKPqHCm-o8CpabsUfhqbNFr5sgUHzcKnBadvL2oIwI,1172
|
|
406
407
|
langwatch/utils/exceptions.py,sha256=J2_0EZ_GMRTJvCQ-ULX4LOG63r1R-0TCbKg9sskgl5A,498
|
|
407
|
-
langwatch/utils/initialization.py,sha256=
|
|
408
|
+
langwatch/utils/initialization.py,sha256=ReDpEtjenCoRfc02YuIBK8KVjAwXx3N8yf9dyPgTOP0,4378
|
|
408
409
|
langwatch/utils/module.py,sha256=KLBNOK3mA9gCSifCcQX_lOtU48BJQDWvFKtF6NMvwVA,688
|
|
409
410
|
langwatch/utils/transformation.py,sha256=5XUnW7Oz8Ck9EMsKeKeoDOrIw3EXpLGMk_fMSeA0Zng,7216
|
|
410
411
|
langwatch/utils/utils.py,sha256=ZCOSie4o9LdJ7odshNfCNjmgwgQ27ojc5ENqt1rXuSs,596
|
|
411
|
-
langwatch-0.2.
|
|
412
|
-
langwatch-0.2.
|
|
413
|
-
langwatch-0.2.
|
|
412
|
+
langwatch-0.2.18.dist-info/METADATA,sha256=23XS5hHgnh3_4eEXv3v6rX7Yj6uxfBdfbjm5nLnvpD4,13124
|
|
413
|
+
langwatch-0.2.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
414
|
+
langwatch-0.2.18.dist-info/RECORD,,
|
|
File without changes
|