lucidicai 1.3.2__py3-none-any.whl → 2.0.1__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.
@@ -1,4 +1,7 @@
1
- """OpenTelemetry-based handlers that maintain backward compatibility"""
1
+ """OpenTelemetry-based handlers that maintain backward compatibility
2
+
3
+ Adds guards to avoid repeated monkey-patching under concurrent init.
4
+ """
2
5
  import logging
3
6
  from typing import Optional
4
7
 
@@ -7,6 +10,11 @@ from .otel_init import LucidicTelemetry
7
10
 
8
11
  logger = logging.getLogger("Lucidic")
9
12
 
13
+ import threading
14
+
15
+ _patch_lock = threading.Lock()
16
+ _openai_patched = False
17
+ _anthropic_patched = False
10
18
 
11
19
  class OTelOpenAIHandler(BaseProvider):
12
20
  """OpenAI handler using OpenTelemetry instrumentation"""
@@ -35,37 +43,27 @@ class OTelOpenAIHandler(BaseProvider):
35
43
 
36
44
  # Also patch OpenAI client to intercept images
37
45
  try:
38
- import openai
39
- from .utils.universal_image_interceptor import UniversalImageInterceptor, patch_openai_client
40
-
41
- # Create interceptor for OpenAI
42
- interceptor = UniversalImageInterceptor.create_interceptor("openai")
43
-
44
- # Patch the module-level create method
45
- if hasattr(openai, 'ChatCompletion'):
46
- # Old API
47
- original = openai.ChatCompletion.create
48
- openai.ChatCompletion.create = interceptor(original)
49
-
50
- # Also patch any client instances that might be created
51
- original_client_init = openai.OpenAI.__init__
52
- def patched_init(self, *args, **kwargs):
53
- original_client_init(self, *args, **kwargs)
54
- # Patch this instance
55
- patch_openai_client(self)
56
-
57
- openai.OpenAI.__init__ = patched_init
58
-
59
- # Also patch AsyncOpenAI
60
- if hasattr(openai, 'AsyncOpenAI'):
61
- original_async_init = openai.AsyncOpenAI.__init__
62
- def patched_async_init(self, *args, **kwargs):
63
- original_async_init(self, *args, **kwargs)
64
- # Patch this instance
65
- patch_openai_client(self)
66
-
67
- openai.AsyncOpenAI.__init__ = patched_async_init
68
-
46
+ with _patch_lock:
47
+ global _openai_patched
48
+ if not _openai_patched:
49
+ import openai
50
+ from .utils.universal_image_interceptor import UniversalImageInterceptor, patch_openai_client
51
+ interceptor = UniversalImageInterceptor.create_interceptor("openai")
52
+ if hasattr(openai, 'ChatCompletion'):
53
+ original = openai.ChatCompletion.create
54
+ openai.ChatCompletion.create = interceptor(original)
55
+ original_client_init = openai.OpenAI.__init__
56
+ def patched_init(self, *args, **kwargs):
57
+ original_client_init(self, *args, **kwargs)
58
+ patch_openai_client(self)
59
+ openai.OpenAI.__init__ = patched_init
60
+ if hasattr(openai, 'AsyncOpenAI'):
61
+ original_async_init = openai.AsyncOpenAI.__init__
62
+ def patched_async_init(self, *args, **kwargs):
63
+ original_async_init(self, *args, **kwargs)
64
+ patch_openai_client(self)
65
+ openai.AsyncOpenAI.__init__ = patched_async_init
66
+ _openai_patched = True
69
67
  except Exception as e:
70
68
  logger.warning(f"Could not patch OpenAI for image interception: {e}")
71
69
 
@@ -108,32 +106,25 @@ class OTelAnthropicHandler(BaseProvider):
108
106
 
109
107
  # Also patch Anthropic client to intercept images
110
108
  try:
111
- import anthropic
112
- from .utils.universal_image_interceptor import UniversalImageInterceptor, patch_anthropic_client
113
-
114
- # Create interceptors for Anthropic
115
- interceptor = UniversalImageInterceptor.create_interceptor("anthropic")
116
- async_interceptor = UniversalImageInterceptor.create_async_interceptor("anthropic")
117
-
118
- # Patch any client instances that might be created
119
- original_client_init = anthropic.Anthropic.__init__
120
- def patched_init(self, *args, **kwargs):
121
- original_client_init(self, *args, **kwargs)
122
- # Patch this instance
123
- patch_anthropic_client(self)
124
-
125
- anthropic.Anthropic.__init__ = patched_init
126
-
127
- # Also patch async client
128
- if hasattr(anthropic, 'AsyncAnthropic'):
129
- original_async_init = anthropic.AsyncAnthropic.__init__
130
- def patched_async_init(self, *args, **kwargs):
131
- original_async_init(self, *args, **kwargs)
132
- # Patch this instance
133
- patch_anthropic_client(self)
134
-
135
- anthropic.AsyncAnthropic.__init__ = patched_async_init
136
-
109
+ with _patch_lock:
110
+ global _anthropic_patched
111
+ if not _anthropic_patched:
112
+ import anthropic
113
+ from .utils.universal_image_interceptor import UniversalImageInterceptor, patch_anthropic_client
114
+ interceptor = UniversalImageInterceptor.create_interceptor("anthropic")
115
+ async_interceptor = UniversalImageInterceptor.create_async_interceptor("anthropic")
116
+ original_client_init = anthropic.Anthropic.__init__
117
+ def patched_init(self, *args, **kwargs):
118
+ original_client_init(self, *args, **kwargs)
119
+ patch_anthropic_client(self)
120
+ anthropic.Anthropic.__init__ = patched_init
121
+ if hasattr(anthropic, 'AsyncAnthropic'):
122
+ original_async_init = anthropic.AsyncAnthropic.__init__
123
+ def patched_async_init(self, *args, **kwargs):
124
+ original_async_init(self, *args, **kwargs)
125
+ patch_anthropic_client(self)
126
+ anthropic.AsyncAnthropic.__init__ = patched_async_init
127
+ _anthropic_patched = True
137
128
  except Exception as e:
138
129
  logger.warning(f"Could not patch Anthropic for image interception: {e}")
139
130
 
@@ -356,4 +347,161 @@ class OTelLiteLLMHandler(BaseProvider):
356
347
  logger.info("[OTel LiteLLM Handler] Instrumentation disabled")
357
348
 
358
349
  except Exception as e:
359
- logger.error(f"Error disabling LiteLLM instrumentation: {e}")
350
+ logger.error(f"Error disabling LiteLLM instrumentation: {e}")
351
+
352
+
353
+ class OTelBedrockHandler(BaseProvider):
354
+ """AWS Bedrock handler using OpenTelemetry instrumentation"""
355
+
356
+ def __init__(self):
357
+ super().__init__()
358
+ self._provider_name = "Bedrock"
359
+ self.telemetry = LucidicTelemetry()
360
+
361
+ def handle_response(self, response, kwargs, session: Optional = None):
362
+ return response
363
+
364
+ def override(self):
365
+ try:
366
+ from lucidicai.client import Client
367
+ client = Client()
368
+ if not self.telemetry.is_initialized():
369
+ self.telemetry.initialize(agent_id=client.agent_id)
370
+ self.telemetry.instrument_providers(["bedrock"])
371
+ logger.info("[OTel Bedrock Handler] Instrumentation enabled")
372
+ except Exception as e:
373
+ logger.error(f"Failed to enable Bedrock instrumentation: {e}")
374
+ raise
375
+
376
+ def undo_override(self):
377
+ logger.info("[OTel Bedrock Handler] Instrumentation will be disabled on shutdown")
378
+
379
+
380
+ class OTelGoogleGenerativeAIHandler(BaseProvider):
381
+ """Google Generative AI handler using OpenTelemetry instrumentation"""
382
+
383
+ def __init__(self):
384
+ super().__init__()
385
+ self._provider_name = "Google Generative AI"
386
+ self.telemetry = LucidicTelemetry()
387
+
388
+ def handle_response(self, response, kwargs, session: Optional = None):
389
+ return response
390
+
391
+ def override(self):
392
+ try:
393
+ from lucidicai.client import Client
394
+ client = Client()
395
+ if not self.telemetry.is_initialized():
396
+ self.telemetry.initialize(agent_id=client.agent_id)
397
+ self.telemetry.instrument_providers(["google"])
398
+ # Best-effort image interception for Google clients where applicable
399
+ try:
400
+ from .utils.universal_image_interceptor import patch_google_client, patch_google_genai
401
+ _ = patch_google_client
402
+ patch_google_genai()
403
+ except Exception as e:
404
+ logger.debug(f"[OTel Google Handler] Image interception not applied: {e}")
405
+ logger.info("[OTel Google Handler] Instrumentation enabled")
406
+ except Exception as e:
407
+ logger.error(f"Failed to enable Google Generative AI instrumentation: {e}")
408
+ raise
409
+
410
+ def undo_override(self):
411
+ logger.info("[OTel Google Handler] Instrumentation will be disabled on shutdown")
412
+
413
+
414
+ class OTelVertexAIHandler(BaseProvider):
415
+ """Vertex AI handler using OpenTelemetry instrumentation"""
416
+
417
+ def __init__(self):
418
+ super().__init__()
419
+ self._provider_name = "Vertex AI"
420
+ self.telemetry = LucidicTelemetry()
421
+
422
+ def handle_response(self, response, kwargs, session: Optional = None):
423
+ return response
424
+
425
+ def override(self):
426
+ try:
427
+ from lucidicai.client import Client
428
+ client = Client()
429
+ if not self.telemetry.is_initialized():
430
+ self.telemetry.initialize(agent_id=client.agent_id)
431
+ self.telemetry.instrument_providers(["vertexai"])
432
+ # Best-effort image interception for Vertex AI clients where applicable
433
+ try:
434
+ from .utils.universal_image_interceptor import patch_vertexai_client
435
+ _ = patch_vertexai_client
436
+ except Exception as e:
437
+ logger.debug(f"[OTel Vertex Handler] Image interception not applied: {e}")
438
+ logger.info("[OTel Vertex Handler] Instrumentation enabled")
439
+ except Exception as e:
440
+ logger.error(f"Failed to enable Vertex AI instrumentation: {e}")
441
+ raise
442
+
443
+ def undo_override(self):
444
+ logger.info("[OTel Vertex Handler] Instrumentation will be disabled on shutdown")
445
+
446
+
447
+ class OTelCohereHandler(BaseProvider):
448
+ """Cohere handler using OpenTelemetry instrumentation"""
449
+
450
+ def __init__(self):
451
+ super().__init__()
452
+ self._provider_name = "Cohere"
453
+ self.telemetry = LucidicTelemetry()
454
+
455
+ def handle_response(self, response, kwargs, session: Optional = None):
456
+ return response
457
+
458
+ def override(self):
459
+ try:
460
+ from lucidicai.client import Client
461
+ client = Client()
462
+ if not self.telemetry.is_initialized():
463
+ self.telemetry.initialize(agent_id=client.agent_id)
464
+ self.telemetry.instrument_providers(["cohere"])
465
+ logger.info("[OTel Cohere Handler] Instrumentation enabled")
466
+ except Exception as e:
467
+ logger.error(f"Failed to enable Cohere instrumentation: {e}")
468
+ raise
469
+
470
+ def undo_override(self):
471
+ logger.info("[OTel Cohere Handler] Instrumentation will be disabled on shutdown")
472
+
473
+
474
+ class OTelGroqHandler(BaseProvider):
475
+ """Groq handler using OpenTelemetry instrumentation"""
476
+
477
+ def __init__(self):
478
+ super().__init__()
479
+ self._provider_name = "Groq"
480
+ self.telemetry = LucidicTelemetry()
481
+
482
+ def handle_response(self, response, kwargs, session: Optional = None):
483
+ return response
484
+
485
+ def override(self):
486
+ try:
487
+ from lucidicai.client import Client
488
+ client = Client()
489
+ if not self.telemetry.is_initialized():
490
+ self.telemetry.initialize(agent_id=client.agent_id)
491
+ self.telemetry.instrument_providers(["groq"])
492
+ # Best-effort image interception for Groq (OpenAI-compatible)
493
+ try:
494
+ import groq # noqa: F401
495
+ from .utils.universal_image_interceptor import UniversalImageInterceptor
496
+ # We cannot reliably patch class constructors here without instance; users calling Groq client
497
+ # will still have images captured via OpenLLMetry attributes; optional future improvement.
498
+ _ = UniversalImageInterceptor
499
+ except Exception as e:
500
+ logger.debug(f"[OTel Groq Handler] Image interception not applied: {e}")
501
+ logger.info("[OTel Groq Handler] Instrumentation enabled")
502
+ except Exception as e:
503
+ logger.error(f"Failed to enable Groq instrumentation: {e}")
504
+ raise
505
+
506
+ def undo_override(self):
507
+ logger.info("[OTel Groq Handler] Instrumentation will be disabled on shutdown")
@@ -1,13 +1,15 @@
1
- """OpenTelemetry initialization and configuration for Lucidic"""
1
+ """OpenTelemetry initialization and configuration for Lucidic
2
+
3
+ Adds thread-safety and idempotence to avoid duplicate tracer provider
4
+ registration and repeated instrumentation under concurrency.
5
+ """
2
6
  import logging
3
7
  from typing import List, Optional
4
8
 
5
9
  from opentelemetry import trace
6
10
  from opentelemetry.sdk.trace import TracerProvider
7
11
  from opentelemetry.sdk.resources import Resource
8
- from opentelemetry.instrumentation.openai import OpenAIInstrumentor
9
- from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
10
- from opentelemetry.instrumentation.langchain import LangchainInstrumentor
12
+ # Instrumentors are imported lazily inside methods to avoid import errors
11
13
 
12
14
  from .lucidic_span_processor import LucidicSpanProcessor
13
15
  from .otel_provider import OpenTelemetryProvider
@@ -15,6 +17,10 @@ from lucidicai.client import Client
15
17
 
16
18
  logger = logging.getLogger("Lucidic")
17
19
 
20
+ import threading
21
+
22
+
23
+ _init_lock = threading.Lock()
18
24
 
19
25
  class LucidicTelemetry:
20
26
  """Manages OpenTelemetry initialization for Lucidic"""
@@ -37,59 +43,73 @@ class LucidicTelemetry:
37
43
 
38
44
  def initialize(self, agent_id: str, service_name: str = "lucidic-ai") -> None:
39
45
  """Initialize OpenTelemetry with Lucidic configuration"""
40
- if self.tracer_provider:
41
- logger.debug("OpenTelemetry already initialized")
42
- return
43
-
44
- try:
45
- # Create resource
46
- resource = Resource.create({
47
- "service.name": service_name,
48
- "service.version": "1.0.0",
49
- "lucidic.agent_id": agent_id,
50
- })
51
-
52
- # Create tracer provider
53
- self.tracer_provider = TracerProvider(resource=resource)
54
-
55
- # Add our custom span processor for real-time event handling
56
- self.span_processor = LucidicSpanProcessor()
57
- self.tracer_provider.add_span_processor(self.span_processor)
58
-
59
- # Set as global provider
60
- trace.set_tracer_provider(self.tracer_provider)
61
-
62
- logger.info("[LucidicTelemetry] OpenTelemetry initialized")
63
-
64
- except Exception as e:
65
- logger.error(f"Failed to initialize OpenTelemetry: {e}")
66
- raise
46
+ with _init_lock:
47
+ if self.tracer_provider:
48
+ logger.debug("OpenTelemetry already initialized")
49
+ return
50
+ try:
51
+ resource = Resource.create({
52
+ "service.name": service_name,
53
+ "service.version": "1.0.0",
54
+ "lucidic.agent_id": agent_id,
55
+ })
56
+ provider = TracerProvider(resource=resource)
57
+ processor = LucidicSpanProcessor()
58
+ provider.add_span_processor(processor)
59
+ try:
60
+ trace.set_tracer_provider(provider)
61
+ except Exception as e:
62
+ # Another provider may already be registered; proceed with ours as a local provider
63
+ logger.debug(f"Global tracer provider already set: {e}")
64
+ self.tracer_provider = provider
65
+ self.span_processor = processor
66
+ logger.info("[LucidicTelemetry] OpenTelemetry initialized")
67
+ except Exception as e:
68
+ logger.error(f"Failed to initialize OpenTelemetry: {e}")
69
+ raise
67
70
 
68
71
  def instrument_providers(self, providers: List[str]) -> None:
69
72
  """Instrument specified providers"""
70
- for provider in providers:
71
- try:
72
- if provider == "openai" and provider not in self.instrumentors:
73
- self._instrument_openai()
74
- elif provider == "anthropic" and provider not in self.instrumentors:
75
- self._instrument_anthropic()
76
- elif provider == "langchain" and provider not in self.instrumentors:
77
- self._instrument_langchain()
78
- elif provider == "pydantic_ai":
79
- # Custom instrumentation needed
80
- logger.info(f"[LucidicTelemetry] Pydantic AI will use manual instrumentation")
81
- elif provider == "openai_agents":
82
- # OpenAI Agents uses the same OpenAI instrumentation
83
- self._instrument_openai_agents()
84
- elif provider == "litellm":
85
- # LiteLLM uses callbacks, not OpenTelemetry instrumentation
86
- logger.info(f"[LucidicTelemetry] LiteLLM will use callback-based instrumentation")
87
- except Exception as e:
88
- logger.error(f"Failed to instrument {provider}: {e}")
73
+ with _init_lock:
74
+ for provider in providers:
75
+ # Map synonyms to canonical names
76
+ canonical = provider
77
+ if provider in ("google_generativeai",):
78
+ canonical = "google"
79
+ elif provider in ("vertex_ai",):
80
+ canonical = "vertexai"
81
+ elif provider in ("aws_bedrock", "amazon_bedrock"):
82
+ canonical = "bedrock"
83
+ try:
84
+ if canonical == "openai" and canonical not in self.instrumentors:
85
+ self._instrument_openai()
86
+ elif canonical == "anthropic" and canonical not in self.instrumentors:
87
+ self._instrument_anthropic()
88
+ elif canonical == "langchain" and canonical not in self.instrumentors:
89
+ self._instrument_langchain()
90
+ elif canonical == "google" and canonical not in self.instrumentors:
91
+ self._instrument_google_generativeai()
92
+ elif canonical == "vertexai" and canonical not in self.instrumentors:
93
+ self._instrument_vertexai()
94
+ elif canonical == "bedrock" and canonical not in self.instrumentors:
95
+ self._instrument_bedrock()
96
+ elif canonical == "cohere" and canonical not in self.instrumentors:
97
+ self._instrument_cohere()
98
+ elif canonical == "groq" and canonical not in self.instrumentors:
99
+ self._instrument_groq()
100
+ elif canonical == "pydantic_ai":
101
+ logger.info(f"[LucidicTelemetry] Pydantic AI will use manual instrumentation")
102
+ elif canonical == "openai_agents":
103
+ self._instrument_openai_agents()
104
+ elif canonical == "litellm":
105
+ logger.info(f"[LucidicTelemetry] LiteLLM will use callback-based instrumentation")
106
+ except Exception as e:
107
+ logger.error(f"Failed to instrument {canonical}: {e}")
89
108
 
90
109
  def _instrument_openai(self) -> None:
91
110
  """Instrument OpenAI"""
92
111
  try:
112
+ from opentelemetry.instrumentation.openai import OpenAIInstrumentor
93
113
  # Get client for masking function
94
114
  client = Client()
95
115
 
@@ -125,6 +145,7 @@ class LucidicTelemetry:
125
145
  def _instrument_anthropic(self) -> None:
126
146
  """Instrument Anthropic"""
127
147
  try:
148
+ from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
128
149
  instrumentor = AnthropicInstrumentor()
129
150
 
130
151
  # Get client for context
@@ -153,6 +174,7 @@ class LucidicTelemetry:
153
174
  def _instrument_langchain(self) -> None:
154
175
  """Instrument LangChain"""
155
176
  try:
177
+ from opentelemetry.instrumentation.langchain import LangchainInstrumentor
156
178
  instrumentor = LangchainInstrumentor()
157
179
  instrumentor.instrument(tracer_provider=self.tracer_provider)
158
180
 
@@ -163,6 +185,73 @@ class LucidicTelemetry:
163
185
  logger.error(f"Failed to instrument LangChain: {e}")
164
186
  raise
165
187
 
188
+ def _instrument_google_generativeai(self) -> None:
189
+ """Instrument Google Generative AI"""
190
+ try:
191
+ from opentelemetry.instrumentation.google_generativeai import GoogleGenerativeAiInstrumentor
192
+ instrumentor = GoogleGenerativeAiInstrumentor(exception_logger=lambda e: logger.error(f"Google Generative AI error: {e}"))
193
+ instrumentor.instrument(tracer_provider=self.tracer_provider)
194
+ self.instrumentors["google"] = instrumentor
195
+ logger.info("[LucidicTelemetry] Instrumented Google Generative AI")
196
+ except Exception as e:
197
+ logger.error(f"Failed to instrument Google Generative AI: {e}")
198
+ raise
199
+
200
+ def _instrument_vertexai(self) -> None:
201
+ """Instrument Vertex AI"""
202
+ try:
203
+ from opentelemetry.instrumentation.vertexai import VertexAIInstrumentor
204
+ instrumentor = VertexAIInstrumentor(exception_logger=lambda e: logger.error(f"Vertex AI error: {e}"))
205
+ instrumentor.instrument(tracer_provider=self.tracer_provider)
206
+ self.instrumentors["vertexai"] = instrumentor
207
+ logger.info("[LucidicTelemetry] Instrumented Vertex AI")
208
+ except Exception as e:
209
+ logger.error(f"Failed to instrument Vertex AI: {e}")
210
+ raise
211
+
212
+ def _instrument_cohere(self) -> None:
213
+ """Instrument Cohere"""
214
+ try:
215
+ from opentelemetry.instrumentation.cohere import CohereInstrumentor
216
+ instrumentor = CohereInstrumentor(exception_logger=lambda e: logger.error(f"Cohere error: {e}"), use_legacy_attributes=True)
217
+ instrumentor.instrument(tracer_provider=self.tracer_provider)
218
+ self.instrumentors["cohere"] = instrumentor
219
+ logger.info("[LucidicTelemetry] Instrumented Cohere")
220
+ except Exception as e:
221
+ logger.error(f"Failed to instrument Cohere: {e}")
222
+ raise
223
+
224
+ def _instrument_bedrock(self) -> None:
225
+ """Instrument AWS Bedrock"""
226
+ try:
227
+ from opentelemetry.instrumentation.bedrock import BedrockInstrumentor
228
+ instrumentor = BedrockInstrumentor(enrich_token_usage=True, exception_logger=lambda e: logger.error(f"Bedrock error: {e}"))
229
+ instrumentor.instrument(tracer_provider=self.tracer_provider)
230
+ self.instrumentors["bedrock"] = instrumentor
231
+ logger.info("[LucidicTelemetry] Instrumented Bedrock")
232
+ except Exception as e:
233
+ logger.error(f"Failed to instrument Bedrock: {e}")
234
+ raise
235
+
236
+ def _instrument_groq(self) -> None:
237
+ """Instrument Groq"""
238
+ try:
239
+ from lucidicai.client import Client
240
+ client = Client()
241
+ def get_custom_attributes():
242
+ attrs = {}
243
+ if client.session and client.session.active_step:
244
+ attrs["lucidic.step_id"] = client.session.active_step.step_id
245
+ return attrs
246
+ from opentelemetry.instrumentation.groq import GroqInstrumentor
247
+ instrumentor = GroqInstrumentor(exception_logger=lambda e: logger.error(f"Groq error: {e}"), use_legacy_attributes=True, get_common_metrics_attributes=get_custom_attributes)
248
+ instrumentor.instrument(tracer_provider=self.tracer_provider)
249
+ self.instrumentors["groq"] = instrumentor
250
+ logger.info("[LucidicTelemetry] Instrumented Groq")
251
+ except Exception as e:
252
+ logger.error(f"Failed to instrument Groq: {e}")
253
+ raise
254
+
166
255
  def _instrument_openai_agents(self) -> None:
167
256
  """Instrument OpenAI Agents SDK"""
168
257
  try:
@@ -197,4 +286,27 @@ class LucidicTelemetry:
197
286
 
198
287
  def is_initialized(self) -> bool:
199
288
  """Check if telemetry is initialized"""
200
- return self.tracer_provider is not None
289
+ return self.tracer_provider is not None
290
+
291
+ def force_flush(self) -> None:
292
+ """Best-effort force flush of telemetry before shutdown.
293
+
294
+ Uses whichever force_flush hooks are available on the provider or span processor.
295
+ Swallows all exceptions to avoid interfering with process shutdown paths.
296
+ """
297
+ try:
298
+ provider = getattr(self, 'tracer_provider', None)
299
+ if provider and hasattr(provider, 'force_flush'):
300
+ try:
301
+ provider.force_flush()
302
+ except Exception:
303
+ pass
304
+ processor = getattr(self, 'span_processor', None)
305
+ if processor and hasattr(processor, 'force_flush'):
306
+ try:
307
+ processor.force_flush()
308
+ except Exception:
309
+ pass
310
+ except Exception:
311
+ # Never raise from force_flush
312
+ pass
@@ -8,12 +8,11 @@ from opentelemetry.trace import Tracer, Span
8
8
  from opentelemetry.sdk.trace import TracerProvider
9
9
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
10
10
  from opentelemetry.sdk.resources import Resource
11
- from opentelemetry.instrumentation.openai import OpenAIInstrumentor
12
- from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
13
- from opentelemetry.instrumentation.langchain import LangchainInstrumentor
11
+ # Instrumentors are imported lazily inside methods to avoid import errors
14
12
  from opentelemetry.semconv_ai import SpanAttributes
15
13
 
16
14
  from .lucidic_exporter import LucidicSpanExporter
15
+ from .lucidic_span_processor import LucidicSpanProcessor
17
16
  from .base_provider import BaseProvider
18
17
  from lucidicai.client import Client
19
18
 
@@ -47,9 +46,17 @@ class OpenTelemetryProvider(BaseProvider):
47
46
  lucidic_exporter = LucidicSpanExporter()
48
47
  span_processor = BatchSpanProcessor(lucidic_exporter)
49
48
  self.tracer_provider.add_span_processor(span_processor)
49
+ # Also add session-stamping processor to ensure correct attribution
50
+ try:
51
+ self.tracer_provider.add_span_processor(LucidicSpanProcessor())
52
+ except Exception:
53
+ pass
50
54
 
51
- # Set as global provider
52
- trace.set_tracer_provider(self.tracer_provider)
55
+ # Set as global provider (ignore if already set)
56
+ try:
57
+ trace.set_tracer_provider(self.tracer_provider)
58
+ except Exception:
59
+ pass
53
60
 
54
61
  # Get tracer
55
62
  self.tracer = trace.get_tracer(__name__)
@@ -93,6 +100,7 @@ class OpenTelemetryProvider(BaseProvider):
93
100
  """Instrument OpenAI with OpenLLMetry"""
94
101
  if "openai" not in self.instrumentors:
95
102
  try:
103
+ from opentelemetry.instrumentation.openai import OpenAIInstrumentor
96
104
  instrumentor = OpenAIInstrumentor()
97
105
  instrumentor.instrument(
98
106
  tracer_provider=self.tracer_provider,
@@ -108,6 +116,7 @@ class OpenTelemetryProvider(BaseProvider):
108
116
  """Instrument Anthropic with OpenLLMetry"""
109
117
  if "anthropic" not in self.instrumentors:
110
118
  try:
119
+ from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
111
120
  instrumentor = AnthropicInstrumentor()
112
121
  instrumentor.instrument(
113
122
  tracer_provider=self.tracer_provider,
@@ -122,6 +131,7 @@ class OpenTelemetryProvider(BaseProvider):
122
131
  """Instrument LangChain with OpenLLMetry"""
123
132
  if "langchain" not in self.instrumentors:
124
133
  try:
134
+ from opentelemetry.instrumentation.langchain import LangchainInstrumentor
125
135
  instrumentor = LangchainInstrumentor()
126
136
  instrumentor.instrument(tracer_provider=self.tracer_provider)
127
137
  self.instrumentors["langchain"] = instrumentor