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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for LangWatch."""
2
2
 
3
- __version__ = "0.2.16"
3
+ __version__ = "0.2.18"
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 Span
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
- _debug: bool = False
37
- _api_key: str
38
- _endpoint_url: str
39
- instrumentors: Sequence[BaseInstrumentor] = []
40
- base_attributes: BaseAttributes = {}
41
- _disable_sending: bool = False
42
- _flush_on_exit: bool = True
43
- _span_exclude_rules: List[SpanProcessingExcludeRule] = []
44
- _ignore_global_tracer_provider_override_warning: bool = False
45
- _skip_open_telemetry_setup: bool = False
46
- _rest_api_client: LangWatchApiClient
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 = False,
56
- disable_sending: bool = False,
57
- flush_on_exit: bool = True,
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 = False,
60
- skip_open_telemetry_setup: bool = False,
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
- self._api_key = api_key or os.getenv("LANGWATCH_API_KEY", "")
79
- self._endpoint_url = (
80
- endpoint_url
81
- or os.getenv("LANGWATCH_ENDPOINT")
82
- or "https://app.langwatch.ai"
83
- )
84
- self._debug = debug or os.getenv("LANGWATCH_DEBUG") == "true"
85
- self._disable_sending = disable_sending
86
- self._flush_on_exit = flush_on_exit
87
- self._span_exclude_rules = span_exclude_rules or []
88
- self._ignore_global_tracer_provider_override_warning = (
89
- ignore_global_tracer_provider_override_warning
90
- )
91
- self._skip_open_telemetry_setup = skip_open_telemetry_setup
92
- self.base_attributes = base_attributes or {}
93
- self.base_attributes[AttributeKey.LangWatchSDKName] = (
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
- self.base_attributes[AttributeKey.LangWatchSDKVersion] = str(__version__)
97
- self.base_attributes[AttributeKey.LangWatchSDKLanguage] = "python"
98
-
99
- if not self._skip_open_telemetry_setup:
100
- self.tracer_provider = self.__ensure_otel_setup(tracer_provider)
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
- self.instrumentors = instrumentors or []
103
- for instrumentor in self.instrumentors:
104
- instrumentor.instrument(tracer_provider=self.tracer_provider)
105
- else:
106
- self.tracer_provider = tracer_provider
107
- self.instrumentors = instrumentors or []
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 self._debug
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
- self._debug = value
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 self._endpoint_url
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 self._flush_on_exit
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 self._api_key
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 == self._api_key:
375
+ if value == Client._api_key:
142
376
  return
143
377
 
144
- api_key_has_changed = bool(self._api_key)
145
-
146
- self._api_key = value
378
+ previous_key = Client._api_key
379
+ Client._api_key = value
147
380
 
148
- if api_key_has_changed and not self._skip_open_telemetry_setup:
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
- # If a new API key is provided and sending is not disabled, set up a new tracer provider.
157
- if value and not self._disable_sending:
158
- self.__setup_tracer_provider()
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 self._disable_sending
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
- return self._rest_api_client
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 self._skip_open_telemetry_setup
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. If enabling, this will create a new global tracer provider."""
181
- if self._disable_sending == value:
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 self.tracer_provider and not self._skip_open_telemetry_setup:
186
- self.tracer_provider.force_flush()
426
+ if Client._tracer_provider and not Client._skip_open_telemetry_setup:
427
+ Client._tracer_provider.force_flush()
187
428
 
188
- self._disable_sending = value
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.tracer_provider:
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.tracer_provider.force_flush)
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
- if hasattr(self.tracer_provider, "force_flush") and callable(
201
- getattr(self.tracer_provider, "force_flush")
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
- self.tracer_provider.force_flush()
445
+ force_flush()
206
446
 
207
- if self._debug:
447
+ if Client._debug:
208
448
  logger.debug("Shutting down tracer provider.")
209
- self.tracer_provider.shutdown()
210
- self.tracer_provider = None
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 self._skip_open_telemetry_setup:
215
- if self._debug:
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 self.tracer_provider:
220
- if self._debug:
460
+ if not Client._tracer_provider:
461
+ if Client._debug:
221
462
  logger.debug("Setting up new tracer provider.")
222
- self.tracer_provider = self.__ensure_otel_setup()
463
+ Client._tracer_provider = self.__ensure_otel_setup()
223
464
 
224
465
  return
225
466
 
226
- if self._debug:
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 not self._ignore_global_tracer_provider_override_warning:
243
- logger.warning(
244
- "An existing global trace provider was found. LangWatch will not override it automatically, but instead is attaching another span processor and exporter to it. You can disable this warning by setting `ignore_global_tracer_provider_override_warning` to `True`."
245
- )
246
- self.__set_langwatch_exporter(settable_tracer_provider)
247
-
248
- return settable_tracer_provider
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: {str(e)}"
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(self.base_attributes)
258
- sampler = ALWAYS_OFF if self._disable_sending else TraceIdRatioBased(1.0)
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
- self.__set_langwatch_exporter(provider)
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 self._flush_on_exit:
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 self.debug:
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: {str(e)}"
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 self.api_key:
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 {self.api_key}",
535
+ "Authorization": f"Bearer {Client._api_key}",
286
536
  "X-LangWatch-SDK-Version": str(__version__),
287
537
  }
288
538
 
289
- if self.debug:
539
+ if Client._debug:
290
540
  logger.info(
291
- f"Configuring OTLP exporter with endpoint: {self._endpoint_url}/api/otel/v1/traces"
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"{self._endpoint_url}/api/otel/v1/traces",
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=self._span_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
- self._rest_api_client = LangWatchApiClient(
320
- base_url=self._endpoint_url,
321
- headers={"X-Auth-Token": self._api_key},
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 self._rest_api_client
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[Span]) -> SpanExportResult:
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
- return self.wrapped_exporter.force_flush(timeout_millis)
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 nanoid
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 f"eval_{nanoid.generate()}",
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}
@@ -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
- return trace.__enter__()
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
- trace = get_current_trace()
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
- try:
95
- return stored_langwatch_span.set(span)
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(token: contextvars.Token):
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()
@@ -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('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
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 = False,
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
- # TODO: get rid of this
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
- if get_instance() is not None and api_key != get_instance().api_key:
62
- logger.warning("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.")
63
- # Set the new API key on the current client to make sure, triggering a nuke of the previous tracing providers too
64
- get_instance().api_key = api_key
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(debug=True, api_key=api_key) # Enable debug logging for auto-created clients
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 is None: # type: ignore
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.16
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=zBSlISkVLNMQm4p_0afBBJuscq6Fwt88EDKwt5vf9_I,65
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=9mLnGOZsVjwQ8AMp0f-J8-nNtg5EPXGkaQ4m8Jck3ew,14197
6
- langwatch/evaluations.py,sha256=U8zMVbn0FmsUuUGiQ-XfmIejNjKY3oW9V4ymAFS8tDw,16828
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=iJ11wdMfNN1zeX99VSSpxoExl6dW1fmk3GPKDAtONbQ,12082
9
+ langwatch/litellm.py,sha256=mPcw5cLykt0SQf9bTNSoT7elMx4gj-wZ_K2PC14Bw50,11998
10
10
  langwatch/login.py,sha256=Wm8Vs7zxJNeGEPO7s_wSQN2DYObteDg56HcyluhTobw,896
11
- langwatch/openai.py,sha256=vKlzSQ7vJ2j0jZdWDT8iByUoxgPLlaBdnQaktbohrgg,25534
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=DkZThS7ptUY0_Ao7PNO6ZGBE9Qj0YmJf5auRkq2Cphw,2570
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=ixKvTBZADFuWglFCK95r8bF4-f-iq_WixzC4ydirCE8,3888
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=uOrJoHScKJcJjV2OqarhmfXMWB8yKK4L16wsQsicL6I,26672
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=LOmGCiox7JRiHprnB04AdO8BhUggZtRbWgBPF9mqR5s,4047
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.16.dist-info/METADATA,sha256=OYg6R9Xvlpydm4Zb3GUc7yFFfrLOwBLvgff0b_jzy0c,13095
412
- langwatch-0.2.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
413
- langwatch-0.2.16.dist-info/RECORD,,
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,,