langtrace-python-sdk 1.0.9__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.
- instrumentation/__init__.py +0 -0
- instrumentation/chroma/__init__.py +0 -0
- instrumentation/chroma/instrumentation.py +33 -0
- instrumentation/chroma/lib/__init__.py +0 -0
- instrumentation/chroma/lib/apis.py +40 -0
- instrumentation/chroma/patch.py +46 -0
- instrumentation/constants.py +18 -0
- instrumentation/langchain/__init__.py +0 -0
- instrumentation/langchain/instrumentation.py +74 -0
- instrumentation/langchain/patch.py +84 -0
- instrumentation/langchain_community/__init__.py +0 -0
- instrumentation/langchain_community/instrumentation.py +99 -0
- instrumentation/langchain_community/patch.py +78 -0
- instrumentation/langchain_core/__init__.py +0 -0
- instrumentation/langchain_core/instrumentation.py +101 -0
- instrumentation/langchain_core/patch.py +168 -0
- instrumentation/llamaindex/__init__.py +0 -0
- instrumentation/llamaindex/instrumentation.py +73 -0
- instrumentation/llamaindex/patch.py +40 -0
- instrumentation/openai/__init__.py +0 -0
- instrumentation/openai/instrumentation.py +41 -0
- instrumentation/openai/lib/__init__.py +0 -0
- instrumentation/openai/lib/apis.py +16 -0
- instrumentation/openai/lib/constants.py +30 -0
- instrumentation/openai/patch.py +209 -0
- instrumentation/pinecone/__init__.py +0 -0
- instrumentation/pinecone/instrumentation.py +43 -0
- instrumentation/pinecone/lib/__init__.py +0 -0
- instrumentation/pinecone/lib/apis.py +19 -0
- instrumentation/pinecone/patch.py +45 -0
- instrumentation/setup.py +50 -0
- instrumentation/utils.py +27 -0
- instrumentation/with_root_span.py +28 -0
- langtrace_python_sdk-1.0.9.dist-info/LICENSE +674 -0
- langtrace_python_sdk-1.0.9.dist-info/METADATA +169 -0
- langtrace_python_sdk-1.0.9.dist-info/RECORD +38 -0
- langtrace_python_sdk-1.0.9.dist-info/WHEEL +5 -0
- langtrace_python_sdk-1.0.9.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
from typing import Collection
|
|
3
|
+
|
|
4
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
5
|
+
from opentelemetry.trace import get_tracer
|
|
6
|
+
from wrapt import wrap_function_wrapper
|
|
7
|
+
|
|
8
|
+
from instrumentation.chroma.lib.apis import APIS
|
|
9
|
+
from instrumentation.chroma.patch import collection_patch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChromaInstrumentation(BaseInstrumentor):
|
|
13
|
+
|
|
14
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
15
|
+
return ["chromadb >= 0.4.23"]
|
|
16
|
+
|
|
17
|
+
def _instrument(self, **kwargs):
|
|
18
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
19
|
+
tracer = get_tracer(__name__, "", tracer_provider)
|
|
20
|
+
version = importlib.metadata.version('chromadb')
|
|
21
|
+
|
|
22
|
+
for operation, details in APIS.items():
|
|
23
|
+
wrap_function_wrapper(
|
|
24
|
+
'chromadb.api.models.Collection',
|
|
25
|
+
f'Collection.{operation.lower()}',
|
|
26
|
+
collection_patch(operation, version, tracer)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def _instrument_module(self, module_name):
|
|
30
|
+
print(module_name)
|
|
31
|
+
|
|
32
|
+
def _uninstrument(self, **kwargs):
|
|
33
|
+
print(kwargs)
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from langtrace.trace_attributes import ChromaDBMethods
|
|
2
|
+
|
|
3
|
+
APIS = {
|
|
4
|
+
"ADD": {
|
|
5
|
+
"METHOD": ChromaDBMethods.ADD.value,
|
|
6
|
+
"OPERATION": "add",
|
|
7
|
+
},
|
|
8
|
+
"GET": {
|
|
9
|
+
"METHOD": ChromaDBMethods.GET.value,
|
|
10
|
+
"OPERATION": "get",
|
|
11
|
+
},
|
|
12
|
+
"QUERY": {
|
|
13
|
+
"METHOD": ChromaDBMethods.QUERY.value,
|
|
14
|
+
"OPERATION": "query",
|
|
15
|
+
},
|
|
16
|
+
"DELETE": {
|
|
17
|
+
"METHOD": ChromaDBMethods.DELETE.value,
|
|
18
|
+
"OPERATION": "delete",
|
|
19
|
+
},
|
|
20
|
+
"PEEK": {
|
|
21
|
+
"METHOD": ChromaDBMethods.PEEK.value,
|
|
22
|
+
"OPERATION": "peek",
|
|
23
|
+
},
|
|
24
|
+
"UPDATE": {
|
|
25
|
+
"METHOD": ChromaDBMethods.UPDATE.value,
|
|
26
|
+
"OPERATION": "update",
|
|
27
|
+
},
|
|
28
|
+
"UPSERT": {
|
|
29
|
+
"METHOD": ChromaDBMethods.UPSERT.value,
|
|
30
|
+
"OPERATION": "upsert",
|
|
31
|
+
},
|
|
32
|
+
"MODIFY": {
|
|
33
|
+
"METHOD": ChromaDBMethods.MODIFY.value,
|
|
34
|
+
"OPERATION": "modify",
|
|
35
|
+
},
|
|
36
|
+
"COUNT": {
|
|
37
|
+
"METHOD": ChromaDBMethods.COUNT.value,
|
|
38
|
+
"OPERATION": "count",
|
|
39
|
+
},
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from langtrace.trace_attributes import DatabaseSpanAttributes
|
|
2
|
+
from opentelemetry.trace import SpanKind, StatusCode
|
|
3
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
4
|
+
|
|
5
|
+
from instrumentation.constants import SERVICE_PROVIDERS
|
|
6
|
+
from instrumentation.chroma.lib.apis import APIS
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def collection_patch(method, version, tracer):
|
|
10
|
+
def traced_method(wrapped, instance, args, kwargs):
|
|
11
|
+
api = APIS[method]
|
|
12
|
+
service_provider = SERVICE_PROVIDERS['CHROMA']
|
|
13
|
+
span_attributes = {
|
|
14
|
+
'langtrace.service.name': service_provider,
|
|
15
|
+
'langtrace.service.type': 'vectordb',
|
|
16
|
+
'langtrace.service.version': version,
|
|
17
|
+
'langtrace.version': '1.0.0',
|
|
18
|
+
"db.system": "chromadb",
|
|
19
|
+
"db.operation": api['OPERATION'],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if hasattr(instance, 'name') and instance.name is not None:
|
|
23
|
+
span_attributes["db.collection.name"] = instance.name
|
|
24
|
+
|
|
25
|
+
attributes = DatabaseSpanAttributes(**span_attributes)
|
|
26
|
+
|
|
27
|
+
with tracer.start_as_current_span(api["METHOD"], kind=SpanKind.CLIENT) as span:
|
|
28
|
+
for field, value in attributes.model_dump(by_alias=True).items():
|
|
29
|
+
if value is not None:
|
|
30
|
+
span.set_attribute(field, value)
|
|
31
|
+
try:
|
|
32
|
+
# Attempt to call the original method
|
|
33
|
+
result = wrapped(*args, **kwargs)
|
|
34
|
+
span.set_status(StatusCode.OK)
|
|
35
|
+
return result
|
|
36
|
+
except Exception as e:
|
|
37
|
+
# Record the exception in the span
|
|
38
|
+
span.record_exception(e)
|
|
39
|
+
|
|
40
|
+
# Set the span status to indicate an error
|
|
41
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
42
|
+
|
|
43
|
+
# Reraise the exception to ensure it's not swallowed
|
|
44
|
+
raise
|
|
45
|
+
|
|
46
|
+
return traced_method
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
TRACE_NAMESPACES = {
|
|
3
|
+
"OPENAI": "Langtrace OpenAI SDK",
|
|
4
|
+
"LANGCHAIN": "Langtrace Langchain SDK",
|
|
5
|
+
"PINECONE": "Langtrace Pinecone SDK",
|
|
6
|
+
"LLAMAINDEX": "Langtrace LlamaIndex SDK",
|
|
7
|
+
"CHROMA": "Langtrace Chroma SDK",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
SERVICE_PROVIDERS = {
|
|
11
|
+
"OPENAI": "OpenAI",
|
|
12
|
+
"AZURE": "Azure",
|
|
13
|
+
"LANGCHAIN": "Langchain",
|
|
14
|
+
"LANGCHAIN_COMMUNITY": "Langchain Community",
|
|
15
|
+
"PINECONE": "Pinecone",
|
|
16
|
+
"LLAMAINDEX": "LlamaIndex",
|
|
17
|
+
"CHROMA": "Chroma",
|
|
18
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Instrumentation for langchain.
|
|
3
|
+
"""
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Collection
|
|
7
|
+
|
|
8
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
9
|
+
from opentelemetry.trace import get_tracer
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
11
|
+
|
|
12
|
+
from instrumentation.langchain.patch import generic_patch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def patch_module_classes(module_name, tracer, version, task, trace_output=True, trace_input=True):
|
|
16
|
+
"""
|
|
17
|
+
Generic function to patch all public methods of all classes in a given module.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- module: The module object containing the classes to patch.
|
|
21
|
+
- module_name: The name of the module, used in the prefix for `wrap_function_wrapper`.
|
|
22
|
+
- tracer: The tracer object used in `generic_patch`.
|
|
23
|
+
- version: The version parameter used in `generic_patch`.
|
|
24
|
+
- task: The name used to identify the type of task in `generic_patch`.
|
|
25
|
+
- exclude_private: Whether to exclude private methods (those starting with '_').
|
|
26
|
+
- trace_output: Whether to trace the output of the patched methods.
|
|
27
|
+
- trace_input: Whether to trace the input of the patched methods.
|
|
28
|
+
"""
|
|
29
|
+
# import the module
|
|
30
|
+
module = importlib.import_module(module_name)
|
|
31
|
+
# loop through all public classes in the module
|
|
32
|
+
for name, obj in inspect.getmembers(module, lambda member: inspect.isclass(member) and
|
|
33
|
+
member.__module__ == module.__name__):
|
|
34
|
+
# loop through all public methods of the class
|
|
35
|
+
for method_name, _ in inspect.getmembers(obj, predicate=inspect.isfunction):
|
|
36
|
+
# Skip private methods
|
|
37
|
+
if method_name.startswith('_'):
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
method_path = f'{name}.{method_name}'
|
|
41
|
+
wrap_function_wrapper(
|
|
42
|
+
module_name,
|
|
43
|
+
method_path,
|
|
44
|
+
generic_patch(
|
|
45
|
+
method_path, task, tracer, version, trace_output, trace_input)
|
|
46
|
+
)
|
|
47
|
+
# pylint: disable=broad-except
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LangchainInstrumentation(BaseInstrumentor):
|
|
53
|
+
"""
|
|
54
|
+
Instrumentor for langchain.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
58
|
+
return ["langchain >= 0.1.9"]
|
|
59
|
+
|
|
60
|
+
def _instrument(self, **kwargs):
|
|
61
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
62
|
+
tracer = get_tracer(__name__, "", tracer_provider)
|
|
63
|
+
version = importlib.metadata.version('langchain')
|
|
64
|
+
|
|
65
|
+
modules_to_patch = [
|
|
66
|
+
('langchain.text_splitter', 'split_text', True, True),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
for module_name, task, trace_output, trace_input in modules_to_patch:
|
|
70
|
+
patch_module_classes(module_name, tracer, version,
|
|
71
|
+
task, trace_output, trace_input)
|
|
72
|
+
|
|
73
|
+
def _uninstrument(self, **kwargs):
|
|
74
|
+
pass
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the patching logic for the langchain package.
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from langtrace.trace_attributes import FrameworkSpanAttributes
|
|
7
|
+
from opentelemetry.trace import SpanKind, StatusCode
|
|
8
|
+
from opentelemetry.trace.status import Status
|
|
9
|
+
|
|
10
|
+
from instrumentation.constants import SERVICE_PROVIDERS
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def generic_patch(method_name, task, tracer, version, trace_output=True, trace_input=True):
|
|
14
|
+
"""
|
|
15
|
+
patch method for generic methods.
|
|
16
|
+
"""
|
|
17
|
+
def traced_method(wrapped, instance, args, kwargs):
|
|
18
|
+
service_provider = SERVICE_PROVIDERS['LANGCHAIN']
|
|
19
|
+
span_attributes = {
|
|
20
|
+
'langtrace.service.name': service_provider,
|
|
21
|
+
'langtrace.service.type': 'framework',
|
|
22
|
+
'langtrace.service.version': version,
|
|
23
|
+
'langtrace.version': '1.0.0',
|
|
24
|
+
'langchain.task.name': task,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if len(args) > 0 and trace_input:
|
|
28
|
+
span_attributes['langchain.inputs'] = to_json_string(args)
|
|
29
|
+
|
|
30
|
+
attributes = FrameworkSpanAttributes(**span_attributes)
|
|
31
|
+
|
|
32
|
+
with tracer.start_as_current_span(method_name, kind=SpanKind.CLIENT) as span:
|
|
33
|
+
for field, value in attributes.model_dump(by_alias=True).items():
|
|
34
|
+
if value is not None:
|
|
35
|
+
span.set_attribute(field, value)
|
|
36
|
+
try:
|
|
37
|
+
# Attempt to call the original method
|
|
38
|
+
result = wrapped(*args, **kwargs)
|
|
39
|
+
if trace_output:
|
|
40
|
+
span.set_attribute(
|
|
41
|
+
'langchain.outputs', to_json_string(result))
|
|
42
|
+
|
|
43
|
+
span.set_status(StatusCode.OK)
|
|
44
|
+
return result
|
|
45
|
+
except Exception as e:
|
|
46
|
+
# Record the exception in the span
|
|
47
|
+
span.record_exception(e)
|
|
48
|
+
|
|
49
|
+
# Set the span status to indicate an error
|
|
50
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
51
|
+
|
|
52
|
+
# Reraise the exception to ensure it's not swallowed
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
return traced_method
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clean_empty(d):
|
|
59
|
+
"""Recursively remove empty lists, empty dicts, or None elements from a dictionary."""
|
|
60
|
+
if not isinstance(d, (dict, list)):
|
|
61
|
+
return d
|
|
62
|
+
if isinstance(d, list):
|
|
63
|
+
return [v for v in (clean_empty(v) for v in d) if v != [] and v is not None]
|
|
64
|
+
return {
|
|
65
|
+
k: v
|
|
66
|
+
for k, v in ((k, clean_empty(v)) for k, v in d.items())
|
|
67
|
+
if v is not None and v != {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def custom_serializer(obj):
|
|
72
|
+
"""Fallback function to convert unserializable objects."""
|
|
73
|
+
if hasattr(obj, "__dict__"):
|
|
74
|
+
# Attempt to serialize custom objects by their __dict__ attribute.
|
|
75
|
+
return clean_empty(obj.__dict__)
|
|
76
|
+
else:
|
|
77
|
+
# For other types, just convert to string
|
|
78
|
+
return str(obj)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def to_json_string(any_object):
|
|
82
|
+
"""Converts any object to a JSON-parseable string, omitting empty or None values."""
|
|
83
|
+
cleaned_object = clean_empty(any_object)
|
|
84
|
+
return json.dumps(cleaned_object, default=custom_serializer, indent=2)
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Instrumentation for langchain-community.
|
|
3
|
+
"""
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Collection
|
|
7
|
+
|
|
8
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
9
|
+
from opentelemetry.trace import get_tracer
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
11
|
+
|
|
12
|
+
from instrumentation.langchain_community.patch import generic_patch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def patch_module_classes(module_name, tracer, version, task, trace_output=True, trace_input=True):
|
|
16
|
+
"""
|
|
17
|
+
Generic function to patch all public methods of all classes in a given module.
|
|
18
|
+
|
|
19
|
+
Parameters:
|
|
20
|
+
- module: The module object containing the classes to patch.
|
|
21
|
+
- module_name: The name of the module, used in the prefix for `wrap_function_wrapper`.
|
|
22
|
+
- tracer: The tracer object used in `generic_patch`.
|
|
23
|
+
- version: The version parameter used in `generic_patch`.
|
|
24
|
+
- task: The name used to identify the type of patch in `generic_patch`.
|
|
25
|
+
- exclude_private: Whether to exclude private methods (those starting with '_').
|
|
26
|
+
- trace_output: Whether to trace the output of the patched methods.
|
|
27
|
+
- trace_input: Whether to trace the input of the patched methods.
|
|
28
|
+
"""
|
|
29
|
+
# import the module
|
|
30
|
+
module = importlib.import_module(module_name)
|
|
31
|
+
# loop through all public classes in the module
|
|
32
|
+
for name, obj in inspect.getmembers(module, lambda member: inspect.isclass(member) and
|
|
33
|
+
member.__module__ == module.__name__):
|
|
34
|
+
# loop through all public methods of the class
|
|
35
|
+
for method_name, _ in inspect.getmembers(obj, predicate=inspect.isfunction):
|
|
36
|
+
# Skip private methods
|
|
37
|
+
if method_name.startswith('_'):
|
|
38
|
+
continue
|
|
39
|
+
try:
|
|
40
|
+
method_path = f'{name}.{method_name}'
|
|
41
|
+
wrap_function_wrapper(
|
|
42
|
+
module_name,
|
|
43
|
+
method_path,
|
|
44
|
+
generic_patch(
|
|
45
|
+
method_path, task, tracer, version, trace_output, trace_input)
|
|
46
|
+
)
|
|
47
|
+
# pylint: disable=broad-except
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class LangchainCommunityInstrumentation(BaseInstrumentor):
|
|
53
|
+
"""
|
|
54
|
+
Instrumentor for langchain-community.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
58
|
+
return ["langchain-community >= 0.0.24"]
|
|
59
|
+
|
|
60
|
+
def _instrument(self, **kwargs):
|
|
61
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
62
|
+
tracer = get_tracer(__name__, "", tracer_provider)
|
|
63
|
+
version = importlib.metadata.version('langchain-community')
|
|
64
|
+
|
|
65
|
+
# List of modules to patch, with their corresponding patch names
|
|
66
|
+
modules_to_patch = [
|
|
67
|
+
('langchain_community.document_loaders.pdf', 'load_pdf', True, True),
|
|
68
|
+
('langchain_community.vectorstores.faiss', 'vector_store', False, False),
|
|
69
|
+
('langchain_community.vectorstores.pgvector',
|
|
70
|
+
'vector_store', False, False),
|
|
71
|
+
('langchain_community.vectorstores.pinecone',
|
|
72
|
+
'vector_store', False, False),
|
|
73
|
+
('langchain_community.vectorstores.mongodb_atlas',
|
|
74
|
+
'vector_store', False, False),
|
|
75
|
+
('langchain_community.vectorstores.azuresearch',
|
|
76
|
+
'vector_store', False, False),
|
|
77
|
+
('langchain_community.vectorstores.azure_cosmos_db',
|
|
78
|
+
'vector_store', False, False),
|
|
79
|
+
('langchain_community.vectorstores.cassandra',
|
|
80
|
+
'vector_store', False, False),
|
|
81
|
+
('langchain_community.vectorstores.chroma', 'vector_store', False, False),
|
|
82
|
+
('langchain_community.vectorstores.clickhouse',
|
|
83
|
+
'vector_store', False, False),
|
|
84
|
+
('langchain_community.vectorstores.elasticsearch',
|
|
85
|
+
'vector_store', False, False),
|
|
86
|
+
('langchain_community.vectorstores.supabase',
|
|
87
|
+
'vector_store', False, False),
|
|
88
|
+
('langchain_community.vectorstores.weaviate',
|
|
89
|
+
'vector_store', False, False),
|
|
90
|
+
('langchain_community.vectorstores.vectara',
|
|
91
|
+
'vector_store', False, False),
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for module_name, task, trace_output, trace_input in modules_to_patch:
|
|
95
|
+
patch_module_classes(module_name, tracer, version,
|
|
96
|
+
task, trace_output, trace_input)
|
|
97
|
+
|
|
98
|
+
def _uninstrument(self, **kwargs):
|
|
99
|
+
pass
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from langtrace.trace_attributes import FrameworkSpanAttributes
|
|
4
|
+
from opentelemetry.trace import SpanKind, StatusCode
|
|
5
|
+
from opentelemetry.trace.status import Status, StatusCode
|
|
6
|
+
|
|
7
|
+
from instrumentation.constants import SERVICE_PROVIDERS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def generic_patch(method_name, task, tracer, version, trace_output=True, trace_input=True):
|
|
11
|
+
def traced_method(wrapped, instance, args, kwargs):
|
|
12
|
+
service_provider = SERVICE_PROVIDERS['LANGCHAIN_COMMUNITY']
|
|
13
|
+
span_attributes = {
|
|
14
|
+
'langtrace.service.name': service_provider,
|
|
15
|
+
'langtrace.service.type': 'framework',
|
|
16
|
+
'langtrace.service.version': version,
|
|
17
|
+
'langtrace.version': '1.0.0',
|
|
18
|
+
'langchain.task.name': task,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if trace_input and len(args) > 0:
|
|
22
|
+
span_attributes['langchain.inputs'] = to_json_string(args)
|
|
23
|
+
|
|
24
|
+
attributes = FrameworkSpanAttributes(**span_attributes)
|
|
25
|
+
|
|
26
|
+
with tracer.start_as_current_span(method_name, kind=SpanKind.CLIENT) as span:
|
|
27
|
+
for field, value in attributes.model_dump(by_alias=True).items():
|
|
28
|
+
if value is not None:
|
|
29
|
+
span.set_attribute(field, value)
|
|
30
|
+
try:
|
|
31
|
+
# Attempt to call the original method
|
|
32
|
+
result = wrapped(*args, **kwargs)
|
|
33
|
+
if trace_output:
|
|
34
|
+
span.set_attribute(
|
|
35
|
+
'langchain.outputs', to_json_string(result))
|
|
36
|
+
|
|
37
|
+
span.set_status(StatusCode.OK)
|
|
38
|
+
return result
|
|
39
|
+
except Exception as e:
|
|
40
|
+
# Record the exception in the span
|
|
41
|
+
span.record_exception(e)
|
|
42
|
+
|
|
43
|
+
# Set the span status to indicate an error
|
|
44
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
45
|
+
|
|
46
|
+
# Reraise the exception to ensure it's not swallowed
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
return traced_method
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def clean_empty(d):
|
|
53
|
+
"""Recursively remove empty lists, empty dicts, or None elements from a dictionary."""
|
|
54
|
+
if not isinstance(d, (dict, list)):
|
|
55
|
+
return d
|
|
56
|
+
if isinstance(d, list):
|
|
57
|
+
return [v for v in (clean_empty(v) for v in d) if v != [] and v is not None]
|
|
58
|
+
return {
|
|
59
|
+
k: v
|
|
60
|
+
for k, v in ((k, clean_empty(v)) for k, v in d.items())
|
|
61
|
+
if v is not None and v != {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def custom_serializer(obj):
|
|
66
|
+
"""Fallback function to convert unserializable objects."""
|
|
67
|
+
if hasattr(obj, "__dict__"):
|
|
68
|
+
# Attempt to serialize custom objects by their __dict__ attribute.
|
|
69
|
+
return clean_empty(obj.__dict__)
|
|
70
|
+
else:
|
|
71
|
+
# For other types, just convert to string
|
|
72
|
+
return str(obj)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_json_string(any_object):
|
|
76
|
+
"""Converts any object to a JSON-parseable string, omitting empty or None values."""
|
|
77
|
+
cleaned_object = clean_empty(any_object)
|
|
78
|
+
return json.dumps(cleaned_object, default=custom_serializer, indent=2)
|
|
File without changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Instrumentation for langchain-core.
|
|
3
|
+
"""
|
|
4
|
+
import importlib.metadata
|
|
5
|
+
import inspect
|
|
6
|
+
from typing import Collection
|
|
7
|
+
|
|
8
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
9
|
+
from opentelemetry.trace import get_tracer
|
|
10
|
+
from wrapt import wrap_function_wrapper
|
|
11
|
+
|
|
12
|
+
from instrumentation.langchain_core.patch import generic_patch, runnable_patch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# pylint: disable=dangerous-default-value
|
|
16
|
+
def patch_module_classes(module_name, tracer, version, task, patch_method,
|
|
17
|
+
exclude_methods=[], exclude_classes=[],
|
|
18
|
+
trace_output=True, trace_input=True):
|
|
19
|
+
"""
|
|
20
|
+
Generic function to patch all public methods of all classes in a given module.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
- module: The module object containing the classes to patch.
|
|
24
|
+
- module_name: The name of the module, used in the prefix for `wrap_function_wrapper`.
|
|
25
|
+
- tracer: The tracer object used in `generic_patch`.
|
|
26
|
+
- version: The version parameter used in `generic_patch`.
|
|
27
|
+
- task: The name used to identify the type of task in `generic_patch`.
|
|
28
|
+
- patch_method: The patch method to use.
|
|
29
|
+
- exclude_methods: A list of methods to exclude from patching.
|
|
30
|
+
- exclude_classes: A list of classes to exclude from patching.
|
|
31
|
+
- trace_output: Whether to trace the output of the patched methods.
|
|
32
|
+
- trace_input: Whether to trace the input of the patched methods.
|
|
33
|
+
"""
|
|
34
|
+
# import the module
|
|
35
|
+
module = importlib.import_module(module_name)
|
|
36
|
+
# loop through all public classes in the module
|
|
37
|
+
for name, obj in inspect.getmembers(module, lambda member: inspect.isclass(member) and
|
|
38
|
+
member.__module__ == module.__name__):
|
|
39
|
+
# Skip private classes
|
|
40
|
+
if name.startswith('_') or name in exclude_classes:
|
|
41
|
+
continue
|
|
42
|
+
# loop through all public methods of the class
|
|
43
|
+
for method_name, _ in inspect.getmembers(obj, predicate=inspect.isfunction):
|
|
44
|
+
# Skip private methods
|
|
45
|
+
if method_name.startswith('_') or method_name in exclude_methods:
|
|
46
|
+
continue
|
|
47
|
+
try:
|
|
48
|
+
method_path = f'{name}.{method_name}'
|
|
49
|
+
wrap_function_wrapper(
|
|
50
|
+
module_name,
|
|
51
|
+
method_path,
|
|
52
|
+
patch_method(
|
|
53
|
+
method_path, task, tracer, version, trace_output, trace_input)
|
|
54
|
+
)
|
|
55
|
+
# pylint: disable=broad-except
|
|
56
|
+
except Exception:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LangchainCoreInstrumentation(BaseInstrumentor):
|
|
61
|
+
"""
|
|
62
|
+
Instrumentor for langchain.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
66
|
+
return ["langchain-core >= 0.1.27"]
|
|
67
|
+
|
|
68
|
+
def _instrument(self, **kwargs):
|
|
69
|
+
tracer_provider = kwargs.get("tracer_provider")
|
|
70
|
+
tracer = get_tracer(__name__, "", tracer_provider)
|
|
71
|
+
version = importlib.metadata.version('langchain-core')
|
|
72
|
+
|
|
73
|
+
exclude_methods = ['get_name', 'get_output_schema',
|
|
74
|
+
'get_input_schema', 'get_graph', 'to_json']
|
|
75
|
+
exclude_classes = ['BaseChatPromptTemplate']
|
|
76
|
+
modules_to_patch = [
|
|
77
|
+
('langchain_core.retrievers', 'retriever',
|
|
78
|
+
generic_patch, True, True),
|
|
79
|
+
('langchain_core.prompts.chat', 'chatprompt',
|
|
80
|
+
generic_patch, True, False),
|
|
81
|
+
('langchain_core.runnables.base',
|
|
82
|
+
'runnableparallel', runnable_patch, True, True),
|
|
83
|
+
('langchain_core.runnables.passthrough',
|
|
84
|
+
'runnablepassthrough', runnable_patch, True, True),
|
|
85
|
+
('langchain_core.output_parsers.string',
|
|
86
|
+
'stroutputparser', runnable_patch, True, True),
|
|
87
|
+
('langchain_core.output_parsers.json',
|
|
88
|
+
'jsonoutputparser', runnable_patch, True, True),
|
|
89
|
+
('langchain_core.output_parsers.list',
|
|
90
|
+
'listoutputparser', runnable_patch, True, True),
|
|
91
|
+
('langchain_core.output_parsers.xml',
|
|
92
|
+
'xmloutputparser', runnable_patch, True, True)
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
for module_name, task, patch_method, trace_output, trace_input in modules_to_patch:
|
|
96
|
+
patch_module_classes(module_name, tracer, version,
|
|
97
|
+
task, patch_method, exclude_methods,
|
|
98
|
+
exclude_classes, trace_output, trace_input)
|
|
99
|
+
|
|
100
|
+
def _uninstrument(self, **kwargs):
|
|
101
|
+
pass
|