genai-otel-instrument 0.1.2.dev0__py3-none-any.whl → 0.1.7.dev0__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.
Potentially problematic release.
This version of genai-otel-instrument might be problematic. Click here for more details.
- genai_otel/__version__.py +2 -2
- genai_otel/auto_instrument.py +18 -1
- genai_otel/config.py +22 -1
- genai_otel/cost_calculator.py +204 -13
- genai_otel/cost_enrichment_processor.py +175 -0
- genai_otel/gpu_metrics.py +50 -0
- genai_otel/instrumentors/base.py +300 -44
- genai_otel/instrumentors/cohere_instrumentor.py +140 -76
- genai_otel/instrumentors/huggingface_instrumentor.py +142 -13
- genai_otel/instrumentors/langchain_instrumentor.py +75 -75
- genai_otel/instrumentors/mistralai_instrumentor.py +234 -38
- genai_otel/instrumentors/ollama_instrumentor.py +104 -35
- genai_otel/instrumentors/replicate_instrumentor.py +59 -14
- genai_otel/instrumentors/togetherai_instrumentor.py +120 -16
- genai_otel/instrumentors/vertexai_instrumentor.py +79 -15
- genai_otel/llm_pricing.json +869 -589
- genai_otel/logging_config.py +45 -45
- genai_otel/py.typed +2 -2
- {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/METADATA +294 -33
- {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/RECORD +24 -23
- {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/WHEEL +0 -0
- {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/entry_points.txt +0 -0
- {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/licenses/LICENSE +0 -0
- {genai_otel_instrument-0.1.2.dev0.dist-info → genai_otel_instrument-0.1.7.dev0.dist-info}/top_level.txt +0 -0
|
@@ -1,76 +1,140 @@
|
|
|
1
|
-
"""OpenTelemetry instrumentor for the Cohere SDK.
|
|
2
|
-
|
|
3
|
-
This instrumentor automatically traces calls to Cohere models, capturing
|
|
4
|
-
relevant attributes such as the model name.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Dict, Optional
|
|
9
|
-
|
|
10
|
-
from ..config import OTelConfig
|
|
11
|
-
from .base import BaseInstrumentor
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class CohereInstrumentor(BaseInstrumentor):
|
|
17
|
-
"""Instrumentor for Cohere"""
|
|
18
|
-
|
|
19
|
-
def __init__(self):
|
|
20
|
-
"""Initialize the instrumentor."""
|
|
21
|
-
super().__init__()
|
|
22
|
-
self._cohere_available = False
|
|
23
|
-
self._check_availability()
|
|
24
|
-
|
|
25
|
-
def _check_availability(self):
|
|
26
|
-
"""Check if cohere library is available."""
|
|
27
|
-
try:
|
|
28
|
-
import cohere
|
|
29
|
-
|
|
30
|
-
self._cohere_available = True
|
|
31
|
-
logger.debug("cohere library detected and available for instrumentation")
|
|
32
|
-
except ImportError:
|
|
33
|
-
logger.debug("cohere library not installed, instrumentation will be skipped")
|
|
34
|
-
self._cohere_available = False
|
|
35
|
-
|
|
36
|
-
def instrument(self, config: OTelConfig):
|
|
37
|
-
"""Instrument
|
|
38
|
-
if not self._cohere_available:
|
|
39
|
-
logger.debug("Skipping instrumentation - library not available")
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
self.config = config
|
|
43
|
-
try:
|
|
44
|
-
import cohere
|
|
45
|
-
|
|
46
|
-
original_init = cohere.Client.__init__
|
|
47
|
-
|
|
48
|
-
def wrapped_init(instance, *args, **kwargs):
|
|
49
|
-
original_init(instance, *args, **kwargs)
|
|
50
|
-
self._instrument_client(instance)
|
|
51
|
-
|
|
52
|
-
cohere.Client.__init__ = wrapped_init
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
"""OpenTelemetry instrumentor for the Cohere SDK.
|
|
2
|
+
|
|
3
|
+
This instrumentor automatically traces calls to Cohere models, capturing
|
|
4
|
+
relevant attributes such as the model name and token usage.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..config import OTelConfig
|
|
11
|
+
from .base import BaseInstrumentor
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CohereInstrumentor(BaseInstrumentor):
|
|
17
|
+
"""Instrumentor for Cohere"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize the instrumentor."""
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._cohere_available = False
|
|
23
|
+
self._check_availability()
|
|
24
|
+
|
|
25
|
+
def _check_availability(self):
|
|
26
|
+
"""Check if cohere library is available."""
|
|
27
|
+
try:
|
|
28
|
+
import cohere
|
|
29
|
+
|
|
30
|
+
self._cohere_available = True
|
|
31
|
+
logger.debug("cohere library detected and available for instrumentation")
|
|
32
|
+
except ImportError:
|
|
33
|
+
logger.debug("cohere library not installed, instrumentation will be skipped")
|
|
34
|
+
self._cohere_available = False
|
|
35
|
+
|
|
36
|
+
def instrument(self, config: OTelConfig):
|
|
37
|
+
"""Instrument cohere if available."""
|
|
38
|
+
if not self._cohere_available:
|
|
39
|
+
logger.debug("Skipping instrumentation - library not available")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
self.config = config
|
|
43
|
+
try:
|
|
44
|
+
import cohere
|
|
45
|
+
|
|
46
|
+
original_init = cohere.Client.__init__
|
|
47
|
+
|
|
48
|
+
def wrapped_init(instance, *args, **kwargs):
|
|
49
|
+
original_init(instance, *args, **kwargs)
|
|
50
|
+
self._instrument_client(instance)
|
|
51
|
+
|
|
52
|
+
cohere.Client.__init__ = wrapped_init
|
|
53
|
+
self._instrumented = True
|
|
54
|
+
logger.info("Cohere instrumentation enabled")
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error("Failed to instrument Cohere: %s", e, exc_info=True)
|
|
58
|
+
if config.fail_on_error:
|
|
59
|
+
raise
|
|
60
|
+
|
|
61
|
+
def _instrument_client(self, client):
|
|
62
|
+
"""Instrument Cohere client methods."""
|
|
63
|
+
original_generate = client.generate
|
|
64
|
+
|
|
65
|
+
# Wrap using create_span_wrapper
|
|
66
|
+
wrapped_generate = self.create_span_wrapper(
|
|
67
|
+
span_name="cohere.generate",
|
|
68
|
+
extract_attributes=self._extract_generate_attributes,
|
|
69
|
+
)(original_generate)
|
|
70
|
+
|
|
71
|
+
client.generate = wrapped_generate
|
|
72
|
+
|
|
73
|
+
def _extract_generate_attributes(self, instance: Any, args: Any, kwargs: Any) -> Dict[str, Any]:
|
|
74
|
+
"""Extract attributes from Cohere generate call.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
instance: The client instance.
|
|
78
|
+
args: Positional arguments.
|
|
79
|
+
kwargs: Keyword arguments.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dict[str, Any]: Dictionary of attributes to set on the span.
|
|
83
|
+
"""
|
|
84
|
+
attrs = {}
|
|
85
|
+
model = kwargs.get("model", "command")
|
|
86
|
+
prompt = kwargs.get("prompt", "")
|
|
87
|
+
|
|
88
|
+
attrs["gen_ai.system"] = "cohere"
|
|
89
|
+
attrs["gen_ai.request.model"] = model
|
|
90
|
+
attrs["gen_ai.operation.name"] = "generate"
|
|
91
|
+
attrs["gen_ai.request.message_count"] = 1 if prompt else 0
|
|
92
|
+
|
|
93
|
+
return attrs
|
|
94
|
+
|
|
95
|
+
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
96
|
+
"""Extract token usage from Cohere response.
|
|
97
|
+
|
|
98
|
+
Cohere responses include meta.tokens with:
|
|
99
|
+
- input_tokens: Input tokens
|
|
100
|
+
- output_tokens: Output tokens
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
result: The API response object.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Optional[Dict[str, int]]: Dictionary with token counts or None.
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
# Handle object response
|
|
110
|
+
if hasattr(result, "meta") and result.meta:
|
|
111
|
+
meta = result.meta
|
|
112
|
+
# Check for tokens object
|
|
113
|
+
if hasattr(meta, "tokens") and meta.tokens:
|
|
114
|
+
tokens = meta.tokens
|
|
115
|
+
input_tokens = getattr(tokens, "input_tokens", 0)
|
|
116
|
+
output_tokens = getattr(tokens, "output_tokens", 0)
|
|
117
|
+
|
|
118
|
+
if input_tokens or output_tokens:
|
|
119
|
+
return {
|
|
120
|
+
"prompt_tokens": int(input_tokens) if input_tokens else 0,
|
|
121
|
+
"completion_tokens": int(output_tokens) if output_tokens else 0,
|
|
122
|
+
"total_tokens": int(input_tokens or 0) + int(output_tokens or 0),
|
|
123
|
+
}
|
|
124
|
+
# Fallback to billed_units
|
|
125
|
+
elif hasattr(meta, "billed_units") and meta.billed_units:
|
|
126
|
+
billed = meta.billed_units
|
|
127
|
+
input_tokens = getattr(billed, "input_tokens", 0)
|
|
128
|
+
output_tokens = getattr(billed, "output_tokens", 0)
|
|
129
|
+
|
|
130
|
+
if input_tokens or output_tokens:
|
|
131
|
+
return {
|
|
132
|
+
"prompt_tokens": int(input_tokens) if input_tokens else 0,
|
|
133
|
+
"completion_tokens": int(output_tokens) if output_tokens else 0,
|
|
134
|
+
"total_tokens": int(input_tokens or 0) + int(output_tokens or 0),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.debug("Failed to extract usage from Cohere response: %s", e)
|
|
140
|
+
return None
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
"""OpenTelemetry instrumentor for HuggingFace Transformers
|
|
1
|
+
"""OpenTelemetry instrumentor for HuggingFace Transformers and Inference API.
|
|
2
2
|
|
|
3
|
-
This instrumentor automatically traces
|
|
4
|
-
|
|
3
|
+
This instrumentor automatically traces:
|
|
4
|
+
1. HuggingFace Transformers pipelines (local model execution)
|
|
5
|
+
2. HuggingFace Inference API calls via InferenceClient (used by smolagents)
|
|
6
|
+
|
|
7
|
+
Note: Transformers runs models locally (no API costs), but InferenceClient makes
|
|
8
|
+
API calls to HuggingFace endpoints which may have costs based on usage.
|
|
5
9
|
"""
|
|
6
10
|
|
|
7
11
|
import logging
|
|
8
|
-
import types
|
|
9
12
|
from typing import Dict, Optional
|
|
10
13
|
|
|
11
14
|
from ..config import OTelConfig
|
|
@@ -15,16 +18,22 @@ logger = logging.getLogger(__name__)
|
|
|
15
18
|
|
|
16
19
|
|
|
17
20
|
class HuggingFaceInstrumentor(BaseInstrumentor):
|
|
18
|
-
"""Instrumentor for HuggingFace Transformers
|
|
21
|
+
"""Instrumentor for HuggingFace Transformers and Inference API.
|
|
22
|
+
|
|
23
|
+
Instruments both:
|
|
24
|
+
- transformers.pipeline (local execution, no API costs)
|
|
25
|
+
- huggingface_hub.InferenceClient (API calls, may have costs)
|
|
26
|
+
"""
|
|
19
27
|
|
|
20
28
|
def __init__(self):
|
|
21
29
|
"""Initialize the instrumentor."""
|
|
22
30
|
super().__init__()
|
|
23
31
|
self._transformers_available = False
|
|
32
|
+
self._inference_client_available = False
|
|
24
33
|
self._check_availability()
|
|
25
34
|
|
|
26
35
|
def _check_availability(self):
|
|
27
|
-
"""Check if Transformers
|
|
36
|
+
"""Check if Transformers and InferenceClient libraries are available."""
|
|
28
37
|
try:
|
|
29
38
|
import transformers
|
|
30
39
|
|
|
@@ -34,12 +43,51 @@ class HuggingFaceInstrumentor(BaseInstrumentor):
|
|
|
34
43
|
logger.debug("Transformers library not installed, instrumentation will be skipped")
|
|
35
44
|
self._transformers_available = False
|
|
36
45
|
|
|
46
|
+
try:
|
|
47
|
+
from huggingface_hub import InferenceClient
|
|
48
|
+
|
|
49
|
+
self._inference_client_available = True
|
|
50
|
+
logger.debug("HuggingFace InferenceClient detected and available for instrumentation")
|
|
51
|
+
except ImportError:
|
|
52
|
+
logger.debug(
|
|
53
|
+
"huggingface_hub not installed, InferenceClient instrumentation will be skipped"
|
|
54
|
+
)
|
|
55
|
+
self._inference_client_available = False
|
|
56
|
+
|
|
37
57
|
def instrument(self, config: OTelConfig):
|
|
58
|
+
"""Instrument HuggingFace Transformers pipelines and InferenceClient."""
|
|
38
59
|
self.config = config
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
instrumented_count = 0
|
|
62
|
+
|
|
63
|
+
# Instrument transformers.pipeline if available
|
|
64
|
+
if self._transformers_available:
|
|
65
|
+
try:
|
|
66
|
+
self._instrument_transformers()
|
|
67
|
+
instrumented_count += 1
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error("Failed to instrument HuggingFace Transformers: %s", e, exc_info=True)
|
|
70
|
+
if config.fail_on_error:
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
# Instrument InferenceClient if available
|
|
74
|
+
if self._inference_client_available:
|
|
75
|
+
try:
|
|
76
|
+
self._instrument_inference_client()
|
|
77
|
+
instrumented_count += 1
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(
|
|
80
|
+
"Failed to instrument HuggingFace InferenceClient: %s", e, exc_info=True
|
|
81
|
+
)
|
|
82
|
+
if config.fail_on_error:
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
if instrumented_count > 0:
|
|
86
|
+
self._instrumented = True
|
|
87
|
+
logger.info(f"HuggingFace instrumentation enabled ({instrumented_count} components)")
|
|
88
|
+
|
|
89
|
+
def _instrument_transformers(self):
|
|
90
|
+
"""Instrument transformers.pipeline for local model execution."""
|
|
43
91
|
try:
|
|
44
92
|
import importlib
|
|
45
93
|
|
|
@@ -68,6 +116,7 @@ class HuggingFaceInstrumentor(BaseInstrumentor):
|
|
|
68
116
|
|
|
69
117
|
span.set_attribute("gen_ai.system", "huggingface")
|
|
70
118
|
span.set_attribute("gen_ai.request.model", model)
|
|
119
|
+
span.set_attribute("gen_ai.operation.name", task)
|
|
71
120
|
span.set_attribute("huggingface.task", task)
|
|
72
121
|
|
|
73
122
|
if instrumentor.request_counter:
|
|
@@ -88,10 +137,90 @@ class HuggingFaceInstrumentor(BaseInstrumentor):
|
|
|
88
137
|
return WrappedPipeline(pipe)
|
|
89
138
|
|
|
90
139
|
transformers_module.pipeline = wrapped_pipeline
|
|
91
|
-
logger.
|
|
92
|
-
|
|
93
|
-
except
|
|
94
|
-
|
|
140
|
+
logger.debug("HuggingFace Transformers pipeline instrumented")
|
|
141
|
+
|
|
142
|
+
except Exception as e:
|
|
143
|
+
raise # Re-raise to be caught by instrument() method
|
|
144
|
+
|
|
145
|
+
def _instrument_inference_client(self):
|
|
146
|
+
"""Instrument HuggingFace InferenceClient for API calls."""
|
|
147
|
+
from huggingface_hub import InferenceClient
|
|
148
|
+
|
|
149
|
+
# Store original methods
|
|
150
|
+
original_chat_completion = InferenceClient.chat_completion
|
|
151
|
+
original_text_generation = InferenceClient.text_generation
|
|
152
|
+
|
|
153
|
+
# Wrap chat_completion method
|
|
154
|
+
wrapped_chat_completion = self.create_span_wrapper(
|
|
155
|
+
span_name="huggingface.inference.chat_completion",
|
|
156
|
+
extract_attributes=self._extract_inference_client_attributes,
|
|
157
|
+
)(original_chat_completion)
|
|
158
|
+
|
|
159
|
+
# Wrap text_generation method
|
|
160
|
+
wrapped_text_generation = self.create_span_wrapper(
|
|
161
|
+
span_name="huggingface.inference.text_generation",
|
|
162
|
+
extract_attributes=self._extract_inference_client_attributes,
|
|
163
|
+
)(original_text_generation)
|
|
164
|
+
|
|
165
|
+
InferenceClient.chat_completion = wrapped_chat_completion
|
|
166
|
+
InferenceClient.text_generation = wrapped_text_generation
|
|
167
|
+
logger.debug("HuggingFace InferenceClient instrumented")
|
|
168
|
+
|
|
169
|
+
def _extract_inference_client_attributes(self, instance, args, kwargs) -> Dict[str, str]:
|
|
170
|
+
"""Extract attributes from Inference API call."""
|
|
171
|
+
attrs = {}
|
|
172
|
+
model = kwargs.get("model") or (args[0] if args else "unknown")
|
|
173
|
+
|
|
174
|
+
attrs["gen_ai.system"] = "huggingface"
|
|
175
|
+
attrs["gen_ai.request.model"] = str(model)
|
|
176
|
+
attrs["gen_ai.operation.name"] = "chat" # Default to chat
|
|
177
|
+
|
|
178
|
+
# Extract parameters if available
|
|
179
|
+
if "max_tokens" in kwargs:
|
|
180
|
+
attrs["gen_ai.request.max_tokens"] = kwargs["max_tokens"]
|
|
181
|
+
if "temperature" in kwargs:
|
|
182
|
+
attrs["gen_ai.request.temperature"] = kwargs["temperature"]
|
|
183
|
+
if "top_p" in kwargs:
|
|
184
|
+
attrs["gen_ai.request.top_p"] = kwargs["top_p"]
|
|
185
|
+
|
|
186
|
+
return attrs
|
|
95
187
|
|
|
96
188
|
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
189
|
+
"""Extract token usage from HuggingFace response.
|
|
190
|
+
|
|
191
|
+
Handles both:
|
|
192
|
+
1. Transformers pipeline (local execution) - returns None
|
|
193
|
+
2. InferenceClient API calls - extracts token usage from response
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
result: The pipeline output or InferenceClient response.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dict with token counts for InferenceClient calls, None for local execution.
|
|
200
|
+
"""
|
|
201
|
+
# Check if this is an InferenceClient API response
|
|
202
|
+
if result is not None and hasattr(result, "usage"):
|
|
203
|
+
usage = result.usage
|
|
204
|
+
|
|
205
|
+
# Extract token counts from usage object
|
|
206
|
+
prompt_tokens = getattr(usage, "prompt_tokens", None)
|
|
207
|
+
completion_tokens = getattr(usage, "completion_tokens", None)
|
|
208
|
+
total_tokens = getattr(usage, "total_tokens", None)
|
|
209
|
+
|
|
210
|
+
# If usage is a dict instead of object
|
|
211
|
+
if isinstance(usage, dict):
|
|
212
|
+
prompt_tokens = usage.get("prompt_tokens")
|
|
213
|
+
completion_tokens = usage.get("completion_tokens")
|
|
214
|
+
total_tokens = usage.get("total_tokens")
|
|
215
|
+
|
|
216
|
+
# Return token counts if available
|
|
217
|
+
if prompt_tokens is not None or completion_tokens is not None:
|
|
218
|
+
return {
|
|
219
|
+
"prompt_tokens": prompt_tokens or 0,
|
|
220
|
+
"completion_tokens": completion_tokens or 0,
|
|
221
|
+
"total_tokens": total_tokens or (prompt_tokens or 0) + (completion_tokens or 0),
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
# HuggingFace Transformers is free (local execution)
|
|
225
|
+
# No token-based costs to track
|
|
97
226
|
return None
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
"""OpenTelemetry instrumentor for the LangChain framework.
|
|
2
|
-
|
|
3
|
-
This instrumentor automatically traces various components within LangChain,
|
|
4
|
-
including chains and agents, capturing relevant attributes for observability.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
from typing import Dict, Optional
|
|
9
|
-
|
|
10
|
-
from ..config import OTelConfig
|
|
11
|
-
from .base import BaseInstrumentor
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class LangChainInstrumentor(BaseInstrumentor):
|
|
17
|
-
"""Instrumentor for LangChain"""
|
|
18
|
-
|
|
19
|
-
def __init__(self):
|
|
20
|
-
"""Initialize the instrumentor."""
|
|
21
|
-
super().__init__()
|
|
22
|
-
self._langchain_available = False
|
|
23
|
-
self._check_availability()
|
|
24
|
-
|
|
25
|
-
def _check_availability(self):
|
|
26
|
-
"""Check if langchain library is available."""
|
|
27
|
-
try:
|
|
28
|
-
import langchain
|
|
29
|
-
|
|
30
|
-
self._langchain_available = True
|
|
31
|
-
logger.debug("langchain library detected and available for instrumentation")
|
|
32
|
-
except ImportError:
|
|
33
|
-
logger.debug("langchain library not installed, instrumentation will be skipped")
|
|
34
|
-
self._langchain_available = False
|
|
35
|
-
|
|
36
|
-
def instrument(self, config: OTelConfig):
|
|
37
|
-
"""Instrument langchain available if available."""
|
|
38
|
-
if not self._langchain_available:
|
|
39
|
-
logger.debug("Skipping instrumentation - library not available")
|
|
40
|
-
return
|
|
41
|
-
|
|
42
|
-
self.config = config
|
|
43
|
-
try:
|
|
44
|
-
from langchain.agents.agent import AgentExecutor
|
|
45
|
-
from langchain.chains.base import Chain
|
|
46
|
-
|
|
47
|
-
# Instrument Chains
|
|
48
|
-
original_call = Chain.__call__
|
|
49
|
-
|
|
50
|
-
def wrapped_call(instance, *args, **kwargs):
|
|
51
|
-
chain_type = instance.__class__.__name__
|
|
52
|
-
with self.tracer.start_as_current_span(f"langchain.chain.{chain_type}") as span:
|
|
53
|
-
span.set_attribute("langchain.chain.type", chain_type)
|
|
54
|
-
result = original_call(instance, *args, **kwargs)
|
|
55
|
-
return result
|
|
56
|
-
|
|
57
|
-
Chain.__call__ = wrapped_call
|
|
58
|
-
|
|
59
|
-
# Instrument Agents
|
|
60
|
-
original_agent_call = AgentExecutor.__call__
|
|
61
|
-
|
|
62
|
-
def wrapped_agent_call(instance, *args, **kwargs):
|
|
63
|
-
with self.tracer.start_as_current_span("langchain.agent.execute") as span:
|
|
64
|
-
agent_name = getattr(instance, "agent", {}).get("name", "unknown")
|
|
65
|
-
span.set_attribute("langchain.agent.name", agent_name)
|
|
66
|
-
result = original_agent_call(instance, *args, **kwargs)
|
|
67
|
-
return result
|
|
68
|
-
|
|
69
|
-
AgentExecutor.__call__ = wrapped_agent_call
|
|
70
|
-
|
|
71
|
-
except ImportError:
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
|
-
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
75
|
-
return None
|
|
1
|
+
"""OpenTelemetry instrumentor for the LangChain framework.
|
|
2
|
+
|
|
3
|
+
This instrumentor automatically traces various components within LangChain,
|
|
4
|
+
including chains and agents, capturing relevant attributes for observability.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..config import OTelConfig
|
|
11
|
+
from .base import BaseInstrumentor
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LangChainInstrumentor(BaseInstrumentor):
|
|
17
|
+
"""Instrumentor for LangChain"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize the instrumentor."""
|
|
21
|
+
super().__init__()
|
|
22
|
+
self._langchain_available = False
|
|
23
|
+
self._check_availability()
|
|
24
|
+
|
|
25
|
+
def _check_availability(self):
|
|
26
|
+
"""Check if langchain library is available."""
|
|
27
|
+
try:
|
|
28
|
+
import langchain
|
|
29
|
+
|
|
30
|
+
self._langchain_available = True
|
|
31
|
+
logger.debug("langchain library detected and available for instrumentation")
|
|
32
|
+
except ImportError:
|
|
33
|
+
logger.debug("langchain library not installed, instrumentation will be skipped")
|
|
34
|
+
self._langchain_available = False
|
|
35
|
+
|
|
36
|
+
def instrument(self, config: OTelConfig):
|
|
37
|
+
"""Instrument langchain available if available."""
|
|
38
|
+
if not self._langchain_available:
|
|
39
|
+
logger.debug("Skipping instrumentation - library not available")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
self.config = config
|
|
43
|
+
try:
|
|
44
|
+
from langchain.agents.agent import AgentExecutor
|
|
45
|
+
from langchain.chains.base import Chain
|
|
46
|
+
|
|
47
|
+
# Instrument Chains
|
|
48
|
+
original_call = Chain.__call__
|
|
49
|
+
|
|
50
|
+
def wrapped_call(instance, *args, **kwargs):
|
|
51
|
+
chain_type = instance.__class__.__name__
|
|
52
|
+
with self.tracer.start_as_current_span(f"langchain.chain.{chain_type}") as span:
|
|
53
|
+
span.set_attribute("langchain.chain.type", chain_type)
|
|
54
|
+
result = original_call(instance, *args, **kwargs)
|
|
55
|
+
return result
|
|
56
|
+
|
|
57
|
+
Chain.__call__ = wrapped_call
|
|
58
|
+
|
|
59
|
+
# Instrument Agents
|
|
60
|
+
original_agent_call = AgentExecutor.__call__
|
|
61
|
+
|
|
62
|
+
def wrapped_agent_call(instance, *args, **kwargs):
|
|
63
|
+
with self.tracer.start_as_current_span("langchain.agent.execute") as span:
|
|
64
|
+
agent_name = getattr(instance, "agent", {}).get("name", "unknown")
|
|
65
|
+
span.set_attribute("langchain.agent.name", agent_name)
|
|
66
|
+
result = original_agent_call(instance, *args, **kwargs)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
AgentExecutor.__call__ = wrapped_agent_call
|
|
70
|
+
|
|
71
|
+
except ImportError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def _extract_usage(self, result) -> Optional[Dict[str, int]]:
|
|
75
|
+
return None
|