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.
- lucidicai/__init__.py +648 -351
- lucidicai/client.py +327 -37
- lucidicai/constants.py +7 -37
- lucidicai/context.py +144 -0
- lucidicai/dataset.py +112 -0
- lucidicai/decorators.py +96 -325
- lucidicai/errors.py +33 -0
- lucidicai/event.py +50 -59
- lucidicai/event_queue.py +466 -0
- lucidicai/feature_flag.py +336 -0
- lucidicai/model_pricing.py +11 -0
- lucidicai/session.py +9 -71
- lucidicai/singleton.py +20 -17
- lucidicai/streaming.py +15 -50
- lucidicai/telemetry/context_capture_processor.py +65 -0
- lucidicai/telemetry/extract.py +192 -0
- lucidicai/telemetry/litellm_bridge.py +80 -45
- lucidicai/telemetry/lucidic_exporter.py +139 -144
- lucidicai/telemetry/lucidic_span_processor.py +67 -49
- lucidicai/telemetry/otel_handlers.py +207 -59
- lucidicai/telemetry/otel_init.py +163 -51
- lucidicai/telemetry/otel_provider.py +15 -5
- lucidicai/telemetry/telemetry_init.py +189 -0
- lucidicai/telemetry/utils/universal_image_interceptor.py +89 -0
- {lucidicai-1.3.2.dist-info → lucidicai-2.0.1.dist-info}/METADATA +1 -1
- {lucidicai-1.3.2.dist-info → lucidicai-2.0.1.dist-info}/RECORD +28 -21
- {lucidicai-1.3.2.dist-info → lucidicai-2.0.1.dist-info}/WHEEL +0 -0
- {lucidicai-1.3.2.dist-info → lucidicai-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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")
|
lucidicai/telemetry/otel_init.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
elif provider
|
|
77
|
-
|
|
78
|
-
elif provider
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|