From 929187fe5cd8074c73f6b12cbabad43e076e1bcd Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 14:59:48 -0500 Subject: [PATCH 01/68] move things out of app.py --- src/core/trulens/core/app.py | 133 +++------------ src/core/trulens/core/utils/signature.py | 208 +++++++++++++++++++++++ 2 files changed, 227 insertions(+), 114 deletions(-) create mode 100644 src/core/trulens/core/utils/signature.py diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index f27ac2cdd..c295275e0 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -3,6 +3,7 @@ from abc import ABC from abc import ABCMeta from abc import abstractmethod +import contextlib import contextvars import datetime import inspect @@ -53,6 +54,7 @@ from trulens.core.utils import pyschema as pyschema_utils from trulens.core.utils import python as python_utils from trulens.core.utils import serial as serial_utils +from trulens.core.utils import signature as signature_utils from trulens.core.utils import threading as threading_utils logger = logging.getLogger(__name__) @@ -421,6 +423,18 @@ def tru(self) -> core_connector.DBConnector: pydantic.PrivateAttr(default_factory=dict) ) + tokens: List[object] = [] + """ + OTEL context tokens for the current context manager. These tokens are how the OTEL + context api keeps track of what is changed in the context, and used to undo the changes. + """ + + span_context: Optional[contextlib.AbstractContextManager] = None + """ + Span context manager. Required to help keep track of the appropriate span context + to enter/exit. + """ + def __init__( self, connector: Optional[core_connector.DBConnector] = None, @@ -692,61 +706,13 @@ def _extract_content(self, value, content_keys=["content"]): The extracted content, which may be a single value, a list of values, or a nested structure with content extracted from all levels. """ - if isinstance(value, pydantic.BaseModel): - content = getattr(value, "content", None) - if content is not None: - return content - - # If 'content' is not found, check for 'choices' attribute which indicates a ChatResponse - choices = getattr(value, "choices", None) - if choices is not None: - # Extract 'content' from the 'message' attribute of each _Choice in 'choices' - return [ - self._extract_content(choice.message) for choice in choices - ] - - # Recursively extract content from nested pydantic models - try: - return { - k: self._extract_content(v) - if isinstance(v, (pydantic.BaseModel, dict, list)) - else v - for k, v in value.model_dump().items() - } - except Exception as e: - logger.warning( - "Failed to extract content from pydantic model: %s", e - ) - # Unsure what is best to do here. Lets just return the exception - # so that it might reach the user if nothing else gets picked up - # as main input/output. - return str(e) - - elif isinstance(value, dict): - # Check for 'content' key in the dictionary - for key in content_keys: - content = value.get(key) - if content is not None: - return content - - # Recursively extract content from nested dictionaries - return { - k: self._extract_content(v) - if isinstance(v, (dict, list)) - else v - for k, v in value.items() - } - - elif isinstance(value, list): - # Handle lists by extracting content from each item - return [self._extract_content(item) for item in value] - - else: - return value + return signature_utils._extract_content( + value, content_keys=content_keys + ) def main_input( self, func: Callable, sig: Signature, bindings: BoundArguments - ) -> serial_utils.JSON: + ) -> str: """Determine (guess) the main input string for a main app call. Args: @@ -759,68 +725,7 @@ def main_input( Returns: The main input string. """ - - if bindings is None: - raise RuntimeError( - f"Cannot determine main input of unbound call to {func}: {sig}." - ) - - # ignore self - all_args = list( - v - for k, v in bindings.arguments.items() - if k not in ["self", "_self"] - ) # llama_index is using "_self" in some places - - # If there is only one string arg, it is a pretty good guess that it is - # the main input. - - # if have only containers of length 1, find the innermost non-container - focus = all_args - - while ( - not isinstance(focus, serial_utils.JSON_BASES) and len(focus) == 1 - ): - focus = focus[0] - focus = self._extract_content( - focus, content_keys=["content", "input"] - ) - - if not isinstance(focus, Sequence): - logger.warning("Focus %s is not a sequence.", focus) - break - - if isinstance(focus, serial_utils.JSON_BASES): - return str(focus) - - # Otherwise we are not sure. - logger.warning( - "Unsure what the main input string is for the call to %s with args %s.", - python_utils.callable_name(func), - bindings, - ) - - # After warning, just take the first item in each container until a - # non-container is reached. - focus = all_args - while ( - not isinstance(focus, serial_utils.JSON_BASES) and len(focus) >= 1 - ): - focus = focus[0] - focus = self._extract_content(focus) - - if not isinstance(focus, Sequence): - logger.warning("Focus %s is not a sequence.", focus) - break - - if isinstance(focus, serial_utils.JSON_BASES): - return str(focus) - - logger.warning( - "Could not determine main input/output of %s.", str(all_args) - ) - - return "TruLens: Could not determine main input from " + str(all_args) + return signature_utils.main_input(func, sig, bindings) def main_output( self, diff --git a/src/core/trulens/core/utils/signature.py b/src/core/trulens/core/utils/signature.py new file mode 100644 index 000000000..d09b18a6d --- /dev/null +++ b/src/core/trulens/core/utils/signature.py @@ -0,0 +1,208 @@ +"""Utilities related to guessing inputs and outputs of functions.""" + +from inspect import BoundArguments +from inspect import Signature +import logging +from typing import Callable, Dict, Sequence + +import pydantic +from trulens.core.utils import python as python_utils +from trulens.core.utils import serial as serial_utils + +logger = logging.getLogger(__name__) + + +def _extract_content(value, content_keys=["content"]): + """ + Extracts the 'content' from various data types commonly used by libraries + like OpenAI, Canopy, LiteLLM, etc. This method navigates nested data + structures (pydantic models, dictionaries, lists) to retrieve the + 'content' field. If 'content' is not directly available, it attempts to + extract from known structures like 'choices' in a ChatResponse. This + standardizes extracting relevant text or data from complex API responses + or internal data representations. + + Args: + value: The input data to extract content from. Can be a pydantic + model, dictionary, list, or basic data type. + + Returns: + The extracted content, which may be a single value, a list of values, + or a nested structure with content extracted from all levels. + """ + if isinstance(value, pydantic.BaseModel): + content = getattr(value, "content", None) + if content is not None: + return content + + # If 'content' is not found, check for 'choices' attribute which indicates a ChatResponse + choices = getattr(value, "choices", None) + if choices is not None: + # Extract 'content' from the 'message' attribute of each _Choice in 'choices' + return [_extract_content(choice.message) for choice in choices] + + # Recursively extract content from nested pydantic models + try: + return { + k: _extract_content(v) + if isinstance(v, (pydantic.BaseModel, dict, list)) + else v + for k, v in value.model_dump().items() + } + except Exception as e: + logger.warning( + "Failed to extract content from pydantic model: %s", e + ) + # Unsure what is best to do here. Lets just return the exception + # so that it might reach the user if nothing else gets picked up + # as main input/output. + return str(e) + + elif isinstance(value, dict): + # Check for 'content' key in the dictionary + for key in content_keys: + content = value.get(key) + if content is not None: + return content + + # Recursively extract content from nested dictionaries + return { + k: _extract_content(v) if isinstance(v, (dict, list)) else v + for k, v in value.items() + } + + elif isinstance(value, list): + # Handle lists by extracting content from each item + return [_extract_content(item) for item in value] + + else: + return value + + +def main_input(func: Callable, sig: Signature, bindings: BoundArguments) -> str: + """Determine (guess) the main input string for a main app call. + + Args: + func: The main function we are targeting in this determination. + + sig: The signature of the above. + + bindings: The arguments to be passed to the function. + + Returns: + The main input string. + """ + + if bindings is None: + raise RuntimeError( + f"Cannot determine main input of unbound call to {func}: {sig}." + ) + + # ignore self + all_args = list( + v for k, v in bindings.arguments.items() if k not in ["self", "_self"] + ) # llama_index is using "_self" in some places + + # If there is only one string arg, it is a pretty good guess that it is + # the main input. + + # if have only containers of length 1, find the innermost non-container + focus = all_args + + while not isinstance(focus, serial_utils.JSON_BASES) and len(focus) == 1: + focus = focus[0] + focus = _extract_content(focus, content_keys=["content", "input"]) + + if not isinstance(focus, Sequence): + logger.warning("Focus %s is not a sequence.", focus) + break + + if isinstance(focus, serial_utils.JSON_BASES): + return str(focus) + + # Otherwise we are not sure. + logger.warning( + "Unsure what the main input string is for the call to %s with args %s.", + python_utils.callable_name(func), + bindings, + ) + + # After warning, just take the first item in each container until a + # non-container is reached. + focus = all_args + while not isinstance(focus, serial_utils.JSON_BASES) and len(focus) >= 1: + focus = focus[0] + focus = _extract_content(focus) + + if not isinstance(focus, Sequence): + logger.warning("Focus %s is not a sequence.", focus) + break + + if isinstance(focus, serial_utils.JSON_BASES): + return str(focus) + + logger.warning( + "Could not determine main input/output of %s.", str(all_args) + ) + + return "TruLens: Could not determine main input from " + str(all_args) + + +def main_output(func: Callable, ret) -> str: + """Determine (guess) the "main output" string for a given main app call. + + This is for functions whose output is not a string. + + Args: + func: The main function whose main output we are guessing. + + sig: The signature of the above function. + + bindings: The arguments that were passed to that function. + + ret: The return value of the function. + """ + + if isinstance(ret, serial_utils.JSON_BASES): + return str(ret) + + if isinstance(ret, Sequence) and all(isinstance(x, str) for x in ret): + # Chunked/streamed outputs. + return "".join(ret) + + # Use _extract_content to get the content out of the return value + content = _extract_content(ret, content_keys=["content", "output"]) + + if isinstance(content, str): + return content + + if isinstance(content, float): + return str(content) + + if isinstance(content, Dict): + return str(next(iter(content.values()), "")) + + elif isinstance(content, Sequence): + if len(content) > 0: + return str(content[0]) + else: + return ( + f"Could not determine main output of {func.__name__}" + f" from {python_utils.class_name(type(content))} value {content}." + ) + + else: + logger.warning( + "Could not determine main output of %s from %s value %s.", + func.__name__, + python_utils.class_name(type(content)), + content, + ) + return ( + str(content) + if content is not None + else ( + f"TruLens: could not determine main output of {func.__name__} " + f"from {python_utils.class_name(type(content))} value {content}." + ) + ) From b4d2c841a3661b637dc59fca7e8560af9244ffeb Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 15:13:40 -0500 Subject: [PATCH 02/68] add tests --- tests/unit/test_signature_utils.py | 152 +++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 tests/unit/test_signature_utils.py diff --git a/tests/unit/test_signature_utils.py b/tests/unit/test_signature_utils.py new file mode 100644 index 000000000..d633e3485 --- /dev/null +++ b/tests/unit/test_signature_utils.py @@ -0,0 +1,152 @@ +"""Tests for signature.py utilities.""" + +from inspect import signature +from unittest import TestCase +from unittest import main + +from pydantic import BaseModel +from trulens.core.utils.signature import main_input +from trulens.core.utils.signature import main_output + + +class MockModel(BaseModel): + content: str + choices: list = [] + + +class MockChoice(BaseModel): + message: MockModel + + +class TestSignatureUtils(TestCase): + def testMainInput(self): + with self.subTest("Single String"): + + def func_string(a): + return a + + sig = signature(func_string) + bindings = sig.bind("test input") + self.assertEqual( + main_input(func_string, sig, bindings), "test input" + ) + + with self.subTest("Single Number"): + + def func_number(a): + return a + + sig = signature(func_number) + bindings = sig.bind(123) + self.assertEqual(main_input(func_number, sig, bindings), "123") + + with self.subTest("Pydantic Model"): + + def func_model(a): + return a + + sig = signature(func_model) + model = MockModel(content="test content") + bindings = sig.bind(model) + self.assertEqual( + main_input(func_model, sig, bindings), "test content" + ) + + with self.subTest("List of Strings"): + + def func_list(a): + return a + + sig = signature(func_list) + bindings = sig.bind(["test1", "test2"]) + self.assertEqual(main_input(func_list, sig, bindings), "test1") + + with self.subTest("List of Different Types"): + + def func_list_different_types(a): + return a + + sig = signature(func_list_different_types) + bindings = sig.bind(["test", 123, 45.6]) + self.assertEqual( + main_input(func_list_different_types, sig, bindings), "test" + ) + + with self.subTest("Keyword Arguments"): + + def func_kwargs(a, b=None): + return a if b is None else b + + sig = signature(func_kwargs) + bindings = sig.bind("test input", b="kwarg input") + self.assertEqual( + main_input(func_kwargs, sig, bindings), "test input" + ) + + with self.subTest("Dict Input"): + + def func_dict(a): + return a + + sig = signature(func_dict) + bindings = sig.bind({"key1": "value1", "key2": "value2"}) + self.assertTrue( + main_input(func_dict, sig, bindings).startswith( + "TruLens: Could not determine main input from " + ) + ) + + def testMainOutput(self): + with self.subTest("String"): + + def func_string(): + return "test output" + + ret = func_string() + self.assertEqual(main_output(func_string, ret), "test output") + + with self.subTest("List of Strings"): + + def func_list(): + return ["part1", "part2"] + + ret = func_list() + self.assertEqual(main_output(func_list, ret), "part1part2") + + with self.subTest("Pydantic Model"): + + def func_model(): + return MockModel(content="test content") + + ret = func_model() + self.assertEqual(main_output(func_model, ret), "test content") + + with self.subTest("List of Different Types"): + + def func_list_different_types(): + return ["test", 123, 45.6] + + ret = func_list_different_types() + self.assertEqual( + main_output(func_list_different_types, ret), "test" + ) + + with self.subTest("Keyword Arguments"): + + def func_kwargs(a, b=None): + return a if b is None else b + + ret = func_kwargs("test output", b="kwarg output") + self.assertEqual(main_output(func_kwargs, ret), "kwarg output") + + with self.subTest("Dict Output"): + + def func_dict(): + return {"key1": "value1", "key2": "value2"} + + ret = func_dict() + self.assertEqual(main_output(func_dict, ret), "value1") + + +if __name__ == "__main__": + main() From f112f38ef3a1462d6fbc815478c5a9faed4a93a2 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 16:02:42 -0500 Subject: [PATCH 03/68] update golden --- ...tel_instrument__test_instrument_decorator.csv | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 40837d509..e141582c3 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,9 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",7870250274962447839,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387607,2024-12-22 11:20:26.392174,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '', 'span_id': '7870250274962447839'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7870250274962447839', 'status': 'STATUS_CODE_UNSET'}",8819384298151247754,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387652,2024-12-22 11:20:26.391272,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '7870250274962447839', 'span_id': '8819384298151247754'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '8819384298151247754', 'status': 'STATUS_CODE_UNSET'}",2622992513876904334,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387679,2024-12-22 11:20:26.389939,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '8819384298151247754', 'span_id': '2622992513876904334'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '2622992513876904334', 'status': 'STATUS_CODE_UNSET'}",11864485227397090485,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387705,2024-12-22 11:20:26.387762,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '2622992513876904334', 'span_id': '11864485227397090485'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",10786111609955477438,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393563,2024-12-22 11:20:26.397446,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '', 'span_id': '10786111609955477438'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10786111609955477438', 'status': 'STATUS_CODE_UNSET'}",7881765616183808794,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393586,2024-12-22 11:20:26.396613,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '10786111609955477438', 'span_id': '7881765616183808794'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7881765616183808794', 'status': 'STATUS_CODE_UNSET'}",4318803655649897130,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393603,2024-12-22 11:20:26.395227,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '7881765616183808794', 'span_id': '4318803655649897130'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4318803655649897130', 'status': 'STATUS_CODE_ERROR'}",11457830288984624191,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393630,2024-12-22 11:20:26.394348,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '4318803655649897130', 'span_id': '11457830288984624191'}" +0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5969803548429788759,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259863,2025-01-03 16:02:25.264734,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '', 'span_id': '5969803548429788759'}" +1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5969803548429788759', 'status': 'STATUS_CODE_UNSET'}",10911343541627320764,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259898,2025-01-03 16:02:25.263680,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '5969803548429788759', 'span_id': '10911343541627320764'}" +2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10911343541627320764', 'status': 'STATUS_CODE_UNSET'}",5626990932821740374,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259927,2025-01-03 16:02:25.262614,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '10911343541627320764', 'span_id': '5626990932821740374'}" +3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5626990932821740374', 'status': 'STATUS_CODE_UNSET'}",13646944316784113308,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259948,2025-01-03 16:02:25.259995,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '5626990932821740374', 'span_id': '13646944316784113308'}" +4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5223223510458292547,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266034,2025-01-03 16:02:25.270569,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '', 'span_id': '5223223510458292547'}" +5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5223223510458292547', 'status': 'STATUS_CODE_UNSET'}",3410201565535727928,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266068,2025-01-03 16:02:25.269595,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '5223223510458292547', 'span_id': '3410201565535727928'}" +6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3410201565535727928', 'status': 'STATUS_CODE_UNSET'}",10512895092329327332,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266096,2025-01-03 16:02:25.268579,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '3410201565535727928', 'span_id': '10512895092329327332'}" +7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10512895092329327332', 'status': 'STATUS_CODE_ERROR'}",2101788849711682907,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266116,2025-01-03 16:02:25.267643,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '10512895092329327332', 'span_id': '2101788849711682907'}" From 197cd0a184a24d49bd29159ba2f400ae0653be92 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 16:12:25 -0500 Subject: [PATCH 04/68] update app.py --- src/core/trulens/core/app.py | 13 ------------- ...tel_instrument__test_instrument_decorator.csv | 16 ++++++++-------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index c295275e0..55fcae912 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -3,7 +3,6 @@ from abc import ABC from abc import ABCMeta from abc import abstractmethod -import contextlib import contextvars import datetime import inspect @@ -423,18 +422,6 @@ def tru(self) -> core_connector.DBConnector: pydantic.PrivateAttr(default_factory=dict) ) - tokens: List[object] = [] - """ - OTEL context tokens for the current context manager. These tokens are how the OTEL - context api keeps track of what is changed in the context, and used to undo the changes. - """ - - span_context: Optional[contextlib.AbstractContextManager] = None - """ - Span context manager. Required to help keep track of the appropriate span context - to enter/exit. - """ - def __init__( self, connector: Optional[core_connector.DBConnector] = None, diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index e141582c3..7bb0f80d5 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,9 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5969803548429788759,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259863,2025-01-03 16:02:25.264734,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '', 'span_id': '5969803548429788759'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5969803548429788759', 'status': 'STATUS_CODE_UNSET'}",10911343541627320764,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259898,2025-01-03 16:02:25.263680,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '5969803548429788759', 'span_id': '10911343541627320764'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10911343541627320764', 'status': 'STATUS_CODE_UNSET'}",5626990932821740374,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259927,2025-01-03 16:02:25.262614,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '10911343541627320764', 'span_id': '5626990932821740374'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5626990932821740374', 'status': 'STATUS_CODE_UNSET'}",13646944316784113308,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.259948,2025-01-03 16:02:25.259995,"{'trace_id': '208166356089925564160103123203680032153', 'parent_id': '5626990932821740374', 'span_id': '13646944316784113308'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5223223510458292547,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266034,2025-01-03 16:02:25.270569,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '', 'span_id': '5223223510458292547'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5223223510458292547', 'status': 'STATUS_CODE_UNSET'}",3410201565535727928,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266068,2025-01-03 16:02:25.269595,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '5223223510458292547', 'span_id': '3410201565535727928'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3410201565535727928', 'status': 'STATUS_CODE_UNSET'}",10512895092329327332,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266096,2025-01-03 16:02:25.268579,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '3410201565535727928', 'span_id': '10512895092329327332'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10512895092329327332', 'status': 'STATUS_CODE_ERROR'}",2101788849711682907,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:02:25.266116,2025-01-03 16:02:25.267643,"{'trace_id': '327329804912645718217465489268934697739', 'parent_id': '10512895092329327332', 'span_id': '2101788849711682907'}" +0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",4811707390867254808,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744613,2025-01-03 16:12:15.749467,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '', 'span_id': '4811707390867254808'}" +1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4811707390867254808', 'status': 'STATUS_CODE_UNSET'}",13240069390338796989,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744651,2025-01-03 16:12:15.748476,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '4811707390867254808', 'span_id': '13240069390338796989'}" +2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '13240069390338796989', 'status': 'STATUS_CODE_UNSET'}",3742031770395817343,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744673,2025-01-03 16:12:15.747341,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '13240069390338796989', 'span_id': '3742031770395817343'}" +3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3742031770395817343', 'status': 'STATUS_CODE_UNSET'}",6662018871918753503,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744693,2025-01-03 16:12:15.744735,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '3742031770395817343', 'span_id': '6662018871918753503'}" +4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",16677211504835074340,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750507,2025-01-03 16:12:15.753945,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '', 'span_id': '16677211504835074340'}" +5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '16677211504835074340', 'status': 'STATUS_CODE_UNSET'}",12244808177419859809,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750537,2025-01-03 16:12:15.752975,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '16677211504835074340', 'span_id': '12244808177419859809'}" +6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '12244808177419859809', 'status': 'STATUS_CODE_UNSET'}",14648833998100173003,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750561,2025-01-03 16:12:15.752127,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '12244808177419859809', 'span_id': '14648833998100173003'}" +7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '14648833998100173003', 'status': 'STATUS_CODE_ERROR'}",9610646787656200026,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750587,2025-01-03 16:12:15.751118,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '14648833998100173003', 'span_id': '9610646787656200026'}" From 062d197d3843c76d0775fbfe921b4da12075fefd Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 16:16:38 -0500 Subject: [PATCH 05/68] update golden --- ...tel_instrument__test_instrument_decorator.csv | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 7bb0f80d5..2e37c67ef 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,9 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",4811707390867254808,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744613,2025-01-03 16:12:15.749467,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '', 'span_id': '4811707390867254808'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4811707390867254808', 'status': 'STATUS_CODE_UNSET'}",13240069390338796989,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744651,2025-01-03 16:12:15.748476,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '4811707390867254808', 'span_id': '13240069390338796989'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '13240069390338796989', 'status': 'STATUS_CODE_UNSET'}",3742031770395817343,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744673,2025-01-03 16:12:15.747341,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '13240069390338796989', 'span_id': '3742031770395817343'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3742031770395817343', 'status': 'STATUS_CODE_UNSET'}",6662018871918753503,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.744693,2025-01-03 16:12:15.744735,"{'trace_id': '131019396712808275374725342199248186962', 'parent_id': '3742031770395817343', 'span_id': '6662018871918753503'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",16677211504835074340,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750507,2025-01-03 16:12:15.753945,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '', 'span_id': '16677211504835074340'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '16677211504835074340', 'status': 'STATUS_CODE_UNSET'}",12244808177419859809,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750537,2025-01-03 16:12:15.752975,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '16677211504835074340', 'span_id': '12244808177419859809'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '12244808177419859809', 'status': 'STATUS_CODE_UNSET'}",14648833998100173003,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750561,2025-01-03 16:12:15.752127,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '12244808177419859809', 'span_id': '14648833998100173003'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '14648833998100173003', 'status': 'STATUS_CODE_ERROR'}",9610646787656200026,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:12:15.750587,2025-01-03 16:12:15.751118,"{'trace_id': '113682791646381480916847651990285575694', 'parent_id': '14648833998100173003', 'span_id': '9610646787656200026'}" +0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",4528122908806174067,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784150,2025-01-03 16:16:21.788774,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '', 'span_id': '4528122908806174067'}" +1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4528122908806174067', 'status': 'STATUS_CODE_UNSET'}",1292155474894025207,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784185,2025-01-03 16:16:21.787858,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '4528122908806174067', 'span_id': '1292155474894025207'}" +2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '1292155474894025207', 'status': 'STATUS_CODE_UNSET'}",3888805675315444561,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784207,2025-01-03 16:16:21.786906,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '1292155474894025207', 'span_id': '3888805675315444561'}" +3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3888805675315444561', 'status': 'STATUS_CODE_UNSET'}",2981064322902398822,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784227,2025-01-03 16:16:21.784270,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '3888805675315444561', 'span_id': '2981064322902398822'}" +4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5769330565877363971,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789759,2025-01-03 16:16:21.792583,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '', 'span_id': '5769330565877363971'}" +5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5769330565877363971', 'status': 'STATUS_CODE_UNSET'}",2752148020960945347,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789784,2025-01-03 16:16:21.791817,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '5769330565877363971', 'span_id': '2752148020960945347'}" +6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '2752148020960945347', 'status': 'STATUS_CODE_UNSET'}",9061628541897664176,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789805,2025-01-03 16:16:21.791101,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '2752148020960945347', 'span_id': '9061628541897664176'}" +7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '9061628541897664176', 'status': 'STATUS_CODE_ERROR'}",8569213229676697562,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789824,2025-01-03 16:16:21.790332,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '9061628541897664176', 'span_id': '8569213229676697562'}" From 1cc2a954c8b8f0934db488b5e11649b2fe08f2d1 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 17:47:07 -0500 Subject: [PATCH 06/68] update golden --- tests/unit/static/golden/api.trulens.3.11.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/static/golden/api.trulens.3.11.yaml b/tests/unit/static/golden/api.trulens.3.11.yaml index 7e2e8c470..38b4284e8 100644 --- a/tests/unit/static/golden/api.trulens.3.11.yaml +++ b/tests/unit/static/golden/api.trulens.3.11.yaml @@ -1654,6 +1654,12 @@ trulens.core.utils.serial.StepItemOrAttribute: __class__: pydantic._internal._model_construction.ModelMetaclass attributes: get_item_or_attribute: builtins.function +trulens.core.utils.signature: + __class__: builtins.module + highs: {} + lows: + main_input: builtins.function + main_output: builtins.function trulens.core.utils.text: __class__: builtins.module highs: {} From dfc3fa50d8d8908631cb88913479c119024e741d Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Sat, 4 Jan 2025 07:51:44 -0500 Subject: [PATCH 07/68] move main output --- src/core/trulens/core/app.py | 60 ++---------------------------------- 1 file changed, 2 insertions(+), 58 deletions(-) diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index 55fcae912..2739d370e 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -720,64 +720,8 @@ def main_output( sig: Signature, # pylint: disable=W0613 bindings: BoundArguments, # pylint: disable=W0613 ret: Any, - ) -> serial_utils.JSON: - """Determine (guess) the "main output" string for a given main app call. - - This is for functions whose output is not a string. - - Args: - func: The main function whose main output we are guessing. - - sig: The signature of the above function. - - bindings: The arguments that were passed to that function. - - ret: The return value of the function. - """ - - if isinstance(ret, serial_utils.JSON_BASES): - return str(ret) - - if isinstance(ret, Sequence) and all(isinstance(x, str) for x in ret): - # Chunked/streamed outputs. - return "".join(ret) - - # Use _extract_content to get the content out of the return value - content = self._extract_content(ret, content_keys=["content", "output"]) - - if isinstance(content, str): - return content - - if isinstance(content, float): - return str(content) - - if isinstance(content, Dict): - return str(next(iter(content.values()), "")) - - elif isinstance(content, Sequence): - if len(content) > 0: - return str(content[0]) - else: - return ( - f"Could not determine main output of {func.__name__}" - f" from {python_utils.class_name(type(content))} value {content}." - ) - - else: - logger.warning( - "Could not determine main output of %s from %s value %s.", - func.__name__, - python_utils.class_name(type(content)), - content, - ) - return ( - str(content) - if content is not None - else ( - f"TruLens: could not determine main output of {func.__name__} " - f"from {python_utils.class_name(type(content))} value {content}." - ) - ) + ) -> str: + return signature_utils.main_output(func, ret) # Experimental OTEL WithInstrumentCallbacks requirement def _on_new_recording_span( From 95f250c5ad587264db302f657f6a7b1f62d43eb4 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 14:02:47 -0800 Subject: [PATCH 08/68] PR feedback --- src/core/trulens/core/utils/signature.py | 28 ++++++++----------- ..._instrument__test_instrument_decorator.csv | 16 +++++------ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/core/trulens/core/utils/signature.py b/src/core/trulens/core/utils/signature.py index d09b18a6d..5c06d0421 100644 --- a/src/core/trulens/core/utils/signature.py +++ b/src/core/trulens/core/utils/signature.py @@ -3,7 +3,7 @@ from inspect import BoundArguments from inspect import Signature import logging -from typing import Callable, Dict, Sequence +from typing import Any, Callable, Dict, Sequence import pydantic from trulens.core.utils import python as python_utils @@ -148,7 +148,7 @@ def main_input(func: Callable, sig: Signature, bindings: BoundArguments) -> str: return "TruLens: Could not determine main input from " + str(all_args) -def main_output(func: Callable, ret) -> str: +def main_output(func: Callable, ret: Any) -> str: """Determine (guess) the "main output" string for a given main app call. This is for functions whose output is not a string. @@ -182,27 +182,21 @@ def main_output(func: Callable, ret) -> str: if isinstance(content, Dict): return str(next(iter(content.values()), "")) - elif isinstance(content, Sequence): + error_message = ( + f"Could not determine main output of {func.__name__}" + f" from {python_utils.class_name(type(content))} value {content}." + ) + + if isinstance(content, Sequence): if len(content) > 0: return str(content[0]) else: - return ( - f"Could not determine main output of {func.__name__}" - f" from {python_utils.class_name(type(content))} value {content}." - ) + return error_message else: - logger.warning( - "Could not determine main output of %s from %s value %s.", - func.__name__, - python_utils.class_name(type(content)), - content, - ) + logger.warning(error_message) return ( str(content) if content is not None - else ( - f"TruLens: could not determine main output of {func.__name__} " - f"from {python_utils.class_name(type(content))} value {content}." - ) + else (f"TruLens: {error_message}") ) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 2e37c67ef..40837d509 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,9 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",4528122908806174067,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784150,2025-01-03 16:16:21.788774,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '', 'span_id': '4528122908806174067'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4528122908806174067', 'status': 'STATUS_CODE_UNSET'}",1292155474894025207,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784185,2025-01-03 16:16:21.787858,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '4528122908806174067', 'span_id': '1292155474894025207'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '1292155474894025207', 'status': 'STATUS_CODE_UNSET'}",3888805675315444561,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784207,2025-01-03 16:16:21.786906,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '1292155474894025207', 'span_id': '3888805675315444561'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3888805675315444561', 'status': 'STATUS_CODE_UNSET'}",2981064322902398822,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.784227,2025-01-03 16:16:21.784270,"{'trace_id': '234675728271965379675807309407197276497', 'parent_id': '3888805675315444561', 'span_id': '2981064322902398822'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5769330565877363971,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789759,2025-01-03 16:16:21.792583,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '', 'span_id': '5769330565877363971'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5769330565877363971', 'status': 'STATUS_CODE_UNSET'}",2752148020960945347,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789784,2025-01-03 16:16:21.791817,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '5769330565877363971', 'span_id': '2752148020960945347'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '2752148020960945347', 'status': 'STATUS_CODE_UNSET'}",9061628541897664176,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789805,2025-01-03 16:16:21.791101,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '2752148020960945347', 'span_id': '9061628541897664176'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '9061628541897664176', 'status': 'STATUS_CODE_ERROR'}",8569213229676697562,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-03 16:16:21.789824,2025-01-03 16:16:21.790332,"{'trace_id': '222451439252168566434747551697590762898', 'parent_id': '9061628541897664176', 'span_id': '8569213229676697562'}" +0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",7870250274962447839,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387607,2024-12-22 11:20:26.392174,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '', 'span_id': '7870250274962447839'}" +1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7870250274962447839', 'status': 'STATUS_CODE_UNSET'}",8819384298151247754,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387652,2024-12-22 11:20:26.391272,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '7870250274962447839', 'span_id': '8819384298151247754'}" +2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '8819384298151247754', 'status': 'STATUS_CODE_UNSET'}",2622992513876904334,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387679,2024-12-22 11:20:26.389939,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '8819384298151247754', 'span_id': '2622992513876904334'}" +3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '2622992513876904334', 'status': 'STATUS_CODE_UNSET'}",11864485227397090485,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387705,2024-12-22 11:20:26.387762,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '2622992513876904334', 'span_id': '11864485227397090485'}" +4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",10786111609955477438,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393563,2024-12-22 11:20:26.397446,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '', 'span_id': '10786111609955477438'}" +5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10786111609955477438', 'status': 'STATUS_CODE_UNSET'}",7881765616183808794,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393586,2024-12-22 11:20:26.396613,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '10786111609955477438', 'span_id': '7881765616183808794'}" +6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7881765616183808794', 'status': 'STATUS_CODE_UNSET'}",4318803655649897130,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393603,2024-12-22 11:20:26.395227,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '7881765616183808794', 'span_id': '4318803655649897130'}" +7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4318803655649897130', 'status': 'STATUS_CODE_ERROR'}",11457830288984624191,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393630,2024-12-22 11:20:26.394348,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '4318803655649897130', 'span_id': '11457830288984624191'}" From 788f945c4edbbe259daf29743ada78be87b54c0b Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 17:50:37 -0800 Subject: [PATCH 09/68] not sure why this is needed --- .../unit/static/golden/api.trulens.3.11.yaml | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/tests/unit/static/golden/api.trulens.3.11.yaml b/tests/unit/static/golden/api.trulens.3.11.yaml index 38b4284e8..6f0605ccf 100644 --- a/tests/unit/static/golden/api.trulens.3.11.yaml +++ b/tests/unit/static/golden/api.trulens.3.11.yaml @@ -538,9 +538,9 @@ trulens.core.feedback.feedback.Feedback: aggregator: typing.Union[trulens.core.utils.pyschema.Function, trulens.core.utils.pyschema.Method, builtins.NoneType] check_selectors: builtins.function - criteria: typing.Optional[builtins.str, builtins.NoneType] combinations: typing.Optional[trulens.core.schema.feedback.FeedbackCombinations, builtins.NoneType] + criteria: typing.Optional[builtins.str, builtins.NoneType] evaluate_deferred: builtins.staticmethod examples: typing.Optional[typing.List[typing.Tuple], builtins.NoneType] extract_selection: builtins.function @@ -2314,6 +2314,7 @@ trulens.feedback.v2.feedback: ClassificationModel: pydantic._internal._model_construction.ModelMetaclass Coherence: pydantic._internal._model_construction.ModelMetaclass CompletionModel: pydantic._internal._model_construction.ModelMetaclass + Comprehensiveness: pydantic._internal._model_construction.ModelMetaclass Conciseness: pydantic._internal._model_construction.ModelMetaclass ContextRelevance: pydantic._internal._model_construction.ModelMetaclass Controversiality: pydantic._internal._model_construction.ModelMetaclass @@ -2398,21 +2399,6 @@ trulens.feedback.v2.feedback.BinarySentimentModel: feedback: trulens.feedback.v2.feedback.Feedback id: builtins.str output_type: trulens.feedback.v2.feedback.FeedbackOutputType -trulens.feedback.v2.feedback.Comprehensiveness: - __bases__: - - trulens.feedback.v2.feedback.Semantics - - trulens.feedback.v2.feedback.WithPrompt - - trulens.feedback.v2.feedback.CriteriaOutputSpaceMixin - __class__: pydantic._internal._model_construction.ModelMetaclass - attributes: - criteria: builtins.str - criteria_template: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] - output_space: builtins.str - output_space_prompt: builtins.str - system_prompt: builtins.str - system_prompt_template: builtins.str - user_prompt: builtins.str trulens.feedback.v2.feedback.COTExplained: __bases__: - trulens.feedback.v2.feedback.Feedback @@ -2446,6 +2432,21 @@ trulens.feedback.v2.feedback.CompletionModel: max_output_tokens: builtins.int max_prompt_tokens: builtins.int of_langchain_llm: builtins.staticmethod +trulens.feedback.v2.feedback.Comprehensiveness: + __bases__: + - trulens.feedback.v2.feedback.Semantics + - trulens.feedback.v2.feedback.WithPrompt + - trulens.feedback.v2.feedback.CriteriaOutputSpaceMixin + __class__: pydantic._internal._model_construction.ModelMetaclass + attributes: + criteria: builtins.str + criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] + output_space: builtins.str + output_space_prompt: builtins.str + system_prompt: builtins.str + system_prompt_template: builtins.str + user_prompt: builtins.str trulens.feedback.v2.feedback.Conciseness: __bases__: - trulens.feedback.v2.feedback.Semantics @@ -2477,9 +2478,9 @@ trulens.feedback.v2.feedback.Controversiality: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2499,9 +2500,9 @@ trulens.feedback.v2.feedback.Criminality: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2633,9 +2634,9 @@ trulens.feedback.v2.feedback.Harmfulness: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2659,9 +2660,9 @@ trulens.feedback.v2.feedback.Helpfulness: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2673,9 +2674,9 @@ trulens.feedback.v2.feedback.Insensitivity: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2699,9 +2700,9 @@ trulens.feedback.v2.feedback.Maliciousness: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2713,9 +2714,9 @@ trulens.feedback.v2.feedback.Misogyny: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2805,9 +2806,9 @@ trulens.feedback.v2.feedback.Sentiment: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str @@ -2832,9 +2833,9 @@ trulens.feedback.v2.feedback.Stereotypes: attributes: criteria: builtins.str criteria_template: builtins.str + languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] output_space: builtins.str output_space_prompt: builtins.str - languages: typing.Optional[typing.List[builtins.str], builtins.NoneType] system_prompt: builtins.str system_prompt_template: builtins.str user_prompt: builtins.str From ef17076fa3ac7dd98e8ac922757ee47c3067baa7 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 15:56:25 -0500 Subject: [PATCH 10/68] add tests --- .../otel_tracing/core/instrument.py | 118 ++++---------- .../experimental/otel_tracing/core/span.py | 140 ++++++++++++++++ tests/unit/test_otel_span.py | 149 ++++++++++++++++++ 3 files changed, 318 insertions(+), 89 deletions(-) create mode 100644 src/core/trulens/experimental/otel_tracing/core/span.py create mode 100644 tests/unit/test_otel_span.py diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index b1a2682ef..0f724c72f 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -1,9 +1,16 @@ from functools import wraps import logging -from typing import Any, Callable, Dict, Optional, Union +from typing import Callable, Optional from opentelemetry import trace from trulens.experimental.otel_tracing.core.init import TRULENS_SERVICE_NAME +from trulens.experimental.otel_tracing.core.span import Attributes +from trulens.experimental.otel_tracing.core.span import ( + set_general_span_attributes, +) +from trulens.experimental.otel_tracing.core.span import ( + set_user_defined_attributes, +) from trulens.otel.semconv.trace import SpanAttributes logger = logging.getLogger(__name__) @@ -11,58 +18,14 @@ def instrument( *, - attributes: Optional[ - Union[ - Dict[str, Any], - Callable[ - [Optional[Any], Optional[Exception], Any, Any], Dict[str, Any] - ], - ] - ] = {}, + span_type: SpanAttributes.SpanType = SpanAttributes.SpanType.UNKNOWN, + attributes: Attributes = {}, ): """ Decorator for marking functions to be instrumented in custom classes that are wrapped by TruCustomApp, with OpenTelemetry tracing. """ - def _validate_selector_name(attributes: Dict[str, Any]) -> Dict[str, Any]: - result = attributes.copy() - - if ( - SpanAttributes.SELECTOR_NAME_KEY in result - and SpanAttributes.SELECTOR_NAME in result - ): - raise ValueError( - f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." - ) - - if SpanAttributes.SELECTOR_NAME in result: - # Transfer the trulens namespaced to the non-trulens namespaced key. - result[SpanAttributes.SELECTOR_NAME_KEY] = result[ - SpanAttributes.SELECTOR_NAME - ] - del result[SpanAttributes.SELECTOR_NAME] - - if SpanAttributes.SELECTOR_NAME_KEY in result: - selector_name = result[SpanAttributes.SELECTOR_NAME_KEY] - if not isinstance(selector_name, str): - raise ValueError( - f"Selector name must be a string, not {type(selector_name)}" - ) - - return result - - def _validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: - if not isinstance(attributes, dict) or any([ - not isinstance(key, str) for key in attributes.keys() - ]): - raise ValueError( - "Attributes must be a dictionary with string keys." - ) - return _validate_selector_name(attributes) - # TODO: validate OTEL attributes. - # TODO: validate span type attributes. - def inner_decorator(func: Callable): @wraps(func) def wrapper(*args, **kwargs): @@ -77,6 +40,8 @@ def wrapper(*args, **kwargs): func_exception: Optional[Exception] = None attributes_exception: Optional[Exception] = None + span.set_attribute("name", func.__name__) + try: ret = func(*args, **kwargs) except Exception as e: @@ -84,55 +49,30 @@ def wrapper(*args, **kwargs): # It's on the user to deal with None as a return value. func_exception = e - try: - attributes_to_add = {} - - # Set the user provider attributes. - if attributes: - if callable(attributes): - attributes_to_add = attributes( - ret, func_exception, *args, **kwargs - ) - else: - attributes_to_add = attributes - - logger.info(f"Attributes to add: {attributes_to_add}") - - final_attributes = _validate_attributes(attributes_to_add) + set_general_span_attributes(span, span_type) + attributes_exception = None - prefix = "trulens." - if ( - SpanAttributes.SPAN_TYPE in final_attributes - and final_attributes[SpanAttributes.SPAN_TYPE] - != SpanAttributes.SpanType.UNKNOWN - ): - prefix += ( - final_attributes[SpanAttributes.SPAN_TYPE] + "." - ) - - for key, value in final_attributes.items(): - span.set_attribute(prefix + key, value) - - if ( - key != SpanAttributes.SELECTOR_NAME_KEY - and SpanAttributes.SELECTOR_NAME_KEY - in final_attributes - ): - span.set_attribute( - f"trulens.{final_attributes[SpanAttributes.SELECTOR_NAME_KEY]}.{key}", - value, - ) + try: + set_user_defined_attributes( + span, + span_type=span_type, + args=args, + kwargs=kwargs, + ret=ret, + func_exception=func_exception, + attributes=attributes, + ) except Exception as e: attributes_exception = e logger.error(f"Error setting attributes: {e}") - if func_exception: - raise func_exception - if attributes_exception: - raise attributes_exception + exception = func_exception or attributes_exception + + if exception: + raise exception - return ret + return ret return wrapper diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py new file mode 100644 index 000000000..72b6c67ff --- /dev/null +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -0,0 +1,140 @@ +""" +This file contains utility functions specific to certain span types. +""" + +from inspect import signature +import logging +from typing import Any, Callable, Dict, Optional, Union + +from opentelemetry.trace.span import Span +from trulens.core.utils import signature as signature_utils +from trulens.otel.semconv.trace import SpanAttributes + +logger = logging.getLogger(__name__) + +""" +Type definitions +""" +Attributes = Optional[ + Union[ + Dict[str, Any], + Callable[ + [Optional[Any], Optional[Exception], Any, Any], Dict[str, Any] + ], + ] +] + +""" +General utilites for all spans +""" + + +def validate_selector_name(attributes: Dict[str, Any]) -> Dict[str, Any]: + """ + Utility function to validate the selector name in the attributes. + + It does the following: + 1. Ensure that the selector name is a string. + 2. Ensure that the selector name is keyed with either the trulens/non-trulens key variations. + 3. Ensure that the selector name is not set in both the trulens and non-trulens key variations. + """ + + result = attributes.copy() + + if ( + SpanAttributes.SELECTOR_NAME_KEY in result + and SpanAttributes.SELECTOR_NAME in result + ): + raise ValueError( + f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." + ) + + if SpanAttributes.SELECTOR_NAME in result: + # Transfer the trulens-namespaced key to the non-trulens-namespaced key. + result[SpanAttributes.SELECTOR_NAME_KEY] = result[ + SpanAttributes.SELECTOR_NAME + ] + del result[SpanAttributes.SELECTOR_NAME] + + if SpanAttributes.SELECTOR_NAME_KEY in result: + selector_name = result[SpanAttributes.SELECTOR_NAME_KEY] + if not isinstance(selector_name, str): + raise ValueError( + f"Selector name must be a string, not {type(selector_name)}" + ) + + return result + + +def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: + """ + Utility function to validate span attributes based on the span type. + """ + if not isinstance(attributes, dict) or any([ + not isinstance(key, str) for key in attributes.keys() + ]): + raise ValueError("Attributes must be a dictionary with string keys.") + + if SpanAttributes.SPAN_TYPE in attributes: + raise ValueError("Span type should not be set in attributes.") + + return validate_selector_name(attributes) + # TODO: validate Span type attributes. + + +def set_general_span_attributes( + span: Span, /, span_type: SpanAttributes.SpanType +) -> Span: + span.set_attribute("kind", "SPAN_KIND_TRULENS") + span.set_attribute(SpanAttributes.SPAN_TYPE, span_type) + + return span + + +def set_user_defined_attributes( + span: Span, + *, + span_type: SpanAttributes.SpanType, + args: tuple, + kwargs: dict, + ret, + func_exception: Optional[Exception], + attributes: Attributes, +) -> None: + attributes_to_add = {} + + # Set the user-provided attributes. + if attributes: + if callable(attributes): + attributes_to_add = attributes(ret, func_exception, *args, **kwargs) + else: + attributes_to_add = attributes + + logger.info(f"Attributes to add: {attributes_to_add}") + + final_attributes = validate_attributes(attributes_to_add) + + prefix = f"trulens.{span_type}." + + for key, value in final_attributes.items(): + span.set_attribute(prefix + key, value) + + if ( + key != SpanAttributes.SELECTOR_NAME_KEY + and SpanAttributes.SELECTOR_NAME_KEY in final_attributes + ): + span.set_attribute( + f"trulens.{final_attributes[SpanAttributes.SELECTOR_NAME_KEY]}.{key}", + value, + ) + + +""" +MAIN SPAN +""" + + +def get_main_input(func: Callable, args: tuple, kwargs: dict) -> str: + sig = signature(func) + bindings = signature(func).bind(*args, **kwargs) + return signature_utils.main_input(func, sig, bindings) diff --git a/tests/unit/test_otel_span.py b/tests/unit/test_otel_span.py new file mode 100644 index 000000000..262c58b8d --- /dev/null +++ b/tests/unit/test_otel_span.py @@ -0,0 +1,149 @@ +""" +Tests for OTEL instrument decorator. +""" + +from unittest import TestCase +from unittest import main +from unittest.mock import Mock + +from trulens.experimental.otel_tracing.core.span import ( + set_user_defined_attributes, +) +from trulens.experimental.otel_tracing.core.span import validate_attributes +from trulens.experimental.otel_tracing.core.span import validate_selector_name +from trulens.otel.semconv.trace import SpanAttributes + + +class TestOTELSpan(TestCase): + def testValidateSelectorName(self): + with self.subTest("No selector name"): + self.assertEqual( + validate_selector_name({}), + {}, + ) + + with self.subTest( + f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." + ): + self.assertRaises( + ValueError, + validate_selector_name, + { + SpanAttributes.SELECTOR_NAME_KEY: "key", + SpanAttributes.SELECTOR_NAME: "name", + }, + ) + + with self.subTest("Non-string"): + self.assertRaises( + ValueError, + validate_selector_name, + { + SpanAttributes.SELECTOR_NAME_KEY: 42, + }, + ) + + with self.subTest(f"Valid {SpanAttributes.SELECTOR_NAME}"): + self.assertEqual( + validate_selector_name({SpanAttributes.SELECTOR_NAME: "name"}), + {SpanAttributes.SELECTOR_NAME_KEY: "name"}, + ) + + with self.subTest(f"Valid {SpanAttributes.SELECTOR_NAME_KEY}"): + self.assertEqual( + validate_selector_name({ + SpanAttributes.SELECTOR_NAME_KEY: "name" + }), + {SpanAttributes.SELECTOR_NAME_KEY: "name"}, + ) + + def testValidateAttributes(self): + with self.subTest("Empty attributes"): + self.assertEqual( + validate_attributes({}), + {}, + ) + + with self.subTest("Valid attributes"): + self.assertEqual( + validate_attributes({ + "key1": "value1", + "key2": 123, + "key3": True, + }), + { + "key1": "value1", + "key2": 123, + "key3": True, + }, + ) + + with self.subTest("Invalid key type"): + self.assertRaises( + ValueError, + validate_attributes, + { + 123: "value", + }, + ) + + with self.subTest("Span type should not be set in attributes"): + self.assertRaises( + ValueError, + validate_attributes, + { + SpanAttributes.SPAN_TYPE: SpanAttributes.SpanType.UNKNOWN, + }, + ) + + def testSetUserDefinedAttributes(self): + span = Mock() + span_type = SpanAttributes.SpanType.UNKNOWN + + with self.subTest("Callable attributes"): + + def attributes_callable(ret, func_exception, *args, **kwargs): + return {"key1": "value1"} + + set_user_defined_attributes( + span, + span_type=span_type, + args=(), + kwargs={}, + ret=None, + func_exception=None, + attributes=attributes_callable, + ) + span.set_attribute.assert_any_call("trulens.unknown.key1", "value1") + + with self.subTest("Dictionary attributes"): + attributes_dict = {"key2": "value2"} + + set_user_defined_attributes( + span, + span_type=span_type, + args=(), + kwargs={}, + ret=None, + func_exception=None, + attributes=attributes_dict, + ) + span.set_attribute.assert_any_call("trulens.unknown.key2", "value2") + + with self.subTest("Invalid attributes"): + attributes_invalid = {123: "value"} + + with self.assertRaises(ValueError): + set_user_defined_attributes( + span, + span_type=span_type, + args=(), + kwargs={}, + ret=None, + func_exception=None, + attributes=attributes_invalid, # type: ignore + ) + + +if __name__ == "__main__": + main() From 2c9d9bb63182c88d3b5e06df616d36b01b1143af Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 16:07:25 -0500 Subject: [PATCH 11/68] update golden --- ...tel_instrument__test_instrument_decorator.csv | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 40837d509..b3d0f1573 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,9 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",7870250274962447839,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387607,2024-12-22 11:20:26.392174,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '', 'span_id': '7870250274962447839'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7870250274962447839', 'status': 'STATUS_CODE_UNSET'}",8819384298151247754,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387652,2024-12-22 11:20:26.391272,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '7870250274962447839', 'span_id': '8819384298151247754'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '8819384298151247754', 'status': 'STATUS_CODE_UNSET'}",2622992513876904334,"{'trulens.nested2_ret': 'nested2: nested3', 'trulens.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387679,2024-12-22 11:20:26.389939,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '8819384298151247754', 'span_id': '2622992513876904334'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '2622992513876904334', 'status': 'STATUS_CODE_UNSET'}",11864485227397090485,"{'trulens.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.387705,2024-12-22 11:20:26.387762,"{'trace_id': '113376089399064103615948241236196474059', 'parent_id': '2622992513876904334', 'span_id': '11864485227397090485'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",10786111609955477438,{},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393563,2024-12-22 11:20:26.397446,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '', 'span_id': '10786111609955477438'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '10786111609955477438', 'status': 'STATUS_CODE_UNSET'}",7881765616183808794,{'trulens.nested_attr1': 'value1'},EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393586,2024-12-22 11:20:26.396613,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '10786111609955477438', 'span_id': '7881765616183808794'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7881765616183808794', 'status': 'STATUS_CODE_UNSET'}",4318803655649897130,"{'trulens.nested2_ret': 'nested2: ', 'trulens.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393603,2024-12-22 11:20:26.395227,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '7881765616183808794', 'span_id': '4318803655649897130'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4318803655649897130', 'status': 'STATUS_CODE_ERROR'}",11457830288984624191,"{'trulens.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.selector_name': 'special', 'trulens.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.28.2', 'service.name': 'trulens'}",2024-12-22 11:20:26.393630,2024-12-22 11:20:26.394348,"{'trace_id': '214293944471171141309178747794638512671', 'parent_id': '4318803655649897130', 'span_id': '11457830288984624191'}" +0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",17551686358848541903,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907878,2025-01-08 14:04:18.911724,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '', 'span_id': '17551686358848541903'}" +1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '17551686358848541903', 'status': 'STATUS_CODE_UNSET'}",13230795512649114901,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907912,2025-01-08 14:04:18.910927,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '17551686358848541903', 'span_id': '13230795512649114901'}" +2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '13230795512649114901', 'status': 'STATUS_CODE_UNSET'}",5423798385037696325,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested2_ret': 'nested2: nested3', 'trulens.SpanType.UNKNOWN.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907936,2025-01-08 14:04:18.909954,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '13230795512649114901', 'span_id': '5423798385037696325'}" +3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5423798385037696325', 'status': 'STATUS_CODE_UNSET'}",11742716721073837298,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.SpanType.UNKNOWN.selector_name': 'special', 'trulens.SpanType.UNKNOWN.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907959,2025-01-08 14:04:18.908018,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '5423798385037696325', 'span_id': '11742716721073837298'}" +4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",16474091981133175119,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912663,2025-01-08 14:04:18.915867,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '', 'span_id': '16474091981133175119'}" +5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '16474091981133175119', 'status': 'STATUS_CODE_UNSET'}",1961264335073830171,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912690,2025-01-08 14:04:18.915033,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '16474091981133175119', 'span_id': '1961264335073830171'}" +6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '1961264335073830171', 'status': 'STATUS_CODE_UNSET'}",15511296828110914503,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested2_ret': 'nested2: ', 'trulens.SpanType.UNKNOWN.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912711,2025-01-08 14:04:18.914314,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '1961264335073830171', 'span_id': '15511296828110914503'}" +7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '15511296828110914503', 'status': 'STATUS_CODE_ERROR'}",16566644698051390835,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.SpanType.UNKNOWN.selector_name': 'special', 'trulens.SpanType.UNKNOWN.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912742,2025-01-08 14:04:18.913306,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '15511296828110914503', 'span_id': '16566644698051390835'}" From 85cbdc6ca82339ad0fea031bdb3e897cf76bd320 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 17:44:32 -0800 Subject: [PATCH 12/68] update test --- src/core/trulens/experimental/otel_tracing/core/span.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index 72b6c67ff..83f0efa46 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -114,7 +114,7 @@ def set_user_defined_attributes( final_attributes = validate_attributes(attributes_to_add) - prefix = f"trulens.{span_type}." + prefix = f"trulens.{span_type.value}." for key, value in final_attributes.items(): span.set_attribute(prefix + key, value) From 65544c93cef62dfcb6dd8766a8097c0447d00503 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 17:46:15 -0800 Subject: [PATCH 13/68] update golden --- ...tel_instrument__test_instrument_decorator.csv | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index b3d0f1573..80f45eeca 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,9 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",17551686358848541903,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907878,2025-01-08 14:04:18.911724,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '', 'span_id': '17551686358848541903'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '17551686358848541903', 'status': 'STATUS_CODE_UNSET'}",13230795512649114901,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907912,2025-01-08 14:04:18.910927,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '17551686358848541903', 'span_id': '13230795512649114901'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '13230795512649114901', 'status': 'STATUS_CODE_UNSET'}",5423798385037696325,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested2_ret': 'nested2: nested3', 'trulens.SpanType.UNKNOWN.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907936,2025-01-08 14:04:18.909954,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '13230795512649114901', 'span_id': '5423798385037696325'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5423798385037696325', 'status': 'STATUS_CODE_UNSET'}",11742716721073837298,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.SpanType.UNKNOWN.selector_name': 'special', 'trulens.SpanType.UNKNOWN.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.907959,2025-01-08 14:04:18.908018,"{'trace_id': '125077011144630993279729517570367379765', 'parent_id': '5423798385037696325', 'span_id': '11742716721073837298'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",16474091981133175119,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912663,2025-01-08 14:04:18.915867,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '', 'span_id': '16474091981133175119'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '16474091981133175119', 'status': 'STATUS_CODE_UNSET'}",1961264335073830171,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912690,2025-01-08 14:04:18.915033,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '16474091981133175119', 'span_id': '1961264335073830171'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '1961264335073830171', 'status': 'STATUS_CODE_UNSET'}",15511296828110914503,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested2_ret': 'nested2: ', 'trulens.SpanType.UNKNOWN.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912711,2025-01-08 14:04:18.914314,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '1961264335073830171', 'span_id': '15511296828110914503'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '15511296828110914503', 'status': 'STATUS_CODE_ERROR'}",16566644698051390835,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.SpanType.UNKNOWN.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.SpanType.UNKNOWN.selector_name': 'special', 'trulens.SpanType.UNKNOWN.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 14:04:18.912742,2025-01-08 14:04:18.913306,"{'trace_id': '73052231751059822193278885092050355919', 'parent_id': '15511296828110914503', 'span_id': '16566644698051390835'}" +0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",6386235298821949964,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411108,2025-01-08 17:45:34.414642,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '', 'span_id': '6386235298821949964'}" +1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '6386235298821949964', 'status': 'STATUS_CODE_UNSET'}",3309777644303901504,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411139,2025-01-08 17:45:34.413800,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '6386235298821949964', 'span_id': '3309777644303901504'}" +2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3309777644303901504', 'status': 'STATUS_CODE_UNSET'}",18092785063582233202,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411159,2025-01-08 17:45:34.413080,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '3309777644303901504', 'span_id': '18092785063582233202'}" +3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '18092785063582233202', 'status': 'STATUS_CODE_UNSET'}",6211919514478763513,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411175,2025-01-08 17:45:34.411204,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '18092785063582233202', 'span_id': '6211919514478763513'}" +4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",7428776760128741730,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415556,2025-01-08 17:45:34.418263,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '', 'span_id': '7428776760128741730'}" +5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7428776760128741730', 'status': 'STATUS_CODE_UNSET'}",15842740311901715118,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415580,2025-01-08 17:45:34.417583,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '7428776760128741730', 'span_id': '15842740311901715118'}" +6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '15842740311901715118', 'status': 'STATUS_CODE_UNSET'}",4022483515734536614,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415599,2025-01-08 17:45:34.416914,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '15842740311901715118', 'span_id': '4022483515734536614'}" +7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4022483515734536614', 'status': 'STATUS_CODE_ERROR'}",15844601691241400614,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415632,2025-01-08 17:45:34.416164,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '4022483515734536614', 'span_id': '15844601691241400614'}" From 9b9644b83fae64b7d3b5c9d2f8a61f7b1bdf6285 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 16 Dec 2024 14:03:32 -0500 Subject: [PATCH 14/68] save --- examples/experimental/otel_exporter.ipynb | 302 +++++++++--------- src/core/trulens/core/app.py | 13 +- .../otel_tracing/core/instrument.py | 18 ++ 3 files changed, 177 insertions(+), 156 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index a3a145b3e..18859e594 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -1,154 +1,154 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install opentelemetry-api\n", - "# !pip install opentelemetry-sdk" - ] + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install opentelemetry-api\n", + "# !pip install opentelemetry-sdk" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sys\n", + "\n", + "# Add base dir to path to be able to access test folder.\n", + "base_dir = Path().cwd().parent.parent.resolve()\n", + "if str(base_dir) not in sys.path:\n", + " print(f\"Adding {base_dir} to sys.path\")\n", + " sys.path.append(str(base_dir))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "root = logging.getLogger()\n", + "root.setLevel(logging.DEBUG)\n", + "handler = logging.StreamHandler(sys.stdout)\n", + "handler.setLevel(logging.DEBUG)\n", + "handler.addFilter(logging.Filter(\"trulens\"))\n", + "formatter = logging.Formatter(\n", + " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", + ")\n", + "handler.setFormatter(formatter)\n", + "root.addHandler(handler)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from trulens.experimental.otel_tracing.core.instrument import instrument\n", + "\n", + "\n", + "class TestApp:\n", + " @instrument()\n", + " def respond_to_query(self, query: str) -> str:\n", + " return f\"answer: {self.nested(query)}\"\n", + "\n", + " @instrument(attributes={\"nested_attr1\": \"value1\"})\n", + " def nested(self, query: str) -> str:\n", + " return f\"nested: {self.nested2(query)}\"\n", + "\n", + " @instrument(\n", + " attributes=lambda ret, exception, *args, **kwargs: {\n", + " \"nested2_ret\": ret,\n", + " \"nested2_args[0]\": args[0],\n", + " }\n", + " )\n", + " def nested2(self, query: str) -> str:\n", + " nested_result = \"\"\n", + "\n", + " try:\n", + " nested_result = self.nested3(query)\n", + " except Exception:\n", + " pass\n", + "\n", + " return f\"nested2: {nested_result}\"\n", + "\n", + " @instrument(\n", + " attributes=lambda ret, exception, *args, **kwargs: {\n", + " \"nested3_ex\": exception.args if exception else None,\n", + " \"nested3_ret\": ret,\n", + " \"selector_name\": \"special\",\n", + " \"cows\": \"moo\",\n", + " }\n", + " )\n", + " def nested3(self, query: str) -> str:\n", + " if query == \"throw\":\n", + " raise ValueError(\"nested3 exception\")\n", + " return \"nested3\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import dotenv\n", + "from trulens.core.session import TruSession\n", + "from trulens.experimental.otel_tracing.core.init import init\n", + "\n", + "dotenv.load_dotenv()\n", + "\n", + "session = TruSession()\n", + "session.experimental_enable_feature(\"otel_tracing\")\n", + "session.reset_database()\n", + "init(session, debug=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from trulens.apps.custom import TruCustomApp\n", + "\n", + "test_app = TestApp()\n", + "custom_app = TruCustomApp(test_app)\n", + "\n", + "with custom_app as recording:\n", + " test_app.respond_to_query(\"test\")\n", + "\n", + "with custom_app as recording:\n", + " test_app.respond_to_query(\"throw\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "trulens", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import sys\n", - "\n", - "# Add base dir to path to be able to access test folder.\n", - "base_dir = Path().cwd().parent.parent.resolve()\n", - "if str(base_dir) not in sys.path:\n", - " print(f\"Adding {base_dir} to sys.path\")\n", - " sys.path.append(str(base_dir))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "\n", - "root = logging.getLogger()\n", - "root.setLevel(logging.DEBUG)\n", - "handler = logging.StreamHandler(sys.stdout)\n", - "handler.setLevel(logging.DEBUG)\n", - "handler.addFilter(logging.Filter(\"trulens\"))\n", - "formatter = logging.Formatter(\n", - " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", - ")\n", - "handler.setFormatter(formatter)\n", - "root.addHandler(handler)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from trulens.experimental.otel_tracing.core.instrument import instrument\n", - "\n", - "\n", - "class TestApp:\n", - " @instrument()\n", - " def respond_to_query(self, query: str) -> str:\n", - " return f\"answer: {self.nested(query)}\"\n", - "\n", - " @instrument(attributes={\"nested_attr1\": \"value1\"})\n", - " def nested(self, query: str) -> str:\n", - " return f\"nested: {self.nested2(query)}\"\n", - "\n", - " @instrument(\n", - " attributes=lambda ret, exception, *args, **kwargs: {\n", - " \"nested2_ret\": ret,\n", - " \"nested2_args[0]\": args[0],\n", - " }\n", - " )\n", - " def nested2(self, query: str) -> str:\n", - " nested_result = \"\"\n", - "\n", - " try:\n", - " nested_result = self.nested3(query)\n", - " except Exception:\n", - " pass\n", - "\n", - " return f\"nested2: {nested_result}\"\n", - "\n", - " @instrument(\n", - " attributes=lambda ret, exception, *args, **kwargs: {\n", - " \"nested3_ex\": exception.args if exception else None,\n", - " \"nested3_ret\": ret,\n", - " \"selector_name\": \"special\",\n", - " \"cows\": \"moo\",\n", - " }\n", - " )\n", - " def nested3(self, query: str) -> str:\n", - " if query == \"throw\":\n", - " raise ValueError(\"nested3 exception\")\n", - " return \"nested3\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import dotenv\n", - "from trulens.core.session import TruSession\n", - "from trulens.experimental.otel_tracing.core.init import init\n", - "\n", - "dotenv.load_dotenv()\n", - "\n", - "session = TruSession()\n", - "session.experimental_enable_feature(\"otel_tracing\")\n", - "session.reset_database()\n", - "init(session, debug=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from trulens.apps.custom import TruCustomApp\n", - "\n", - "test_app = TestApp()\n", - "custom_app = TruCustomApp(test_app)\n", - "\n", - "with custom_app as recording:\n", - " test_app.respond_to_query(\"test\")\n", - "\n", - "with custom_app as recording:\n", - " test_app.respond_to_query(\"throw\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "trulens", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index 2739d370e..ed3d66e6c 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -884,15 +884,16 @@ def __enter__(self): if self.session.experimental_feature( core_experimental.Feature.OTEL_TRACING ): - from trulens.experimental.otel_tracing.core.app import _App + from trulens.experimental.otel_tracing.core.instrument import ( + App as OTELApp, + ) - return _App.__enter__(self) + return OTELApp.__enter__(self) ctx = core_instruments._RecordingContext(app=self) token = self.recording_contexts.set(ctx) ctx.token = token - # self._set_context_vars() return ctx @@ -902,9 +903,11 @@ def __exit__(self, exc_type, exc_value, exc_tb): if self.session.experimental_feature( core_experimental.Feature.OTEL_TRACING ): - from trulens.experimental.otel_tracing.core.app import _App + from trulens.experimental.otel_tracing.core.instrument import ( + App as OTELApp, + ) - return _App.__exit__(self, exc_type, exc_value, exc_tb) + return OTELApp.__exit__(self, exc_type, exc_value, exc_tb) ctx = self.recording_contexts.get() self.recording_contexts.reset(ctx.token) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 0f724c72f..e2573612c 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -3,6 +3,8 @@ from typing import Callable, Optional from opentelemetry import trace +from trulens.apps.custom import instrument as custom_instrument +from trulens.core import app as core_app from trulens.experimental.otel_tracing.core.init import TRULENS_SERVICE_NAME from trulens.experimental.otel_tracing.core.span import Attributes from trulens.experimental.otel_tracing.core.span import ( @@ -77,3 +79,19 @@ def wrapper(*args, **kwargs): return wrapper return inner_decorator + + +class App(core_app.App): + # For use as a context manager. + def __enter__(self): + return ( + trace.get_tracer_provider() + .get_tracer(TRULENS_SERVICE_NAME) + .start_as_current_span( + name="root", + ) + .__enter__() + ) + + def __exit__(self, exc_type, exc_value, exc_tb): + print("exit") From 02855f92f4ed0ee23ea487f7c068d4d41cceff25 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 19 Dec 2024 12:25:03 -0500 Subject: [PATCH 15/68] draft --- examples/experimental/otel_exporter.ipynb | 136 +++++++++++++++++- src/core/trulens/core/app.py | 13 +- .../otel_tracing/core/instrument.py | 60 ++++++-- .../semconv/trulens/otel/semconv/trace.py | 7 + 4 files changed, 200 insertions(+), 16 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 18859e594..67b972b42 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -149,6 +149,138 @@ "pygments_lexer": "ipython3" } }, - "nbformat": 4, - "nbformat_minor": 2 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sys\n", + "\n", + "# Add base dir to path to be able to access test folder.\n", + "base_dir = Path().cwd().parent.parent.resolve()\n", + "if str(base_dir) not in sys.path:\n", + " print(f\"Adding {base_dir} to sys.path\")\n", + " sys.path.append(str(base_dir))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "root = logging.getLogger()\n", + "root.setLevel(logging.DEBUG)\n", + "handler = logging.StreamHandler(sys.stdout)\n", + "handler.setLevel(logging.DEBUG)\n", + "handler.addFilter(logging.Filter(\"trulens\"))\n", + "formatter = logging.Formatter(\n", + " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", + ")\n", + "handler.setFormatter(formatter)\n", + "root.addHandler(handler)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from trulens.experimental.otel_tracing.core.instrument import instrument\n", + "\n", + "\n", + "class TestApp:\n", + " @instrument()\n", + " def respond_to_query(self, query: str) -> str:\n", + " return f\"answer: {self.nested(query)}\"\n", + "\n", + " @instrument(attributes={\"nested_attr1\": \"value1\"})\n", + " def nested(self, query: str) -> str:\n", + " return f\"nested: {self.nested2(query)}\"\n", + "\n", + " @instrument(\n", + " attributes=lambda ret, *args, **kwargs: {\n", + " \"nested2_ret\": ret,\n", + " \"nested2_args[0]\": args[0],\n", + " }\n", + " )\n", + " def nested2(self, query: str) -> str:\n", + " return f\"nested2: {query}\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "import dotenv\n", + "from trulens.connectors.snowflake import SnowflakeConnector\n", + "from trulens.core.session import TruSession\n", + "from trulens.experimental.otel_tracing.core.init import init\n", + "\n", + "dotenv.load_dotenv()\n", + "\n", + "connection_params = {\n", + " \"account\": os.environ[\"SNOWFLAKE_ACCOUNT\"],\n", + " \"user\": os.environ[\"SNOWFLAKE_USER\"],\n", + " \"password\": os.environ[\"SNOWFLAKE_USER_PASSWORD\"],\n", + " \"database\": os.environ[\"SNOWFLAKE_DATABASE\"],\n", + " \"schema\": os.environ[\"SNOWFLAKE_SCHEMA\"],\n", + " \"warehouse\": os.environ[\"SNOWFLAKE_WAREHOUSE\"],\n", + " \"role\": os.environ[\"SNOWFLAKE_ROLE\"],\n", + "}\n", + "\n", + "connector = SnowflakeConnector(\n", + " **connection_params, database_redact_keys=True, database_args=None\n", + ")\n", + "session = TruSession(connector=connector)\n", + "session.experimental_enable_feature(\"otel_tracing\")\n", + "session.reset_database()\n", + "init(session, debug=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from trulens.apps.custom import TruCustomApp\n", + "\n", + "test_app = TestApp()\n", + "custom_app = TruCustomApp(test_app)\n", + "\n", + "with custom_app as recording:\n", + " test_app.respond_to_query(\"test\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "trulens", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index ed3d66e6c..424898e99 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -894,7 +894,6 @@ def __enter__(self): token = self.recording_contexts.set(ctx) ctx.token = token - # self._set_context_vars() return ctx @@ -924,9 +923,11 @@ async def __aenter__(self): if self.session.experimental_feature( core_experimental.Feature.OTEL_TRACING ): - from trulens.experimental.otel_tracing.core.app import _App + from trulens.experimental.otel_tracing.core.instrument import ( + App as OTELApp, + ) - return await _App.__aenter__(self) + return OTELApp.__enter__(self) ctx = core_instruments._RecordingContext(app=self) @@ -942,9 +943,11 @@ async def __aexit__(self, exc_type, exc_value, exc_tb): if self.session.experimental_feature( core_experimental.Feature.OTEL_TRACING ): - from trulens.experimental.otel_tracing.core.app import _App + from trulens.experimental.otel_tracing.core.instrument import ( + App as OTELApp, + ) - return await _App.__aexit__(self, exc_type, exc_value, exc_tb) + return OTELApp.__exit__(self, exc_type, exc_value, exc_tb) ctx = self.recording_contexts.get() self.recording_contexts.reset(ctx.token) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index e2573612c..65c0ce520 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -1,9 +1,13 @@ from functools import wraps import logging from typing import Callable, Optional +import uuid from opentelemetry import trace -from trulens.apps.custom import instrument as custom_instrument +from opentelemetry.baggage import get_baggage +from opentelemetry.baggage import remove_baggage +from opentelemetry.baggage import set_baggage +import opentelemetry.context as context_api from trulens.core import app as core_app from trulens.experimental.otel_tracing.core.init import TRULENS_SERVICE_NAME from trulens.experimental.otel_tracing.core.span import Attributes @@ -84,14 +88,52 @@ def wrapper(*args, **kwargs): class App(core_app.App): # For use as a context manager. def __enter__(self): - return ( - trace.get_tracer_provider() - .get_tracer(TRULENS_SERVICE_NAME) - .start_as_current_span( - name="root", - ) - .__enter__() + logging.debug("Entering the OTEL app context.") + + # Note: This is not the same as the record_id in the core app since the OTEL + # tracing is currently separate from the old records behavior + otel_record_id = str(uuid.uuid4()) + + tracer = trace.get_tracer_provider().get_tracer(TRULENS_SERVICE_NAME) + + # Calling set_baggage does not actually add the baggage to the current context, but returns a new one + # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. + self.token = context_api.attach( + set_baggage(SpanAttributes.RECORD_ID, otel_record_id) ) + # Use start_as_current_span as a context manager + self.span_context = tracer.start_as_current_span("root") + root_span = self.span_context.__enter__() + + logger.debug(str(get_baggage(SpanAttributes.RECORD_ID))) + + root_span.set_attribute("kind", "SPAN_KIND_TRULENS") + root_span.set_attribute("name", "root") + root_span.set_attribute( + SpanAttributes.SPAN_TYPE, SpanAttributes.SpanType.RECORD_ROOT + ) + root_span.set_attribute( + SpanAttributes.RECORD_ROOT.APP_NAME, self.app_name + ) + root_span.set_attribute( + SpanAttributes.RECORD_ROOT.APP_VERSION, self.app_version + ) + root_span.set_attribute(SpanAttributes.RECORD_ROOT.APP_ID, self.app_id) + root_span.set_attribute( + SpanAttributes.RECORD_ROOT.RECORD_ID, otel_record_id + ) + + return root_span + def __exit__(self, exc_type, exc_value, exc_tb): - print("exit") + remove_baggage(SpanAttributes.RECORD_ID) + logging.debug("Exiting the OTEL app context.") + + if self.token: + # Clearing the context once we're done with this root span. + # See https://github.com/open-telemetry/opentelemetry-python/issues/2432#issuecomment-1593458684 + context_api.detach(self.token) + + if self.span_context: + self.span_context.__exit__(exc_type, exc_value, exc_tb) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 9050f81ae..e6ffa384f 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -55,6 +55,13 @@ class SpanAttributes: """ User-defined selector name for the current span. """ + SPAN_TYPE = "trulens.span_type" + """Key for the span type attribute.""" + + RECORD_ID = "trulens.record_id" + """ID of the record that the span belongs to.""" + + SPAN_TYPES = "trulens.span_types" class SpanType(str, Enum): """Span type attribute values. From d9a4694a659c8bdf9a7c0bfb1147f5e3d88d0625 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 20 Dec 2024 12:42:48 -0500 Subject: [PATCH 16/68] save --- examples/experimental/otel_exporter.ipynb | 39 +---------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 67b972b42..3cb2adabc 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -165,26 +165,6 @@ " sys.path.append(str(base_dir))" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "\n", - "root = logging.getLogger()\n", - "root.setLevel(logging.DEBUG)\n", - "handler = logging.StreamHandler(sys.stdout)\n", - "handler.setLevel(logging.DEBUG)\n", - "handler.addFilter(logging.Filter(\"trulens\"))\n", - "formatter = logging.Formatter(\n", - " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", - ")\n", - "handler.setFormatter(formatter)\n", - "root.addHandler(handler)" - ] - }, { "cell_type": "code", "execution_count": null, @@ -219,30 +199,13 @@ "metadata": {}, "outputs": [], "source": [ - "import os\n", - "\n", "import dotenv\n", - "from trulens.connectors.snowflake import SnowflakeConnector\n", "from trulens.core.session import TruSession\n", "from trulens.experimental.otel_tracing.core.init import init\n", "\n", "dotenv.load_dotenv()\n", "\n", - "connection_params = {\n", - " \"account\": os.environ[\"SNOWFLAKE_ACCOUNT\"],\n", - " \"user\": os.environ[\"SNOWFLAKE_USER\"],\n", - " \"password\": os.environ[\"SNOWFLAKE_USER_PASSWORD\"],\n", - " \"database\": os.environ[\"SNOWFLAKE_DATABASE\"],\n", - " \"schema\": os.environ[\"SNOWFLAKE_SCHEMA\"],\n", - " \"warehouse\": os.environ[\"SNOWFLAKE_WAREHOUSE\"],\n", - " \"role\": os.environ[\"SNOWFLAKE_ROLE\"],\n", - "}\n", - "\n", - "connector = SnowflakeConnector(\n", - " **connection_params, database_redact_keys=True, database_args=None\n", - ")\n", - "session = TruSession(connector=connector)\n", - "session.experimental_enable_feature(\"otel_tracing\")\n", + "session = TruSession()\n", "session.reset_database()\n", "init(session, debug=True)" ] From 5c939ac21b1214889576bb0ee7c4a1908b2fdcd2 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 20 Dec 2024 12:50:31 -0500 Subject: [PATCH 17/68] add back debugger --- examples/experimental/otel_exporter.ipynb | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 3cb2adabc..62f492a9f 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -165,6 +165,26 @@ " sys.path.append(str(base_dir))" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "root = logging.getLogger()\n", + "root.setLevel(logging.DEBUG)\n", + "handler = logging.StreamHandler(sys.stdout)\n", + "handler.setLevel(logging.DEBUG)\n", + "handler.addFilter(logging.Filter(\"trulens\"))\n", + "formatter = logging.Formatter(\n", + " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", + ")\n", + "handler.setFormatter(formatter)\n", + "root.addHandler(handler)" + ] + }, { "cell_type": "code", "execution_count": null, From d025a07b9ce8658f684ecb84b916a8514b90b716 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 20 Dec 2024 12:56:45 -0500 Subject: [PATCH 18/68] update notebook --- examples/experimental/otel_exporter.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 62f492a9f..bb75e1d03 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -226,6 +226,7 @@ "dotenv.load_dotenv()\n", "\n", "session = TruSession()\n", + "session.experimental_enable_feature(\"otel_tracing\")\n", "session.reset_database()\n", "init(session, debug=True)" ] From bcd677f938f6dfd7ed8e7ce1a9153dc0185b3129 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 20 Dec 2024 19:28:19 -0500 Subject: [PATCH 19/68] fix --- src/otel/semconv/trulens/otel/semconv/trace.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index e6ffa384f..13c59a247 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -39,11 +39,6 @@ class SpanAttributes: Base prefix for the other keys. """ - SPAN_TYPE = BASE + "span_type" - """ - Span type attribute. - """ - SELECTOR_NAME_KEY = "selector_name" """ Key for the user-defined selector name for the current span. From 84560197dfc6d6a2a4340d10db44312399d930de Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 20 Dec 2024 19:30:40 -0500 Subject: [PATCH 20/68] update semcov --- src/otel/semconv/trulens/otel/semconv/trace.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 13c59a247..85dff9ca5 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -39,6 +39,11 @@ class SpanAttributes: Base prefix for the other keys. """ + SPAN_TYPE = BASE + "span_type" + """ + Span type attribute. + """ + SELECTOR_NAME_KEY = "selector_name" """ Key for the user-defined selector name for the current span. @@ -50,13 +55,11 @@ class SpanAttributes: """ User-defined selector name for the current span. """ - SPAN_TYPE = "trulens.span_type" - """Key for the span type attribute.""" - RECORD_ID = "trulens.record_id" + RECORD_ID = BASE + "record_id" """ID of the record that the span belongs to.""" - SPAN_TYPES = "trulens.span_types" + SPAN_TYPES = BASE + "span_types" class SpanType(str, Enum): """Span type attribute values. From d2d2361dd75616a560f45e1c8b6e0b6df06ee640 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 23 Dec 2024 21:08:52 -0500 Subject: [PATCH 21/68] remove artifacts --- examples/experimental/otel_exporter.ipynb | 188 ++++-------------- .../otel_tracing/core/instrument.py | 4 + 2 files changed, 40 insertions(+), 152 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index bb75e1d03..a3a145b3e 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -1,153 +1,14 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install opentelemetry-api\n", - "# !pip install opentelemetry-sdk" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from pathlib import Path\n", - "import sys\n", - "\n", - "# Add base dir to path to be able to access test folder.\n", - "base_dir = Path().cwd().parent.parent.resolve()\n", - "if str(base_dir) not in sys.path:\n", - " print(f\"Adding {base_dir} to sys.path\")\n", - " sys.path.append(str(base_dir))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import logging\n", - "\n", - "root = logging.getLogger()\n", - "root.setLevel(logging.DEBUG)\n", - "handler = logging.StreamHandler(sys.stdout)\n", - "handler.setLevel(logging.DEBUG)\n", - "handler.addFilter(logging.Filter(\"trulens\"))\n", - "formatter = logging.Formatter(\n", - " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", - ")\n", - "handler.setFormatter(formatter)\n", - "root.addHandler(handler)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from trulens.experimental.otel_tracing.core.instrument import instrument\n", - "\n", - "\n", - "class TestApp:\n", - " @instrument()\n", - " def respond_to_query(self, query: str) -> str:\n", - " return f\"answer: {self.nested(query)}\"\n", - "\n", - " @instrument(attributes={\"nested_attr1\": \"value1\"})\n", - " def nested(self, query: str) -> str:\n", - " return f\"nested: {self.nested2(query)}\"\n", - "\n", - " @instrument(\n", - " attributes=lambda ret, exception, *args, **kwargs: {\n", - " \"nested2_ret\": ret,\n", - " \"nested2_args[0]\": args[0],\n", - " }\n", - " )\n", - " def nested2(self, query: str) -> str:\n", - " nested_result = \"\"\n", - "\n", - " try:\n", - " nested_result = self.nested3(query)\n", - " except Exception:\n", - " pass\n", - "\n", - " return f\"nested2: {nested_result}\"\n", - "\n", - " @instrument(\n", - " attributes=lambda ret, exception, *args, **kwargs: {\n", - " \"nested3_ex\": exception.args if exception else None,\n", - " \"nested3_ret\": ret,\n", - " \"selector_name\": \"special\",\n", - " \"cows\": \"moo\",\n", - " }\n", - " )\n", - " def nested3(self, query: str) -> str:\n", - " if query == \"throw\":\n", - " raise ValueError(\"nested3 exception\")\n", - " return \"nested3\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import dotenv\n", - "from trulens.core.session import TruSession\n", - "from trulens.experimental.otel_tracing.core.init import init\n", - "\n", - "dotenv.load_dotenv()\n", - "\n", - "session = TruSession()\n", - "session.experimental_enable_feature(\"otel_tracing\")\n", - "session.reset_database()\n", - "init(session, debug=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from trulens.apps.custom import TruCustomApp\n", - "\n", - "test_app = TestApp()\n", - "custom_app = TruCustomApp(test_app)\n", - "\n", - "with custom_app as recording:\n", - " test_app.respond_to_query(\"test\")\n", - "\n", - "with custom_app as recording:\n", - " test_app.respond_to_query(\"throw\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "trulens", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3" - } + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# !pip install opentelemetry-api\n", + "# !pip install opentelemetry-sdk" + ] }, { "cell_type": "code", @@ -204,13 +65,33 @@ " return f\"nested: {self.nested2(query)}\"\n", "\n", " @instrument(\n", - " attributes=lambda ret, *args, **kwargs: {\n", + " attributes=lambda ret, exception, *args, **kwargs: {\n", " \"nested2_ret\": ret,\n", " \"nested2_args[0]\": args[0],\n", " }\n", " )\n", " def nested2(self, query: str) -> str:\n", - " return f\"nested2: {query}\"" + " nested_result = \"\"\n", + "\n", + " try:\n", + " nested_result = self.nested3(query)\n", + " except Exception:\n", + " pass\n", + "\n", + " return f\"nested2: {nested_result}\"\n", + "\n", + " @instrument(\n", + " attributes=lambda ret, exception, *args, **kwargs: {\n", + " \"nested3_ex\": exception.args if exception else None,\n", + " \"nested3_ret\": ret,\n", + " \"selector_name\": \"special\",\n", + " \"cows\": \"moo\",\n", + " }\n", + " )\n", + " def nested3(self, query: str) -> str:\n", + " if query == \"throw\":\n", + " raise ValueError(\"nested3 exception\")\n", + " return \"nested3\"" ] }, { @@ -243,7 +124,10 @@ "custom_app = TruCustomApp(test_app)\n", "\n", "with custom_app as recording:\n", - " test_app.respond_to_query(\"test\")" + " test_app.respond_to_query(\"test\")\n", + "\n", + "with custom_app as recording:\n", + " test_app.respond_to_query(\"throw\")" ] } ], diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 65c0ce520..54a02ae4b 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -1,6 +1,10 @@ from functools import wraps import logging +<<<<<<< HEAD from typing import Callable, Optional +======= +from typing import Any, Callable, Dict, Optional, Union +>>>>>>> d4bbf065b (remove artifacts) import uuid from opentelemetry import trace From b3ff929f5f550eeaa5924a151cbfeb1b45c29128 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 23 Dec 2024 21:17:06 -0500 Subject: [PATCH 22/68] remove span_types from SpanAttributes --- src/otel/semconv/trulens/otel/semconv/trace.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 85dff9ca5..6200ef668 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -59,8 +59,6 @@ class SpanAttributes: RECORD_ID = BASE + "record_id" """ID of the record that the span belongs to.""" - SPAN_TYPES = BASE + "span_types" - class SpanType(str, Enum): """Span type attribute values. From d4603b1b404367e31334955ae9586f7dbec75fa5 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 23 Dec 2024 21:35:49 -0500 Subject: [PATCH 23/68] modified it to accept multiple tokens --- src/core/trulens/core/app.py | 13 +++++++++++ .../otel_tracing/core/instrument.py | 22 ++++++++++++------- .../semconv/trulens/otel/semconv/trace.py | 3 +++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index 424898e99..24f5d45b3 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -3,6 +3,7 @@ from abc import ABC from abc import ABCMeta from abc import abstractmethod +import contextlib import contextvars import datetime import inspect @@ -422,6 +423,18 @@ def tru(self) -> core_connector.DBConnector: pydantic.PrivateAttr(default_factory=dict) ) + tokens: list[object] = [] + """ + OTEL context tokens for the current context manager. These tokens are how the OTEL + context api keeps track of what is changed in the context, and used to undo the changes. + """ + + span_context: Optional[contextlib.AbstractContextManager] = None + """ + Span context manager. Required to help keep track of the appropriate span context + to enter/exit. + """ + def __init__( self, connector: Optional[core_connector.DBConnector] = None, diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 54a02ae4b..304d05c78 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -1,10 +1,6 @@ from functools import wraps import logging -<<<<<<< HEAD from typing import Callable, Optional -======= -from typing import Any, Callable, Dict, Optional, Union ->>>>>>> d4bbf065b (remove artifacts) import uuid from opentelemetry import trace @@ -102,9 +98,14 @@ def __enter__(self): # Calling set_baggage does not actually add the baggage to the current context, but returns a new one # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. - self.token = context_api.attach( - set_baggage(SpanAttributes.RECORD_ID, otel_record_id) + self.tokens.append( + context_api.attach( + set_baggage(SpanAttributes.RECORD_ID, otel_record_id) + ) ) + # self.tokens.append(context_api.attach( + # set_baggage(SpanAttributes.APP_ID, self.app_id) + # )) # Use start_as_current_span as a context manager self.span_context = tracer.start_as_current_span("root") @@ -112,11 +113,16 @@ def __enter__(self): logger.debug(str(get_baggage(SpanAttributes.RECORD_ID))) + # Set general span attributes root_span.set_attribute("kind", "SPAN_KIND_TRULENS") root_span.set_attribute("name", "root") root_span.set_attribute( SpanAttributes.SPAN_TYPE, SpanAttributes.SpanType.RECORD_ROOT ) + root_span.set_attribute(SpanAttributes.APP_ID, self.app_id) + root_span.set_attribute(SpanAttributes.RECORD_ID, otel_record_id) + + # Set record root specific attributes root_span.set_attribute( SpanAttributes.RECORD_ROOT.APP_NAME, self.app_name ) @@ -134,10 +140,10 @@ def __exit__(self, exc_type, exc_value, exc_tb): remove_baggage(SpanAttributes.RECORD_ID) logging.debug("Exiting the OTEL app context.") - if self.token: + while len(self.tokens) > 0: # Clearing the context once we're done with this root span. # See https://github.com/open-telemetry/opentelemetry-python/issues/2432#issuecomment-1593458684 - context_api.detach(self.token) + context_api.detach(self.tokens.pop()) if self.span_context: self.span_context.__exit__(exc_type, exc_value, exc_tb) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 6200ef668..3175e6bff 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -59,6 +59,9 @@ class SpanAttributes: RECORD_ID = BASE + "record_id" """ID of the record that the span belongs to.""" + APP_ID = BASE + "app_id" + """ID of the app that the span belongs to.""" + class SpanType(str, Enum): """Span type attribute values. From cbeb5a461a3c03e387d9e565391b3c25ce9bdc40 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 23 Dec 2024 21:53:41 -0500 Subject: [PATCH 24/68] fix bug with multiple func calls --- examples/experimental/otel_exporter.ipynb | 1 + .../experimental/otel_tracing/core/instrument.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index a3a145b3e..bc57df4ed 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -109,6 +109,7 @@ "session = TruSession()\n", "session.experimental_enable_feature(\"otel_tracing\")\n", "session.reset_database()\n", + "\n", "init(session, debug=True)" ] }, diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 304d05c78..b4149be9c 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -88,7 +88,7 @@ def wrapper(*args, **kwargs): class App(core_app.App): # For use as a context manager. def __enter__(self): - logging.debug("Entering the OTEL app context.") + logger.debug("Entering the OTEL app context.") # Note: This is not the same as the record_id in the core app since the OTEL # tracing is currently separate from the old records behavior @@ -103,9 +103,9 @@ def __enter__(self): set_baggage(SpanAttributes.RECORD_ID, otel_record_id) ) ) - # self.tokens.append(context_api.attach( - # set_baggage(SpanAttributes.APP_ID, self.app_id) - # )) + self.tokens.append( + context_api.attach(set_baggage(SpanAttributes.APP_ID, self.app_id)) + ) # Use start_as_current_span as a context manager self.span_context = tracer.start_as_current_span("root") @@ -138,7 +138,9 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_tb): remove_baggage(SpanAttributes.RECORD_ID) - logging.debug("Exiting the OTEL app context.") + remove_baggage(SpanAttributes.APP_ID) + + logger.debug("Exiting the OTEL app context.") while len(self.tokens) > 0: # Clearing the context once we're done with this root span. From 1846dba4ede332295cfa22a1c4ac539741a05f1a Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Tue, 24 Dec 2024 10:17:55 -0500 Subject: [PATCH 25/68] PR feedback --- src/core/trulens/core/app.py | 2 +- src/core/trulens/experimental/otel_tracing/core/instrument.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index 24f5d45b3..0e1409843 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -423,7 +423,7 @@ def tru(self) -> core_connector.DBConnector: pydantic.PrivateAttr(default_factory=dict) ) - tokens: list[object] = [] + tokens: List[object] = [] """ OTEL context tokens for the current context manager. These tokens are how the OTEL context api keeps track of what is changed in the context, and used to undo the changes. diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index b4149be9c..b12609df8 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -142,7 +142,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): logger.debug("Exiting the OTEL app context.") - while len(self.tokens) > 0: + while self.tokens: # Clearing the context once we're done with this root span. # See https://github.com/open-telemetry/opentelemetry-python/issues/2432#issuecomment-1593458684 context_api.detach(self.tokens.pop()) From eab220e60984f150882ea2751e1151ee867888d2 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 2 Jan 2025 15:35:17 -0500 Subject: [PATCH 26/68] add tests --- examples/experimental/otel_exporter.ipynb | 21 ++++++-- .../otel_tracing/core/exporter.py | 2 + .../otel_tracing/core/instrument.py | 50 +++++++++++++++-- .../semconv/trulens/otel/semconv/trace.py | 37 ++++++++----- tests/unit/test_otel_instrument.py | 53 ++++++++++++++++++- 5 files changed, 142 insertions(+), 21 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index bc57df4ed..603b87340 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -38,7 +38,6 @@ "root.setLevel(logging.DEBUG)\n", "handler = logging.StreamHandler(sys.stdout)\n", "handler.setLevel(logging.DEBUG)\n", - "handler.addFilter(logging.Filter(\"trulens\"))\n", "formatter = logging.Formatter(\n", " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", ")\n", @@ -53,9 +52,14 @@ "outputs": [], "source": [ "from trulens.experimental.otel_tracing.core.instrument import instrument\n", + "from trulens.otel.semconv.trace import SpanAttributes\n", "\n", "\n", "class TestApp:\n", + " @instrument(span_type=SpanAttributes.SpanType.MAIN)\n", + " def run(self, query: str) -> str:\n", + " return self.respond_to_query(query)\n", + "\n", " @instrument()\n", " def respond_to_query(self, query: str) -> str:\n", " return f\"answer: {self.nested(query)}\"\n", @@ -94,6 +98,17 @@ " return \"nested3\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from opentelemetry import trace\n", + "\n", + "tracer = trace.get_tracer_provider().get_tracer(\"Cows\")" + ] + }, { "cell_type": "code", "execution_count": null, @@ -125,10 +140,10 @@ "custom_app = TruCustomApp(test_app)\n", "\n", "with custom_app as recording:\n", - " test_app.respond_to_query(\"test\")\n", + " test_app.run(\"test\")\n", "\n", "with custom_app as recording:\n", - " test_app.respond_to_query(\"throw\")" + " test_app.run(\"throw\")" ] } ], diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 9c44b9eeb..6d42c2901 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -36,6 +36,8 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: if context is None: raise ValueError("Span context is None") + print(span.attributes) + return event_schema.Event( event_id=str(context.span_id), record={ diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index b12609df8..0955613e2 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -1,10 +1,9 @@ from functools import wraps import logging -from typing import Callable, Optional +from typing import Any, Callable, Dict, Optional import uuid from opentelemetry import trace -from opentelemetry.baggage import get_baggage from opentelemetry.baggage import remove_baggage from opentelemetry.baggage import set_baggage import opentelemetry.context as context_api @@ -22,6 +21,51 @@ logger = logging.getLogger(__name__) +def validate_selector_name(attributes: Dict[str, Any]) -> Dict[str, Any]: + """ + Utility function to validate the selector name in the attributes. + """ + + result = attributes.copy() + + if ( + SpanAttributes.SELECTOR_NAME_KEY in result + and SpanAttributes.SELECTOR_NAME in result + ): + raise ValueError( + f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." + ) + + if SpanAttributes.SELECTOR_NAME in result: + # Transfer the trulens namespaced to the non-trulens namespaced key. + result[SpanAttributes.SELECTOR_NAME_KEY] = result[ + SpanAttributes.SELECTOR_NAME + ] + del result[SpanAttributes.SELECTOR_NAME] + + if SpanAttributes.SELECTOR_NAME_KEY in result: + selector_name = result[SpanAttributes.SELECTOR_NAME_KEY] + if not isinstance(selector_name, str): + raise ValueError( + f"Selector name must be a string, not {type(selector_name)}" + ) + + return result + + +def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: + """ + Utility function to validate span attributes based on the span type. + """ + if not isinstance(attributes, dict) or any([ + not isinstance(key, str) for key in attributes.keys() + ]): + raise ValueError("Attributes must be a dictionary with string keys.") + return validate_selector_name(attributes) + # TODO: validate OTEL attributes. + # TODO: validate span type attributes. + + def instrument( *, span_type: SpanAttributes.SpanType = SpanAttributes.SpanType.UNKNOWN, @@ -111,8 +155,6 @@ def __enter__(self): self.span_context = tracer.start_as_current_span("root") root_span = self.span_context.__enter__() - logger.debug(str(get_baggage(SpanAttributes.RECORD_ID))) - # Set general span attributes root_span.set_attribute("kind", "SPAN_KIND_TRULENS") root_span.set_attribute("name", "root") diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 3175e6bff..3cbd2621a 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -62,6 +62,9 @@ class SpanAttributes: APP_ID = BASE + "app_id" """ID of the app that the span belongs to.""" + ROOT_SPAN_ID = BASE + "root_span_id" + """ID of the root span of the record that the span belongs to.""" + class SpanType(str, Enum): """Span type attribute values. @@ -96,6 +99,9 @@ class SpanType(str, Enum): RECORD_ROOT = "record_root" """Spans as collected by tracing system.""" + MAIN = "main" + """The main span of a record.""" + EVAL_ROOT = "eval_root" """Feedback function evaluation span.""" @@ -159,6 +165,25 @@ class UNKNOWN: base = "trulens.unknown" + class MAIN: + """Attributes for the main span of a record.""" + + base = "trulens.main" + + SPAN_NAME_PREFIX = base + "." + + MAIN_INPUT = base + ".main_input" + """Main input to the app.""" + + MAIN_OUTPUT = base + ".main_output" + """Main output of the app.""" + + MAIN_ERROR = base + ".main_error" + """Main error of the app. + + Exclusive with main output. + """ + class RECORD_ROOT: """Attributes for the root span of a record. @@ -188,18 +213,6 @@ class RECORD_ROOT: all those costs. """ - MAIN_INPUT = base + ".main_input" - """Main input to the app.""" - - MAIN_OUTPUT = base + ".main_output" - """Main output of the app.""" - - MAIN_ERROR = base + ".main_error" - """Main error of the app. - - Exclusive with main output. - """ - class EVAL_ROOT: """Attributes for the root span of a feedback evaluation. diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index cc6b37b21..6451359b2 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -2,6 +2,7 @@ Tests for OTEL instrument decorator. """ +from unittest import TestCase from unittest import main import pandas as pd @@ -11,6 +12,10 @@ from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument +from trulens.experimental.otel_tracing.core.instrument import ( + validate_selector_name, +) +from trulens.otel.semconv.trace import SpanAttributes from tests.test import TruTestCase from tests.util.df_comparison import ( @@ -124,7 +129,7 @@ def test_instrument_decorator(self) -> None: # Compare results to expected. GOLDEN_FILENAME = "tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv" actual = self._get_events() - self.assertEqual(len(actual), 8) + self.assertEqual(len(actual), 10) self.write_golden(GOLDEN_FILENAME, actual) expected = self.load_golden(GOLDEN_FILENAME) self._convert_column_types(expected) @@ -134,10 +139,54 @@ def test_instrument_decorator(self) -> None: actual, ignore_locators=[ f"df.iloc[{i}][resource_attributes][telemetry.sdk.version]" - for i in range(8) + for i in range(10) ], ) +class TestOtelValidation(TestCase): + def test_validate_selector_name(self): + with self.subTest("No selector name"): + self.assertEqual( + validate_selector_name({}), + {}, + ) + + with self.subTest( + f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." + ): + self.assertRaises( + ValueError, + validate_selector_name, + { + SpanAttributes.SELECTOR_NAME_KEY: "key", + SpanAttributes.SELECTOR_NAME: "name", + }, + ) + + with self.subTest("Non-string"): + self.assertRaises( + ValueError, + validate_selector_name, + { + SpanAttributes.SELECTOR_NAME_KEY: 42, + }, + ) + + with self.subTest(f"Valid {SpanAttributes.SELECTOR_NAME}"): + self.assertEqual( + validate_selector_name({SpanAttributes.SELECTOR_NAME: "name"}), + {SpanAttributes.SELECTOR_NAME_KEY: "name"}, + ) + + with self.subTest(f"Valid {SpanAttributes.SELECTOR_NAME_KEY}"): + self.assertEqual( + validate_selector_name({ + SpanAttributes.SELECTOR_NAME_KEY: "name" + }), + {SpanAttributes.SELECTOR_NAME_KEY: "name"}, + ) + + if __name__ == "__main__": main() From 093f8ad9675b3f74568ac791553713d187aea62d Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 2 Jan 2025 16:30:30 -0500 Subject: [PATCH 27/68] add tests --- .../otel_tracing/core/instrument.py | 40 ++++++++++++- tests/unit/test_otel_instrument.py | 57 +++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 0955613e2..f06447058 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -21,9 +21,46 @@ logger = logging.getLogger(__name__) +VALID_ATTR_VALUE_TYPES = (bool, str, int, float) +""" +Per the OTEL [documentation](https://opentelemetry.io/docs/specs/otel/common/#attribute), +valid attribute value types are either: +- A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or signed 64 bit integer. +- An array of primitive type values. The array MUST be homogeneous, i.e., it MUST NOT contain values of different types. +""" + + +def validate_value_for_attribute(value): + """ + Ensure that value is a valid attribute value type, and coerce it to a string if it is not. + + This is helpful for lists/etc because if any single value is not a valid attribute value type, the entire list + will not be added as a span attribute. + """ + arg_type = type(value) + + # Coerge the argument to a string if it is not a valid attribute value type. + if arg_type not in VALID_ATTR_VALUE_TYPES: + return str(value) + + return value + + +def validate_list_of_values_for_attribute(arguments: list): + """ + Ensure that all values in a list are valid attribute value types, and coerce them to strings if they are not. + """ + return list(map(validate_value_for_attribute, arguments)) + + def validate_selector_name(attributes: Dict[str, Any]) -> Dict[str, Any]: """ Utility function to validate the selector name in the attributes. + + It does the following: + 1. Ensure that the selector name is a string. + 2. Ensure that the selector name is keyed with either the trulens/non-trulens key variations. + 3. Ensure that the selector name is not set in both the trulens and non-trulens key variations. """ result = attributes.copy() @@ -62,8 +99,7 @@ def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: ]): raise ValueError("Attributes must be a dictionary with string keys.") return validate_selector_name(attributes) - # TODO: validate OTEL attributes. - # TODO: validate span type attributes. + # TODO: validate Span type attributes. def instrument( diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 6451359b2..ccb072a7c 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -12,9 +12,15 @@ from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument +from trulens.experimental.otel_tracing.core.instrument import ( + validate_list_of_values_for_attribute, +) from trulens.experimental.otel_tracing.core.instrument import ( validate_selector_name, ) +from trulens.experimental.otel_tracing.core.instrument import ( + validate_value_for_attribute, +) from trulens.otel.semconv.trace import SpanAttributes from tests.test import TruTestCase @@ -187,6 +193,57 @@ def test_validate_selector_name(self): {SpanAttributes.SELECTOR_NAME_KEY: "name"}, ) + def test_validate_value_for_attribute(self): + with self.subTest("None"): + self.assertEqual(validate_value_for_attribute(None), "None") + + with self.subTest("number"): + self.assertEqual( + validate_value_for_attribute(42), + 42, + ) + + with self.subTest("string"): + self.assertEqual( + validate_value_for_attribute("31"), + "31", + ) + + with self.subTest("bool"): + self.assertFalse( + validate_value_for_attribute(False), + ) + + with self.subTest("float"): + self.assertEqual( + validate_value_for_attribute(3.14), + 3.14, + ) + + with self.subTest("dict"): + self.assertEqual( + validate_value_for_attribute({"key": "value"}), + "{'key': 'value'}", + ) + + def test_validate_list_of_values_for_attribute(self): + with self.subTest("list of primitives"): + self.assertEqual( + validate_list_of_values_for_attribute([1, "2", True]), + [1, "2", True], + ) + + with self.subTest("list of primitives and non-primitives"): + self.assertEqual( + validate_list_of_values_for_attribute([ + 1, + "2", + True, + {"key": "value"}, + ]), + [1, "2", True, "{'key': 'value'}"], + ) + if __name__ == "__main__": main() From 02367988b0cd34b91eaf501f58bce3502b0d8ceb Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 2 Jan 2025 16:32:08 -0500 Subject: [PATCH 28/68] nits --- examples/experimental/otel_exporter.ipynb | 11 ----------- .../experimental/otel_tracing/core/exporter.py | 2 -- 2 files changed, 13 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 603b87340..8ec786506 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -98,17 +98,6 @@ " return \"nested3\"" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from opentelemetry import trace\n", - "\n", - "tracer = trace.get_tracer_provider().get_tracer(\"Cows\")" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 6d42c2901..9c44b9eeb 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -36,8 +36,6 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: if context is None: raise ValueError("Span context is None") - print(span.attributes) - return event_schema.Event( event_id=str(context.span_id), record={ From 7d0e800aba125677965adc0f6b374a54f8dd6d14 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 14:56:26 -0500 Subject: [PATCH 29/68] pr feedback --- examples/experimental/otel_exporter.ipynb | 8 +- .../otel_tracing/core/instrument.py | 100 +++--------------- .../experimental/otel_tracing/core/span.py | 31 +++++- tests/unit/test_otel_instrument.py | 57 ---------- 4 files changed, 46 insertions(+), 150 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 8ec786506..77f6c54a8 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -57,10 +57,6 @@ "\n", "class TestApp:\n", " @instrument(span_type=SpanAttributes.SpanType.MAIN)\n", - " def run(self, query: str) -> str:\n", - " return self.respond_to_query(query)\n", - "\n", - " @instrument()\n", " def respond_to_query(self, query: str) -> str:\n", " return f\"answer: {self.nested(query)}\"\n", "\n", @@ -129,10 +125,10 @@ "custom_app = TruCustomApp(test_app)\n", "\n", "with custom_app as recording:\n", - " test_app.run(\"test\")\n", + " test_app.respond_to_query(\"test\")\n", "\n", "with custom_app as recording:\n", - " test_app.run(\"throw\")" + " test_app.respond_to_query(\"throw\")" ] } ], diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index f06447058..bac3cf2d5 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -16,92 +16,14 @@ from trulens.experimental.otel_tracing.core.span import ( set_user_defined_attributes, ) +from trulens.experimental.otel_tracing.core.span import ( + set_main_span_attributes, +) from trulens.otel.semconv.trace import SpanAttributes logger = logging.getLogger(__name__) -VALID_ATTR_VALUE_TYPES = (bool, str, int, float) -""" -Per the OTEL [documentation](https://opentelemetry.io/docs/specs/otel/common/#attribute), -valid attribute value types are either: -- A primitive type: string, boolean, double precision floating point (IEEE 754-1985) or signed 64 bit integer. -- An array of primitive type values. The array MUST be homogeneous, i.e., it MUST NOT contain values of different types. -""" - - -def validate_value_for_attribute(value): - """ - Ensure that value is a valid attribute value type, and coerce it to a string if it is not. - - This is helpful for lists/etc because if any single value is not a valid attribute value type, the entire list - will not be added as a span attribute. - """ - arg_type = type(value) - - # Coerge the argument to a string if it is not a valid attribute value type. - if arg_type not in VALID_ATTR_VALUE_TYPES: - return str(value) - - return value - - -def validate_list_of_values_for_attribute(arguments: list): - """ - Ensure that all values in a list are valid attribute value types, and coerce them to strings if they are not. - """ - return list(map(validate_value_for_attribute, arguments)) - - -def validate_selector_name(attributes: Dict[str, Any]) -> Dict[str, Any]: - """ - Utility function to validate the selector name in the attributes. - - It does the following: - 1. Ensure that the selector name is a string. - 2. Ensure that the selector name is keyed with either the trulens/non-trulens key variations. - 3. Ensure that the selector name is not set in both the trulens and non-trulens key variations. - """ - - result = attributes.copy() - - if ( - SpanAttributes.SELECTOR_NAME_KEY in result - and SpanAttributes.SELECTOR_NAME in result - ): - raise ValueError( - f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." - ) - - if SpanAttributes.SELECTOR_NAME in result: - # Transfer the trulens namespaced to the non-trulens namespaced key. - result[SpanAttributes.SELECTOR_NAME_KEY] = result[ - SpanAttributes.SELECTOR_NAME - ] - del result[SpanAttributes.SELECTOR_NAME] - - if SpanAttributes.SELECTOR_NAME_KEY in result: - selector_name = result[SpanAttributes.SELECTOR_NAME_KEY] - if not isinstance(selector_name, str): - raise ValueError( - f"Selector name must be a string, not {type(selector_name)}" - ) - - return result - - -def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: - """ - Utility function to validate span attributes based on the span type. - """ - if not isinstance(attributes, dict) or any([ - not isinstance(key, str) for key in attributes.keys() - ]): - raise ValueError("Attributes must be a dictionary with string keys.") - return validate_selector_name(attributes) - # TODO: validate Span type attributes. - - def instrument( *, span_type: SpanAttributes.SpanType = SpanAttributes.SpanType.UNKNOWN, @@ -138,6 +60,14 @@ def wrapper(*args, **kwargs): set_general_span_attributes(span, span_type) attributes_exception = None + if span_type == SpanAttributes.SpanType.MAIN: + # Only an exception in calling the function should determine whether + # to set the main error. Errors in setting attributes should not be classified + # as main errors. + set_main_span_attributes( + span, func, args, kwargs, ret, func_exception + ) + try: set_user_defined_attributes( span, @@ -192,13 +122,10 @@ def __enter__(self): root_span = self.span_context.__enter__() # Set general span attributes - root_span.set_attribute("kind", "SPAN_KIND_TRULENS") root_span.set_attribute("name", "root") - root_span.set_attribute( - SpanAttributes.SPAN_TYPE, SpanAttributes.SpanType.RECORD_ROOT + set_general_span_attributes( + root_span, SpanAttributes.SpanType.RECORD_ROOT ) - root_span.set_attribute(SpanAttributes.APP_ID, self.app_id) - root_span.set_attribute(SpanAttributes.RECORD_ID, otel_record_id) # Set record root specific attributes root_span.set_attribute( @@ -226,4 +153,5 @@ def __exit__(self, exc_type, exc_value, exc_tb): context_api.detach(self.tokens.pop()) if self.span_context: + # TODO[SNOW-1854360]: Add in feature function spans. self.span_context.__exit__(exc_type, exc_value, exc_tb) diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index 83f0efa46..0bbf7920f 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -5,7 +5,7 @@ from inspect import signature import logging from typing import Any, Callable, Dict, Optional, Union - +from opentelemetry.baggage import get_baggage from opentelemetry.trace.span import Span from trulens.core.utils import signature as signature_utils from trulens.otel.semconv.trace import SpanAttributes @@ -87,6 +87,12 @@ def set_general_span_attributes( ) -> Span: span.set_attribute("kind", "SPAN_KIND_TRULENS") span.set_attribute(SpanAttributes.SPAN_TYPE, span_type) + span.set_attribute( + SpanAttributes.APP_ID, str(get_baggage(SpanAttributes.APP_ID)) + ) + span.set_attribute( + SpanAttributes.RECORD_ID, str(get_baggage(SpanAttributes.RECORD_ID)) + ) return span @@ -138,3 +144,26 @@ def get_main_input(func: Callable, args: tuple, kwargs: dict) -> str: sig = signature(func) bindings = signature(func).bind(*args, **kwargs) return signature_utils.main_input(func, sig, bindings) + + +def set_main_span_attributes( + span: Span, + /, + func: Callable, + args: tuple, + kwargs: dict, + ret, + exception: Optional[Exception], +) -> None: + span.set_attribute( + SpanAttributes.MAIN.MAIN_INPUT, get_main_input(func, args, kwargs) + ) + + if exception: + span.set_attribute(SpanAttributes.MAIN.MAIN_ERROR, str(exception)) + + elif ret is not None: + span.set_attribute( + SpanAttributes.MAIN.MAIN_OUTPUT, + signature_utils.main_output(func, ret), + ) diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index ccb072a7c..6451359b2 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -12,15 +12,9 @@ from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument -from trulens.experimental.otel_tracing.core.instrument import ( - validate_list_of_values_for_attribute, -) from trulens.experimental.otel_tracing.core.instrument import ( validate_selector_name, ) -from trulens.experimental.otel_tracing.core.instrument import ( - validate_value_for_attribute, -) from trulens.otel.semconv.trace import SpanAttributes from tests.test import TruTestCase @@ -193,57 +187,6 @@ def test_validate_selector_name(self): {SpanAttributes.SELECTOR_NAME_KEY: "name"}, ) - def test_validate_value_for_attribute(self): - with self.subTest("None"): - self.assertEqual(validate_value_for_attribute(None), "None") - - with self.subTest("number"): - self.assertEqual( - validate_value_for_attribute(42), - 42, - ) - - with self.subTest("string"): - self.assertEqual( - validate_value_for_attribute("31"), - "31", - ) - - with self.subTest("bool"): - self.assertFalse( - validate_value_for_attribute(False), - ) - - with self.subTest("float"): - self.assertEqual( - validate_value_for_attribute(3.14), - 3.14, - ) - - with self.subTest("dict"): - self.assertEqual( - validate_value_for_attribute({"key": "value"}), - "{'key': 'value'}", - ) - - def test_validate_list_of_values_for_attribute(self): - with self.subTest("list of primitives"): - self.assertEqual( - validate_list_of_values_for_attribute([1, "2", True]), - [1, "2", True], - ) - - with self.subTest("list of primitives and non-primitives"): - self.assertEqual( - validate_list_of_values_for_attribute([ - 1, - "2", - True, - {"key": "value"}, - ]), - [1, "2", True, "{'key': 'value'}"], - ) - if __name__ == "__main__": main() From a8e90845a895436e2e56d7c0595cd72172b5cea2 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 16:24:39 -0500 Subject: [PATCH 30/68] update golden --- tests/unit/test_otel_instrument.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 6451359b2..2f1470939 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -12,9 +12,7 @@ from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument -from trulens.experimental.otel_tracing.core.instrument import ( - validate_selector_name, -) +from trulens.experimental.otel_tracing.core.span import validate_selector_name from trulens.otel.semconv.trace import SpanAttributes from tests.test import TruTestCase From 5d35d11b0c22f9cee77bc2dcab5aa1482651bd44 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 16:28:19 -0500 Subject: [PATCH 31/68] update test file --- tests/unit/test_otel_instrument.py | 47 ------------------------------ 1 file changed, 47 deletions(-) diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 2f1470939..796be01e1 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -2,7 +2,6 @@ Tests for OTEL instrument decorator. """ -from unittest import TestCase from unittest import main import pandas as pd @@ -12,8 +11,6 @@ from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument -from trulens.experimental.otel_tracing.core.span import validate_selector_name -from trulens.otel.semconv.trace import SpanAttributes from tests.test import TruTestCase from tests.util.df_comparison import ( @@ -142,49 +139,5 @@ def test_instrument_decorator(self) -> None: ) -class TestOtelValidation(TestCase): - def test_validate_selector_name(self): - with self.subTest("No selector name"): - self.assertEqual( - validate_selector_name({}), - {}, - ) - - with self.subTest( - f"Both {SpanAttributes.SELECTOR_NAME_KEY} and {SpanAttributes.SELECTOR_NAME} cannot be set." - ): - self.assertRaises( - ValueError, - validate_selector_name, - { - SpanAttributes.SELECTOR_NAME_KEY: "key", - SpanAttributes.SELECTOR_NAME: "name", - }, - ) - - with self.subTest("Non-string"): - self.assertRaises( - ValueError, - validate_selector_name, - { - SpanAttributes.SELECTOR_NAME_KEY: 42, - }, - ) - - with self.subTest(f"Valid {SpanAttributes.SELECTOR_NAME}"): - self.assertEqual( - validate_selector_name({SpanAttributes.SELECTOR_NAME: "name"}), - {SpanAttributes.SELECTOR_NAME_KEY: "name"}, - ) - - with self.subTest(f"Valid {SpanAttributes.SELECTOR_NAME_KEY}"): - self.assertEqual( - validate_selector_name({ - SpanAttributes.SELECTOR_NAME_KEY: "name" - }), - {SpanAttributes.SELECTOR_NAME_KEY: "name"}, - ) - - if __name__ == "__main__": main() From af18dde5ea7b1dc24194320ed9128a589e6fa12f Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 6 Jan 2025 10:40:27 -0500 Subject: [PATCH 32/68] update golden --- tests/unit/test_otel_instrument.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 796be01e1..6472614c6 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -11,6 +11,7 @@ from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument +from trulens.otel.semconv.trace import SpanAttributes from tests.test import TruTestCase from tests.util.df_comparison import ( @@ -19,7 +20,7 @@ class _TestApp: - @instrument() + @instrument(span_type=SpanAttributes.SpanType.MAIN) def respond_to_query(self, query: str) -> str: return f"answer: {self.nested(query)}" From f3c9e22f134d429f39adfadaff95ae7ad59da480 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 17:53:46 -0800 Subject: [PATCH 33/68] PR feedback --- src/core/trulens/experimental/otel_tracing/core/span.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index 0bbf7920f..22b881c40 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -5,6 +5,7 @@ from inspect import signature import logging from typing import Any, Callable, Dict, Optional, Union + from opentelemetry.baggage import get_baggage from opentelemetry.trace.span import Span from trulens.core.utils import signature as signature_utils @@ -152,7 +153,7 @@ def set_main_span_attributes( func: Callable, args: tuple, kwargs: dict, - ret, + ret: Any, exception: Optional[Exception], ) -> None: span.set_attribute( @@ -162,7 +163,7 @@ def set_main_span_attributes( if exception: span.set_attribute(SpanAttributes.MAIN.MAIN_ERROR, str(exception)) - elif ret is not None: + if ret is not None: span.set_attribute( SpanAttributes.MAIN.MAIN_OUTPUT, signature_utils.main_output(func, ret), From e94147fc3c64b7d2c08c87c10b440ed0977bb274 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 20:22:26 -0500 Subject: [PATCH 34/68] draft --- examples/experimental/otel_exporter.ipynb | 21 +++++- .../versions/10_create_event_table.py | 6 ++ .../otel_tracing/core/exporter.py | 68 +++++++++++++++++-- .../experimental/otel_tracing/core/init.py | 7 +- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 77f6c54a8..ab1953623 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -100,17 +100,34 @@ "metadata": {}, "outputs": [], "source": [ + "import os\n", + "\n", "import dotenv\n", + "from trulens.connectors.snowflake import SnowflakeConnector\n", "from trulens.core.session import TruSession\n", "from trulens.experimental.otel_tracing.core.init import init\n", "\n", "dotenv.load_dotenv()\n", "\n", - "session = TruSession()\n", + "connection_params = {\n", + " \"account\": os.environ[\"SNOWFLAKE_ACCOUNT\"],\n", + " \"user\": os.environ[\"SNOWFLAKE_USER\"],\n", + " \"password\": os.environ[\"SNOWFLAKE_USER_PASSWORD\"],\n", + " \"role\": os.environ.get(\"SNOWFLAKE_ROLE\", \"ENGINEER\"),\n", + " \"database\": os.environ.get(\"SNOWFLAKE_DATABASE\"),\n", + " \"schema\": os.environ.get(\"SNOWFLAKE_SCHEMA\"),\n", + " \"warehouse\": os.environ.get(\"SNOWFLAKE_WAREHOUSE\"),\n", + "}\n", + "\n", + "connector = SnowflakeConnector(\n", + " **connection_params,\n", + ")\n", + "\n", + "session = TruSession(connector=connector)\n", "session.experimental_enable_feature(\"otel_tracing\")\n", "session.reset_database()\n", "\n", - "init(session, debug=True)" + "init(session, debug=False)" ] }, { diff --git a/src/core/trulens/core/database/migrations/versions/10_create_event_table.py b/src/core/trulens/core/database/migrations/versions/10_create_event_table.py index 3dfe4eae6..c4f9ed4e7 100644 --- a/src/core/trulens/core/database/migrations/versions/10_create_event_table.py +++ b/src/core/trulens/core/database/migrations/versions/10_create_event_table.py @@ -21,6 +21,9 @@ def upgrade(config) -> None: if prefix is None: raise RuntimeError("trulens.table_prefix is not set") + if op.get_context().dialect.name == "snowflake": + return + op.create_table( prefix + "events", sa.Column("event_id", sa.VARCHAR(length=256), nullable=False), @@ -38,6 +41,9 @@ def upgrade(config) -> None: def downgrade(config) -> None: prefix = config.get_main_option("trulens.table_prefix") + if op.get_context().dialect.name == "snowflake": + return + if prefix is None: raise RuntimeError("trulens.table_prefix is not set") diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 9c44b9eeb..18ed7cedf 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -1,11 +1,15 @@ +import csv from datetime import datetime import logging +import tempfile +import traceback from typing import Optional, Sequence from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import StatusCode +from trulens.connectors.snowflake import SnowflakeConnector from trulens.core.database import connector as core_connector from trulens.core.schema import event as event_schema @@ -24,8 +28,6 @@ class TruLensDBSpanExporter(SpanExporter): Implementation of `SpanExporter` that flushes the spans to the database in the TruLens session. """ - connector: core_connector.DBConnector - def __init__(self, connector: core_connector.DBConnector): self.connector = connector @@ -59,12 +61,70 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: ) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + def check_if_trulens_span(span: ReadableSpan) -> bool: + if not span.attributes: + return False + + return span.attributes.get("kind") == "SPAN_KIND_TRULENS" + + trulens_spans = list(filter(check_if_trulens_span, spans)) + + if not trulens_spans: + return SpanExportResult.SUCCESS + + if isinstance(self.connector, SnowflakeConnector): + tmp_file_path = "" + + try: + with tempfile.NamedTemporaryFile( + delete=False, suffix=".csv", mode="w", newline="" + ) as tmp_file: + tmp_file_path = tmp_file.name + logger.debug( + f"Writing spans to the csv file: {tmp_file_path}" + ) + writer = csv.writer(tmp_file) + writer.writerow(["span"]) + for span in trulens_spans: + writer.writerow([span.to_json()]) + logger.debug( + f"Spans written to the csv file: {tmp_file_path}" + ) + except Exception as e: + logger.error(f"Error writing spans to the csv file: {e}") + return SpanExportResult.FAILURE + + try: + logger.debug("Uploading file to Snowflake stage") + snowpark_session = self.connector.snowpark_session + + logger.debug("Creating Snowflake stage if it does not exist") + snowpark_session.sql( + "CREATE STAGE IF NOT EXISTS trulens_spans" + ).collect() + + logger.debug("Uploading the csv file to the stage") + snowpark_session.sql( + f"PUT file://{tmp_file_path} @trulens_spans" + ).collect() + + except Exception as e: + print(f"Error uploading the csv file to the stage: {e}") + traceback.print_exc() + logger.error(f"Error uploading the csv file to the stage: {e}") + return SpanExportResult.FAILURE + + return SpanExportResult.SUCCESS + + # For non-snowflake: try: - events = list(map(self._construct_event, spans)) + events = list(map(self._construct_event, trulens_spans)) self.connector.add_events(events) except Exception as e: - logger.error("Error exporting spans to the database: %s", e) + logger.error( + f"Error exporting spans to the database: {e}", + ) return SpanExportResult.FAILURE return SpanExportResult.SUCCESS diff --git a/src/core/trulens/experimental/otel_tracing/core/init.py b/src/core/trulens/experimental/otel_tracing/core/init.py index aa6f0f67e..5804f42c9 100644 --- a/src/core/trulens/experimental/otel_tracing/core/init.py +++ b/src/core/trulens/experimental/otel_tracing/core/init.py @@ -3,6 +3,7 @@ from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleSpanProcessor from trulens.core.session import TruSession @@ -23,7 +24,7 @@ def init(session: TruSession, debug: bool = False): trace.set_tracer_provider(provider) if debug: - logging.debug( + logger.debug( "Initializing OpenTelemetry with TruLens configuration for console debugging" ) # Add a console exporter for debugging purposes @@ -32,7 +33,7 @@ def init(session: TruSession, debug: bool = False): provider.add_span_processor(console_processor) if session.connector: - logging.debug("Exporting traces to the TruLens database") + logger.debug("Exporting traces to the TruLens database") # Check the database revision try: @@ -50,5 +51,5 @@ def init(session: TruSession, debug: bool = False): # Add the TruLens database exporter db_exporter = TruLensDBSpanExporter(session.connector) - db_processor = SimpleSpanProcessor(db_exporter) + db_processor = BatchSpanProcessor(db_exporter) provider.add_span_processor(db_processor) From 2a74796d626e3d83889c2af8dfe5ac28481611bf Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 20:24:13 -0500 Subject: [PATCH 35/68] remove file --- .../trulens/experimental/otel_tracing/core/exporter.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 18ed7cedf..0773b17e2 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -1,6 +1,7 @@ import csv from datetime import datetime import logging +import os import tempfile import traceback from typing import Optional, Sequence @@ -114,6 +115,13 @@ def check_if_trulens_span(span: ReadableSpan) -> bool: logger.error(f"Error uploading the csv file to the stage: {e}") return SpanExportResult.FAILURE + try: + logger.debug("Removing the temporary csv file") + os.remove(tmp_file_path) + except Exception as e: + # Not returning failure here since the export was technically a success + logger.error(f"Error removing the temporary csv file: {e}") + return SpanExportResult.SUCCESS # For non-snowflake: From fd645d18d260dae963611da61ae33e1bfb4b4e42 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 20:30:46 -0500 Subject: [PATCH 36/68] don't compress --- src/core/trulens/experimental/otel_tracing/core/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 0773b17e2..f65d1188f 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -106,7 +106,7 @@ def check_if_trulens_span(span: ReadableSpan) -> bool: logger.debug("Uploading the csv file to the stage") snowpark_session.sql( - f"PUT file://{tmp_file_path} @trulens_spans" + f"PUT file://{tmp_file_path} @trulens_spans AUTO_COMPRESS=FALSE" ).collect() except Exception as e: From 258abd2b75363ed09c732cc63322817fe35d71ed Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 3 Jan 2025 20:31:18 -0500 Subject: [PATCH 37/68] remove print --- src/core/trulens/experimental/otel_tracing/core/exporter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index f65d1188f..2c6ed065e 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -3,7 +3,6 @@ import logging import os import tempfile -import traceback from typing import Optional, Sequence from opentelemetry.sdk.trace import ReadableSpan @@ -110,8 +109,6 @@ def check_if_trulens_span(span: ReadableSpan) -> bool: ).collect() except Exception as e: - print(f"Error uploading the csv file to the stage: {e}") - traceback.print_exc() logger.error(f"Error uploading the csv file to the stage: {e}") return SpanExportResult.FAILURE From 28cee0855932218d837e304b1ad1d5a7622dd2a1 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 6 Jan 2025 10:36:38 -0500 Subject: [PATCH 38/68] update golden --- src/core/trulens/experimental/otel_tracing/core/init.py | 6 +++++- tests/unit/test_otel_instrument.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/init.py b/src/core/trulens/experimental/otel_tracing/core/init.py index 5804f42c9..abc2bbcc0 100644 --- a/src/core/trulens/experimental/otel_tracing/core/init.py +++ b/src/core/trulens/experimental/otel_tracing/core/init.py @@ -51,5 +51,9 @@ def init(session: TruSession, debug: bool = False): # Add the TruLens database exporter db_exporter = TruLensDBSpanExporter(session.connector) - db_processor = BatchSpanProcessor(db_exporter) + db_processor = ( + SimpleSpanProcessor(db_exporter) + if debug + else BatchSpanProcessor(db_exporter) + ) provider.add_span_processor(db_processor) diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 6472614c6..8b82071c1 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -122,6 +122,7 @@ def test_instrument_decorator(self) -> None: test_app.respond_to_query("test") with custom_app: test_app.respond_to_query("throw") + # Compare results to expected. GOLDEN_FILENAME = "tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv" actual = self._get_events() From 7dc4a1c279acd8ce43f5ab2e924dada008a4defd Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 6 Jan 2025 10:39:37 -0500 Subject: [PATCH 39/68] added comments --- src/core/trulens/experimental/otel_tracing/core/init.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/trulens/experimental/otel_tracing/core/init.py b/src/core/trulens/experimental/otel_tracing/core/init.py index abc2bbcc0..3543b2175 100644 --- a/src/core/trulens/experimental/otel_tracing/core/init.py +++ b/src/core/trulens/experimental/otel_tracing/core/init.py @@ -51,6 +51,10 @@ def init(session: TruSession, debug: bool = False): # Add the TruLens database exporter db_exporter = TruLensDBSpanExporter(session.connector) + + # When testing, use a simple span processor to avoid issues with batching/ + # asynchronous processing of the spans that results in the database not + # being updated in time for the tests. db_processor = ( SimpleSpanProcessor(db_exporter) if debug From d8422ad88f06116f4e1ee325e66f56a375dd1593 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 6 Jan 2025 13:03:48 -0500 Subject: [PATCH 40/68] gzip --- src/core/trulens/experimental/otel_tracing/core/exporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 2c6ed065e..e562a90f5 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -105,7 +105,7 @@ def check_if_trulens_span(span: ReadableSpan) -> bool: logger.debug("Uploading the csv file to the stage") snowpark_session.sql( - f"PUT file://{tmp_file_path} @trulens_spans AUTO_COMPRESS=FALSE" + f"PUT file://{tmp_file_path} @trulens_spans" ).collect() except Exception as e: From 09f69ca814a752298d5f484ed4851f372a1e78c8 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 18:42:48 -0800 Subject: [PATCH 41/68] update --- .../otel_tracing/core/exporter.py | 82 ++- .../experimental/otel_tracing/core/span.py | 1 - .../unit/static/golden/api.trulens.3.11.yaml | 2 + .../static/golden/api.trulens_eval.3.11.yaml | 470 +++++++++--------- ..._instrument__test_instrument_decorator.csv | 18 +- tests/unit/test_exporter.py | 70 +++ 6 files changed, 399 insertions(+), 244 deletions(-) create mode 100644 tests/unit/test_exporter.py diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index e562a90f5..864ba9a31 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -1,10 +1,14 @@ -import csv from datetime import datetime import logging import os import tempfile from typing import Optional, Sequence +from opentelemetry.proto.common.v1.common_pb2 import AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ArrayValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValueList +from opentelemetry.proto.trace.v1.trace_pb2 import Span as SpanProto from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.export import SpanExportResult @@ -16,6 +20,61 @@ logger = logging.getLogger(__name__) +def convert_to_any_value(value) -> AnyValue: + any_value = AnyValue() + + if isinstance(value, str): + any_value.string_value = value + elif isinstance(value, bool): + any_value.bool_value = value + elif isinstance(value, int): + any_value.int_value = value + elif isinstance(value, float): + any_value.double_value = value + elif isinstance(value, bytes): + any_value.bytes_value = value + elif isinstance(value, list): + array_value = ArrayValue() + for item in value: + array_value.values.append(convert_to_any_value(item)) + any_value.array_value.CopyFrom(array_value) + elif isinstance(value, dict): + kv_list = KeyValueList() + for k, v in value.items(): + kv = KeyValue(key=k, value=convert_to_any_value(v)) + kv_list.values.append(kv) + any_value.kvlist_value.CopyFrom(kv_list) + else: + raise ValueError(f"Unsupported value type: {type(value)}") + + return any_value + + +def convert_readable_span_to_proto(span: ReadableSpan) -> SpanProto: + span_proto = SpanProto( + trace_id=span.context.trace_id.to_bytes(16, byteorder="big") + if span.context + else b"", + span_id=span.context.span_id.to_bytes(8, byteorder="big") + if span.context + else b"", + parent_span_id=span.parent.span_id.to_bytes(8, byteorder="big") + if span.parent + else b"", + name=span.name, + kind=SpanProto.SpanKind.SPAN_KIND_INTERNAL, + start_time_unix_nano=span.start_time if span.start_time else 0, + end_time_unix_nano=span.end_time if span.end_time else 0, + attributes=[ + KeyValue(key=k, value=convert_to_any_value(v)) + for k, v in span.attributes.items() + ] + if span.attributes + else None, + ) + return span_proto + + def to_timestamp(timestamp: Optional[int]) -> datetime: if timestamp: return datetime.fromtimestamp(timestamp * 1e-9) @@ -42,7 +101,7 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: event_id=str(context.span_id), record={ "name": span.name, - "kind": "SPAN_KIND_TRULENS", + "kind": SpanProto.SpanKind.SPAN_KIND_INTERNAL, "parent_span_id": str(parent.span_id if parent else ""), "status": "STATUS_CODE_ERROR" if span.status.status_code == StatusCode.ERROR @@ -62,10 +121,7 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: def check_if_trulens_span(span: ReadableSpan) -> bool: - if not span.attributes: - return False - - return span.attributes.get("kind") == "SPAN_KIND_TRULENS" + return span.resource.attributes.get("service.name") == "trulens" trulens_spans = list(filter(check_if_trulens_span, spans)) @@ -77,21 +133,21 @@ def check_if_trulens_span(span: ReadableSpan) -> bool: try: with tempfile.NamedTemporaryFile( - delete=False, suffix=".csv", mode="w", newline="" + delete=False, suffix=".pb", mode="wb" ) as tmp_file: tmp_file_path = tmp_file.name logger.debug( - f"Writing spans to the csv file: {tmp_file_path}" + f"Writing spans to the protobuf file: {tmp_file_path}" ) - writer = csv.writer(tmp_file) - writer.writerow(["span"]) + for span in trulens_spans: - writer.writerow([span.to_json()]) + span_proto = convert_readable_span_to_proto(span) + tmp_file.write(span_proto.SerializeToString()) logger.debug( - f"Spans written to the csv file: {tmp_file_path}" + f"Spans written to the protobuf file: {tmp_file_path}" ) except Exception as e: - logger.error(f"Error writing spans to the csv file: {e}") + logger.error(f"Error writing spans to the protobuf file: {e}") return SpanExportResult.FAILURE try: diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index 22b881c40..de361988b 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -86,7 +86,6 @@ def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: def set_general_span_attributes( span: Span, /, span_type: SpanAttributes.SpanType ) -> Span: - span.set_attribute("kind", "SPAN_KIND_TRULENS") span.set_attribute(SpanAttributes.SPAN_TYPE, span_type) span.set_attribute( SpanAttributes.APP_ID, str(get_baggage(SpanAttributes.APP_ID)) diff --git a/tests/unit/static/golden/api.trulens.3.11.yaml b/tests/unit/static/golden/api.trulens.3.11.yaml index 6f0605ccf..99497a22c 100644 --- a/tests/unit/static/golden/api.trulens.3.11.yaml +++ b/tests/unit/static/golden/api.trulens.3.11.yaml @@ -141,7 +141,9 @@ trulens.core.app.App: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class wait_for_feedback_results: builtins.function diff --git a/tests/unit/static/golden/api.trulens_eval.3.11.yaml b/tests/unit/static/golden/api.trulens_eval.3.11.yaml index 495adda17..189906424 100644 --- a/tests/unit/static/golden/api.trulens_eval.3.11.yaml +++ b/tests/unit/static/golden/api.trulens_eval.3.11.yaml @@ -78,7 +78,7 @@ trulens_eval.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -86,7 +86,7 @@ trulens_eval.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -170,7 +170,7 @@ trulens_eval.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -178,7 +178,7 @@ trulens_eval.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -256,7 +256,7 @@ trulens_eval.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -264,7 +264,7 @@ trulens_eval.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -332,14 +332,14 @@ trulens_eval.Feedback: load: builtins.staticmethod max_score_val: typing.Optional[builtins.int, builtins.NoneType] min_score_val: typing.Optional[builtins.int, builtins.NoneType] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -407,14 +407,14 @@ trulens_eval.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -457,14 +457,14 @@ trulens_eval.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -533,7 +533,7 @@ trulens_eval.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -541,7 +541,7 @@ trulens_eval.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -624,7 +624,7 @@ trulens_eval.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -632,7 +632,7 @@ trulens_eval.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -707,7 +707,7 @@ trulens_eval.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -715,7 +715,7 @@ trulens_eval.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -769,14 +769,14 @@ trulens_eval.Provider: get_class: builtins.staticmethod json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -869,14 +869,14 @@ trulens_eval.Tru: get_records_and_feedback: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -947,14 +947,14 @@ trulens_eval.TruBasicApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -989,7 +989,9 @@ trulens_eval.TruBasicApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1046,14 +1048,14 @@ trulens_eval.TruChain: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1088,7 +1090,9 @@ trulens_eval.TruChain: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1146,14 +1150,14 @@ trulens_eval.TruCustomApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1188,7 +1192,9 @@ trulens_eval.TruCustomApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1243,14 +1249,14 @@ trulens_eval.TruLlama: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1286,7 +1292,9 @@ trulens_eval.TruLlama: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1341,14 +1349,14 @@ trulens_eval.TruRails: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1383,7 +1391,9 @@ trulens_eval.TruRails: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1440,14 +1450,14 @@ trulens_eval.TruVirtual: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1482,7 +1492,9 @@ trulens_eval.TruVirtual: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1570,14 +1582,14 @@ trulens_eval.app.App: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1611,7 +1623,9 @@ trulens_eval.app.App: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1832,14 +1846,14 @@ trulens_eval.database.base.DB: insert_record: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1991,14 +2005,14 @@ trulens_eval.database.sqlalchemy.SQLAlchemyDB: insert_record: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2103,7 +2117,7 @@ trulens_eval.feedback.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2111,7 +2125,7 @@ trulens_eval.feedback.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2195,7 +2209,7 @@ trulens_eval.feedback.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2203,7 +2217,7 @@ trulens_eval.feedback.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -2281,7 +2295,7 @@ trulens_eval.feedback.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2289,7 +2303,7 @@ trulens_eval.feedback.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2337,14 +2351,14 @@ trulens_eval.feedback.Embeddings: json: builtins.function load: builtins.staticmethod manhattan_distance: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2402,14 +2416,14 @@ trulens_eval.feedback.Feedback: load: builtins.staticmethod max_score_val: typing.Optional[builtins.int, builtins.NoneType] min_score_val: typing.Optional[builtins.int, builtins.NoneType] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2471,14 +2485,14 @@ trulens_eval.feedback.GroundTruthAgreement: json: builtins.function load: builtins.staticmethod mae: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2523,14 +2537,14 @@ trulens_eval.feedback.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2573,14 +2587,14 @@ trulens_eval.feedback.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2649,7 +2663,7 @@ trulens_eval.feedback.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2657,7 +2671,7 @@ trulens_eval.feedback.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2733,7 +2747,7 @@ trulens_eval.feedback.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2741,7 +2755,7 @@ trulens_eval.feedback.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2816,7 +2830,7 @@ trulens_eval.feedback.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2824,7 +2838,7 @@ trulens_eval.feedback.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2886,14 +2900,14 @@ trulens_eval.feedback.embeddings.Embeddings: json: builtins.function load: builtins.staticmethod manhattan_distance: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2960,14 +2974,14 @@ trulens_eval.feedback.feedback.Feedback: load: builtins.staticmethod max_score_val: typing.Optional[builtins.int, builtins.NoneType] min_score_val: typing.Optional[builtins.int, builtins.NoneType] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3045,14 +3059,14 @@ trulens_eval.feedback.groundtruth.GroundTruthAgreement: json: builtins.function load: builtins.staticmethod mae: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3183,7 +3197,7 @@ trulens_eval.feedback.provider.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3191,7 +3205,7 @@ trulens_eval.feedback.provider.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3275,7 +3289,7 @@ trulens_eval.feedback.provider.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3283,7 +3297,7 @@ trulens_eval.feedback.provider.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -3361,7 +3375,7 @@ trulens_eval.feedback.provider.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3369,7 +3383,7 @@ trulens_eval.feedback.provider.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3418,14 +3432,14 @@ trulens_eval.feedback.provider.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3468,14 +3482,14 @@ trulens_eval.feedback.provider.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3544,7 +3558,7 @@ trulens_eval.feedback.provider.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3552,7 +3566,7 @@ trulens_eval.feedback.provider.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3628,7 +3642,7 @@ trulens_eval.feedback.provider.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3636,7 +3650,7 @@ trulens_eval.feedback.provider.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3711,7 +3725,7 @@ trulens_eval.feedback.provider.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3719,7 +3733,7 @@ trulens_eval.feedback.provider.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3773,14 +3787,14 @@ trulens_eval.feedback.provider.Provider: get_class: builtins.staticmethod json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3852,7 +3866,7 @@ trulens_eval.feedback.provider.base.LLMProvider: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3860,7 +3874,7 @@ trulens_eval.feedback.provider.base.LLMProvider: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3905,14 +3919,14 @@ trulens_eval.feedback.provider.base.Provider: get_class: builtins.staticmethod json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3984,7 +3998,7 @@ trulens_eval.feedback.provider.bedrock.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3992,7 +4006,7 @@ trulens_eval.feedback.provider.bedrock.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -4076,7 +4090,7 @@ trulens_eval.feedback.provider.cortex.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -4084,7 +4098,7 @@ trulens_eval.feedback.provider.cortex.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4157,14 +4171,14 @@ trulens_eval.feedback.provider.endpoint.BedrockEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4223,14 +4237,14 @@ trulens_eval.feedback.provider.endpoint.CortexEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4298,14 +4312,14 @@ trulens_eval.feedback.provider.endpoint.DummyEndpoint: load: builtins.staticmethod loading_prob: builtins.property loading_time: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4368,14 +4382,14 @@ trulens_eval.feedback.provider.endpoint.Endpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4436,14 +4450,14 @@ trulens_eval.feedback.provider.endpoint.HuggingfaceEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4504,14 +4518,14 @@ trulens_eval.feedback.provider.endpoint.LangchainEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4570,14 +4584,14 @@ trulens_eval.feedback.provider.endpoint.LiteLLMEndpoint: json: builtins.function litellm_provider: builtins.str load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4626,14 +4640,14 @@ trulens_eval.feedback.provider.endpoint.OpenAIClient: formatted_objects: _contextvars.ContextVar from_orm: builtins.classmethod json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4676,14 +4690,14 @@ trulens_eval.feedback.provider.endpoint.OpenAIEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4762,14 +4776,14 @@ trulens_eval.feedback.provider.endpoint.base.DummyEndpoint: load: builtins.staticmethod loading_prob: builtins.property loading_time: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4832,14 +4846,14 @@ trulens_eval.feedback.provider.endpoint.base.Endpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4892,14 +4906,14 @@ trulens_eval.feedback.provider.endpoint.base.EndpointCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4943,14 +4957,14 @@ trulens_eval.feedback.provider.endpoint.bedrock.BedrockCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4993,14 +5007,14 @@ trulens_eval.feedback.provider.endpoint.bedrock.BedrockEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5061,14 +5075,14 @@ trulens_eval.feedback.provider.endpoint.cortex.CortexCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5110,14 +5124,14 @@ trulens_eval.feedback.provider.endpoint.cortex.CortexEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5177,14 +5191,14 @@ trulens_eval.feedback.provider.endpoint.hugs.HuggingfaceCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5229,14 +5243,14 @@ trulens_eval.feedback.provider.endpoint.hugs.HuggingfaceEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5298,14 +5312,14 @@ trulens_eval.feedback.provider.endpoint.langchain.LangchainCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5348,14 +5362,14 @@ trulens_eval.feedback.provider.endpoint.langchain.LangchainEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5415,14 +5429,14 @@ trulens_eval.feedback.provider.endpoint.litellm.LiteLLMCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5465,14 +5479,14 @@ trulens_eval.feedback.provider.endpoint.litellm.LiteLLMEndpoint: json: builtins.function litellm_provider: builtins.str load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5535,14 +5549,14 @@ trulens_eval.feedback.provider.endpoint.openai.OpenAICallback: handle_generation_chunk: builtins.function json: builtins.function langchain_handler: langchain_community.callbacks.openai_info.OpenAICallbackHandler - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5575,14 +5589,14 @@ trulens_eval.feedback.provider.endpoint.openai.OpenAIClient: formatted_objects: _contextvars.ContextVar from_orm: builtins.classmethod json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5625,14 +5639,14 @@ trulens_eval.feedback.provider.endpoint.openai.OpenAIEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5702,14 +5716,14 @@ trulens_eval.feedback.provider.hugs.Dummy: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5752,14 +5766,14 @@ trulens_eval.feedback.provider.hugs.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5802,14 +5816,14 @@ trulens_eval.feedback.provider.hugs.HuggingfaceBase: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5852,14 +5866,14 @@ trulens_eval.feedback.provider.hugs.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5934,7 +5948,7 @@ trulens_eval.feedback.provider.langchain.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -5942,7 +5956,7 @@ trulens_eval.feedback.provider.langchain.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6024,7 +6038,7 @@ trulens_eval.feedback.provider.litellm.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -6032,7 +6046,7 @@ trulens_eval.feedback.provider.litellm.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6115,7 +6129,7 @@ trulens_eval.feedback.provider.openai.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -6123,7 +6137,7 @@ trulens_eval.feedback.provider.openai.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6207,7 +6221,7 @@ trulens_eval.feedback.provider.openai.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -6215,7 +6229,7 @@ trulens_eval.feedback.provider.openai.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6445,14 +6459,14 @@ trulens_eval.schema.app.AppDefinition: jsonify_extra: builtins.function load: builtins.staticmethod metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6555,14 +6569,14 @@ trulens_eval.schema.feedback.FeedbackCall: from_orm: builtins.classmethod json: builtins.function meta: typing.Dict[builtins.str, typing.Any] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6620,14 +6634,14 @@ trulens_eval.schema.feedback.FeedbackDefinition: builtins.NoneType] json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6693,14 +6707,14 @@ trulens_eval.schema.feedback.FeedbackResult: from_orm: builtins.classmethod json: builtins.function last_ts: datetime.datetime - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6800,14 +6814,14 @@ trulens_eval.schema.record.Record: builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] meta: typing.Union[builtins.str, builtins.int, builtins.float, builtins.bytes, builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6846,14 +6860,14 @@ trulens_eval.schema.record.RecordAppCall: from_orm: builtins.classmethod json: builtins.function method: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6890,14 +6904,14 @@ trulens_eval.schema.record.RecordAppCallMethod: from_orm: builtins.classmethod json: builtins.function method: trulens.core.utils.pyschema.Method - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6987,14 +7001,14 @@ trulens_eval.tru.Tru: get_records_and_feedback: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7073,14 +7087,14 @@ trulens_eval.tru_basic_app.TruBasicApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7115,7 +7129,9 @@ trulens_eval.tru_basic_app.TruBasicApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7216,14 +7232,14 @@ trulens_eval.tru_chain.TruChain: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7258,7 +7274,9 @@ trulens_eval.tru_chain.TruChain: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7324,14 +7342,14 @@ trulens_eval.tru_custom_app.TruCustomApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7366,7 +7384,9 @@ trulens_eval.tru_custom_app.TruCustomApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7452,14 +7472,14 @@ trulens_eval.tru_llama.TruLlama: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7495,7 +7515,9 @@ trulens_eval.tru_llama.TruLlama: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7618,14 +7640,14 @@ trulens_eval.tru_rails.TruRails: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7660,7 +7682,9 @@ trulens_eval.tru_rails.TruRails: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7725,14 +7749,14 @@ trulens_eval.tru_virtual.TruVirtual: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7767,7 +7791,9 @@ trulens_eval.tru_virtual.TruVirtual: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession + span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str + tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7814,14 +7840,14 @@ trulens_eval.tru_virtual.VirtualRecord: builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] meta: typing.Union[builtins.str, builtins.int, builtins.float, builtins.bytes, builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8155,14 +8181,14 @@ trulens_eval.utils.pyschema.Bindings: json: builtins.function kwargs: typing.Dict[builtins.str, typing.Any] load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8195,14 +8221,14 @@ trulens_eval.utils.pyschema.Class: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8239,14 +8265,14 @@ trulens_eval.utils.pyschema.Function: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8280,14 +8306,14 @@ trulens_eval.utils.pyschema.FunctionOrMethod: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8318,14 +8344,14 @@ trulens_eval.utils.pyschema.Method: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8359,14 +8385,14 @@ trulens_eval.utils.pyschema.Module: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8403,14 +8429,14 @@ trulens_eval.utils.pyschema.Obj: init_bindings: typing.Optional[trulens.core.utils.pyschema.Bindings, builtins.NoneType] json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8563,14 +8589,14 @@ trulens_eval.utils.serial.Collect: get: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8601,14 +8627,14 @@ trulens_eval.utils.serial.GetAttribute: get_item_or_attribute: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8638,14 +8664,14 @@ trulens_eval.utils.serial.GetIndex: get_sole_item: builtins.function index: builtins.int json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8675,14 +8701,14 @@ trulens_eval.utils.serial.GetIndices: get_sole_item: builtins.function indices: typing.Tuple[builtins.int, ...] json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8713,14 +8739,14 @@ trulens_eval.utils.serial.GetItem: get_sole_item: builtins.function item: builtins.str json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8751,14 +8777,14 @@ trulens_eval.utils.serial.GetItemOrAttribute: get_sole_item: builtins.function item_or_attribute: builtins.str json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8788,14 +8814,14 @@ trulens_eval.utils.serial.GetItems: get_sole_item: builtins.function items: typing.Tuple[builtins.str, ...] json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8824,14 +8850,14 @@ trulens_eval.utils.serial.GetSlice: get: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8913,14 +8939,14 @@ trulens_eval.utils.serial.StepItemOrAttribute: get_item_or_attribute: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 80f45eeca..67825bf87 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,9 +1,11 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",6386235298821949964,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411108,2025-01-08 17:45:34.414642,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '', 'span_id': '6386235298821949964'}" -1,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '6386235298821949964', 'status': 'STATUS_CODE_UNSET'}",3309777644303901504,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411139,2025-01-08 17:45:34.413800,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '6386235298821949964', 'span_id': '3309777644303901504'}" -2,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3309777644303901504', 'status': 'STATUS_CODE_UNSET'}",18092785063582233202,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411159,2025-01-08 17:45:34.413080,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '3309777644303901504', 'span_id': '18092785063582233202'}" -3,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '18092785063582233202', 'status': 'STATUS_CODE_UNSET'}",6211919514478763513,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.411175,2025-01-08 17:45:34.411204,"{'trace_id': '19520107341878365188412345160402396760', 'parent_id': '18092785063582233202', 'span_id': '6211919514478763513'}" -4,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",7428776760128741730,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415556,2025-01-08 17:45:34.418263,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '', 'span_id': '7428776760128741730'}" -5,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '7428776760128741730', 'status': 'STATUS_CODE_UNSET'}",15842740311901715118,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415580,2025-01-08 17:45:34.417583,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '7428776760128741730', 'span_id': '15842740311901715118'}" -6,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '15842740311901715118', 'status': 'STATUS_CODE_UNSET'}",4022483515734536614,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415599,2025-01-08 17:45:34.416914,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '15842740311901715118', 'span_id': '4022483515734536614'}" -7,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '4022483515734536614', 'status': 'STATUS_CODE_ERROR'}",15844601691241400614,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 17:45:34.415632,2025-01-08 17:45:34.416164,"{'trace_id': '134611545344801963096512965212999931605', 'parent_id': '4022483515734536614', 'span_id': '15844601691241400614'}" +0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",3267034738241115265,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761146,2025-01-08 18:29:47.765119,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '', 'span_id': '3267034738241115265'}" +1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '3267034738241115265', 'status': 'STATUS_CODE_UNSET'}",14155491579394485383,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761206,2025-01-08 18:29:47.764385,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '3267034738241115265', 'span_id': '14155491579394485383'}" +2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '14155491579394485383', 'status': 'STATUS_CODE_UNSET'}",17277530544584287791,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761232,2025-01-08 18:29:47.763511,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '14155491579394485383', 'span_id': '17277530544584287791'}" +3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '17277530544584287791', 'status': 'STATUS_CODE_UNSET'}",2889980798172968462,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761249,2025-01-08 18:29:47.762733,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '17277530544584287791', 'span_id': '2889980798172968462'}" +4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '2889980798172968462', 'status': 'STATUS_CODE_UNSET'}",16303319555009668024,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761266,2025-01-08 18:29:47.761293,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '2889980798172968462', 'span_id': '16303319555009668024'}" +5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",3317505627894968847,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.765946,2025-01-08 18:29:47.770238,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '', 'span_id': '3317505627894968847'}" +6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '3317505627894968847', 'status': 'STATUS_CODE_UNSET'}",2940165257583146187,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.765981,2025-01-08 18:29:47.769457,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '3317505627894968847', 'span_id': '2940165257583146187'}" +7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '2940165257583146187', 'status': 'STATUS_CODE_UNSET'}",14638858047259569279,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.766008,2025-01-08 18:29:47.768684,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '2940165257583146187', 'span_id': '14638858047259569279'}" +8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '14638858047259569279', 'status': 'STATUS_CODE_UNSET'}",15572150746830590999,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.766025,2025-01-08 18:29:47.767898,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '14638858047259569279', 'span_id': '15572150746830590999'}" +9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '15572150746830590999', 'status': 'STATUS_CODE_ERROR'}",1144650033552264284,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.766041,2025-01-08 18:29:47.766703,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '15572150746830590999', 'span_id': '1144650033552264284'}" diff --git a/tests/unit/test_exporter.py b/tests/unit/test_exporter.py new file mode 100644 index 000000000..446ce8d1f --- /dev/null +++ b/tests/unit/test_exporter.py @@ -0,0 +1,70 @@ +from opentelemetry.proto.common.v1.common_pb2 import AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ArrayValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValueList +import pytest +from trulens.experimental.otel_tracing.core.exporter import convert_to_any_value + + +def test_convert_to_any_value_string(): + value = "test_string" + any_value = convert_to_any_value(value) + assert any_value.string_value == value + + +def test_convert_to_any_value_bool(): + value = True + any_value = convert_to_any_value(value) + assert any_value.bool_value == value + + +def test_convert_to_any_value_int(): + value = 123 + any_value = convert_to_any_value(value) + assert any_value.int_value == value + + +def test_convert_to_any_value_float(): + value = 123.45 + any_value = convert_to_any_value(value) + assert any_value.double_value == pytest.approx(value) + + +def test_convert_to_any_value_bytes(): + value = b"test_bytes" + any_value = convert_to_any_value(value) + assert any_value.bytes_value == value + + +def test_convert_to_any_value_list(): + value = ["test_string", 123, 123.45, True] + any_value = convert_to_any_value(value) + assert any_value.array_value == ArrayValue( + values=[ + AnyValue(string_value="test_string"), + AnyValue(int_value=123), + AnyValue(double_value=123.45), + AnyValue(bool_value=True), + ] + ) + + +def test_convert_to_any_value_dict(): + value = {"key1": "value1", "key2": 123, "key3": 123.45, "key4": True} + any_value = convert_to_any_value(value) + assert any_value.kvlist_value == KeyValueList( + values=[ + KeyValue(key="key1", value=AnyValue(string_value="value1")), + KeyValue(key="key2", value=AnyValue(int_value=123)), + KeyValue(key="key3", value=AnyValue(double_value=123.45)), + KeyValue(key="key4", value=AnyValue(bool_value=True)), + ] + ) + + +def test_convert_to_any_value_unsupported_type(): + value = set([1, 2, 3]) + with pytest.raises( + ValueError, match="Unsupported value type: " + ): + convert_to_any_value(value) From ab0bb6a0a7c7ca0bff7de6d9664a967a17dcb378 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Wed, 8 Jan 2025 19:16:46 -0800 Subject: [PATCH 42/68] update protobuf --- examples/experimental/otel_exporter.ipynb | 1 - .../otel_tracing/core/exporter.py | 24 +++++++++++++------ .../experimental/otel_tracing/core/span.py | 1 + .../semconv/trulens/otel/semconv/trace.py | 6 +++++ tests/unit/test_exporter.py | 13 ++++++++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index ab1953623..206233568 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -125,7 +125,6 @@ "\n", "session = TruSession(connector=connector)\n", "session.experimental_enable_feature(\"otel_tracing\")\n", - "session.reset_database()\n", "\n", "init(session, debug=False)" ] diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 864ba9a31..3f08a6c8c 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -2,7 +2,7 @@ import logging import os import tempfile -from typing import Optional, Sequence +from typing import Any, Optional, Sequence from opentelemetry.proto.common.v1.common_pb2 import AnyValue from opentelemetry.proto.common.v1.common_pb2 import ArrayValue @@ -16,11 +16,14 @@ from trulens.connectors.snowflake import SnowflakeConnector from trulens.core.database import connector as core_connector from trulens.core.schema import event as event_schema +from trulens.otel.semconv.trace import SpanAttributes logger = logging.getLogger(__name__) -def convert_to_any_value(value) -> AnyValue: +def convert_to_any_value(value: Any) -> AnyValue: + if isinstance(value, tuple): + value = list(value) any_value = AnyValue() if isinstance(value, str): @@ -121,7 +124,12 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: def check_if_trulens_span(span: ReadableSpan) -> bool: - return span.resource.attributes.get("service.name") == "trulens" + if not span.attributes: + return False + + return bool( + span.attributes.get(SpanAttributes.GENERATED_BY_INSTRUMENTATION) + ) trulens_spans = list(filter(check_if_trulens_span, spans)) @@ -159,21 +167,23 @@ def check_if_trulens_span(span: ReadableSpan) -> bool: "CREATE STAGE IF NOT EXISTS trulens_spans" ).collect() - logger.debug("Uploading the csv file to the stage") + logger.debug("Uploading the protobuf file to the stage") snowpark_session.sql( f"PUT file://{tmp_file_path} @trulens_spans" ).collect() except Exception as e: - logger.error(f"Error uploading the csv file to the stage: {e}") + logger.error( + f"Error uploading the protobuf file to the stage: {e}" + ) return SpanExportResult.FAILURE try: - logger.debug("Removing the temporary csv file") + logger.debug("Removing the temporary protobuf file") os.remove(tmp_file_path) except Exception as e: # Not returning failure here since the export was technically a success - logger.error(f"Error removing the temporary csv file: {e}") + logger.error(f"Error removing the temporary protobuf file: {e}") return SpanExportResult.SUCCESS diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index de361988b..afd750ce6 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -86,6 +86,7 @@ def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: def set_general_span_attributes( span: Span, /, span_type: SpanAttributes.SpanType ) -> Span: + span.set_attribute(SpanAttributes.GENERATED_BY_INSTRUMENTATION, True) span.set_attribute(SpanAttributes.SPAN_TYPE, span_type) span.set_attribute( SpanAttributes.APP_ID, str(get_baggage(SpanAttributes.APP_ID)) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 3cbd2621a..9bc1fd011 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -39,6 +39,12 @@ class SpanAttributes: Base prefix for the other keys. """ + GENERATED_BY_INSTRUMENTATION = BASE + "generated_by_instrumentation" + """ + Key for the boolean value indicating whether the span was generated by + trulens instrumentation. + """ + SPAN_TYPE = BASE + "span_type" """ Span type attribute. diff --git a/tests/unit/test_exporter.py b/tests/unit/test_exporter.py index 446ce8d1f..d1ff8ec8b 100644 --- a/tests/unit/test_exporter.py +++ b/tests/unit/test_exporter.py @@ -68,3 +68,16 @@ def test_convert_to_any_value_unsupported_type(): ValueError, match="Unsupported value type: " ): convert_to_any_value(value) + + +def test_convert_to_any_value_tuple(): + value = ("test_string", 123, 123.45, True) + any_value = convert_to_any_value(value) + assert any_value.array_value == ArrayValue( + values=[ + AnyValue(string_value="test_string"), + AnyValue(int_value=123), + AnyValue(double_value=123.45), + AnyValue(bool_value=True), + ] + ) From 21f9b72a4cf61030017c00a3b28260c0469ecc45 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 9 Jan 2025 15:09:05 -0800 Subject: [PATCH 43/68] pr feedback --- .../otel_tracing/core/exporter.py | 152 +++++++++++------- .../semconv/trulens/otel/semconv/trace.py | 6 - 2 files changed, 95 insertions(+), 63 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 3f08a6c8c..995e82353 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -13,6 +13,7 @@ from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import StatusCode +from snowflake.snowpark import Session from trulens.connectors.snowflake import SnowflakeConnector from trulens.core.database import connector as core_connector from trulens.core.schema import event as event_schema @@ -22,6 +23,19 @@ def convert_to_any_value(value: Any) -> AnyValue: + """ + Converts a given value to an AnyValue object. + This function takes a value of various types (str, bool, int, float, bytes, list, dict) + and converts it into an AnyValue object. If the value is a list or a dictionary, it + recursively converts the elements or key-value pairs. Tuples are converted into lists. + Args: + value (Any): The value to be converted. It can be of type str, bool, int, float, + bytes, list, tuple, or dict. + Returns: + AnyValue: The converted AnyValue object. + Raises: + ValueError: If the value type is unsupported. + """ if isinstance(value, tuple): value = list(value) any_value = AnyValue() @@ -54,6 +68,13 @@ def convert_to_any_value(value: Any) -> AnyValue: def convert_readable_span_to_proto(span: ReadableSpan) -> SpanProto: + """ + Converts a ReadableSpan object to a the protobuf object for a Span. + Args: + span (ReadableSpan): The span to be converted. + Returns: + SpanProto: The converted span in SpanProto format. + """ span_proto = SpanProto( trace_id=span.context.trace_id.to_bytes(16, byteorder="big") if span.context @@ -79,12 +100,83 @@ def convert_readable_span_to_proto(span: ReadableSpan) -> SpanProto: def to_timestamp(timestamp: Optional[int]) -> datetime: + """ + Utility function for converting OTEL timestamps to datetime objects. + """ if timestamp: return datetime.fromtimestamp(timestamp * 1e-9) return datetime.now() +def export_to_snowflake_stage( + spans: Sequence[ReadableSpan], snowpark_session: Session +) -> SpanExportResult: + """ + Exports a list of spans to a Snowflake stage as a protobuf file. + This function performs the following steps: + 1. Writes the provided spans to a temporary protobuf file. + 2. Creates a Snowflake stage if it does not already exist. + 3. Uploads the temporary protobuf file to the Snowflake stage. + 4. Removes the temporary protobuf file. + Args: + spans (Sequence[ReadableSpan]): A sequence of spans to be exported. + snowpark_session (Session): The Snowpark session used to execute SQL commands. + Returns: + SpanExportResult: The result of the export operation, either SUCCESS or FAILURE. + """ + + tmp_file_path = "" + + try: + with tempfile.NamedTemporaryFile( + delete=False, suffix=".pb", mode="wb" + ) as tmp_file: + tmp_file_path = tmp_file.name + logger.debug(f"Writing spans to the protobuf file: {tmp_file_path}") + + for span in spans: + span_proto = convert_readable_span_to_proto(span) + tmp_file.write(span_proto.SerializeToString()) + logger.debug(f"Spans written to the protobuf file: {tmp_file_path}") + except Exception as e: + logger.error(f"Error writing spans to the protobuf file: {e}") + return SpanExportResult.FAILURE + + try: + logger.debug("Uploading file to Snowflake stage") + + logger.debug("Creating Snowflake stage if it does not exist") + snowpark_session.sql( + "CREATE TEMP STAGE IF NOT EXISTS trulens_spans" + ).collect() + + logger.debug("Uploading the protobuf file to the stage") + snowpark_session.sql( + f"PUT file://{tmp_file_path} @trulens_spans" + ).collect() + + except Exception as e: + logger.error(f"Error uploading the protobuf file to the stage: {e}") + return SpanExportResult.FAILURE + + try: + logger.debug("Removing the temporary protobuf file") + os.remove(tmp_file_path) + except Exception as e: + # Not returning failure here since the export was technically a success + logger.error(f"Error removing the temporary protobuf file: {e}") + + return SpanExportResult.SUCCESS + + +def check_if_trulens_span(span: ReadableSpan) -> bool: + if not span.attributes: + return False + + return bool(span.attributes.get(SpanAttributes.RECORD_ID)) + + class TruLensDBSpanExporter(SpanExporter): """ Implementation of `SpanExporter` that flushes the spans to the database in the TruLens session. @@ -123,69 +215,15 @@ def _construct_event(self, span: ReadableSpan) -> event_schema.Event: ) def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - def check_if_trulens_span(span: ReadableSpan) -> bool: - if not span.attributes: - return False - - return bool( - span.attributes.get(SpanAttributes.GENERATED_BY_INSTRUMENTATION) - ) - trulens_spans = list(filter(check_if_trulens_span, spans)) if not trulens_spans: return SpanExportResult.SUCCESS if isinstance(self.connector, SnowflakeConnector): - tmp_file_path = "" - - try: - with tempfile.NamedTemporaryFile( - delete=False, suffix=".pb", mode="wb" - ) as tmp_file: - tmp_file_path = tmp_file.name - logger.debug( - f"Writing spans to the protobuf file: {tmp_file_path}" - ) - - for span in trulens_spans: - span_proto = convert_readable_span_to_proto(span) - tmp_file.write(span_proto.SerializeToString()) - logger.debug( - f"Spans written to the protobuf file: {tmp_file_path}" - ) - except Exception as e: - logger.error(f"Error writing spans to the protobuf file: {e}") - return SpanExportResult.FAILURE - - try: - logger.debug("Uploading file to Snowflake stage") - snowpark_session = self.connector.snowpark_session - - logger.debug("Creating Snowflake stage if it does not exist") - snowpark_session.sql( - "CREATE STAGE IF NOT EXISTS trulens_spans" - ).collect() - - logger.debug("Uploading the protobuf file to the stage") - snowpark_session.sql( - f"PUT file://{tmp_file_path} @trulens_spans" - ).collect() - - except Exception as e: - logger.error( - f"Error uploading the protobuf file to the stage: {e}" - ) - return SpanExportResult.FAILURE - - try: - logger.debug("Removing the temporary protobuf file") - os.remove(tmp_file_path) - except Exception as e: - # Not returning failure here since the export was technically a success - logger.error(f"Error removing the temporary protobuf file: {e}") - - return SpanExportResult.SUCCESS + return export_to_snowflake_stage( + trulens_spans, self.connector.snowpark_session + ) # For non-snowflake: try: diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 9bc1fd011..3cbd2621a 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -39,12 +39,6 @@ class SpanAttributes: Base prefix for the other keys. """ - GENERATED_BY_INSTRUMENTATION = BASE + "generated_by_instrumentation" - """ - Key for the boolean value indicating whether the span was generated by - trulens instrumentation. - """ - SPAN_TYPE = BASE + "span_type" """ Span type attribute. From 37b6dcc2acdc1af81b84b8c4d196df7cf5f2173d Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 9 Jan 2025 15:12:57 -0800 Subject: [PATCH 44/68] is this where I add it? --- src/core/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/pyproject.toml b/src/core/pyproject.toml index d3e4cb38a..040239558 100644 --- a/src/core/pyproject.toml +++ b/src/core/pyproject.toml @@ -49,6 +49,7 @@ importlib-resources = "^6.0" trulens-otel-semconv = { version = "^1.0.0", optional = true } opentelemetry-api = { version = "^1.0.0", optional = true } opentelemetry-sdk = { version = "^1.0.0", optional = true } +opentelemetry-proto = { version = "^1.0.0", optional = true } [tool.poetry.group.tqdm] optional = true From e23b2fcd2e88778ae4881e97f934ea15758b5d55 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 9 Jan 2025 16:35:26 -0800 Subject: [PATCH 45/68] maybe here? --- Makefile | 1 + src/core/trulens/core/utils/requirements.txt | 1 + src/core/trulens/experimental/otel_tracing/_feature.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 84170bb67..5e798ec00 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ env-tests: nbconvert \ nbformat \ opentelemetry-sdk \ + opentelemetry-proto \ pre-commit \ pytest \ pytest-azurepipelines \ diff --git a/src/core/trulens/core/utils/requirements.txt b/src/core/trulens/core/utils/requirements.txt index 5fd0388bb..9996219ce 100644 --- a/src/core/trulens/core/utils/requirements.txt +++ b/src/core/trulens/core/utils/requirements.txt @@ -9,3 +9,4 @@ trulens-dashboard >= 1.0.0 # talk about these packages as required. opentelemetry-api >= 1.0.0 opentelemetry-sdk >= 1.0.0 +opentelemetry-proto >= 1.0.0 diff --git a/src/core/trulens/experimental/otel_tracing/_feature.py b/src/core/trulens/experimental/otel_tracing/_feature.py index 20d6c04f3..bfe9e113e 100644 --- a/src/core/trulens/experimental/otel_tracing/_feature.py +++ b/src/core/trulens/experimental/otel_tracing/_feature.py @@ -8,7 +8,7 @@ """Feature controlling the use of this module.""" REQUIREMENT = import_utils.format_import_errors( - ["opentelemetry-api", "opentelemetry-sdk"], + ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-proto"], purpose="otel_tracing experimental feature", ) """Optional modules required for the otel_tracing experimental feature.""" From af90428a185c2719fc310963a32b35ed24d7fb9f Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 08:46:08 -0800 Subject: [PATCH 46/68] undo migration --- Makefile | 1 + .../database/migrations/versions/10_create_event_table.py | 6 ------ 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 5e798ec00..e14f9d693 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,7 @@ env-tests: pytest-cov \ pytest-subtests \ ruff \ + snowflake env-tests-required: poetry install --only required \ diff --git a/src/core/trulens/core/database/migrations/versions/10_create_event_table.py b/src/core/trulens/core/database/migrations/versions/10_create_event_table.py index c4f9ed4e7..3dfe4eae6 100644 --- a/src/core/trulens/core/database/migrations/versions/10_create_event_table.py +++ b/src/core/trulens/core/database/migrations/versions/10_create_event_table.py @@ -21,9 +21,6 @@ def upgrade(config) -> None: if prefix is None: raise RuntimeError("trulens.table_prefix is not set") - if op.get_context().dialect.name == "snowflake": - return - op.create_table( prefix + "events", sa.Column("event_id", sa.VARCHAR(length=256), nullable=False), @@ -41,9 +38,6 @@ def upgrade(config) -> None: def downgrade(config) -> None: prefix = config.get_main_option("trulens.table_prefix") - if op.get_context().dialect.name == "snowflake": - return - if prefix is None: raise RuntimeError("trulens.table_prefix is not set") From 0fce605e061d823517ec3174b34b338176dceff7 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 09:05:25 -0800 Subject: [PATCH 47/68] update --- examples/experimental/otel_exporter.ipynb | 1 - .../trulens/experimental/otel_tracing/core/init.py | 10 +--------- tests/unit/test_otel_instrument.py | 1 - 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 206233568..cd1fbeebd 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -124,7 +124,6 @@ ")\n", "\n", "session = TruSession(connector=connector)\n", - "session.experimental_enable_feature(\"otel_tracing\")\n", "\n", "init(session, debug=False)" ] diff --git a/src/core/trulens/experimental/otel_tracing/core/init.py b/src/core/trulens/experimental/otel_tracing/core/init.py index 3543b2175..6e3d9e687 100644 --- a/src/core/trulens/experimental/otel_tracing/core/init.py +++ b/src/core/trulens/experimental/otel_tracing/core/init.py @@ -4,7 +4,6 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.export import SimpleSpanProcessor from trulens.core.session import TruSession from trulens.experimental.otel_tracing.core.exporter import ( @@ -23,14 +22,7 @@ def init(session: TruSession, debug: bool = False): provider = TracerProvider(resource=resource) trace.set_tracer_provider(provider) - if debug: - logger.debug( - "Initializing OpenTelemetry with TruLens configuration for console debugging" - ) - # Add a console exporter for debugging purposes - console_exporter = ConsoleSpanExporter() - console_processor = SimpleSpanProcessor(console_exporter) - provider.add_span_processor(console_processor) + session.experimental_enable_feature("otel_tracing") if session.connector: logger.debug("Exporting traces to the TruLens database") diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 8b82071c1..6472614c6 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -122,7 +122,6 @@ def test_instrument_decorator(self) -> None: test_app.respond_to_query("test") with custom_app: test_app.respond_to_query("throw") - # Compare results to expected. GOLDEN_FILENAME = "tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv" actual = self._get_events() From 04223737d3f33490c21dcf96e09162f4000f0457 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 16:05:08 -0800 Subject: [PATCH 48/68] save --- examples/experimental/otel_exporter.ipynb | 17 +- src/core/trulens/core/session.py | 34 +-- .../otel_tracing/core/exporter.py | 212 ++++++++++-------- .../experimental/otel_tracing/core/init.py | 55 ----- .../otel_tracing/core/instrument.py | 2 +- .../experimental/otel_tracing/core/session.py | 60 +++-- .../experimental/otel_tracing/core/span.py | 1 - 7 files changed, 193 insertions(+), 188 deletions(-) delete mode 100644 src/core/trulens/experimental/otel_tracing/core/init.py diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index cd1fbeebd..6a36c0760 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -41,6 +41,7 @@ "formatter = logging.Formatter(\n", " \"%(asctime)s - %(name)s - %(levelname)s - %(message)s\"\n", ")\n", + "handler.addFilter(logging.Filter(\"trulens\"))\n", "handler.setFormatter(formatter)\n", "root.addHandler(handler)" ] @@ -105,7 +106,9 @@ "import dotenv\n", "from trulens.connectors.snowflake import SnowflakeConnector\n", "from trulens.core.session import TruSession\n", - "from trulens.experimental.otel_tracing.core.init import init\n", + "from trulens.experimental.otel_tracing.core.exporter import (\n", + " TruLensSnowflakeSpanExporter,\n", + ")\n", "\n", "dotenv.load_dotenv()\n", "\n", @@ -123,9 +126,10 @@ " **connection_params,\n", ")\n", "\n", - "session = TruSession(connector=connector)\n", + "exporter = TruLensSnowflakeSpanExporter(connector=connector)\n", "\n", - "init(session, debug=False)" + "# Note that the connector for the session can be different from the connector for the exporter.\n", + "session = TruSession(_experimental_otel_exporter=exporter)" ] }, { @@ -137,7 +141,12 @@ "from trulens.apps.custom import TruCustomApp\n", "\n", "test_app = TestApp()\n", - "custom_app = TruCustomApp(test_app)\n", + "custom_app = TruCustomApp(\n", + " test_app,\n", + " app_name=\"database.schema.app\",\n", + " app_version=\"1.0.0\",\n", + " session=session,\n", + ")\n", "\n", "with custom_app as recording:\n", " test_app.respond_to_query(\"test\")\n", diff --git a/src/core/trulens/core/session.py b/src/core/trulens/core/session.py index e7b9fcfbf..4912f821f 100644 --- a/src/core/trulens/core/session.py +++ b/src/core/trulens/core/session.py @@ -45,6 +45,7 @@ from trulens.experimental.otel_tracing import _feature as otel_tracing_feature if TYPE_CHECKING: + from opentelemetry.sdk.trace.export import SpanExporter from trulens.core import app as base_app tqdm = None @@ -157,16 +158,14 @@ class TruSession( """Database Connector to use. If not provided, a default is created and used.""" - _experimental_otel_exporter: Optional[ - Any - ] = ( # Any = otel_export_sdk.SpanExporter - pydantic.PrivateAttr(None) + _experimental_otel_exporter: Optional[SpanExporter] = pydantic.PrivateAttr( + None ) @property def experimental_otel_exporter( self, - ) -> Any: # Any = Optional[otel_export_sdk.SpanExporter] + ) -> Optional[SpanExporter]: # Any = Optional[otel_export_sdk.SpanExporter] """EXPERIMENTAL(otel_tracing): OpenTelemetry SpanExporter to send spans to. @@ -178,7 +177,7 @@ def experimental_otel_exporter( @experimental_otel_exporter.setter def experimental_otel_exporter( - self, value: Optional[Any] + self, value: Optional[SpanExporter] ): # Any = otel_export_sdk.SpanExporter otel_tracing_feature._FeatureSetup.assert_optionals_installed() @@ -205,9 +204,7 @@ def __init__( Iterable[core_experimental.Feature], ] ] = None, - _experimental_otel_exporter: Optional[ - Any - ] = None, # Any = otel_export_sdk.SpanExporter + _experimental_otel_exporter: Optional[SpanExporter] = None, **kwargs, ): if python_utils.safe_hasattr(self, "connector"): @@ -236,24 +233,33 @@ def __init__( f"Cannot provide both `connector` and connector argument(s) {extra_keys}." ) + connector = connector or core_connector.DefaultDBConnector( + **connector_args + ) + super().__init__( - connector=connector - or core_connector.DefaultDBConnector(**connector_args), + connector=connector, **self_args, ) + self.connector + # for WithExperimentalSettings mixin if experimental_feature_flags is not None: self.experimental_set_features(experimental_feature_flags) - if _experimental_otel_exporter is not None: - otel_tracing_feature.assert_optionals_installed() + if _experimental_otel_exporter is not None or self.experimental_feature( + core_experimental.Feature.OTEL_TRACING + ): + otel_tracing_feature._FeatureSetup.assert_optionals_installed() from trulens.experimental.otel_tracing.core.session import ( _TruSession, ) - _TruSession._setup_otel_exporter(self, _experimental_otel_exporter) + _TruSession._setup_otel_exporter( + self, connector, _experimental_otel_exporter + ) def App(self, *args, app: Optional[Any] = None, **kwargs) -> base_app.App: """Create an App from the given App constructor arguments by guessing diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 995e82353..7fcec3ab6 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -13,7 +13,6 @@ from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import StatusCode -from snowflake.snowpark import Session from trulens.connectors.snowflake import SnowflakeConnector from trulens.core.database import connector as core_connector from trulens.core.schema import event as event_schema @@ -109,125 +108,150 @@ def to_timestamp(timestamp: Optional[int]) -> datetime: return datetime.now() -def export_to_snowflake_stage( - spans: Sequence[ReadableSpan], snowpark_session: Session -) -> SpanExportResult: +def check_if_trulens_span(span: ReadableSpan) -> bool: + if not span.attributes: + return False + + return bool(span.attributes.get(SpanAttributes.RECORD_ID)) + + +def construct_event(span: ReadableSpan) -> event_schema.Event: + context = span.get_span_context() + parent = span.parent + + if context is None: + raise ValueError("Span context is None") + + return event_schema.Event( + event_id=str(context.span_id), + record={ + "name": span.name, + "kind": SpanProto.SpanKind.SPAN_KIND_INTERNAL, + "parent_span_id": str(parent.span_id if parent else ""), + "status": "STATUS_CODE_ERROR" + if span.status.status_code == StatusCode.ERROR + else "STATUS_CODE_UNSET", + }, + record_attributes=span.attributes, + record_type=event_schema.EventRecordType.SPAN, + resource_attributes=span.resource.attributes, + start_timestamp=to_timestamp(span.start_time), + timestamp=to_timestamp(span.end_time), + trace={ + "span_id": str(context.span_id), + "trace_id": str(context.trace_id), + "parent_id": str(parent.span_id if parent else ""), + }, + ) + + +class TruLensSnowflakeSpanExporter(SpanExporter): """ - Exports a list of spans to a Snowflake stage as a protobuf file. - This function performs the following steps: - 1. Writes the provided spans to a temporary protobuf file. - 2. Creates a Snowflake stage if it does not already exist. - 3. Uploads the temporary protobuf file to the Snowflake stage. - 4. Removes the temporary protobuf file. - Args: - spans (Sequence[ReadableSpan]): A sequence of spans to be exported. - snowpark_session (Session): The Snowpark session used to execute SQL commands. - Returns: - SpanExportResult: The result of the export operation, either SUCCESS or FAILURE. + Implementation of `SpanExporter` that flushes the spans in the TruLens session to a Snowflake Stage. """ - tmp_file_path = "" + connector: core_connector.DBConnector + """ + The database connector used to export the spans. + """ - try: - with tempfile.NamedTemporaryFile( - delete=False, suffix=".pb", mode="wb" - ) as tmp_file: - tmp_file_path = tmp_file.name - logger.debug(f"Writing spans to the protobuf file: {tmp_file_path}") + def __init__(self, connector: core_connector.DBConnector): + self.connector = connector - for span in spans: - span_proto = convert_readable_span_to_proto(span) - tmp_file.write(span_proto.SerializeToString()) - logger.debug(f"Spans written to the protobuf file: {tmp_file_path}") - except Exception as e: - logger.error(f"Error writing spans to the protobuf file: {e}") - return SpanExportResult.FAILURE + def _export_to_snowflake_stage( + self, spans: Sequence[ReadableSpan] + ) -> SpanExportResult: + """ + Exports a list of spans to a Snowflake stage as a protobuf file. + This function performs the following steps: + 1. Writes the provided spans to a temporary protobuf file. + 2. Creates a Snowflake stage if it does not already exist. + 3. Uploads the temporary protobuf file to the Snowflake stage. + 4. Removes the temporary protobuf file. + Args: + spans (Sequence[ReadableSpan]): A sequence of spans to be exported. + Returns: + SpanExportResult: The result of the export operation, either SUCCESS or FAILURE. + """ + if not isinstance(self.connector, SnowflakeConnector): + return SpanExportResult.FAILURE - try: - logger.debug("Uploading file to Snowflake stage") + # Avoid uploading empty files to the stage + if not spans: + return SpanExportResult.SUCCESS - logger.debug("Creating Snowflake stage if it does not exist") - snowpark_session.sql( - "CREATE TEMP STAGE IF NOT EXISTS trulens_spans" - ).collect() + snowpark_session = self.connector.snowpark_session + tmp_file_path = "" - logger.debug("Uploading the protobuf file to the stage") - snowpark_session.sql( - f"PUT file://{tmp_file_path} @trulens_spans" - ).collect() + try: + with tempfile.NamedTemporaryFile( + delete=False, suffix=".pb", mode="wb" + ) as tmp_file: + tmp_file_path = tmp_file.name + logger.debug( + f"Writing spans to the protobuf file: {tmp_file_path}" + ) + + for span in spans: + span_proto = convert_readable_span_to_proto(span) + tmp_file.write(span_proto.SerializeToString()) + logger.debug( + f"Spans written to the protobuf file: {tmp_file_path}" + ) + except Exception as e: + logger.error(f"Error writing spans to the protobuf file: {e}") + return SpanExportResult.FAILURE - except Exception as e: - logger.error(f"Error uploading the protobuf file to the stage: {e}") - return SpanExportResult.FAILURE + try: + logger.debug("Uploading file to Snowflake stage") - try: - logger.debug("Removing the temporary protobuf file") - os.remove(tmp_file_path) - except Exception as e: - # Not returning failure here since the export was technically a success - logger.error(f"Error removing the temporary protobuf file: {e}") + logger.debug("Creating Snowflake stage if it does not exist") + snowpark_session.sql( + "CREATE TEMP STAGE IF NOT EXISTS trulens_spans" + ).collect() - return SpanExportResult.SUCCESS + logger.debug("Uploading the protobuf file to the stage") + snowpark_session.sql( + f"PUT file://{tmp_file_path} @trulens_spans" + ).collect() + except Exception as e: + logger.error(f"Error uploading the protobuf file to the stage: {e}") + return SpanExportResult.FAILURE -def check_if_trulens_span(span: ReadableSpan) -> bool: - if not span.attributes: - return False + try: + logger.debug("Removing the temporary protobuf file") + os.remove(tmp_file_path) + except Exception as e: + # Not returning failure here since the export was technically a success + logger.error(f"Error removing the temporary protobuf file: {e}") - return bool(span.attributes.get(SpanAttributes.RECORD_ID)) + return SpanExportResult.SUCCESS + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + trulens_spans = list(filter(check_if_trulens_span, spans)) -class TruLensDBSpanExporter(SpanExporter): + return self._export_to_snowflake_stage(trulens_spans) + + +class TruLensOTELSpanExporter(SpanExporter): + """ + Implementation of `SpanExporter` that flushes the spans in the TruLens session to the connector. + """ + + connector: core_connector.DBConnector """ - Implementation of `SpanExporter` that flushes the spans to the database in the TruLens session. + The database connector used to export the spans. """ def __init__(self, connector: core_connector.DBConnector): self.connector = connector - def _construct_event(self, span: ReadableSpan) -> event_schema.Event: - context = span.get_span_context() - parent = span.parent - - if context is None: - raise ValueError("Span context is None") - - return event_schema.Event( - event_id=str(context.span_id), - record={ - "name": span.name, - "kind": SpanProto.SpanKind.SPAN_KIND_INTERNAL, - "parent_span_id": str(parent.span_id if parent else ""), - "status": "STATUS_CODE_ERROR" - if span.status.status_code == StatusCode.ERROR - else "STATUS_CODE_UNSET", - }, - record_attributes=span.attributes, - record_type=event_schema.EventRecordType.SPAN, - resource_attributes=span.resource.attributes, - start_timestamp=to_timestamp(span.start_time), - timestamp=to_timestamp(span.end_time), - trace={ - "span_id": str(context.span_id), - "trace_id": str(context.trace_id), - "parent_id": str(parent.span_id if parent else ""), - }, - ) - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: trulens_spans = list(filter(check_if_trulens_span, spans)) - if not trulens_spans: - return SpanExportResult.SUCCESS - - if isinstance(self.connector, SnowflakeConnector): - return export_to_snowflake_stage( - trulens_spans, self.connector.snowpark_session - ) - - # For non-snowflake: try: - events = list(map(self._construct_event, trulens_spans)) + events = list(map(construct_event, trulens_spans)) self.connector.add_events(events) except Exception as e: diff --git a/src/core/trulens/experimental/otel_tracing/core/init.py b/src/core/trulens/experimental/otel_tracing/core/init.py deleted file mode 100644 index 6e3d9e687..000000000 --- a/src/core/trulens/experimental/otel_tracing/core/init.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging - -from opentelemetry import trace -from opentelemetry.sdk.resources import Resource -from opentelemetry.sdk.trace import TracerProvider -from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from trulens.core.session import TruSession -from trulens.experimental.otel_tracing.core.exporter import ( - TruLensDBSpanExporter, -) - -TRULENS_SERVICE_NAME = "trulens" - - -logger = logging.getLogger(__name__) - - -def init(session: TruSession, debug: bool = False): - """Initialize the OpenTelemetry SDK with TruLens configuration.""" - resource = Resource.create({"service.name": TRULENS_SERVICE_NAME}) - provider = TracerProvider(resource=resource) - trace.set_tracer_provider(provider) - - session.experimental_enable_feature("otel_tracing") - - if session.connector: - logger.debug("Exporting traces to the TruLens database") - - # Check the database revision - try: - db_revision = session.connector.db.get_db_revision() - if db_revision is None: - raise ValueError( - "Database revision is not set. Please run the migrations." - ) - if int(db_revision) < 10: - raise ValueError( - "Database revision is too low. Please run the migrations." - ) - except Exception: - raise ValueError("Error checking the database revision.") - - # Add the TruLens database exporter - db_exporter = TruLensDBSpanExporter(session.connector) - - # When testing, use a simple span processor to avoid issues with batching/ - # asynchronous processing of the spans that results in the database not - # being updated in time for the tests. - db_processor = ( - SimpleSpanProcessor(db_exporter) - if debug - else BatchSpanProcessor(db_exporter) - ) - provider.add_span_processor(db_processor) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 4864d2887..15aa735c6 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -8,7 +8,7 @@ from opentelemetry.baggage import set_baggage import opentelemetry.context as context_api from trulens.core import app as core_app -from trulens.experimental.otel_tracing.core.init import TRULENS_SERVICE_NAME +from trulens.experimental.otel_tracing.core.session import TRULENS_SERVICE_NAME from trulens.experimental.otel_tracing.core.span import Attributes from trulens.experimental.otel_tracing.core.span import ( set_general_span_attributes, diff --git a/src/core/trulens/experimental/otel_tracing/core/session.py b/src/core/trulens/experimental/otel_tracing/core/session.py index a87116742..74dcf2db8 100644 --- a/src/core/trulens/experimental/otel_tracing/core/session.py +++ b/src/core/trulens/experimental/otel_tracing/core/session.py @@ -1,37 +1,59 @@ -from typing import Any, Optional +import logging +from typing import Optional +from opentelemetry import trace +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import export as otel_export_sdk from trulens.core import experimental as core_experimental from trulens.core import session as core_session +from trulens.core.database.connector import DBConnector from trulens.core.utils import python as python_utils from trulens.core.utils import text as text_utils +from trulens.experimental.otel_tracing.core.exporter import ( + TruLensOTELSpanExporter, +) + +TRULENS_SERVICE_NAME = "trulens" + +logger = logging.getLogger(__name__) class _TruSession(core_session.TruSession): def _setup_otel_exporter( - self, val: Optional[Any] - ): # any actually otel_export_sdk.SpanExporter - if val is None: - self._experimental_otel_exporter = None - return - - # from opentelemetry import sdk as otel_sdk - try: - from opentelemetry.sdk.trace import export as otel_export_sdk - except ImportError: - raise ImportError( - "OpenTelemetry SDK not found. Please install OpenTelemetry SDK to use OpenTelemetry tracing." - ) - + self, + connector: DBConnector, + exporter: Optional[otel_export_sdk.SpanExporter], + ): assert isinstance( - val, otel_export_sdk.SpanExporter + exporter, otel_export_sdk.SpanExporter ), "otel_exporter must be an OpenTelemetry SpanExporter." self._experimental_feature( flag=core_experimental.Feature.OTEL_TRACING, value=True, freeze=True ) - print( - f"{text_utils.UNICODE_CHECK} OpenTelemetry exporter set: {python_utils.class_name(val.__class__)}" + logger.info( + f"{text_utils.UNICODE_CHECK} OpenTelemetry exporter set: {python_utils.class_name(exporter.__class__)}" + ) + + self._experimental_otel_exporter = exporter + + """Initialize the OpenTelemetry SDK with TruLens configuration.""" + resource = Resource.create({"service.name": TRULENS_SERVICE_NAME}) + provider = TracerProvider(resource=resource) + trace.set_tracer_provider(provider) + + # Export to the connector provided. + provider.add_span_processor( + otel_export_sdk.BatchSpanProcessor( + TruLensOTELSpanExporter(connector) + ) ) - self._experimental_otel_exporter = val + if exporter: + # When testing, use a simple span processor to avoid issues with batching/ + # asynchronous processing of the spans that results in the database not + # being updated in time for the tests. + db_processor = otel_export_sdk.BatchSpanProcessor(exporter) + provider.add_span_processor(db_processor) diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index afd750ce6..de361988b 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -86,7 +86,6 @@ def validate_attributes(attributes: Dict[str, Any]) -> Dict[str, Any]: def set_general_span_attributes( span: Span, /, span_type: SpanAttributes.SpanType ) -> Span: - span.set_attribute(SpanAttributes.GENERATED_BY_INSTRUMENTATION, True) span.set_attribute(SpanAttributes.SPAN_TYPE, span_type) span.set_attribute( SpanAttributes.APP_ID, str(get_baggage(SpanAttributes.APP_ID)) From 3083dd1861f6a53feaa0295fe0d509a4bb487a15 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 19:57:54 -0800 Subject: [PATCH 49/68] improvements --- examples/experimental/otel_exporter.ipynb | 10 ------ src/core/trulens/core/session.py | 36 ++++++++++++++++--- .../experimental/otel_tracing/core/session.py | 9 ++--- tests/unit/test_otel_instrument.py | 15 +++++--- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 6a36c0760..059c93e27 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -1,15 +1,5 @@ { "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# !pip install opentelemetry-api\n", - "# !pip install opentelemetry-sdk" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/src/core/trulens/core/session.py b/src/core/trulens/core/session.py index 4912f821f..f2e459bc6 100644 --- a/src/core/trulens/core/session.py +++ b/src/core/trulens/core/session.py @@ -45,6 +45,7 @@ from trulens.experimental.otel_tracing import _feature as otel_tracing_feature if TYPE_CHECKING: + from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SpanExporter from trulens.core import app as base_app @@ -162,10 +163,14 @@ class TruSession( None ) + _experimental_tracer_provider: Optional[TracerProvider] = ( + pydantic.PrivateAttr(None) + ) + @property def experimental_otel_exporter( self, - ) -> Optional[SpanExporter]: # Any = Optional[otel_export_sdk.SpanExporter] + ) -> Optional[SpanExporter]: """EXPERIMENTAL(otel_tracing): OpenTelemetry SpanExporter to send spans to. @@ -176,15 +181,38 @@ def experimental_otel_exporter( return self._experimental_otel_exporter @experimental_otel_exporter.setter - def experimental_otel_exporter( - self, value: Optional[SpanExporter] - ): # Any = otel_export_sdk.SpanExporter + def experimental_otel_exporter(self, value: Optional[SpanExporter]): otel_tracing_feature._FeatureSetup.assert_optionals_installed() from trulens.experimental.otel_tracing.core.session import _TruSession _TruSession._setup_otel_exporter(self, value) + def experimental_force_flush( + self, timeout_millis: Optional[int] = None + ) -> bool: + """ + Force flush the OpenTelemetry exporters. + + Args: + timeout_millis: The maximum amount of time to wait for spans to be + processed. + + Returns: + False if the timeout is exceeded, feature is not enabled, or the provider doesn't exist, True otherwise. + """ + timeout_millis = timeout_millis or 300000 + + if ( + not self.experimental_feature( + core_experimental.Feature.OTEL_TRACING + ) + or self._experimental_tracer_provider is None + ): + return False + + return self._experimental_tracer_provider.force_flush(timeout_millis) + def __str__(self) -> str: return f"TruSession({self.connector})" diff --git a/src/core/trulens/experimental/otel_tracing/core/session.py b/src/core/trulens/experimental/otel_tracing/core/session.py index 74dcf2db8..b2c4568a8 100644 --- a/src/core/trulens/experimental/otel_tracing/core/session.py +++ b/src/core/trulens/experimental/otel_tracing/core/session.py @@ -25,10 +25,6 @@ def _setup_otel_exporter( connector: DBConnector, exporter: Optional[otel_export_sdk.SpanExporter], ): - assert isinstance( - exporter, otel_export_sdk.SpanExporter - ), "otel_exporter must be an OpenTelemetry SpanExporter." - self._experimental_feature( flag=core_experimental.Feature.OTEL_TRACING, value=True, freeze=True ) @@ -43,6 +39,7 @@ def _setup_otel_exporter( resource = Resource.create({"service.name": TRULENS_SERVICE_NAME}) provider = TracerProvider(resource=resource) trace.set_tracer_provider(provider) + self._experimental_tracer_provider = provider # Export to the connector provided. provider.add_span_processor( @@ -52,6 +49,10 @@ def _setup_otel_exporter( ) if exporter: + assert isinstance( + exporter, otel_export_sdk.SpanExporter + ), "otel_exporter must be an OpenTelemetry SpanExporter." + # When testing, use a simple span processor to avoid issues with batching/ # asynchronous processing of the spans that results in the database not # being updated in time for the tests. diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index 6472614c6..2c7e5f510 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -7,9 +7,9 @@ import pandas as pd import sqlalchemy as sa from trulens.apps.custom import TruCustomApp +from trulens.core.experimental import Feature from trulens.core.schema.event import EventRecordType from trulens.core.session import TruSession -from trulens.experimental.otel_tracing.core.init import init from trulens.experimental.otel_tracing.core.instrument import instrument from trulens.otel.semconv.trace import SpanAttributes @@ -73,7 +73,9 @@ def clear_TruSession_singleton(cls) -> None: @classmethod def setUpClass(cls) -> None: cls.clear_TruSession_singleton() - tru_session = TruSession() + tru_session = TruSession( + experimental_feature_flags=[Feature.OTEL_TRACING] + ) tru_session.experimental_enable_feature("otel_tracing") return super().setUpClass() @@ -112,9 +114,11 @@ def _convert_column_types(df: pd.DataFrame) -> None: def test_instrument_decorator(self) -> None: # Set up. - tru_session = TruSession() + tru_session = TruSession( + experimental_feature_flags=[Feature.OTEL_TRACING] + ) tru_session.reset_database() - init(tru_session, debug=True) + # Create and run app. test_app = _TestApp() custom_app = TruCustomApp(test_app) @@ -124,7 +128,10 @@ def test_instrument_decorator(self) -> None: test_app.respond_to_query("throw") # Compare results to expected. GOLDEN_FILENAME = "tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv" + + tru_session.experimental_force_flush() actual = self._get_events() + self.assertEqual(len(actual), 10) self.write_golden(GOLDEN_FILENAME, actual) expected = self.load_golden(GOLDEN_FILENAME) From 18d1094c2bdfda67029464996e86fde9da6686b7 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 20:01:50 -0800 Subject: [PATCH 50/68] feedback --- .../experimental/otel_tracing/core/instrument.py | 13 ++++++++++--- .../trulens/experimental/otel_tracing/core/span.py | 6 +++++- src/otel/semconv/trulens/otel/semconv/trace.py | 9 +++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 15aa735c6..906f6fcd4 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -112,7 +112,14 @@ def __enter__(self): ) ) self.tokens.append( - context_api.attach(set_baggage(SpanAttributes.APP_ID, self.app_id)) + context_api.attach( + set_baggage(SpanAttributes.APP_NAME, self.app_name) + ) + ) + self.tokens.append( + context_api.attach( + set_baggage(SpanAttributes.APP_VERSION, self.app_version) + ) ) # Use start_as_current_span as a context manager @@ -132,7 +139,6 @@ def __enter__(self): root_span.set_attribute( SpanAttributes.RECORD_ROOT.APP_VERSION, self.app_version ) - root_span.set_attribute(SpanAttributes.RECORD_ROOT.APP_ID, self.app_id) root_span.set_attribute( SpanAttributes.RECORD_ROOT.RECORD_ID, otel_record_id ) @@ -141,7 +147,8 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, exc_tb): remove_baggage(SpanAttributes.RECORD_ID) - remove_baggage(SpanAttributes.APP_ID) + remove_baggage(SpanAttributes.APP_NAME) + remove_baggage(SpanAttributes.APP_VERSION) logger.debug("Exiting the OTEL app context.") diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index de361988b..6bce00e4f 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -87,8 +87,12 @@ def set_general_span_attributes( span: Span, /, span_type: SpanAttributes.SpanType ) -> Span: span.set_attribute(SpanAttributes.SPAN_TYPE, span_type) + + span.set_attribute( + SpanAttributes.APP_NAME, str(get_baggage(SpanAttributes.APP_NAME)) + ) span.set_attribute( - SpanAttributes.APP_ID, str(get_baggage(SpanAttributes.APP_ID)) + SpanAttributes.APP_VERSION, str(get_baggage(SpanAttributes.APP_VERSION)) ) span.set_attribute( SpanAttributes.RECORD_ID, str(get_baggage(SpanAttributes.RECORD_ID)) diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 3cbd2621a..9092805f8 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -59,8 +59,11 @@ class SpanAttributes: RECORD_ID = BASE + "record_id" """ID of the record that the span belongs to.""" - APP_ID = BASE + "app_id" - """ID of the app that the span belongs to.""" + APP_NAME = BASE + "app_name" + """Fully qualified name of the app that the span belongs to.""" + + APP_VERSION = BASE + "app_version" + """Name of the version that the span belongs to.""" ROOT_SPAN_ID = BASE + "root_span_id" """ID of the root span of the record that the span belongs to.""" @@ -202,8 +205,6 @@ class RECORD_ROOT: APP_VERSION = base + ".app_version" """Version of the app for whom this is the root.""" - APP_ID = base + ".app_id" - RECORD_ID = base + ".record_id" TOTAL_COST = base + ".total_cost" From 910265bccc13f0c001df00ad68b3b86b5e5137d3 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 20:04:46 -0800 Subject: [PATCH 51/68] remove app.py --- .../experimental/otel_tracing/core/app.py | 178 ------------------ 1 file changed, 178 deletions(-) delete mode 100644 src/core/trulens/experimental/otel_tracing/core/app.py diff --git a/src/core/trulens/experimental/otel_tracing/core/app.py b/src/core/trulens/experimental/otel_tracing/core/app.py deleted file mode 100644 index ca378808f..000000000 --- a/src/core/trulens/experimental/otel_tracing/core/app.py +++ /dev/null @@ -1,178 +0,0 @@ -from __future__ import annotations - -import contextvars -import time -from typing import ( - Iterable, -) - -from trulens.core import app as core_app -from trulens.core import instruments as core_instruments -from trulens.core.schema import feedback as feedback_schema -from trulens.core.schema import record as record_schema -from trulens.core.utils import python as python_utils -from trulens.core.utils import text as text_utils -from trulens.experimental.otel_tracing.core import trace as core_otel -from trulens.experimental.otel_tracing.core import trace as core_trace - - -class _App(core_app.App): - # TODO(otel_tracing): Roll into core_app.App once no longer experimental. - - # WithInstrumentCallbacks requirement - def get_active_contexts( - self, - ) -> Iterable[core_instruments._RecordingContext]: - """Get all active recording contexts.""" - - recording = self.recording_contexts.get(contextvars.Token.MISSING) - - while recording is not contextvars.Token.MISSING: - yield recording - recording = recording.token.old_value - - # WithInstrumentCallbacks requirement - def _on_new_recording_span( - self, - recording_span: core_trace.Span, - ): - if self.session._experimental_otel_exporter is not None: - # Export to otel exporter if exporter was set in workspace. - to_export = [] - for span in recording_span.iter_family(include_phantom=True): - if isinstance(span, core_otel.Span): - e_span = span.otel_freeze() - to_export.append(e_span) - else: - print(f"Warning, span {span.name} is not exportable.") - - print( - f"{text_utils.UNICODE_CHECK} Exporting {len(to_export)} spans to {python_utils.class_name(self.session._experimental_otel_exporter)}." - ) - self.session._experimental_otel_exporter.export(to_export) - - # WithInstrumentCallbacks requirement - def _on_new_root_span( - self, - recording: core_instruments._RecordingContext, - root_span: core_trace.Span, - ) -> record_schema.Record: - tracer = root_span.context.tracer - - record = tracer.record_of_root_span( - root_span=root_span, recording=recording - ) - recording.records.append(record) - # need to jsonify? - - error = root_span.error - - if error is not None: - # May block on DB. - self._handle_error(record=record, error=error) - raise error - - # Will block on DB, but not on feedback evaluation, depending on - # FeedbackMode: - record.feedback_and_future_results = self._handle_record(record=record) - if record.feedback_and_future_results is not None: - record.feedback_results = [ - tup[1] for tup in record.feedback_and_future_results - ] - - if record.feedback_and_future_results is None: - return record - - if self.feedback_mode == feedback_schema.FeedbackMode.WITH_APP_THREAD: - # Add the record to ones with pending feedback. - - self.records_with_pending_feedback_results.add(record) - - elif self.feedback_mode == feedback_schema.FeedbackMode.WITH_APP: - # If in blocking mode ("WITH_APP"), wait for feedbacks to finished - # evaluating before returning the record. - - record.wait_for_feedback_results() - - return record - - # For use as a context manager. - def __enter__(self): - # EXPERIMENTAL(otel_tracing): replacement to recording context manager. - - tracer: core_trace.Tracer = core_trace.trulens_tracer() - - recording_span_ctx = tracer.recording() - recording_span: core_trace.PhantomSpanRecordingContext = ( - recording_span_ctx.__enter__() - ) - recording = core_trace._RecordingContext( - app=self, - tracer=tracer, - span=recording_span, - span_ctx=recording_span_ctx, - ) - recording_span.recording = recording - recording_span._start_timestamp = time.time_ns() # move to trace - - # recording.ctx = ctx - - token = self.recording_contexts.set(recording) - recording.token = token - - return recording - - # For use as a context manager. - def __exit__(self, exc_type, exc_value, exc_tb): - # EXPERIMENTAL(otel_tracing): replacement to recording context manager. - - recording: core_trace._RecordingContext = self.recording_contexts.get() - - assert recording is not None, "Not in a tracing context." - assert recording.tracer is not None, "Not in a tracing context." - assert recording.span is not None, "Not in a tracing context." - - recording.span._end_timestamp = time.time_ns() # move to trace - - self.recording_contexts.reset(recording.token) - return recording.span_ctx.__exit__(exc_type, exc_value, exc_tb) - - # For use as an async context manager. - async def __aenter__(self): - # EXPERIMENTAL(otel_tracing) - - tracer: core_trace.Tracer = core_trace.trulens_tracer() - - recording_span_ctx = await tracer.arecording() - recording_span: core_trace.PhantomSpanRecordingContext = ( - await recording_span_ctx.__aenter__() - ) - recording = core_trace._RecordingContext( - app=self, - tracer=tracer, - span=recording_span, - span_ctx=recording_span_ctx, - ) - recording_span.recording = recording - recording_span.start_timestamp = time.time_ns() - - # recording.ctx = ctx - - token = self.recording_contexts.set(recording) - recording.token = token - - return recording - - # For use as a context manager. - async def __aexit__(self, exc_type, exc_value, exc_tb): - # EXPERIMENTAL(otel_tracing) - - recording: core_trace._RecordingContext = self.recording_contexts.get() - - assert recording is not None, "Not in a tracing context." - assert recording.tracer is not None, "Not in a tracing context." - - recording.span.end_timestamp = time.time_ns() - - self.recording_contexts.reset(recording.token) - return await recording.span_ctx.__aexit__(exc_type, exc_value, exc_tb) From 9280281f08d626c5fa7f06d9ed1510c2e77a62e8 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 20:05:06 -0800 Subject: [PATCH 52/68] remove instruments.py --- .../otel_tracing/core/instruments.py | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 src/core/trulens/experimental/otel_tracing/core/instruments.py diff --git a/src/core/trulens/experimental/otel_tracing/core/instruments.py b/src/core/trulens/experimental/otel_tracing/core/instruments.py deleted file mode 100644 index 180147655..000000000 --- a/src/core/trulens/experimental/otel_tracing/core/instruments.py +++ /dev/null @@ -1,54 +0,0 @@ -from __future__ import annotations - -import logging -from typing import Callable, TypeVar -import weakref - -from trulens.core import instruments as core_instruments -from trulens.core.utils import python as python_utils -from trulens.core.utils import serial as serial_utils -from trulens.experimental.otel_tracing.core import trace as mod_trace -from trulens.experimental.otel_tracing.core._utils import wrap as wrap_utils - -logger = logging.getLogger(__name__) - -T = TypeVar("T") - - -def deproxy(proxy: weakref.ProxyType[T]) -> T: - """Return the object being proxied.""" - - return proxy.__init__.__self__ - - -class _Instrument(core_instruments.Instrument): - def tracked_method_wrapper( - self, - query: serial_utils.Lens, - func: Callable, - method_name: str, - cls: type, - obj: object, - ): - """Wrap a method to capture its inputs/outputs/errors.""" - - if self.app is None: - raise ValueError("Instrumentation requires an app but is None.") - - if python_utils.safe_hasattr(func, "__func__"): - raise ValueError("Function expected but method received.") - - if python_utils.safe_hasattr(func, mod_trace.INSTRUMENT): - logger.debug("\t\t\t%s: %s is already instrumented", query, func) - - # Notify the app instrumenting this method where it is located: - self.app.on_method_instrumented(obj, func, path=query) - - logger.debug("\t\t\t%s: instrumenting %s=%s", query, method_name, func) - - return wrap_utils.wrap_callable( - func=func, - callback_class=mod_trace.AppTracingCallbacks, - func_name=method_name, - app=deproxy(self.app), - ) From 6ca1f3cd1093466f7e3eb63a301dc2a8d7b7067c Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 20:07:23 -0800 Subject: [PATCH 53/68] remove unused --- .../experimental/otel_tracing/core/otel.py | 749 ----------- .../experimental/otel_tracing/core/trace.py | 1098 ----------------- 2 files changed, 1847 deletions(-) delete mode 100644 src/core/trulens/experimental/otel_tracing/core/otel.py delete mode 100644 src/core/trulens/experimental/otel_tracing/core/trace.py diff --git a/src/core/trulens/experimental/otel_tracing/core/otel.py b/src/core/trulens/experimental/otel_tracing/core/otel.py deleted file mode 100644 index e65c0b8e7..000000000 --- a/src/core/trulens/experimental/otel_tracing/core/otel.py +++ /dev/null @@ -1,749 +0,0 @@ -# ruff: noqa: E402 - -"""OTEL Compatibility Classes - -This module contains classes to support interacting with the OTEL ecosystem. -Additions on top of these meant for TruLens uses outside of OTEL compatibility -are found in `traces.py`. -""" - -from __future__ import annotations - -import contextlib -import contextvars -import logging -import random -import time -from types import TracebackType -from typing import ( - Any, - Dict, - Iterable, - List, - Mapping, - Optional, - Sequence, - Tuple, - Type, - TypeVar, - Union, -) - -import pydantic -from trulens.core._utils.pycompat import NoneType # import style exception -from trulens.core._utils.pycompat import TypeAlias # import style exception -from trulens.core._utils.pycompat import TypeAliasType # import style exception -from trulens.core.utils import pyschema as pyschema_utils -from trulens.core.utils import python as python_utils -from trulens.core.utils import serial as serial_utils -from trulens.experimental.otel_tracing import _feature - -_feature._FeatureSetup.assert_optionals_installed() # checks to make sure otel is installed - -from opentelemetry import context as context_api -from opentelemetry import trace as trace_api -from opentelemetry.sdk import resources as resources_sdk -from opentelemetry.sdk import trace as trace_sdk -from opentelemetry.util import types as types_api - -logger = logging.getLogger(__name__) - -# Type alises - -A = TypeVar("A") -B = TypeVar("B") - -TSpanID: TypeAlias = int -"""Type of span identifiers. -64 bit int as per OpenTelemetry. -""" -NUM_SPANID_BITS: int = 64 -"""Number of bits in a span identifier.""" - -TTraceID: TypeAlias = int -"""Type of trace identifiers. -128 bit int as per OpenTelemetry. -""" -NUM_TRACEID_BITS: int = 128 -"""Number of bits in a trace identifier.""" - -TTimestamp: TypeAlias = int -"""Type of timestamps in spans. - -64 bit int representing nanoseconds since epoch as per OpenTelemetry. -""" -NUM_TIMESTAMP_BITS = 64 - -TLensedBaseType: TypeAlias = Union[str, int, float, bool] -"""Type of base types in span attributes. - -!!! Warning - OpenTelemetry does not allow None as an attribute value. Handling None is to - be decided. -""" - -TLensedAttributeValue = TypeAliasType( - "TLensedAttributeValue", - Union[ - str, - int, - float, - bool, - NoneType, # TODO(SNOW-1711929): None is not technically allowed as an attribute value. - Sequence["TLensedAttributeValue"], # type: ignore - "TLensedAttributes", - ], -) -"""Type of values in span attributes.""" - -# NOTE(piotrm): pydantic will fail if you specify a recursive type alias without -# the TypeAliasType schema as above. - -TLensedAttributes: TypeAlias = Dict[str, TLensedAttributeValue] -"""Attribute dictionaries. - -Note that this deviates from what OTEL allows as attribute values. Because OTEL -does not allow general recursive values to be stored as attributes, we employ a -system of flattening values before exporting to OTEL. In this process we encode -a single generic value as multiple attributes where the attribute name include -paths/lenses to the parts of the generic value they are representing. For -example, an attribute/value like `{"a": {"b": 1, "c": 2}}` would be encoded as -`{"a.b": 1, "a.c": 2}`. This process is implemented in the -`flatten_lensed_attributes` method. -""" - - -def flatten_value( - v: TLensedAttributeValue, lens: Optional[serial_utils.Lens] = None -) -> Iterable[Tuple[serial_utils.Lens, types_api.AttributeValue]]: - """Flatten recursive value into OTEL-compatible attribute values. - - See `TLensedAttributes` for more details. - """ - - if lens is None: - lens = serial_utils.Lens() - - # TODO(SNOW-1711929): OpenTelemetry does not allow None as an attribute - # value. Unsure what is best to do here. - - # if v is None: - # yield (path, "None") - - elif v is None: - pass - - elif isinstance(v, TLensedBaseType): - yield (lens, v) - - elif isinstance(v, Sequence) and all( - isinstance(e, TLensedBaseType) for e in v - ): - yield (lens, v) - - elif isinstance(v, Sequence): - for i, e in enumerate(v): - yield from flatten_value(v=e, lens=lens[i]) - - elif isinstance(v, Mapping): - for k, e in v.items(): - yield from flatten_value(v=e, lens=lens[k]) - - else: - raise ValueError( - f"Do not know how to flatten value of type {type(v)} to OTEL attributes." - ) - - -def flatten_lensed_attributes( - m: TLensedAttributes, - path: Optional[serial_utils.Lens] = None, - prefix: str = "", -) -> types_api.Attributes: - """Flatten lensed attributes into OpenTelemetry attributes.""" - - if path is None: - path = serial_utils.Lens() - - ret = {} - for k, v in m.items(): - if k.startswith(prefix): - # Only flattening those attributes that begin with `prefix` are - # those are the ones coming from trulens_eval. - for p, a in flatten_value(v, path[k]): - ret[str(p)] = a - else: - ret[k] = v - - return ret - - -def new_trace_id(): - return int( - random.getrandbits(NUM_TRACEID_BITS) - & trace_api.span._TRACE_ID_MAX_VALUE - ) - - -def new_span_id(): - return int( - random.getrandbits(NUM_SPANID_BITS) & trace_api.span._SPAN_ID_MAX_VALUE - ) - - -class TraceState(serial_utils.SerialModel, trace_api.span.TraceState): - """[OTEL TraceState][opentelemetry.trace.TraceState] requirements.""" - - # Hackish: trace_api.span.TraceState uses _dict internally. - _dict: Dict[str, str] = pydantic.PrivateAttr(default_factory=dict) - - -class SpanContext(serial_utils.SerialModel): - """[OTEL SpanContext][opentelemetry.trace.SpanContext] requirements.""" - - model_config = pydantic.ConfigDict( - arbitrary_types_allowed=True, - use_enum_values=True, # needed for enums that do not inherit from str - ) - - trace_id: int = pydantic.Field(default_factory=new_trace_id) - """Unique identifier for the trace. - - Each root span has a unique trace id.""" - - span_id: int = pydantic.Field(default_factory=new_span_id) - """Identifier for the span. - - Meant to be at least unique within the same trace_id. - """ - - trace_flags: trace_api.TraceFlags = pydantic.Field( - trace_api.DEFAULT_TRACE_OPTIONS - ) - - @pydantic.field_validator("trace_flags", mode="before") - @classmethod - def _validate_trace_flags(cls, v): - """Validate trace flags. - - Pydantic does not seem to like classes that inherit from int without this. - """ - return trace_api.TraceFlags(v) - - trace_state: TraceState = pydantic.Field(default_factory=TraceState) - - is_remote: bool = False - - _tracer: Tracer = pydantic.PrivateAttr(None) - - @property - def tracer(self) -> Tracer: - return self._tracer - - def __init__(self, **kwargs): - super().__init__(**kwargs) - for k, v in kwargs.items(): - if v is None: - continue - # pydantic does not set private attributes in init - if k.startswith("_") and hasattr(self, k): - setattr(self, k, v) - - -def lens_of_flat_key(key: str) -> serial_utils.Lens: - """Convert a flat dict key to a lens.""" - lens = serial_utils.Lens() - for step in key.split("."): - lens = lens[step] - - return lens - - -class Span( - pyschema_utils.WithClassInfo, serial_utils.SerialModel, trace_api.Span -): - """[OTEL Span][opentelemetry.trace.Span] requirements. - - See also [OpenTelemetry - Span](https://opentelemetry.io/docs/specs/otel/trace/api/#span) and - [OpenTelemetry Span - specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md). - """ - - model_config = pydantic.ConfigDict( - arbitrary_types_allowed=True, - use_enum_values=True, # model_validate will fail without this - ) - - name: Optional[str] = None - - kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL - - context: SpanContext = pydantic.Field(default_factory=SpanContext) - parent: Optional[SpanContext] = None - - status: trace_api.status.StatusCode = trace_api.status.StatusCode.UNSET - status_description: Optional[str] = None - - events: List[Tuple[str, trace_api.types.Attributes, TTimestamp]] = ( - pydantic.Field(default_factory=list) - ) - links: trace_api._Links = pydantic.Field(default_factory=dict) - - # attributes: trace_api.types.Attributes = pydantic.Field(default_factory=dict) - attributes: Dict = pydantic.Field(default_factory=dict) - - start_timestamp: int = pydantic.Field(default_factory=time.time_ns) - - end_timestamp: Optional[int] = None - - _record_exception: bool = pydantic.PrivateAttr(True) - _set_status_on_exception: bool = pydantic.PrivateAttr(True) - - _tracer: Tracer = pydantic.PrivateAttr(None) - """NON-STANDARD: The Tracer that produced this span.""" - - @property - def tracer(self) -> Tracer: - return self._tracer - - def __init__(self, **kwargs): - if kwargs.get("start_timestamp") is None: - kwargs["start_timestamp"] = time.time_ns() - - super().__init__(**kwargs) - - for k, v in kwargs.items(): - if v is None: - continue - # pydantic does not set private attributes in init - if k.startswith("_") and hasattr(self, k): - setattr(self, k, v) - - def update_name(self, name: str) -> None: - """See [OTEL update_name][opentelemetry.trace.span.Span.update_name].""" - - self.name = name - - def get_span_context(self) -> trace_api.span.SpanContext: - """See [OTEL get_span_context][opentelemetry.trace.span.Span.get_span_context].""" - - return self.context - - def set_status( - self, - status: Union[trace_api.span.Status, trace_api.span.StatusCode], - description: Optional[str] = None, - ) -> None: - """See [OTEL set_status][opentelemetry.trace.span.Span.set_status].""" - - if isinstance(status, trace_api.span.Status): - if description is not None: - raise ValueError( - "Ambiguous status description provided both in `status.description` and in `description`." - ) - - self.status = status.status_code - self.status_description = status.description - else: - self.status = status - self.status_description = description - - def add_event( - self, - name: str, - attributes: types_api.Attributes = None, - timestamp: Optional[TTimestamp] = None, - ) -> None: - """See [OTEL add_event][opentelemetry.trace.span.Span.add_event].""" - - self.events.append((name, attributes, timestamp or time.time_ns())) - - def add_link( - self, - context: trace_api.span.SpanContext, - attributes: types_api.Attributes = None, - ) -> None: - """See [OTEL add_link][opentelemetry.trace.span.Span.add_link].""" - - if attributes is None: - attributes = {} - - self.links[context] = attributes - - def is_recording(self) -> bool: - """See [OTEL is_recording][opentelemetry.trace.span.Span.is_recording].""" - - return self.status == trace_api.status.StatusCode.UNSET - - def set_attributes( - self, attributes: Dict[str, types_api.AttributeValue] - ) -> None: - """See [OTEL set_attributes][opentelemetry.trace.span.Span.set_attributes].""" - - for key, value in attributes.items(): - self.set_attribute(key, value) - - def set_attribute(self, key: str, value: types_api.AttributeValue) -> None: - """See [OTEL set_attribute][opentelemetry.trace.span.Span.set_attribute].""" - - self.attributes[key] = value - - def record_exception( - self, - exception: BaseException, - attributes: types_api.Attributes = None, - timestamp: Optional[TTimestamp] = None, - escaped: bool = False, # purpose unknown - ) -> None: - """See [OTEL record_exception][opentelemetry.trace.span.Span.record_exception].""" - - if self._set_status_on_exception: - self.set_status( - trace_api.status.Status(trace_api.status.StatusCode.ERROR) - ) - - if self._record_exception: - if attributes is None: - attributes = {} - - attributes["exc_type"] = python_utils.class_name(type(exception)) - attributes["exc_val"] = str(exception) - if exception.__traceback__ is not None: - attributes["code_line"] = python_utils.code_line( - exception.__traceback__.tb_frame, show_source=True - ) - - self.add_event("trulens.exception", attributes, timestamp) - - def end(self, end_time: Optional[TTimestamp] = None): - """See [OTEL end][opentelemetry.trace.span.Span.end]""" - - if end_time is None: - end_time = time.time_ns() - - self.end_timestamp = end_time - - if self.is_recording(): - self.set_status( - trace_api.status.Status(trace_api.status.StatusCode.OK) - ) - - def __enter__(self) -> Span: - """See [OTEL __enter__][opentelemetry.trace.span.Span.__enter__].""" - - return self - - def __exit__( - self, - exc_type: Optional[BaseException], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> None: - """See [OTEL __exit__][opentelemetry.trace.span.Span.__exit__].""" - - try: - if exc_val is not None: - self.record_exception(exception=exc_val) - raise exc_val - finally: - self.end() - - async def __aenter__(self) -> Span: - return self.__enter__() - - async def __aexit__( - self, - exc_type: Optional[BaseException], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], - ) -> None: - return self.__exit__(exc_type, exc_val, exc_tb) - - # Rest of these methods are for exporting spans to ReadableSpan. All are not standard OTEL. - - @staticmethod - def otel_context_of_context(context: SpanContext) -> trace_api.SpanContext: - return trace_api.SpanContext( - trace_id=context.trace_id, - span_id=context.span_id, - is_remote=False, - ) - - def otel_name(self) -> str: - return self.name - - def otel_context(self) -> types_api.SpanContext: - return self.otel_context_of_context(self.context) - - def otel_parent_context(self) -> Optional[types_api.SpanContext]: - if self.parent is None: - return None - - return self.otel_context_of_context(self.parent) - - def otel_attributes(self) -> types_api.Attributes: - return flatten_lensed_attributes(self.attributes) - - def otel_kind(self) -> types_api.SpanKind: - return trace_api.SpanKind.INTERNAL - - def otel_status(self) -> trace_api.status.Status: - return trace_api.status.Status(self.status, self.status_description) - - def otel_resource_attributes(self) -> Dict[str, Any]: - # TODO(SNOW-1711959) - return { - "service.namespace": "trulens", - } - - def otel_resource(self) -> resources_sdk.Resource: - return resources_sdk.Resource( - attributes=self.otel_resource_attributes() - ) - - def otel_events(self) -> List[types_api.Event]: - return self.events - - def otel_links(self) -> List[types_api.Link]: - return self.links - - def otel_freeze(self) -> trace_sdk.ReadableSpan: - """Convert span to an OTEL compatible span for exporting to OTEL collectors.""" - - return trace_sdk.ReadableSpan( - name=self.otel_name(), - context=self.otel_context(), - parent=self.otel_parent_context(), - resource=self.otel_resource(), - attributes=self.otel_attributes(), - events=self.otel_events(), - links=self.otel_links(), - kind=self.otel_kind(), - instrumentation_info=None, # TODO(SNOW-1711959) - status=self.otel_status(), - start_time=self.start_timestamp, - end_time=self.end_timestamp, - instrumentation_scope=None, # TODO(SNOW-1711959) - ) - - -class Tracer(serial_utils.SerialModel, trace_api.Tracer): - """[OTEL Tracer][opentelemetry.trace.Tracer] requirements.""" - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - _instrumenting_module_name: Optional[str] = pydantic.PrivateAttr(None) - """Name of the library/module that is instrumenting the code.""" - - _instrumenting_library_version: Optional[str] = pydantic.PrivateAttr(None) - """Version of the library that is instrumenting the code.""" - - _attributes: Optional[trace_api.types.Attributes] = pydantic.PrivateAttr( - None - ) - """Common attributes to add to all spans.""" - - _schema_url: Optional[str] = pydantic.PrivateAttr(None) - """Use unknown.""" - - _tracer_provider: TracerProvider = pydantic.PrivateAttr(None) - """NON-STANDARD: The TracerProvider that made this tracer.""" - - _span_class: Type[Span] = pydantic.PrivateAttr(Span) - """NON-STANDARD: The default span class to use when creating spans.""" - - _span_context_class: Type[SpanContext] = pydantic.PrivateAttr(SpanContext) - """NON-STANDARD: The default span context class to use when creating spans.""" - - def __init__(self, _context: context_api.context.Context, **kwargs): - super().__init__(**kwargs) - - for k, v in kwargs.items(): - if v is None: - continue - # pydantic does not set private attributes in init - if k.startswith("_") and hasattr(self, k): - setattr(self, k, v) - - self._context_cvar.set(_context) - - _context_cvar: contextvars.ContextVar[context_api.context.Context] = ( - pydantic.PrivateAttr( - default_factory=lambda: contextvars.ContextVar( - f"context_Tracer_{python_utils.context_id()}", default=None - ) - ) - ) - - @property - def context_cvar( - self, - ) -> contextvars.ContextVar[context_api.context.Context]: - """NON-STANDARD: The context variable to store the current span context.""" - - return self._context_cvar - - @property - def trace_id(self) -> int: - return self._tracer_provider.trace_id - - def start_span( - self, - name: Optional[str] = None, - *args, # non-standard - context: Optional[context_api.context.Context] = None, - kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - attributes: trace_api.types.Attributes = None, - links: trace_api._Links = None, - start_time: Optional[int] = None, - record_exception: bool = True, - set_status_on_exception: bool = True, - cls: Optional[Type[Span]] = None, # non-standard - **kwargs, # non-standard - ) -> Span: - """See [OTEL - Tracer.start_span][opentelemetry.trace.Tracer.start_span].""" - - if context is None: - parent_context = self.context_cvar.get() - - else: - if len(context) != 1: - raise ValueError("Only one context span is allowed.") - parent_span_encoding = next(iter(context.values())) - - parent_context = self._span_context_class( - trace_id=parent_span_encoding.trace_id, - span_id=parent_span_encoding.span_id, - _tracer=self, - ) - - new_context = self._span_context_class( - *args, trace_id=self.trace_id, _tracer=self, **kwargs - ) - - if name is None: - name = python_utils.class_name(self._span_class) - - if attributes is None: - attributes = {} - - if self._attributes is not None: - attributes.update(self._attributes) - - if cls is None: - cls = self._span_class - - new_span = cls( - name=name, - context=new_context, - parent=parent_context, - kind=kind, - attributes=attributes, - links=links, - start_timestamp=start_time, - _record_exception=record_exception, - _status_on_exception=set_status_on_exception, - _tracer=self, - ) - - return new_span - - @contextlib.contextmanager - def start_as_current_span( - self, - name: Optional[str] = None, - context: Optional[trace_api.context.Context] = None, - kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, - attributes: trace_api.types.Attributes = None, - links: trace_api._Links = None, - start_time: Optional[int] = None, - record_exception: bool = True, - set_status_on_exception: bool = True, - end_on_exit: bool = True, - ): - """See [OTEL - Tracer.start_as_current_span][opentelemetry.trace.Tracer.start_as_current_span].""" - - span = self.start_span( - name=name, - context=context, - kind=kind, - attributes=attributes, - links=links, - start_time=start_time, - record_exception=record_exception, - set_status_on_exception=set_status_on_exception, - ) - - token = self.context_cvar.set(span.context) - - try: - yield span - - except BaseException as e: - if record_exception: - span.record_exception(e) - - finally: - self.context_cvar.reset(token) - - if end_on_exit: - span.end() - - -class TracerProvider(serial_utils.SerialModel, trace_api.TracerProvider): - """[OTEL TracerProvider][opentelemetry.trace.TracerProvider] - requirements.""" - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - _tracer_class: Type[Tracer] = pydantic.PrivateAttr(Tracer) - """NON-STANDARD: The default tracer class to use when creating tracers.""" - - _context_cvar: contextvars.ContextVar[context_api.context.Context] = ( - pydantic.PrivateAttr( - default_factory=lambda: contextvars.ContextVar( - f"context_TracerProvider_{python_utils.context_id()}", - default=None, - ) - ) - ) - - @property - def context_cvar( - self, - ) -> contextvars.ContextVar[context_api.context.Context]: - """NON-STANDARD: The context variable to store the current span context.""" - - return self._context_cvar - - _trace_id: int = pydantic.PrivateAttr(default_factory=new_trace_id) - - @property - def trace_id(self) -> int: - """NON-STANDARD: The current trace id.""" - - return self._trace_id - - def get_tracer( - self, - instrumenting_module_name: str, - instrumenting_library_version: Optional[str] = None, - schema_url: Optional[str] = None, - attributes: Optional[types_api.Attributes] = None, - ): - """See [OTEL - TracerProvider.get_tracer][opentelemetry.trace.TracerProvider.get_tracer].""" - - tracer = self._tracer_class( - _instrumenting_module_name=instrumenting_module_name, - _instrumenting_library_version=instrumenting_library_version, - _attributes=attributes, - _schema_url=schema_url, - _tracer_provider=self, - _context=self.context_cvar.get(), - ) - - return tracer diff --git a/src/core/trulens/experimental/otel_tracing/core/trace.py b/src/core/trulens/experimental/otel_tracing/core/trace.py deleted file mode 100644 index 2d21c3ce8..000000000 --- a/src/core/trulens/experimental/otel_tracing/core/trace.py +++ /dev/null @@ -1,1098 +0,0 @@ -# ruff: noqa: E402 - -"""Implementation of recording that resembles the tracing process in OpenTelemetry. - -!!! Note - Most of the module is (EXPERIMENTAL(otel_tracing)) though it includes some existing - non-experimental classes moved here to resolve some circular import issues. -""" - -from __future__ import annotations - -import contextlib -import contextvars -import inspect -import logging -import os -import sys -import threading as th -from threading import Lock -from typing import ( - Any, - Callable, - ContextManager, - Dict, - Generic, - Hashable, - Iterable, - List, - Optional, - Tuple, - Type, - TypeVar, - Union, -) -import uuid -import weakref - -import pydantic -from trulens.core.schema import base as base_schema -from trulens.core.schema import record as record_schema -from trulens.core.schema import types as types_schema -from trulens.core.utils import json as json_utils -from trulens.core.utils import pyschema as pyschema_utils -from trulens.core.utils import python as python_utils -from trulens.core.utils import serial as serial_utils -from trulens.experimental.otel_tracing import _feature -from trulens.experimental.otel_tracing.core import otel as core_otel -from trulens.experimental.otel_tracing.core._utils import wrap as wrap_utils - -_feature._FeatureSetup.assert_optionals_installed() # checks to make sure otel is installed - -if sys.version_info < (3, 9): - from functools import lru_cache as fn_cache -else: - from functools import cache as fn_cache - -from opentelemetry.semconv.resource import ResourceAttributes -from opentelemetry.trace import span as span_api -from opentelemetry.util import types as types_api - -T = TypeVar("T") -R = TypeVar("R") # callable return type -E = TypeVar("E") # iterator/generator element type - -logger = logging.getLogger(__name__) - -INSTRUMENT: str = "__tru_instrumented" -"""Attribute name to be used to flag instrumented objects/methods/others.""" - -APPS: str = "__tru_apps" -"""Attribute name for storing apps that expect to be notified of calls.""" - - -class SpanContext(core_otel.SpanContext, Hashable): - """TruLens additions on top of OTEL SpanContext to add Hashable and - reference to tracer that made the span.""" - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - def __str__(self): - return f"{self.trace_id % 0xFF:02x}/{self.span_id % 0xFF:02x}" - - def __repr__(self): - return str(self) - - def __hash__(self): - return self.trace_id + self.span_id - - def __eq__(self, other: SpanContextLike): - if other is None: - return False - - return self.trace_id == other.trace_id and self.span_id == other.span_id - - @staticmethod - def of_spancontextlike(span_context: SpanContextLike) -> SpanContext: - if isinstance(span_context, SpanContext): - return span_context - - elif isinstance(span_context, core_otel.SpanContext): - return SpanContext( - trace_id=span_context.trace_id, - span_id=span_context.span_id, - is_remote=span_context.is_remote, - ) - elif isinstance(span_context, span_api.SpanContext): - return SpanContext( - trace_id=span_context.trace_id, - span_id=span_context.span_id, - is_remote=span_context.is_remote, - ) - elif isinstance(span_context, Dict): - return SpanContext.model_validate(span_context) - else: - raise ValueError(f"Unrecognized span context type: {span_context}") - - -SpanContextLike = Union[ - SpanContext, core_otel.SpanContext, span_api.SpanContext, serial_utils.JSON -] - - -class Span(core_otel.Span): - """TruLens additions on top of OTEL spans.""" - - model_config = pydantic.ConfigDict( - arbitrary_types_allowed=True, - use_enum_values=True, # model_validate will fail without this - ) - - def __str__(self): - return ( - f"{type(self).__name__}({self.name}, {self.context}->{self.parent})" - ) - - def __repr__(self): - return str(self) - - _lensed_attributes: serial_utils.LensedDict[Any] = pydantic.PrivateAttr( - default_factory=serial_utils.LensedDict - ) - - @property - def lensed_attributes(self) -> serial_utils.LensedDict[Any]: - return self._lensed_attributes - - @property - def parent_span(self) -> Optional[Span]: - if self.parent is None: - return None - - if self._tracer is None: - return None - - if (span := self._tracer.spans.get(self.parent)) is None: - return None - - return span - - _children_spans: List[Span] = pydantic.PrivateAttr(default_factory=list) - - @property - def children_spans(self) -> List[Span]: - return self._children_spans - - error: Optional[Exception] = pydantic.Field(None) - """Optional error if the observed computation raised an exception.""" - - def __init__(self, **kwargs): - # Convert any contexts to our hashable context class: - if (context := kwargs.get("context")) is not None: - kwargs["context"] = SpanContext.of_spancontextlike(context) - if (parent := kwargs.get("parent", None)) is not None: - kwargs["parent"] = SpanContext.of_spancontextlike(parent) - - super().__init__(**kwargs) - - if (parent_span := self.parent_span) is not None: - parent_span.children_spans.append(self) - - def iter_children( - self, transitive: bool = True, include_phantom: bool = False - ) -> Iterable[Span]: - """Iterate over all spans that are children of this span. - - Args: - transitive: Iterate recursively over children. - - include_phantom: Include phantom spans. If not set, phantom spans - will not be included but will be iterated over even if - transitive is false. - """ - - for child_span in self.children_spans: - if isinstance(child_span, PhantomSpan) and not include_phantom: - # Note that transitive being false is ignored if phantom is skipped. - yield from child_span.iter_children( - transitive=transitive, include_phantom=include_phantom - ) - else: - yield child_span - if transitive: - yield from child_span.iter_children( - transitive=transitive, - include_phantom=include_phantom, - ) - - def iter_family(self, include_phantom: bool = False) -> Iterable[Span]: - """Iterate itself and all children transitively.""" - - if (not isinstance(self, PhantomSpan)) or include_phantom: - yield self - - yield from self.iter_children( - include_phantom=include_phantom, transitive=True - ) - - def total_cost(self) -> base_schema.Cost: - """Total costs of this span and all its transitive children.""" - - total = base_schema.Cost() - - for span in self.iter_family(include_phantom=True): - if isinstance(span, WithCost) and span.cost is not None: - total += span.cost - - return total - - -class PhantomSpan(Span): - """A span type that indicates that it does not correspond to a - computation to be recorded but instead is an element of the tracing system. - - It is to be removed from the spans presented to the users. - """ - - -class LiveSpan(Span): - """A a span type that indicates that it contains live python objects. - - It is to be converted to a non-live span before being output to the user or - otherwise. - """ - - -class PhantomSpanRecordingContext(PhantomSpan): - """Tracks the context of an app used as a context manager.""" - - recording: Optional[Any] = pydantic.Field(None, exclude=True) - # TODO: app.RecordingContext # circular import issues - - def otel_resource_attributes(self) -> Dict[str, Any]: - ret = super().otel_resource_attributes() - - ret[ResourceAttributes.SERVICE_NAME] = ( - self.recording.app.app_name if self.recording is not None else None - ) - - return ret - - # override - def end(self, *args, **kwargs): - super().end(*args, **kwargs) - - self._finalize_recording() - - # override - def record_exception( - self, - exception: BaseException, - attributes: types_api.Attributes = None, - timestamp: int | None = None, - escaped: bool = False, - ) -> None: - super().record_exception(exception, attributes, timestamp, escaped) - - self._finalize_recording() - - def _finalize_recording(self): - assert self.recording is not None - - app = self.recording.app - - for span in Tracer.find_each_child( - span=self, span_filter=lambda s: isinstance(s, LiveSpanCall) - ): - app._on_new_root_span(recording=self.recording, root_span=span) - - app._on_new_recording_span(recording_span=self) - - def otel_name(self) -> str: - return "trulens.recording" - - -class SpanCall(Span): - """Non-live fields of a function call span.""" - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - call_id: Optional[uuid.UUID] = pydantic.Field(None) - """Unique identifier for the call.""" - - stack: Optional[List[record_schema.RecordAppCallMethod]] = pydantic.Field( - None - ) - """Call stack of instrumented methods only.""" - - sig: Optional[inspect.Signature] = pydantic.Field(None) - """Signature of the function.""" - - func_name: Optional[str] = None - """Function name.""" - - pid: Optional[int] = None - """Process id.""" - - tid: Optional[int] = None - """Thread id.""" - - def end(self): - super().end() - - self.set_attribute(ResourceAttributes.PROCESS_PID, self.pid) - self.set_attribute("thread.id", self.tid) # TODO: semconv - - self.set_attribute("trulens.call_id", str(self.call_id)) - self.set_attribute("trulens.stack", json_utils.jsonify(self.stack)) - self.set_attribute("trulens.sig", str(self.sig)) - - def otel_name(self) -> str: - return f"trulens.call.{self.func_name}" - - -class LiveSpanCall(LiveSpan, SpanCall): - """Track a function call.""" - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - live_obj: Optional[Any] = pydantic.Field(None, exclude=True) - """Self object if method call.""" - - live_cls: Optional[Type] = pydantic.Field(None, exclude=True) - """Class if method/static/class method call.""" - - live_func: Optional[Callable] = pydantic.Field(None, exclude=True) - """Function object.""" - - live_args: Optional[Tuple[Any, ...]] = pydantic.Field(None, exclude=True) - """Positional arguments to the function call.""" - - live_kwargs: Optional[Dict[str, Any]] = pydantic.Field(None, exclude=True) - """Keyword arguments to the function call.""" - - live_bindings: Optional[inspect.BoundArguments] = pydantic.Field( - None, exclude=True - ) - """Bound arguments to the function call if can be bound.""" - - live_ret: Optional[Any] = pydantic.Field(None, exclude=True) - """Return value of the function call. - - Exclusive with `error`. - """ - - live_error: Optional[Any] = pydantic.Field(None, exclude=True) - """Error raised by the function call. - - Exclusive with `ret`. - """ - - def end(self): - super().end() - - if self.live_cls is not None: - self.set_attribute( - "trulens.cls", - pyschema_utils.Class.of_class(self.live_cls).model_dump(), - ) - - if self.live_func is not None: - self.set_attribute( - "trulens.func", - pyschema_utils.FunctionOrMethod.of_callable( - self.live_func - ).model_dump(), - ) - - if self.live_ret is not None: - self.set_attribute("trulens.ret", json_utils.jsonify(self.live_ret)) - - if self.live_bindings is not None: - self.set_attribute( - "trulens.bindings", - pyschema_utils.Bindings.of_bound_arguments( - self.live_bindings, arguments_only=True, skip_self=True - ).model_dump()["kwargs"], - ) - - if self.live_error is not None: - self.set_attribute( - "trulens.error", json_utils.jsonify(self.live_error) - ) - - -S = TypeVar("S", bound=LiveSpanCall) - - -class WithCost(LiveSpan): - """Mixin to indicate the span has costs tracked.""" - - cost: base_schema.Cost = pydantic.Field(default_factory=base_schema.Cost) - """Cost of the computation spanned.""" - - endpoint: Optional[Any] = pydantic.Field( - None, exclude=True - ) # Any actually core_endpoint.Endpoint - """Endpoint handling cost extraction for this span/call.""" - - def end(self): - super().end() - - self.set_attribute("trulens.cost", self.cost.model_dump()) - - def __init__(self, cost: Optional[base_schema.Cost] = None, **kwargs): - if cost is None: - cost = base_schema.Cost() - - super().__init__(cost=cost, **kwargs) - - -class LiveSpanCallWithCost(LiveSpanCall, WithCost): - pass - - -class Tracer(core_otel.Tracer): - """TruLens additions on top of [OTEL Tracer][opentelemetry.trace.Tracer].""" - - # TODO: Tracer that does not record anything. Can either be a setting to - # this tracer or a separate "NullTracer". We need non-recording users to not - # incur much overhead hence need to be able to disable most of the tracing - # logic when appropriate. - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - # Overrides core_otel.Tracer._span_class - _span_class: Type[Span] = pydantic.PrivateAttr(Span) - - # Overrides core_otel.Tracer._span_context_class - _span_context_class: Type[SpanContext] = pydantic.PrivateAttr(SpanContext) - - @property - def spans(self) -> Dict[SpanContext, Span]: - return self._tracer_provider.spans - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - def __str__(self): - return f"{type(self).__name__} {self.instrumenting_module_name} {self.instrumenting_library_version}" - - def __repr__(self): - return str(self) - - def start_span(self, *args, **kwargs): - new_span = super().start_span(*args, **kwargs) - - self.spans[new_span.context] = new_span - - return new_span - - @staticmethod - def _fill_stacks( - span: Span, - get_method_path: Callable, - stack: List[record_schema.RecordAppCallMethod] = [], - ): - if isinstance(span, LiveSpanCall): - path = get_method_path(obj=span.live_obj, func=span.live_func) - - frame_ident = record_schema.RecordAppCallMethod( - path=path - if path is not None - else serial_utils.Lens().static, # placeholder path for functions - method=pyschema_utils.Method.of_method( - cls=span.live_cls, obj=span.live_obj, meth=span.live_func - ), - ) - - stack = stack + [frame_ident] - span.stack = stack - - for subspan in span.iter_children(transitive=False): - Tracer._fill_stacks( - subspan, stack=stack, get_method_path=get_method_path - ) - - def _call_of_spancall( - self, span: LiveSpanCall - ) -> record_schema.RecordAppCall: - """Convert a SpanCall to a RecordAppCall.""" - - args = ( - dict(span.live_bindings.arguments) - if span.live_bindings is not None - else None - ) - if args is not None: - if "self" in args: - del args["self"] # remove self - - assert span.start_timestamp is not None - if span.end_timestamp is None: - logger.warning( - "Span %s has no end timestamp. It might not have yet finished recording.", - span, - ) - - return record_schema.RecordAppCall( - call_id=str(span.call_id), - stack=span.stack, - args=args, - rets=json_utils.jsonify(span.live_ret), - error=str(span.live_error), - perf=base_schema.Perf.of_ns_timestamps( - start_ns_timestamp=span.start_timestamp, - end_ns_timestamp=span.end_timestamp, - ), - pid=span.pid, - tid=span.tid, - ) - - def record_of_root_span( - self, recording: Any, root_span: LiveSpanCall - ) -> record_schema.Record: - """Convert a root span to a record. - - This span has to be a call span so we can extract things like main input and output. - """ - - assert isinstance(root_span, LiveSpanCall), type(root_span) - - app = recording.app - - self._fill_stacks(root_span, get_method_path=app.get_method_path) - - root_perf = ( - base_schema.Perf.of_ns_timestamps( - start_ns_timestamp=root_span.start_timestamp, - end_ns_timestamp=root_span.end_timestamp, - ) - if root_span.end_timestamp is not None - else None - ) - - total_cost = root_span.total_cost() - - calls = [] - if isinstance(root_span, LiveSpanCall): - calls.append(self._call_of_spancall(root_span)) - - spans = [root_span] - - for span in root_span.iter_children(include_phantom=True): - if isinstance(span, LiveSpanCall): - calls.append(self._call_of_spancall(span)) - - spans.append(span) - - bindings = root_span.live_bindings - main_error = root_span.live_error - - if bindings is not None: - main_input = app.main_input( - func=root_span.live_func, - sig=root_span.sig, - bindings=root_span.live_bindings, - ) - if main_error is None: - main_output = app.main_output( - func=root_span.live_func, - sig=root_span.sig, - bindings=root_span.live_bindings, - ret=root_span.live_ret, - ) - else: - main_output = None - else: - main_input = None - main_output = None - - record = record_schema.Record( - record_id="placeholder", - app_id=app.app_id, - main_input=json_utils.jsonify(main_input), - main_output=json_utils.jsonify(main_output), - main_error=json_utils.jsonify(main_error), - calls=calls, - perf=root_perf, - cost=total_cost, - experimental_otel_spans=spans, - ) - - # record_id determinism - record.record_id = json_utils.obj_id_of_obj( - record.model_dump(), prefix="record" - ) - - return record - - @staticmethod - def find_each_child(span: Span, span_filter: Callable) -> Iterable[Span]: - """For each family rooted at each child of this span, find the top-most - span that satisfies the filter.""" - - for child_span in span.children_spans: - if span_filter(child_span): - yield child_span - else: - yield from Tracer.find_each_child(child_span, span_filter) - - def records_of_recording( - self, recording: PhantomSpanRecordingContext - ) -> Iterable[record_schema.Record]: - """Convert a recording based on spans to a list of records.""" - - for root_span in Tracer.find_each_child( - span=recording, span_filter=lambda s: isinstance(s, LiveSpanCall) - ): - assert isinstance(root_span, LiveSpanCall) - yield self.record_of_root_span( - recording=recording, root_span=root_span - ) - - @contextlib.contextmanager - def _span(self, cls: Type[S], **kwargs) -> ContextManager[S]: - with self.start_span(cls=cls, **kwargs) as span: - with python_utils.with_context({self.context_cvar: span.context}): - yield span - - @contextlib.asynccontextmanager - async def _aspan(self, cls: Type[S], **kwargs) -> ContextManager[S]: - async with self.start_span(cls=cls, **kwargs) as span: - async with python_utils.awith_context({ - self.context_cvar: span.context - }): - yield span - - # context manager - def recording(self) -> ContextManager[PhantomSpanRecordingContext]: - return self._span( - name="trulens.recording", cls=PhantomSpanRecordingContext - ) - - # context manager - def method(self, method_name: str) -> ContextManager[LiveSpanCall]: - return self._span(name="trulens.call." + method_name, cls=LiveSpanCall) - - # context manager - def cost( - self, method_name: str, cost: Optional[base_schema.Cost] = None - ) -> ContextManager[LiveSpanCallWithCost]: - return self._span( - name="trulens.call." + method_name, - cls=LiveSpanCallWithCost, - cost=cost, - ) - - # context manager - def phantom(self) -> ContextManager[PhantomSpan]: - return self._span(name="trulens.phantom", cls=PhantomSpan) - - # context manager - async def arecording(self) -> ContextManager[PhantomSpanRecordingContext]: - return self._aspan( - name="trulens.recording", cls=PhantomSpanRecordingContext - ) - - # context manager - async def amethod(self, method_name: str) -> ContextManager[LiveSpanCall]: - return self._aspan(name="trulens.call." + method_name, cls=LiveSpanCall) - - # context manager - async def acost( - self, method_name: str, cost: Optional[base_schema.Cost] = None - ) -> ContextManager[LiveSpanCallWithCost]: - return self._aspan( - name="trulens.call." + method_name, - cls=LiveSpanCallWithCost, - cost=cost, - ) - - # context manager - async def aphantom(self) -> ContextManager[PhantomSpan]: - return self._aspan(name="trulens.phantom", cls=PhantomSpan) - - -class TracerProvider( - core_otel.TracerProvider, metaclass=python_utils.PydanticSingletonMeta -): - """TruLens additions on top of [OTEL TracerProvider][opentelemetry.trace.TracerProvider].""" - - model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) - - _trace_id: int = pydantic.PrivateAttr( - default_factory=core_otel.new_trace_id - ) - - def __str__(self): - # Pydantic will not print anything useful otherwise. - return f"{self.__module__}.{type(self).__name__}()" - - @property - def trace_id(self): - return self._trace_id - - # Overrides core_otel.TracerProvider._tracer_class - _tracer_class: Type[Tracer] = pydantic.PrivateAttr(default=Tracer) - - _tracers: Dict[str, Tracer] = pydantic.PrivateAttr(default_factory=dict) - - _spans: Dict[SpanContext, Span] = pydantic.PrivateAttr(default_factory=dict) - - @property - def spans(self) -> Dict[SpanContext, Span]: - return self._spans - - def get_tracer( - self, - instrumenting_module_name: str, - instrumenting_library_version: Optional[str] = None, - schema_url: Optional[str] = None, - attributes: Optional[types_api.Attributes] = None, - ): - if instrumenting_module_name in self._tracers: - return self._tracers[instrumenting_module_name] - - tracer = super().get_tracer( - instrumenting_module_name=instrumenting_module_name, - instrumenting_library_version=instrumenting_library_version, - attributes=attributes, - schema_url=schema_url, - ) - - self._tracers[instrumenting_module_name] = tracer - - return tracer - - -tracer_provider = TracerProvider() -"""Global tracer provider. -All trulens tracers are made by this provider even if a different one is -configured for OTEL. -""" - - -@fn_cache -def trulens_tracer(): - from trulens.core import __version__ - - return tracer_provider.get_tracer( - instrumenting_module_name="trulens.experimental.otel_tracing.core.trace", - instrumenting_library_version=__version__, - ) - - -class TracingCallbacks(wrap_utils.CallableCallbacks[R], Generic[R, S]): - """Extension of CallableCallbacks that adds tracing to the wrapped callable - as implemented using tracer and spans.""" - - def __init__( - self, - func_name: str, - span_type: Type[S] = LiveSpanCall, - **kwargs: Dict[str, Any], - ): - super().__init__(**kwargs) - - self.func_name: str = func_name - - self.obj: Optional[object] = None - self.obj_cls: Optional[Type] = None - self.obj_id: Optional[int] = None - - if not issubclass(span_type, LiveSpanCall): - raise ValueError("span_type must be a subclass of LiveSpanCall.") - - self.span_context: ContextManager = trulens_tracer()._span( - span_type, name="trulens.call." + func_name - ) - self.span: S = self.span_context.__enter__() - - def on_callable_call( - self, bindings: inspect.BoundArguments, **kwargs: Dict[str, Any] - ) -> inspect.BoundArguments: - temp = super().on_callable_call(bindings=bindings, **kwargs) - - if "self" in bindings.arguments: - # TODO: need some generalization - self.obj = bindings.arguments["self"] - self.obj_cls = type(self.obj) - self.obj_id = id(self.obj) - else: - logger.warning("No self in bindings for %s.", self) - - span = self.span - span.pid = os.getpid() - span.tid = th.get_native_id() - - return temp - - def on_callable_end(self): - super().on_callable_end() - - span = self.span - - # SpanCall attributes - span.call_id = self.call_id - span.func_name = self.func_name - span.sig = self.sig - - # LiveSpanCall attributes - span.live_obj = self.obj - span.live_cls = self.obj_cls - span.live_func = self.func - span.live_args = self.call_args - span.live_kwargs = self.call_kwargs - span.live_bindings = self.bindings - span.live_ret = self.ret - span.live_error = self.error - - if self.error is not None: - self.span_context.__exit__( - type(self.error), self.error, self.error.__traceback__ - ) - else: - self.span_context.__exit__(None, None, None) - - -class _RecordingContext: - """Manager of the creation of records from record calls. - - An instance of this class is produced when using an - [App][trulens_eval.app.App] as a context mananger, i.e.: - Example: - ```python - app = ... # your app - truapp: TruChain = TruChain(app, ...) # recorder for LangChain apps - with truapp as recorder: - app.invoke(...) # use your app - recorder: RecordingContext - ``` - - Each instance of this class produces a record for every "root" instrumented - method called. Root method here means the first instrumented method in a - call stack. Note that there may be more than one of these contexts in play - at the same time due to: - - More than one wrapper of the same app. - - More than one context manager ("with" statement) surrounding calls to the - same app. - - Calls to "with_record" on methods that themselves contain recording. - - Calls to apps that use trulens internally to track records in any of the - supported ways. - - Combinations of the above. - """ - - def __init__( - self, - app: _WithInstrumentCallbacks, - record_metadata: serial_utils.JSON = None, - tracer: Optional[Tracer] = None, - span: Optional[PhantomSpanRecordingContext] = None, - span_ctx: Optional[SpanContext] = None, - ): - self.calls: Dict[types_schema.CallID, record_schema.RecordAppCall] = {} - """A record (in terms of its RecordAppCall) in process of being created. - - Storing as a map as we want to override calls with the same id which may - happen due to methods producing awaitables or generators. These result - in calls before the awaitables are awaited and then get updated after - the result is ready. - """ - # TODEP: To deprecated after migration to span-based tracing. - - self.records: List[record_schema.Record] = [] - """Completed records.""" - - self.lock: Lock = Lock() - """Lock blocking access to `records` when adding calls or - finishing a record.""" - - self.token: Optional[contextvars.Token] = None - """Token for context management.""" - - self.app: _WithInstrumentCallbacks = app - """App for which we are recording.""" - - self.record_metadata = record_metadata - """Metadata to attach to all records produced in this context.""" - - self.tracer: Optional[Tracer] = tracer - """EXPERIMENTAL(otel_tracing): OTEL-like tracer for recording. - """ - - self.span: Optional[PhantomSpanRecordingContext] = span - """EXPERIMENTAL(otel_tracing): Span that represents a recording context - (the with block).""" - - self.span_ctx = span_ctx - """EXPERIMENTAL(otel_tracing): The context manager for the above span. - """ - - @property - def spans(self) -> Dict[SpanContext, Span]: - """EXPERIMENTAL(otel_tracing): Get the spans of the tracer in this context.""" - - if self.tracer is None: - return {} - - return self.tracer.spans - - def __iter__(self): - return iter(self.records) - - def get(self) -> record_schema.Record: - """Get the single record only if there was exactly one or throw - an error otherwise.""" - - if len(self.records) == 0: - raise RuntimeError("Recording context did not record any records.") - - if len(self.records) > 1: - raise RuntimeError( - "Recording context recorded more than 1 record. " - "You can get them with ctx.records, ctx[i], or `for r in ctx: ...`." - ) - - return self.records[0] - - def __getitem__(self, idx: int) -> record_schema.Record: - return self.records[idx] - - def __len__(self): - return len(self.records) - - def __hash__(self) -> int: - # The same app can have multiple recording contexts. - return hash(id(self.app)) + hash(id(self.records)) - - def __eq__(self, other): - return hash(self) == hash(other) - - def add_call(self, call: record_schema.RecordAppCall): - """Add the given call to the currently tracked call list.""" - # TODEP: To deprecated after migration to span-based tracing. - - with self.lock: - # NOTE: This might override existing call record which happens when - # processing calls with awaitable or generator results. - self.calls[call.call_id] = call - - def finish_record( - self, - calls_to_record: Callable[ - [ - List[record_schema.RecordAppCall], - types_schema.Metadata, - Optional[record_schema.Record], - ], - record_schema.Record, - ], - existing_record: Optional[record_schema.Record] = None, - ): - """Run the given function to build a record from the tracked calls and any - pre-specified metadata.""" - # TODEP: To deprecated after migration to span-based tracing. - - with self.lock: - record = calls_to_record( - list(self.calls.values()), self.record_metadata, existing_record - ) - self.calls = {} - - if existing_record is None: - # If existing record was given, we assume it was already - # inserted into this list. - self.records.append(record) - - return record - - -class _WithInstrumentCallbacks: - """Abstract definition of callbacks invoked by Instrument during - instrumentation or when instrumented methods are called. - - Needs to be mixed into [App][trulens_eval.app.App]. - """ - - # Called during instrumentation. - def on_method_instrumented( - self, obj: object, func: Callable, path: serial_utils.Lens - ): - """Callback to be called by instrumentation system for every function - requested to be instrumented. - - Given are the object of the class in which `func` belongs - (i.e. the "self" for that function), the `func` itsels, and the `path` - of the owner object in the app hierarchy. - - Args: - obj: The object of the class in which `func` belongs (i.e. the - "self" for that method). - - func: The function that was instrumented. Expects the unbound - version (self not yet bound). - - path: The path of the owner object in the app hierarchy. - """ - - raise NotImplementedError - - # Called during invocation. - def get_method_path(self, obj: object, func: Callable) -> serial_utils.Lens: - """Get the path of the instrumented function `func`, a member of the class - of `obj` relative to this app. - - Args: - obj: The object of the class in which `func` belongs (i.e. the - "self" for that method). - - func: The function that was instrumented. Expects the unbound - version (self not yet bound). - """ - - raise NotImplementedError - - # WithInstrumentCallbacks requirement - def get_methods_for_func( - self, func: Callable - ) -> Iterable[Tuple[int, Callable, serial_utils.Lens]]: - """EXPERIMENTAL(otel_tracing): Get the methods (rather the inner - functions) matching the given `func` and the path of each. - - Args: - func: The function to match. - """ - - raise NotImplementedError - - # Called after recording of an invocation. - def _on_new_root_span( - self, - ctx: _RecordingContext, - root_span: LiveSpanCall, - ) -> record_schema.Record: - """EXPERIMENTAL(otel_tracing): Called by instrumented methods if they - are root calls (first instrumented methods in a call stack). - - Args: - ctx: The context of the recording. - - root_span: The root span that was recorded. - """ - # EXPERIMENTAL(otel_tracing) - - raise NotImplementedError - - -class AppTracingCallbacks(TracingCallbacks[R, S]): - """Extension to TracingCallbacks that keep track of apps that are - instrumenting their constituent calls.""" - - @classmethod - def on_callable_wrapped( - cls, - wrapper: Callable[..., R], - app: _WithInstrumentCallbacks, - **kwargs: Dict[str, Any], - ): - if not python_utils.safe_hasattr(wrapper, APPS): - apps: weakref.WeakSet[_WithInstrumentCallbacks] = weakref.WeakSet() - setattr(wrapper, APPS, apps) - else: - apps = python_utils.safe_getattr(wrapper, APPS) - - apps.add(app) - - return super().on_callable_wrapped(wrapper=wrapper, **kwargs) - - def __init__( - self, - app: _WithInstrumentCallbacks, - span_type: Type[Span] = LiveSpanCall, - **kwargs: Dict[str, Any], - ): - super().__init__(span_type=span_type, **kwargs) - - self.app = app - self.apps = python_utils.safe_getattr(self.wrapper, APPS) From a91b0eb4c7b25dbeb570a0379ce745f22a815885 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 21:12:32 -0800 Subject: [PATCH 54/68] update --- examples/experimental/otel_exporter.ipynb | 13 ++++- src/core/trulens/core/app.py | 26 +++++++-- .../otel_tracing/core/instrument.py | 55 ++++++++++++------- .../experimental/otel_tracing/core/span.py | 6 ++ .../semconv/trulens/otel/semconv/trace.py | 6 ++ 5 files changed, 78 insertions(+), 28 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 059c93e27..475728d47 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -138,12 +138,21 @@ " session=session,\n", ")\n", "\n", - "with custom_app as recording:\n", + "with custom_app(run_name=\"test run\", input_id=\"123\") as recording:\n", " test_app.respond_to_query(\"test\")\n", "\n", - "with custom_app as recording:\n", + "with custom_app(run_name=\"test run\", input_id=\"456\") as recording:\n", " test_app.respond_to_query(\"throw\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "session.experimental_force_flush()" + ] } ], "metadata": { diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index 0e1409843..78eb26c5e 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -898,7 +898,7 @@ def __enter__(self): core_experimental.Feature.OTEL_TRACING ): from trulens.experimental.otel_tracing.core.instrument import ( - App as OTELApp, + OTELRecordingContext as OTELApp, ) return OTELApp.__enter__(self) @@ -916,7 +916,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): core_experimental.Feature.OTEL_TRACING ): from trulens.experimental.otel_tracing.core.instrument import ( - App as OTELApp, + OTELRecordingContext as OTELApp, ) return OTELApp.__exit__(self, exc_type, exc_value, exc_tb) @@ -924,8 +924,6 @@ def __exit__(self, exc_type, exc_value, exc_tb): ctx = self.recording_contexts.get() self.recording_contexts.reset(ctx.token) - # self._reset_context_vars() - if exc_type is not None: raise exc_value @@ -937,7 +935,7 @@ async def __aenter__(self): core_experimental.Feature.OTEL_TRACING ): from trulens.experimental.otel_tracing.core.instrument import ( - App as OTELApp, + OTELRecordingContext as OTELApp, ) return OTELApp.__enter__(self) @@ -957,7 +955,7 @@ async def __aexit__(self, exc_type, exc_value, exc_tb): core_experimental.Feature.OTEL_TRACING ): from trulens.experimental.otel_tracing.core.instrument import ( - App as OTELApp, + OTELRecordingContext as OTELApp, ) return OTELApp.__exit__(self, exc_type, exc_value, exc_tb) @@ -972,6 +970,22 @@ async def __aexit__(self, exc_type, exc_value, exc_tb): return + def __call__(self, *, run_name: str, input_id: str): + if not self.session.experimental_feature( + core_experimental.Feature.OTEL_TRACING + ): + raise RuntimeError("OTEL Tracing is not enabled for this session.") + + from trulens.experimental.otel_tracing.core.instrument import ( + OTELRecordingContext as OTELApp, + ) + + # Pylance shows an error here, but it is likely a false positive. due to the overriden + # model dump returning json instead of a dict. + return OTELApp.model_construct( + **self.model_dump(), run_name=run_name, input_id=input_id + ) + def _set_context_vars(self): # HACK: For debugging purposes, try setting/resetting all context vars # used in trulens around the app context managers due to bugs in trying diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 906f6fcd4..12855c1f8 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -4,7 +4,7 @@ import uuid from opentelemetry import trace -from opentelemetry.baggage import remove_baggage +from opentelemetry.baggage import clear as clear_baggage from opentelemetry.baggage import set_baggage import opentelemetry.context as context_api from trulens.core import app as core_app @@ -93,7 +93,27 @@ def wrapper(*args, **kwargs): return inner_decorator -class App(core_app.App): +class OTELRecordingContext(core_app.App): + run_name: Optional[str] + """ + The name of the run that the recording context is currently processing. + """ + + input_id: Optional[str] + """ + The ID of the input that the recording context is currently processing. + """ + + @classmethod + def for_run(cls, app: core_app.App, run_name: str, input_id: str): + cls.model_construct(None, app) + + def _set_run_information(self, *, run_name: str, input_id: str): + self.run_name = run_name + self.input_id = input_id + + return self + # For use as a context manager. def __enter__(self): logger.debug("Entering the OTEL app context.") @@ -106,21 +126,18 @@ def __enter__(self): # Calling set_baggage does not actually add the baggage to the current context, but returns a new one # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. - self.tokens.append( - context_api.attach( - set_baggage(SpanAttributes.RECORD_ID, otel_record_id) - ) - ) - self.tokens.append( - context_api.attach( - set_baggage(SpanAttributes.APP_NAME, self.app_name) - ) - ) - self.tokens.append( - context_api.attach( - set_baggage(SpanAttributes.APP_VERSION, self.app_version) - ) - ) + def attach_to_context(key: str, value: object): + self.tokens.append(context_api.attach(set_baggage(key, value))) + + attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) + attach_to_context(SpanAttributes.APP_NAME, self.app_name) + attach_to_context(SpanAttributes.APP_VERSION, self.app_version) + + if hasattr(self, "run_name") and self.run_name: + attach_to_context(SpanAttributes.RUN_NAME, self.run_name) + + if hasattr(self, "run_name") and self.input_id: + attach_to_context(SpanAttributes.INPUT_ID, self.input_id) # Use start_as_current_span as a context manager self.span_context = tracer.start_as_current_span("root") @@ -146,9 +163,7 @@ def __enter__(self): return root_span def __exit__(self, exc_type, exc_value, exc_tb): - remove_baggage(SpanAttributes.RECORD_ID) - remove_baggage(SpanAttributes.APP_NAME) - remove_baggage(SpanAttributes.APP_VERSION) + clear_baggage() logger.debug("Exiting the OTEL app context.") diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index 6bce00e4f..d561f663f 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -97,6 +97,12 @@ def set_general_span_attributes( span.set_attribute( SpanAttributes.RECORD_ID, str(get_baggage(SpanAttributes.RECORD_ID)) ) + span.set_attribute( + SpanAttributes.RUN_NAME, str(get_baggage(SpanAttributes.RUN_NAME)) + ) + span.set_attribute( + SpanAttributes.INPUT_ID, str(get_baggage(SpanAttributes.INPUT_ID)) + ) return span diff --git a/src/otel/semconv/trulens/otel/semconv/trace.py b/src/otel/semconv/trulens/otel/semconv/trace.py index 9092805f8..0eb46de52 100644 --- a/src/otel/semconv/trulens/otel/semconv/trace.py +++ b/src/otel/semconv/trulens/otel/semconv/trace.py @@ -68,6 +68,12 @@ class SpanAttributes: ROOT_SPAN_ID = BASE + "root_span_id" """ID of the root span of the record that the span belongs to.""" + RUN_NAME = BASE + "run_name" + """Name of the run that the span belongs to.""" + + INPUT_ID = BASE + "input_id" + """ID of the input that the span belongs to.""" + class SpanType(str, Enum): """Span type attribute values. From bd3fb501c528af96451e0c3e429e7283cac55567 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 21:20:48 -0800 Subject: [PATCH 55/68] update --- .../experimental/otel_tracing/core/otel.py | 749 +++++++++++ .../experimental/otel_tracing/core/trace.py | 1098 +++++++++++++++++ ..._instrument__test_instrument_decorator.csv | 20 +- 3 files changed, 1857 insertions(+), 10 deletions(-) create mode 100644 src/core/trulens/experimental/otel_tracing/core/otel.py create mode 100644 src/core/trulens/experimental/otel_tracing/core/trace.py diff --git a/src/core/trulens/experimental/otel_tracing/core/otel.py b/src/core/trulens/experimental/otel_tracing/core/otel.py new file mode 100644 index 000000000..e65c0b8e7 --- /dev/null +++ b/src/core/trulens/experimental/otel_tracing/core/otel.py @@ -0,0 +1,749 @@ +# ruff: noqa: E402 + +"""OTEL Compatibility Classes + +This module contains classes to support interacting with the OTEL ecosystem. +Additions on top of these meant for TruLens uses outside of OTEL compatibility +are found in `traces.py`. +""" + +from __future__ import annotations + +import contextlib +import contextvars +import logging +import random +import time +from types import TracebackType +from typing import ( + Any, + Dict, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + TypeVar, + Union, +) + +import pydantic +from trulens.core._utils.pycompat import NoneType # import style exception +from trulens.core._utils.pycompat import TypeAlias # import style exception +from trulens.core._utils.pycompat import TypeAliasType # import style exception +from trulens.core.utils import pyschema as pyschema_utils +from trulens.core.utils import python as python_utils +from trulens.core.utils import serial as serial_utils +from trulens.experimental.otel_tracing import _feature + +_feature._FeatureSetup.assert_optionals_installed() # checks to make sure otel is installed + +from opentelemetry import context as context_api +from opentelemetry import trace as trace_api +from opentelemetry.sdk import resources as resources_sdk +from opentelemetry.sdk import trace as trace_sdk +from opentelemetry.util import types as types_api + +logger = logging.getLogger(__name__) + +# Type alises + +A = TypeVar("A") +B = TypeVar("B") + +TSpanID: TypeAlias = int +"""Type of span identifiers. +64 bit int as per OpenTelemetry. +""" +NUM_SPANID_BITS: int = 64 +"""Number of bits in a span identifier.""" + +TTraceID: TypeAlias = int +"""Type of trace identifiers. +128 bit int as per OpenTelemetry. +""" +NUM_TRACEID_BITS: int = 128 +"""Number of bits in a trace identifier.""" + +TTimestamp: TypeAlias = int +"""Type of timestamps in spans. + +64 bit int representing nanoseconds since epoch as per OpenTelemetry. +""" +NUM_TIMESTAMP_BITS = 64 + +TLensedBaseType: TypeAlias = Union[str, int, float, bool] +"""Type of base types in span attributes. + +!!! Warning + OpenTelemetry does not allow None as an attribute value. Handling None is to + be decided. +""" + +TLensedAttributeValue = TypeAliasType( + "TLensedAttributeValue", + Union[ + str, + int, + float, + bool, + NoneType, # TODO(SNOW-1711929): None is not technically allowed as an attribute value. + Sequence["TLensedAttributeValue"], # type: ignore + "TLensedAttributes", + ], +) +"""Type of values in span attributes.""" + +# NOTE(piotrm): pydantic will fail if you specify a recursive type alias without +# the TypeAliasType schema as above. + +TLensedAttributes: TypeAlias = Dict[str, TLensedAttributeValue] +"""Attribute dictionaries. + +Note that this deviates from what OTEL allows as attribute values. Because OTEL +does not allow general recursive values to be stored as attributes, we employ a +system of flattening values before exporting to OTEL. In this process we encode +a single generic value as multiple attributes where the attribute name include +paths/lenses to the parts of the generic value they are representing. For +example, an attribute/value like `{"a": {"b": 1, "c": 2}}` would be encoded as +`{"a.b": 1, "a.c": 2}`. This process is implemented in the +`flatten_lensed_attributes` method. +""" + + +def flatten_value( + v: TLensedAttributeValue, lens: Optional[serial_utils.Lens] = None +) -> Iterable[Tuple[serial_utils.Lens, types_api.AttributeValue]]: + """Flatten recursive value into OTEL-compatible attribute values. + + See `TLensedAttributes` for more details. + """ + + if lens is None: + lens = serial_utils.Lens() + + # TODO(SNOW-1711929): OpenTelemetry does not allow None as an attribute + # value. Unsure what is best to do here. + + # if v is None: + # yield (path, "None") + + elif v is None: + pass + + elif isinstance(v, TLensedBaseType): + yield (lens, v) + + elif isinstance(v, Sequence) and all( + isinstance(e, TLensedBaseType) for e in v + ): + yield (lens, v) + + elif isinstance(v, Sequence): + for i, e in enumerate(v): + yield from flatten_value(v=e, lens=lens[i]) + + elif isinstance(v, Mapping): + for k, e in v.items(): + yield from flatten_value(v=e, lens=lens[k]) + + else: + raise ValueError( + f"Do not know how to flatten value of type {type(v)} to OTEL attributes." + ) + + +def flatten_lensed_attributes( + m: TLensedAttributes, + path: Optional[serial_utils.Lens] = None, + prefix: str = "", +) -> types_api.Attributes: + """Flatten lensed attributes into OpenTelemetry attributes.""" + + if path is None: + path = serial_utils.Lens() + + ret = {} + for k, v in m.items(): + if k.startswith(prefix): + # Only flattening those attributes that begin with `prefix` are + # those are the ones coming from trulens_eval. + for p, a in flatten_value(v, path[k]): + ret[str(p)] = a + else: + ret[k] = v + + return ret + + +def new_trace_id(): + return int( + random.getrandbits(NUM_TRACEID_BITS) + & trace_api.span._TRACE_ID_MAX_VALUE + ) + + +def new_span_id(): + return int( + random.getrandbits(NUM_SPANID_BITS) & trace_api.span._SPAN_ID_MAX_VALUE + ) + + +class TraceState(serial_utils.SerialModel, trace_api.span.TraceState): + """[OTEL TraceState][opentelemetry.trace.TraceState] requirements.""" + + # Hackish: trace_api.span.TraceState uses _dict internally. + _dict: Dict[str, str] = pydantic.PrivateAttr(default_factory=dict) + + +class SpanContext(serial_utils.SerialModel): + """[OTEL SpanContext][opentelemetry.trace.SpanContext] requirements.""" + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + use_enum_values=True, # needed for enums that do not inherit from str + ) + + trace_id: int = pydantic.Field(default_factory=new_trace_id) + """Unique identifier for the trace. + + Each root span has a unique trace id.""" + + span_id: int = pydantic.Field(default_factory=new_span_id) + """Identifier for the span. + + Meant to be at least unique within the same trace_id. + """ + + trace_flags: trace_api.TraceFlags = pydantic.Field( + trace_api.DEFAULT_TRACE_OPTIONS + ) + + @pydantic.field_validator("trace_flags", mode="before") + @classmethod + def _validate_trace_flags(cls, v): + """Validate trace flags. + + Pydantic does not seem to like classes that inherit from int without this. + """ + return trace_api.TraceFlags(v) + + trace_state: TraceState = pydantic.Field(default_factory=TraceState) + + is_remote: bool = False + + _tracer: Tracer = pydantic.PrivateAttr(None) + + @property + def tracer(self) -> Tracer: + return self._tracer + + def __init__(self, **kwargs): + super().__init__(**kwargs) + for k, v in kwargs.items(): + if v is None: + continue + # pydantic does not set private attributes in init + if k.startswith("_") and hasattr(self, k): + setattr(self, k, v) + + +def lens_of_flat_key(key: str) -> serial_utils.Lens: + """Convert a flat dict key to a lens.""" + lens = serial_utils.Lens() + for step in key.split("."): + lens = lens[step] + + return lens + + +class Span( + pyschema_utils.WithClassInfo, serial_utils.SerialModel, trace_api.Span +): + """[OTEL Span][opentelemetry.trace.Span] requirements. + + See also [OpenTelemetry + Span](https://opentelemetry.io/docs/specs/otel/trace/api/#span) and + [OpenTelemetry Span + specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md). + """ + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + use_enum_values=True, # model_validate will fail without this + ) + + name: Optional[str] = None + + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL + + context: SpanContext = pydantic.Field(default_factory=SpanContext) + parent: Optional[SpanContext] = None + + status: trace_api.status.StatusCode = trace_api.status.StatusCode.UNSET + status_description: Optional[str] = None + + events: List[Tuple[str, trace_api.types.Attributes, TTimestamp]] = ( + pydantic.Field(default_factory=list) + ) + links: trace_api._Links = pydantic.Field(default_factory=dict) + + # attributes: trace_api.types.Attributes = pydantic.Field(default_factory=dict) + attributes: Dict = pydantic.Field(default_factory=dict) + + start_timestamp: int = pydantic.Field(default_factory=time.time_ns) + + end_timestamp: Optional[int] = None + + _record_exception: bool = pydantic.PrivateAttr(True) + _set_status_on_exception: bool = pydantic.PrivateAttr(True) + + _tracer: Tracer = pydantic.PrivateAttr(None) + """NON-STANDARD: The Tracer that produced this span.""" + + @property + def tracer(self) -> Tracer: + return self._tracer + + def __init__(self, **kwargs): + if kwargs.get("start_timestamp") is None: + kwargs["start_timestamp"] = time.time_ns() + + super().__init__(**kwargs) + + for k, v in kwargs.items(): + if v is None: + continue + # pydantic does not set private attributes in init + if k.startswith("_") and hasattr(self, k): + setattr(self, k, v) + + def update_name(self, name: str) -> None: + """See [OTEL update_name][opentelemetry.trace.span.Span.update_name].""" + + self.name = name + + def get_span_context(self) -> trace_api.span.SpanContext: + """See [OTEL get_span_context][opentelemetry.trace.span.Span.get_span_context].""" + + return self.context + + def set_status( + self, + status: Union[trace_api.span.Status, trace_api.span.StatusCode], + description: Optional[str] = None, + ) -> None: + """See [OTEL set_status][opentelemetry.trace.span.Span.set_status].""" + + if isinstance(status, trace_api.span.Status): + if description is not None: + raise ValueError( + "Ambiguous status description provided both in `status.description` and in `description`." + ) + + self.status = status.status_code + self.status_description = status.description + else: + self.status = status + self.status_description = description + + def add_event( + self, + name: str, + attributes: types_api.Attributes = None, + timestamp: Optional[TTimestamp] = None, + ) -> None: + """See [OTEL add_event][opentelemetry.trace.span.Span.add_event].""" + + self.events.append((name, attributes, timestamp or time.time_ns())) + + def add_link( + self, + context: trace_api.span.SpanContext, + attributes: types_api.Attributes = None, + ) -> None: + """See [OTEL add_link][opentelemetry.trace.span.Span.add_link].""" + + if attributes is None: + attributes = {} + + self.links[context] = attributes + + def is_recording(self) -> bool: + """See [OTEL is_recording][opentelemetry.trace.span.Span.is_recording].""" + + return self.status == trace_api.status.StatusCode.UNSET + + def set_attributes( + self, attributes: Dict[str, types_api.AttributeValue] + ) -> None: + """See [OTEL set_attributes][opentelemetry.trace.span.Span.set_attributes].""" + + for key, value in attributes.items(): + self.set_attribute(key, value) + + def set_attribute(self, key: str, value: types_api.AttributeValue) -> None: + """See [OTEL set_attribute][opentelemetry.trace.span.Span.set_attribute].""" + + self.attributes[key] = value + + def record_exception( + self, + exception: BaseException, + attributes: types_api.Attributes = None, + timestamp: Optional[TTimestamp] = None, + escaped: bool = False, # purpose unknown + ) -> None: + """See [OTEL record_exception][opentelemetry.trace.span.Span.record_exception].""" + + if self._set_status_on_exception: + self.set_status( + trace_api.status.Status(trace_api.status.StatusCode.ERROR) + ) + + if self._record_exception: + if attributes is None: + attributes = {} + + attributes["exc_type"] = python_utils.class_name(type(exception)) + attributes["exc_val"] = str(exception) + if exception.__traceback__ is not None: + attributes["code_line"] = python_utils.code_line( + exception.__traceback__.tb_frame, show_source=True + ) + + self.add_event("trulens.exception", attributes, timestamp) + + def end(self, end_time: Optional[TTimestamp] = None): + """See [OTEL end][opentelemetry.trace.span.Span.end]""" + + if end_time is None: + end_time = time.time_ns() + + self.end_timestamp = end_time + + if self.is_recording(): + self.set_status( + trace_api.status.Status(trace_api.status.StatusCode.OK) + ) + + def __enter__(self) -> Span: + """See [OTEL __enter__][opentelemetry.trace.span.Span.__enter__].""" + + return self + + def __exit__( + self, + exc_type: Optional[BaseException], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + """See [OTEL __exit__][opentelemetry.trace.span.Span.__exit__].""" + + try: + if exc_val is not None: + self.record_exception(exception=exc_val) + raise exc_val + finally: + self.end() + + async def __aenter__(self) -> Span: + return self.__enter__() + + async def __aexit__( + self, + exc_type: Optional[BaseException], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + return self.__exit__(exc_type, exc_val, exc_tb) + + # Rest of these methods are for exporting spans to ReadableSpan. All are not standard OTEL. + + @staticmethod + def otel_context_of_context(context: SpanContext) -> trace_api.SpanContext: + return trace_api.SpanContext( + trace_id=context.trace_id, + span_id=context.span_id, + is_remote=False, + ) + + def otel_name(self) -> str: + return self.name + + def otel_context(self) -> types_api.SpanContext: + return self.otel_context_of_context(self.context) + + def otel_parent_context(self) -> Optional[types_api.SpanContext]: + if self.parent is None: + return None + + return self.otel_context_of_context(self.parent) + + def otel_attributes(self) -> types_api.Attributes: + return flatten_lensed_attributes(self.attributes) + + def otel_kind(self) -> types_api.SpanKind: + return trace_api.SpanKind.INTERNAL + + def otel_status(self) -> trace_api.status.Status: + return trace_api.status.Status(self.status, self.status_description) + + def otel_resource_attributes(self) -> Dict[str, Any]: + # TODO(SNOW-1711959) + return { + "service.namespace": "trulens", + } + + def otel_resource(self) -> resources_sdk.Resource: + return resources_sdk.Resource( + attributes=self.otel_resource_attributes() + ) + + def otel_events(self) -> List[types_api.Event]: + return self.events + + def otel_links(self) -> List[types_api.Link]: + return self.links + + def otel_freeze(self) -> trace_sdk.ReadableSpan: + """Convert span to an OTEL compatible span for exporting to OTEL collectors.""" + + return trace_sdk.ReadableSpan( + name=self.otel_name(), + context=self.otel_context(), + parent=self.otel_parent_context(), + resource=self.otel_resource(), + attributes=self.otel_attributes(), + events=self.otel_events(), + links=self.otel_links(), + kind=self.otel_kind(), + instrumentation_info=None, # TODO(SNOW-1711959) + status=self.otel_status(), + start_time=self.start_timestamp, + end_time=self.end_timestamp, + instrumentation_scope=None, # TODO(SNOW-1711959) + ) + + +class Tracer(serial_utils.SerialModel, trace_api.Tracer): + """[OTEL Tracer][opentelemetry.trace.Tracer] requirements.""" + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + _instrumenting_module_name: Optional[str] = pydantic.PrivateAttr(None) + """Name of the library/module that is instrumenting the code.""" + + _instrumenting_library_version: Optional[str] = pydantic.PrivateAttr(None) + """Version of the library that is instrumenting the code.""" + + _attributes: Optional[trace_api.types.Attributes] = pydantic.PrivateAttr( + None + ) + """Common attributes to add to all spans.""" + + _schema_url: Optional[str] = pydantic.PrivateAttr(None) + """Use unknown.""" + + _tracer_provider: TracerProvider = pydantic.PrivateAttr(None) + """NON-STANDARD: The TracerProvider that made this tracer.""" + + _span_class: Type[Span] = pydantic.PrivateAttr(Span) + """NON-STANDARD: The default span class to use when creating spans.""" + + _span_context_class: Type[SpanContext] = pydantic.PrivateAttr(SpanContext) + """NON-STANDARD: The default span context class to use when creating spans.""" + + def __init__(self, _context: context_api.context.Context, **kwargs): + super().__init__(**kwargs) + + for k, v in kwargs.items(): + if v is None: + continue + # pydantic does not set private attributes in init + if k.startswith("_") and hasattr(self, k): + setattr(self, k, v) + + self._context_cvar.set(_context) + + _context_cvar: contextvars.ContextVar[context_api.context.Context] = ( + pydantic.PrivateAttr( + default_factory=lambda: contextvars.ContextVar( + f"context_Tracer_{python_utils.context_id()}", default=None + ) + ) + ) + + @property + def context_cvar( + self, + ) -> contextvars.ContextVar[context_api.context.Context]: + """NON-STANDARD: The context variable to store the current span context.""" + + return self._context_cvar + + @property + def trace_id(self) -> int: + return self._tracer_provider.trace_id + + def start_span( + self, + name: Optional[str] = None, + *args, # non-standard + context: Optional[context_api.context.Context] = None, + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + attributes: trace_api.types.Attributes = None, + links: trace_api._Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + cls: Optional[Type[Span]] = None, # non-standard + **kwargs, # non-standard + ) -> Span: + """See [OTEL + Tracer.start_span][opentelemetry.trace.Tracer.start_span].""" + + if context is None: + parent_context = self.context_cvar.get() + + else: + if len(context) != 1: + raise ValueError("Only one context span is allowed.") + parent_span_encoding = next(iter(context.values())) + + parent_context = self._span_context_class( + trace_id=parent_span_encoding.trace_id, + span_id=parent_span_encoding.span_id, + _tracer=self, + ) + + new_context = self._span_context_class( + *args, trace_id=self.trace_id, _tracer=self, **kwargs + ) + + if name is None: + name = python_utils.class_name(self._span_class) + + if attributes is None: + attributes = {} + + if self._attributes is not None: + attributes.update(self._attributes) + + if cls is None: + cls = self._span_class + + new_span = cls( + name=name, + context=new_context, + parent=parent_context, + kind=kind, + attributes=attributes, + links=links, + start_timestamp=start_time, + _record_exception=record_exception, + _status_on_exception=set_status_on_exception, + _tracer=self, + ) + + return new_span + + @contextlib.contextmanager + def start_as_current_span( + self, + name: Optional[str] = None, + context: Optional[trace_api.context.Context] = None, + kind: trace_api.SpanKind = trace_api.SpanKind.INTERNAL, + attributes: trace_api.types.Attributes = None, + links: trace_api._Links = None, + start_time: Optional[int] = None, + record_exception: bool = True, + set_status_on_exception: bool = True, + end_on_exit: bool = True, + ): + """See [OTEL + Tracer.start_as_current_span][opentelemetry.trace.Tracer.start_as_current_span].""" + + span = self.start_span( + name=name, + context=context, + kind=kind, + attributes=attributes, + links=links, + start_time=start_time, + record_exception=record_exception, + set_status_on_exception=set_status_on_exception, + ) + + token = self.context_cvar.set(span.context) + + try: + yield span + + except BaseException as e: + if record_exception: + span.record_exception(e) + + finally: + self.context_cvar.reset(token) + + if end_on_exit: + span.end() + + +class TracerProvider(serial_utils.SerialModel, trace_api.TracerProvider): + """[OTEL TracerProvider][opentelemetry.trace.TracerProvider] + requirements.""" + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + _tracer_class: Type[Tracer] = pydantic.PrivateAttr(Tracer) + """NON-STANDARD: The default tracer class to use when creating tracers.""" + + _context_cvar: contextvars.ContextVar[context_api.context.Context] = ( + pydantic.PrivateAttr( + default_factory=lambda: contextvars.ContextVar( + f"context_TracerProvider_{python_utils.context_id()}", + default=None, + ) + ) + ) + + @property + def context_cvar( + self, + ) -> contextvars.ContextVar[context_api.context.Context]: + """NON-STANDARD: The context variable to store the current span context.""" + + return self._context_cvar + + _trace_id: int = pydantic.PrivateAttr(default_factory=new_trace_id) + + @property + def trace_id(self) -> int: + """NON-STANDARD: The current trace id.""" + + return self._trace_id + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: Optional[str] = None, + schema_url: Optional[str] = None, + attributes: Optional[types_api.Attributes] = None, + ): + """See [OTEL + TracerProvider.get_tracer][opentelemetry.trace.TracerProvider.get_tracer].""" + + tracer = self._tracer_class( + _instrumenting_module_name=instrumenting_module_name, + _instrumenting_library_version=instrumenting_library_version, + _attributes=attributes, + _schema_url=schema_url, + _tracer_provider=self, + _context=self.context_cvar.get(), + ) + + return tracer diff --git a/src/core/trulens/experimental/otel_tracing/core/trace.py b/src/core/trulens/experimental/otel_tracing/core/trace.py new file mode 100644 index 000000000..2d21c3ce8 --- /dev/null +++ b/src/core/trulens/experimental/otel_tracing/core/trace.py @@ -0,0 +1,1098 @@ +# ruff: noqa: E402 + +"""Implementation of recording that resembles the tracing process in OpenTelemetry. + +!!! Note + Most of the module is (EXPERIMENTAL(otel_tracing)) though it includes some existing + non-experimental classes moved here to resolve some circular import issues. +""" + +from __future__ import annotations + +import contextlib +import contextvars +import inspect +import logging +import os +import sys +import threading as th +from threading import Lock +from typing import ( + Any, + Callable, + ContextManager, + Dict, + Generic, + Hashable, + Iterable, + List, + Optional, + Tuple, + Type, + TypeVar, + Union, +) +import uuid +import weakref + +import pydantic +from trulens.core.schema import base as base_schema +from trulens.core.schema import record as record_schema +from trulens.core.schema import types as types_schema +from trulens.core.utils import json as json_utils +from trulens.core.utils import pyschema as pyschema_utils +from trulens.core.utils import python as python_utils +from trulens.core.utils import serial as serial_utils +from trulens.experimental.otel_tracing import _feature +from trulens.experimental.otel_tracing.core import otel as core_otel +from trulens.experimental.otel_tracing.core._utils import wrap as wrap_utils + +_feature._FeatureSetup.assert_optionals_installed() # checks to make sure otel is installed + +if sys.version_info < (3, 9): + from functools import lru_cache as fn_cache +else: + from functools import cache as fn_cache + +from opentelemetry.semconv.resource import ResourceAttributes +from opentelemetry.trace import span as span_api +from opentelemetry.util import types as types_api + +T = TypeVar("T") +R = TypeVar("R") # callable return type +E = TypeVar("E") # iterator/generator element type + +logger = logging.getLogger(__name__) + +INSTRUMENT: str = "__tru_instrumented" +"""Attribute name to be used to flag instrumented objects/methods/others.""" + +APPS: str = "__tru_apps" +"""Attribute name for storing apps that expect to be notified of calls.""" + + +class SpanContext(core_otel.SpanContext, Hashable): + """TruLens additions on top of OTEL SpanContext to add Hashable and + reference to tracer that made the span.""" + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + def __str__(self): + return f"{self.trace_id % 0xFF:02x}/{self.span_id % 0xFF:02x}" + + def __repr__(self): + return str(self) + + def __hash__(self): + return self.trace_id + self.span_id + + def __eq__(self, other: SpanContextLike): + if other is None: + return False + + return self.trace_id == other.trace_id and self.span_id == other.span_id + + @staticmethod + def of_spancontextlike(span_context: SpanContextLike) -> SpanContext: + if isinstance(span_context, SpanContext): + return span_context + + elif isinstance(span_context, core_otel.SpanContext): + return SpanContext( + trace_id=span_context.trace_id, + span_id=span_context.span_id, + is_remote=span_context.is_remote, + ) + elif isinstance(span_context, span_api.SpanContext): + return SpanContext( + trace_id=span_context.trace_id, + span_id=span_context.span_id, + is_remote=span_context.is_remote, + ) + elif isinstance(span_context, Dict): + return SpanContext.model_validate(span_context) + else: + raise ValueError(f"Unrecognized span context type: {span_context}") + + +SpanContextLike = Union[ + SpanContext, core_otel.SpanContext, span_api.SpanContext, serial_utils.JSON +] + + +class Span(core_otel.Span): + """TruLens additions on top of OTEL spans.""" + + model_config = pydantic.ConfigDict( + arbitrary_types_allowed=True, + use_enum_values=True, # model_validate will fail without this + ) + + def __str__(self): + return ( + f"{type(self).__name__}({self.name}, {self.context}->{self.parent})" + ) + + def __repr__(self): + return str(self) + + _lensed_attributes: serial_utils.LensedDict[Any] = pydantic.PrivateAttr( + default_factory=serial_utils.LensedDict + ) + + @property + def lensed_attributes(self) -> serial_utils.LensedDict[Any]: + return self._lensed_attributes + + @property + def parent_span(self) -> Optional[Span]: + if self.parent is None: + return None + + if self._tracer is None: + return None + + if (span := self._tracer.spans.get(self.parent)) is None: + return None + + return span + + _children_spans: List[Span] = pydantic.PrivateAttr(default_factory=list) + + @property + def children_spans(self) -> List[Span]: + return self._children_spans + + error: Optional[Exception] = pydantic.Field(None) + """Optional error if the observed computation raised an exception.""" + + def __init__(self, **kwargs): + # Convert any contexts to our hashable context class: + if (context := kwargs.get("context")) is not None: + kwargs["context"] = SpanContext.of_spancontextlike(context) + if (parent := kwargs.get("parent", None)) is not None: + kwargs["parent"] = SpanContext.of_spancontextlike(parent) + + super().__init__(**kwargs) + + if (parent_span := self.parent_span) is not None: + parent_span.children_spans.append(self) + + def iter_children( + self, transitive: bool = True, include_phantom: bool = False + ) -> Iterable[Span]: + """Iterate over all spans that are children of this span. + + Args: + transitive: Iterate recursively over children. + + include_phantom: Include phantom spans. If not set, phantom spans + will not be included but will be iterated over even if + transitive is false. + """ + + for child_span in self.children_spans: + if isinstance(child_span, PhantomSpan) and not include_phantom: + # Note that transitive being false is ignored if phantom is skipped. + yield from child_span.iter_children( + transitive=transitive, include_phantom=include_phantom + ) + else: + yield child_span + if transitive: + yield from child_span.iter_children( + transitive=transitive, + include_phantom=include_phantom, + ) + + def iter_family(self, include_phantom: bool = False) -> Iterable[Span]: + """Iterate itself and all children transitively.""" + + if (not isinstance(self, PhantomSpan)) or include_phantom: + yield self + + yield from self.iter_children( + include_phantom=include_phantom, transitive=True + ) + + def total_cost(self) -> base_schema.Cost: + """Total costs of this span and all its transitive children.""" + + total = base_schema.Cost() + + for span in self.iter_family(include_phantom=True): + if isinstance(span, WithCost) and span.cost is not None: + total += span.cost + + return total + + +class PhantomSpan(Span): + """A span type that indicates that it does not correspond to a + computation to be recorded but instead is an element of the tracing system. + + It is to be removed from the spans presented to the users. + """ + + +class LiveSpan(Span): + """A a span type that indicates that it contains live python objects. + + It is to be converted to a non-live span before being output to the user or + otherwise. + """ + + +class PhantomSpanRecordingContext(PhantomSpan): + """Tracks the context of an app used as a context manager.""" + + recording: Optional[Any] = pydantic.Field(None, exclude=True) + # TODO: app.RecordingContext # circular import issues + + def otel_resource_attributes(self) -> Dict[str, Any]: + ret = super().otel_resource_attributes() + + ret[ResourceAttributes.SERVICE_NAME] = ( + self.recording.app.app_name if self.recording is not None else None + ) + + return ret + + # override + def end(self, *args, **kwargs): + super().end(*args, **kwargs) + + self._finalize_recording() + + # override + def record_exception( + self, + exception: BaseException, + attributes: types_api.Attributes = None, + timestamp: int | None = None, + escaped: bool = False, + ) -> None: + super().record_exception(exception, attributes, timestamp, escaped) + + self._finalize_recording() + + def _finalize_recording(self): + assert self.recording is not None + + app = self.recording.app + + for span in Tracer.find_each_child( + span=self, span_filter=lambda s: isinstance(s, LiveSpanCall) + ): + app._on_new_root_span(recording=self.recording, root_span=span) + + app._on_new_recording_span(recording_span=self) + + def otel_name(self) -> str: + return "trulens.recording" + + +class SpanCall(Span): + """Non-live fields of a function call span.""" + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + call_id: Optional[uuid.UUID] = pydantic.Field(None) + """Unique identifier for the call.""" + + stack: Optional[List[record_schema.RecordAppCallMethod]] = pydantic.Field( + None + ) + """Call stack of instrumented methods only.""" + + sig: Optional[inspect.Signature] = pydantic.Field(None) + """Signature of the function.""" + + func_name: Optional[str] = None + """Function name.""" + + pid: Optional[int] = None + """Process id.""" + + tid: Optional[int] = None + """Thread id.""" + + def end(self): + super().end() + + self.set_attribute(ResourceAttributes.PROCESS_PID, self.pid) + self.set_attribute("thread.id", self.tid) # TODO: semconv + + self.set_attribute("trulens.call_id", str(self.call_id)) + self.set_attribute("trulens.stack", json_utils.jsonify(self.stack)) + self.set_attribute("trulens.sig", str(self.sig)) + + def otel_name(self) -> str: + return f"trulens.call.{self.func_name}" + + +class LiveSpanCall(LiveSpan, SpanCall): + """Track a function call.""" + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + live_obj: Optional[Any] = pydantic.Field(None, exclude=True) + """Self object if method call.""" + + live_cls: Optional[Type] = pydantic.Field(None, exclude=True) + """Class if method/static/class method call.""" + + live_func: Optional[Callable] = pydantic.Field(None, exclude=True) + """Function object.""" + + live_args: Optional[Tuple[Any, ...]] = pydantic.Field(None, exclude=True) + """Positional arguments to the function call.""" + + live_kwargs: Optional[Dict[str, Any]] = pydantic.Field(None, exclude=True) + """Keyword arguments to the function call.""" + + live_bindings: Optional[inspect.BoundArguments] = pydantic.Field( + None, exclude=True + ) + """Bound arguments to the function call if can be bound.""" + + live_ret: Optional[Any] = pydantic.Field(None, exclude=True) + """Return value of the function call. + + Exclusive with `error`. + """ + + live_error: Optional[Any] = pydantic.Field(None, exclude=True) + """Error raised by the function call. + + Exclusive with `ret`. + """ + + def end(self): + super().end() + + if self.live_cls is not None: + self.set_attribute( + "trulens.cls", + pyschema_utils.Class.of_class(self.live_cls).model_dump(), + ) + + if self.live_func is not None: + self.set_attribute( + "trulens.func", + pyschema_utils.FunctionOrMethod.of_callable( + self.live_func + ).model_dump(), + ) + + if self.live_ret is not None: + self.set_attribute("trulens.ret", json_utils.jsonify(self.live_ret)) + + if self.live_bindings is not None: + self.set_attribute( + "trulens.bindings", + pyschema_utils.Bindings.of_bound_arguments( + self.live_bindings, arguments_only=True, skip_self=True + ).model_dump()["kwargs"], + ) + + if self.live_error is not None: + self.set_attribute( + "trulens.error", json_utils.jsonify(self.live_error) + ) + + +S = TypeVar("S", bound=LiveSpanCall) + + +class WithCost(LiveSpan): + """Mixin to indicate the span has costs tracked.""" + + cost: base_schema.Cost = pydantic.Field(default_factory=base_schema.Cost) + """Cost of the computation spanned.""" + + endpoint: Optional[Any] = pydantic.Field( + None, exclude=True + ) # Any actually core_endpoint.Endpoint + """Endpoint handling cost extraction for this span/call.""" + + def end(self): + super().end() + + self.set_attribute("trulens.cost", self.cost.model_dump()) + + def __init__(self, cost: Optional[base_schema.Cost] = None, **kwargs): + if cost is None: + cost = base_schema.Cost() + + super().__init__(cost=cost, **kwargs) + + +class LiveSpanCallWithCost(LiveSpanCall, WithCost): + pass + + +class Tracer(core_otel.Tracer): + """TruLens additions on top of [OTEL Tracer][opentelemetry.trace.Tracer].""" + + # TODO: Tracer that does not record anything. Can either be a setting to + # this tracer or a separate "NullTracer". We need non-recording users to not + # incur much overhead hence need to be able to disable most of the tracing + # logic when appropriate. + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + # Overrides core_otel.Tracer._span_class + _span_class: Type[Span] = pydantic.PrivateAttr(Span) + + # Overrides core_otel.Tracer._span_context_class + _span_context_class: Type[SpanContext] = pydantic.PrivateAttr(SpanContext) + + @property + def spans(self) -> Dict[SpanContext, Span]: + return self._tracer_provider.spans + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def __str__(self): + return f"{type(self).__name__} {self.instrumenting_module_name} {self.instrumenting_library_version}" + + def __repr__(self): + return str(self) + + def start_span(self, *args, **kwargs): + new_span = super().start_span(*args, **kwargs) + + self.spans[new_span.context] = new_span + + return new_span + + @staticmethod + def _fill_stacks( + span: Span, + get_method_path: Callable, + stack: List[record_schema.RecordAppCallMethod] = [], + ): + if isinstance(span, LiveSpanCall): + path = get_method_path(obj=span.live_obj, func=span.live_func) + + frame_ident = record_schema.RecordAppCallMethod( + path=path + if path is not None + else serial_utils.Lens().static, # placeholder path for functions + method=pyschema_utils.Method.of_method( + cls=span.live_cls, obj=span.live_obj, meth=span.live_func + ), + ) + + stack = stack + [frame_ident] + span.stack = stack + + for subspan in span.iter_children(transitive=False): + Tracer._fill_stacks( + subspan, stack=stack, get_method_path=get_method_path + ) + + def _call_of_spancall( + self, span: LiveSpanCall + ) -> record_schema.RecordAppCall: + """Convert a SpanCall to a RecordAppCall.""" + + args = ( + dict(span.live_bindings.arguments) + if span.live_bindings is not None + else None + ) + if args is not None: + if "self" in args: + del args["self"] # remove self + + assert span.start_timestamp is not None + if span.end_timestamp is None: + logger.warning( + "Span %s has no end timestamp. It might not have yet finished recording.", + span, + ) + + return record_schema.RecordAppCall( + call_id=str(span.call_id), + stack=span.stack, + args=args, + rets=json_utils.jsonify(span.live_ret), + error=str(span.live_error), + perf=base_schema.Perf.of_ns_timestamps( + start_ns_timestamp=span.start_timestamp, + end_ns_timestamp=span.end_timestamp, + ), + pid=span.pid, + tid=span.tid, + ) + + def record_of_root_span( + self, recording: Any, root_span: LiveSpanCall + ) -> record_schema.Record: + """Convert a root span to a record. + + This span has to be a call span so we can extract things like main input and output. + """ + + assert isinstance(root_span, LiveSpanCall), type(root_span) + + app = recording.app + + self._fill_stacks(root_span, get_method_path=app.get_method_path) + + root_perf = ( + base_schema.Perf.of_ns_timestamps( + start_ns_timestamp=root_span.start_timestamp, + end_ns_timestamp=root_span.end_timestamp, + ) + if root_span.end_timestamp is not None + else None + ) + + total_cost = root_span.total_cost() + + calls = [] + if isinstance(root_span, LiveSpanCall): + calls.append(self._call_of_spancall(root_span)) + + spans = [root_span] + + for span in root_span.iter_children(include_phantom=True): + if isinstance(span, LiveSpanCall): + calls.append(self._call_of_spancall(span)) + + spans.append(span) + + bindings = root_span.live_bindings + main_error = root_span.live_error + + if bindings is not None: + main_input = app.main_input( + func=root_span.live_func, + sig=root_span.sig, + bindings=root_span.live_bindings, + ) + if main_error is None: + main_output = app.main_output( + func=root_span.live_func, + sig=root_span.sig, + bindings=root_span.live_bindings, + ret=root_span.live_ret, + ) + else: + main_output = None + else: + main_input = None + main_output = None + + record = record_schema.Record( + record_id="placeholder", + app_id=app.app_id, + main_input=json_utils.jsonify(main_input), + main_output=json_utils.jsonify(main_output), + main_error=json_utils.jsonify(main_error), + calls=calls, + perf=root_perf, + cost=total_cost, + experimental_otel_spans=spans, + ) + + # record_id determinism + record.record_id = json_utils.obj_id_of_obj( + record.model_dump(), prefix="record" + ) + + return record + + @staticmethod + def find_each_child(span: Span, span_filter: Callable) -> Iterable[Span]: + """For each family rooted at each child of this span, find the top-most + span that satisfies the filter.""" + + for child_span in span.children_spans: + if span_filter(child_span): + yield child_span + else: + yield from Tracer.find_each_child(child_span, span_filter) + + def records_of_recording( + self, recording: PhantomSpanRecordingContext + ) -> Iterable[record_schema.Record]: + """Convert a recording based on spans to a list of records.""" + + for root_span in Tracer.find_each_child( + span=recording, span_filter=lambda s: isinstance(s, LiveSpanCall) + ): + assert isinstance(root_span, LiveSpanCall) + yield self.record_of_root_span( + recording=recording, root_span=root_span + ) + + @contextlib.contextmanager + def _span(self, cls: Type[S], **kwargs) -> ContextManager[S]: + with self.start_span(cls=cls, **kwargs) as span: + with python_utils.with_context({self.context_cvar: span.context}): + yield span + + @contextlib.asynccontextmanager + async def _aspan(self, cls: Type[S], **kwargs) -> ContextManager[S]: + async with self.start_span(cls=cls, **kwargs) as span: + async with python_utils.awith_context({ + self.context_cvar: span.context + }): + yield span + + # context manager + def recording(self) -> ContextManager[PhantomSpanRecordingContext]: + return self._span( + name="trulens.recording", cls=PhantomSpanRecordingContext + ) + + # context manager + def method(self, method_name: str) -> ContextManager[LiveSpanCall]: + return self._span(name="trulens.call." + method_name, cls=LiveSpanCall) + + # context manager + def cost( + self, method_name: str, cost: Optional[base_schema.Cost] = None + ) -> ContextManager[LiveSpanCallWithCost]: + return self._span( + name="trulens.call." + method_name, + cls=LiveSpanCallWithCost, + cost=cost, + ) + + # context manager + def phantom(self) -> ContextManager[PhantomSpan]: + return self._span(name="trulens.phantom", cls=PhantomSpan) + + # context manager + async def arecording(self) -> ContextManager[PhantomSpanRecordingContext]: + return self._aspan( + name="trulens.recording", cls=PhantomSpanRecordingContext + ) + + # context manager + async def amethod(self, method_name: str) -> ContextManager[LiveSpanCall]: + return self._aspan(name="trulens.call." + method_name, cls=LiveSpanCall) + + # context manager + async def acost( + self, method_name: str, cost: Optional[base_schema.Cost] = None + ) -> ContextManager[LiveSpanCallWithCost]: + return self._aspan( + name="trulens.call." + method_name, + cls=LiveSpanCallWithCost, + cost=cost, + ) + + # context manager + async def aphantom(self) -> ContextManager[PhantomSpan]: + return self._aspan(name="trulens.phantom", cls=PhantomSpan) + + +class TracerProvider( + core_otel.TracerProvider, metaclass=python_utils.PydanticSingletonMeta +): + """TruLens additions on top of [OTEL TracerProvider][opentelemetry.trace.TracerProvider].""" + + model_config = pydantic.ConfigDict(arbitrary_types_allowed=True) + + _trace_id: int = pydantic.PrivateAttr( + default_factory=core_otel.new_trace_id + ) + + def __str__(self): + # Pydantic will not print anything useful otherwise. + return f"{self.__module__}.{type(self).__name__}()" + + @property + def trace_id(self): + return self._trace_id + + # Overrides core_otel.TracerProvider._tracer_class + _tracer_class: Type[Tracer] = pydantic.PrivateAttr(default=Tracer) + + _tracers: Dict[str, Tracer] = pydantic.PrivateAttr(default_factory=dict) + + _spans: Dict[SpanContext, Span] = pydantic.PrivateAttr(default_factory=dict) + + @property + def spans(self) -> Dict[SpanContext, Span]: + return self._spans + + def get_tracer( + self, + instrumenting_module_name: str, + instrumenting_library_version: Optional[str] = None, + schema_url: Optional[str] = None, + attributes: Optional[types_api.Attributes] = None, + ): + if instrumenting_module_name in self._tracers: + return self._tracers[instrumenting_module_name] + + tracer = super().get_tracer( + instrumenting_module_name=instrumenting_module_name, + instrumenting_library_version=instrumenting_library_version, + attributes=attributes, + schema_url=schema_url, + ) + + self._tracers[instrumenting_module_name] = tracer + + return tracer + + +tracer_provider = TracerProvider() +"""Global tracer provider. +All trulens tracers are made by this provider even if a different one is +configured for OTEL. +""" + + +@fn_cache +def trulens_tracer(): + from trulens.core import __version__ + + return tracer_provider.get_tracer( + instrumenting_module_name="trulens.experimental.otel_tracing.core.trace", + instrumenting_library_version=__version__, + ) + + +class TracingCallbacks(wrap_utils.CallableCallbacks[R], Generic[R, S]): + """Extension of CallableCallbacks that adds tracing to the wrapped callable + as implemented using tracer and spans.""" + + def __init__( + self, + func_name: str, + span_type: Type[S] = LiveSpanCall, + **kwargs: Dict[str, Any], + ): + super().__init__(**kwargs) + + self.func_name: str = func_name + + self.obj: Optional[object] = None + self.obj_cls: Optional[Type] = None + self.obj_id: Optional[int] = None + + if not issubclass(span_type, LiveSpanCall): + raise ValueError("span_type must be a subclass of LiveSpanCall.") + + self.span_context: ContextManager = trulens_tracer()._span( + span_type, name="trulens.call." + func_name + ) + self.span: S = self.span_context.__enter__() + + def on_callable_call( + self, bindings: inspect.BoundArguments, **kwargs: Dict[str, Any] + ) -> inspect.BoundArguments: + temp = super().on_callable_call(bindings=bindings, **kwargs) + + if "self" in bindings.arguments: + # TODO: need some generalization + self.obj = bindings.arguments["self"] + self.obj_cls = type(self.obj) + self.obj_id = id(self.obj) + else: + logger.warning("No self in bindings for %s.", self) + + span = self.span + span.pid = os.getpid() + span.tid = th.get_native_id() + + return temp + + def on_callable_end(self): + super().on_callable_end() + + span = self.span + + # SpanCall attributes + span.call_id = self.call_id + span.func_name = self.func_name + span.sig = self.sig + + # LiveSpanCall attributes + span.live_obj = self.obj + span.live_cls = self.obj_cls + span.live_func = self.func + span.live_args = self.call_args + span.live_kwargs = self.call_kwargs + span.live_bindings = self.bindings + span.live_ret = self.ret + span.live_error = self.error + + if self.error is not None: + self.span_context.__exit__( + type(self.error), self.error, self.error.__traceback__ + ) + else: + self.span_context.__exit__(None, None, None) + + +class _RecordingContext: + """Manager of the creation of records from record calls. + + An instance of this class is produced when using an + [App][trulens_eval.app.App] as a context mananger, i.e.: + Example: + ```python + app = ... # your app + truapp: TruChain = TruChain(app, ...) # recorder for LangChain apps + with truapp as recorder: + app.invoke(...) # use your app + recorder: RecordingContext + ``` + + Each instance of this class produces a record for every "root" instrumented + method called. Root method here means the first instrumented method in a + call stack. Note that there may be more than one of these contexts in play + at the same time due to: + - More than one wrapper of the same app. + - More than one context manager ("with" statement) surrounding calls to the + same app. + - Calls to "with_record" on methods that themselves contain recording. + - Calls to apps that use trulens internally to track records in any of the + supported ways. + - Combinations of the above. + """ + + def __init__( + self, + app: _WithInstrumentCallbacks, + record_metadata: serial_utils.JSON = None, + tracer: Optional[Tracer] = None, + span: Optional[PhantomSpanRecordingContext] = None, + span_ctx: Optional[SpanContext] = None, + ): + self.calls: Dict[types_schema.CallID, record_schema.RecordAppCall] = {} + """A record (in terms of its RecordAppCall) in process of being created. + + Storing as a map as we want to override calls with the same id which may + happen due to methods producing awaitables or generators. These result + in calls before the awaitables are awaited and then get updated after + the result is ready. + """ + # TODEP: To deprecated after migration to span-based tracing. + + self.records: List[record_schema.Record] = [] + """Completed records.""" + + self.lock: Lock = Lock() + """Lock blocking access to `records` when adding calls or + finishing a record.""" + + self.token: Optional[contextvars.Token] = None + """Token for context management.""" + + self.app: _WithInstrumentCallbacks = app + """App for which we are recording.""" + + self.record_metadata = record_metadata + """Metadata to attach to all records produced in this context.""" + + self.tracer: Optional[Tracer] = tracer + """EXPERIMENTAL(otel_tracing): OTEL-like tracer for recording. + """ + + self.span: Optional[PhantomSpanRecordingContext] = span + """EXPERIMENTAL(otel_tracing): Span that represents a recording context + (the with block).""" + + self.span_ctx = span_ctx + """EXPERIMENTAL(otel_tracing): The context manager for the above span. + """ + + @property + def spans(self) -> Dict[SpanContext, Span]: + """EXPERIMENTAL(otel_tracing): Get the spans of the tracer in this context.""" + + if self.tracer is None: + return {} + + return self.tracer.spans + + def __iter__(self): + return iter(self.records) + + def get(self) -> record_schema.Record: + """Get the single record only if there was exactly one or throw + an error otherwise.""" + + if len(self.records) == 0: + raise RuntimeError("Recording context did not record any records.") + + if len(self.records) > 1: + raise RuntimeError( + "Recording context recorded more than 1 record. " + "You can get them with ctx.records, ctx[i], or `for r in ctx: ...`." + ) + + return self.records[0] + + def __getitem__(self, idx: int) -> record_schema.Record: + return self.records[idx] + + def __len__(self): + return len(self.records) + + def __hash__(self) -> int: + # The same app can have multiple recording contexts. + return hash(id(self.app)) + hash(id(self.records)) + + def __eq__(self, other): + return hash(self) == hash(other) + + def add_call(self, call: record_schema.RecordAppCall): + """Add the given call to the currently tracked call list.""" + # TODEP: To deprecated after migration to span-based tracing. + + with self.lock: + # NOTE: This might override existing call record which happens when + # processing calls with awaitable or generator results. + self.calls[call.call_id] = call + + def finish_record( + self, + calls_to_record: Callable[ + [ + List[record_schema.RecordAppCall], + types_schema.Metadata, + Optional[record_schema.Record], + ], + record_schema.Record, + ], + existing_record: Optional[record_schema.Record] = None, + ): + """Run the given function to build a record from the tracked calls and any + pre-specified metadata.""" + # TODEP: To deprecated after migration to span-based tracing. + + with self.lock: + record = calls_to_record( + list(self.calls.values()), self.record_metadata, existing_record + ) + self.calls = {} + + if existing_record is None: + # If existing record was given, we assume it was already + # inserted into this list. + self.records.append(record) + + return record + + +class _WithInstrumentCallbacks: + """Abstract definition of callbacks invoked by Instrument during + instrumentation or when instrumented methods are called. + + Needs to be mixed into [App][trulens_eval.app.App]. + """ + + # Called during instrumentation. + def on_method_instrumented( + self, obj: object, func: Callable, path: serial_utils.Lens + ): + """Callback to be called by instrumentation system for every function + requested to be instrumented. + + Given are the object of the class in which `func` belongs + (i.e. the "self" for that function), the `func` itsels, and the `path` + of the owner object in the app hierarchy. + + Args: + obj: The object of the class in which `func` belongs (i.e. the + "self" for that method). + + func: The function that was instrumented. Expects the unbound + version (self not yet bound). + + path: The path of the owner object in the app hierarchy. + """ + + raise NotImplementedError + + # Called during invocation. + def get_method_path(self, obj: object, func: Callable) -> serial_utils.Lens: + """Get the path of the instrumented function `func`, a member of the class + of `obj` relative to this app. + + Args: + obj: The object of the class in which `func` belongs (i.e. the + "self" for that method). + + func: The function that was instrumented. Expects the unbound + version (self not yet bound). + """ + + raise NotImplementedError + + # WithInstrumentCallbacks requirement + def get_methods_for_func( + self, func: Callable + ) -> Iterable[Tuple[int, Callable, serial_utils.Lens]]: + """EXPERIMENTAL(otel_tracing): Get the methods (rather the inner + functions) matching the given `func` and the path of each. + + Args: + func: The function to match. + """ + + raise NotImplementedError + + # Called after recording of an invocation. + def _on_new_root_span( + self, + ctx: _RecordingContext, + root_span: LiveSpanCall, + ) -> record_schema.Record: + """EXPERIMENTAL(otel_tracing): Called by instrumented methods if they + are root calls (first instrumented methods in a call stack). + + Args: + ctx: The context of the recording. + + root_span: The root span that was recorded. + """ + # EXPERIMENTAL(otel_tracing) + + raise NotImplementedError + + +class AppTracingCallbacks(TracingCallbacks[R, S]): + """Extension to TracingCallbacks that keep track of apps that are + instrumenting their constituent calls.""" + + @classmethod + def on_callable_wrapped( + cls, + wrapper: Callable[..., R], + app: _WithInstrumentCallbacks, + **kwargs: Dict[str, Any], + ): + if not python_utils.safe_hasattr(wrapper, APPS): + apps: weakref.WeakSet[_WithInstrumentCallbacks] = weakref.WeakSet() + setattr(wrapper, APPS, apps) + else: + apps = python_utils.safe_getattr(wrapper, APPS) + + apps.add(app) + + return super().on_callable_wrapped(wrapper=wrapper, **kwargs) + + def __init__( + self, + app: _WithInstrumentCallbacks, + span_type: Type[Span] = LiveSpanCall, + **kwargs: Dict[str, Any], + ): + super().__init__(span_type=span_type, **kwargs) + + self.app = app + self.apps = python_utils.safe_getattr(self.wrapper, APPS) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 67825bf87..90ab172ac 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,11 +1,11 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",3267034738241115265,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761146,2025-01-08 18:29:47.765119,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '', 'span_id': '3267034738241115265'}" -1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '3267034738241115265', 'status': 'STATUS_CODE_UNSET'}",14155491579394485383,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761206,2025-01-08 18:29:47.764385,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '3267034738241115265', 'span_id': '14155491579394485383'}" -2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '14155491579394485383', 'status': 'STATUS_CODE_UNSET'}",17277530544584287791,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761232,2025-01-08 18:29:47.763511,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '14155491579394485383', 'span_id': '17277530544584287791'}" -3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '17277530544584287791', 'status': 'STATUS_CODE_UNSET'}",2889980798172968462,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761249,2025-01-08 18:29:47.762733,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '17277530544584287791', 'span_id': '2889980798172968462'}" -4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '2889980798172968462', 'status': 'STATUS_CODE_UNSET'}",16303319555009668024,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'e5de0e71-959e-47b6-92fb-8265057787a0', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.761266,2025-01-08 18:29:47.761293,"{'trace_id': '160508733273977759427879726370844608365', 'parent_id': '2889980798172968462', 'span_id': '16303319555009668024'}" -5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",3317505627894968847,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.765946,2025-01-08 18:29:47.770238,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '', 'span_id': '3317505627894968847'}" -6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '3317505627894968847', 'status': 'STATUS_CODE_UNSET'}",2940165257583146187,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.765981,2025-01-08 18:29:47.769457,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '3317505627894968847', 'span_id': '2940165257583146187'}" -7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '2940165257583146187', 'status': 'STATUS_CODE_UNSET'}",14638858047259569279,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.766008,2025-01-08 18:29:47.768684,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '2940165257583146187', 'span_id': '14638858047259569279'}" -8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '14638858047259569279', 'status': 'STATUS_CODE_UNSET'}",15572150746830590999,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.766025,2025-01-08 18:29:47.767898,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '14638858047259569279', 'span_id': '15572150746830590999'}" -9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '15572150746830590999', 'status': 'STATUS_CODE_ERROR'}",1144650033552264284,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '2d681a41-c558-426d-a1f9-d2cc6e4ed0d9', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-08 18:29:47.766041,2025-01-08 18:29:47.766703,"{'trace_id': '275183805772535379666400700563094519430', 'parent_id': '15572150746830590999', 'span_id': '1144650033552264284'}" +0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",7548148256254219236,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '750e88b5-d761-4792-8231-cf03222500ee', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '750e88b5-d761-4792-8231-cf03222500ee'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905134,2025-01-10 21:19:52.905406,"{'trace_id': '310519708816654904362902898292285143895', 'parent_id': '', 'span_id': '7548148256254219236'}" +1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '7548148256254219236', 'status': 'STATUS_CODE_UNSET'}",11373312075260521003,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '750e88b5-d761-4792-8231-cf03222500ee', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905198,2025-01-10 21:19:52.905388,"{'trace_id': '310519708816654904362902898292285143895', 'parent_id': '7548148256254219236', 'span_id': '11373312075260521003'}" +2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '11373312075260521003', 'status': 'STATUS_CODE_UNSET'}",16895419932990415012,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '750e88b5-d761-4792-8231-cf03222500ee', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905221,2025-01-10 21:19:52.905337,"{'trace_id': '310519708816654904362902898292285143895', 'parent_id': '11373312075260521003', 'span_id': '16895419932990415012'}" +3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '16895419932990415012', 'status': 'STATUS_CODE_UNSET'}",1860793978023597373,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '750e88b5-d761-4792-8231-cf03222500ee', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905244,2025-01-10 21:19:52.905322,"{'trace_id': '310519708816654904362902898292285143895', 'parent_id': '16895419932990415012', 'span_id': '1860793978023597373'}" +4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '1860793978023597373', 'status': 'STATUS_CODE_UNSET'}",6441428657975209823,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '750e88b5-d761-4792-8231-cf03222500ee', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905264,2025-01-10 21:19:52.905299,"{'trace_id': '310519708816654904362902898292285143895', 'parent_id': '1860793978023597373', 'span_id': '6441428657975209823'}" +5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",8386364717609220237,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '343a1a58-a06d-4294-b1b0-1403985aaf0e', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '343a1a58-a06d-4294-b1b0-1403985aaf0e'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905463,2025-01-10 21:19:52.906250,"{'trace_id': '223539604261655058588634017117567127595', 'parent_id': '', 'span_id': '8386364717609220237'}" +6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '8386364717609220237', 'status': 'STATUS_CODE_UNSET'}",14546609519876426956,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '343a1a58-a06d-4294-b1b0-1403985aaf0e', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905495,2025-01-10 21:19:52.906233,"{'trace_id': '223539604261655058588634017117567127595', 'parent_id': '8386364717609220237', 'span_id': '14546609519876426956'}" +7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '14546609519876426956', 'status': 'STATUS_CODE_UNSET'}",5906025350601651481,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '343a1a58-a06d-4294-b1b0-1403985aaf0e', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905525,2025-01-10 21:19:52.906191,"{'trace_id': '223539604261655058588634017117567127595', 'parent_id': '14546609519876426956', 'span_id': '5906025350601651481'}" +8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '5906025350601651481', 'status': 'STATUS_CODE_UNSET'}",12754995952332404913,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '343a1a58-a06d-4294-b1b0-1403985aaf0e', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905607,2025-01-10 21:19:52.906177,"{'trace_id': '223539604261655058588634017117567127595', 'parent_id': '5906025350601651481', 'span_id': '12754995952332404913'}" +9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '12754995952332404913', 'status': 'STATUS_CODE_ERROR'}",8979115968086545704,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '343a1a58-a06d-4294-b1b0-1403985aaf0e', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-10 21:19:52.905626,2025-01-10 21:19:52.906153,"{'trace_id': '223539604261655058588634017117567127595', 'parent_id': '12754995952332404913', 'span_id': '8979115968086545704'}" From 881c68d65f5b25ff24e075bf596751f54bd519cb Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 21:28:06 -0800 Subject: [PATCH 56/68] update --- Makefile | 3 +-- src/core/trulens/core/session.py | 2 -- src/core/trulens/experimental/otel_tracing/core/instrument.py | 4 ---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Makefile b/Makefile index e14f9d693..d2fe5177f 100644 --- a/Makefile +++ b/Makefile @@ -38,8 +38,7 @@ env-tests: pytest-azurepipelines \ pytest-cov \ pytest-subtests \ - ruff \ - snowflake + ruff env-tests-required: poetry install --only required \ diff --git a/src/core/trulens/core/session.py b/src/core/trulens/core/session.py index f2e459bc6..9fb3c436d 100644 --- a/src/core/trulens/core/session.py +++ b/src/core/trulens/core/session.py @@ -270,8 +270,6 @@ def __init__( **self_args, ) - self.connector - # for WithExperimentalSettings mixin if experimental_feature_flags is not None: self.experimental_set_features(experimental_feature_flags) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 12855c1f8..5b16892c1 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -104,10 +104,6 @@ class OTELRecordingContext(core_app.App): The ID of the input that the recording context is currently processing. """ - @classmethod - def for_run(cls, app: core_app.App, run_name: str, input_id: str): - cls.model_construct(None, app) - def _set_run_information(self, *, run_name: str, input_id: str): self.run_name = run_name self.input_id = input_id From 5fff6fcb6edd5dd0fb48085e1911b08ee6f052cc Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 21:32:21 -0800 Subject: [PATCH 57/68] extract --- Makefile | 1 + src/core/pyproject.toml | 1 + src/core/trulens/core/utils/requirements.txt | 1 + .../experimental/otel_tracing/_feature.py | 2 +- .../otel_tracing/core/exporter.py | 172 ++++++++++++++---- tests/unit/test_exporter.py | 83 +++++++++ 6 files changed, 227 insertions(+), 33 deletions(-) create mode 100644 tests/unit/test_exporter.py diff --git a/Makefile b/Makefile index 84170bb67..5e798ec00 100644 --- a/Makefile +++ b/Makefile @@ -32,6 +32,7 @@ env-tests: nbconvert \ nbformat \ opentelemetry-sdk \ + opentelemetry-proto \ pre-commit \ pytest \ pytest-azurepipelines \ diff --git a/src/core/pyproject.toml b/src/core/pyproject.toml index d3e4cb38a..040239558 100644 --- a/src/core/pyproject.toml +++ b/src/core/pyproject.toml @@ -49,6 +49,7 @@ importlib-resources = "^6.0" trulens-otel-semconv = { version = "^1.0.0", optional = true } opentelemetry-api = { version = "^1.0.0", optional = true } opentelemetry-sdk = { version = "^1.0.0", optional = true } +opentelemetry-proto = { version = "^1.0.0", optional = true } [tool.poetry.group.tqdm] optional = true diff --git a/src/core/trulens/core/utils/requirements.txt b/src/core/trulens/core/utils/requirements.txt index 5fd0388bb..9996219ce 100644 --- a/src/core/trulens/core/utils/requirements.txt +++ b/src/core/trulens/core/utils/requirements.txt @@ -9,3 +9,4 @@ trulens-dashboard >= 1.0.0 # talk about these packages as required. opentelemetry-api >= 1.0.0 opentelemetry-sdk >= 1.0.0 +opentelemetry-proto >= 1.0.0 diff --git a/src/core/trulens/experimental/otel_tracing/_feature.py b/src/core/trulens/experimental/otel_tracing/_feature.py index 20d6c04f3..bfe9e113e 100644 --- a/src/core/trulens/experimental/otel_tracing/_feature.py +++ b/src/core/trulens/experimental/otel_tracing/_feature.py @@ -8,7 +8,7 @@ """Feature controlling the use of this module.""" REQUIREMENT = import_utils.format_import_errors( - ["opentelemetry-api", "opentelemetry-sdk"], + ["opentelemetry-api", "opentelemetry-sdk", "opentelemetry-proto"], purpose="otel_tracing experimental feature", ) """Optional modules required for the otel_tracing experimental feature.""" diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 9c44b9eeb..98ba4360e 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -1,66 +1,174 @@ from datetime import datetime import logging -from typing import Optional, Sequence +from typing import Any, Optional, Sequence +from opentelemetry.proto.common.v1.common_pb2 import AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ArrayValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValueList +from opentelemetry.proto.trace.v1.trace_pb2 import Span as SpanProto from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import SpanExporter from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import StatusCode from trulens.core.database import connector as core_connector from trulens.core.schema import event as event_schema +from trulens.otel.semconv.trace import SpanAttributes logger = logging.getLogger(__name__) +def convert_to_any_value(value: Any) -> AnyValue: + """ + Converts a given value to an AnyValue object. + This function takes a value of various types (str, bool, int, float, bytes, list, dict) + and converts it into an AnyValue object. If the value is a list or a dictionary, it + recursively converts the elements or key-value pairs. Tuples are converted into lists. + Args: + value (Any): The value to be converted. It can be of type str, bool, int, float, + bytes, list, tuple, or dict. + Returns: + AnyValue: The converted AnyValue object. + Raises: + ValueError: If the value type is unsupported. + """ + if isinstance(value, tuple): + value = list(value) + any_value = AnyValue() + + if isinstance(value, str): + any_value.string_value = value + elif isinstance(value, bool): + any_value.bool_value = value + elif isinstance(value, int): + any_value.int_value = value + elif isinstance(value, float): + any_value.double_value = value + elif isinstance(value, bytes): + any_value.bytes_value = value + elif isinstance(value, list): + array_value = ArrayValue() + for item in value: + array_value.values.append(convert_to_any_value(item)) + any_value.array_value.CopyFrom(array_value) + elif isinstance(value, dict): + kv_list = KeyValueList() + for k, v in value.items(): + kv = KeyValue(key=k, value=convert_to_any_value(v)) + kv_list.values.append(kv) + any_value.kvlist_value.CopyFrom(kv_list) + else: + raise ValueError(f"Unsupported value type: {type(value)}") + + return any_value + + +def convert_readable_span_to_proto(span: ReadableSpan) -> SpanProto: + """ + Converts a ReadableSpan object to a the protobuf object for a Span. + Args: + span (ReadableSpan): The span to be converted. + Returns: + SpanProto: The converted span in SpanProto format. + """ + span_proto = SpanProto( + trace_id=span.context.trace_id.to_bytes(16, byteorder="big") + if span.context + else b"", + span_id=span.context.span_id.to_bytes(8, byteorder="big") + if span.context + else b"", + parent_span_id=span.parent.span_id.to_bytes(8, byteorder="big") + if span.parent + else b"", + name=span.name, + kind=SpanProto.SpanKind.SPAN_KIND_INTERNAL, + start_time_unix_nano=span.start_time if span.start_time else 0, + end_time_unix_nano=span.end_time if span.end_time else 0, + attributes=[ + KeyValue(key=k, value=convert_to_any_value(v)) + for k, v in span.attributes.items() + ] + if span.attributes + else None, + ) + return span_proto + + def to_timestamp(timestamp: Optional[int]) -> datetime: + """ + Utility function for converting OTEL timestamps to datetime objects. + """ if timestamp: return datetime.fromtimestamp(timestamp * 1e-9) return datetime.now() +def check_if_trulens_span(span: ReadableSpan) -> bool: + """ + Check if a given span is a TruLens span. + This function checks the attributes of the provided span to determine if it + contains a TruLens-specific attribute, identified by the presence of + `SpanAttributes.RECORD_ID`. + Args: + span (ReadableSpan): The span to be checked. + Returns: + bool: True if the span contains the TruLens-specific attribute, False otherwise. + """ + + if not span.attributes: + return False + + return bool(span.attributes.get(SpanAttributes.RECORD_ID)) + + +def construct_event(span: ReadableSpan) -> event_schema.Event: + context = span.get_span_context() + parent = span.parent + + if context is None: + raise ValueError("Span context is None") + + return event_schema.Event( + event_id=str(context.span_id), + record={ + "name": span.name, + "kind": SpanProto.SpanKind.SPAN_KIND_INTERNAL, + "parent_span_id": str(parent.span_id if parent else ""), + "status": "STATUS_CODE_ERROR" + if span.status.status_code == StatusCode.ERROR + else "STATUS_CODE_UNSET", + }, + record_attributes=span.attributes, + record_type=event_schema.EventRecordType.SPAN, + resource_attributes=span.resource.attributes, + start_timestamp=to_timestamp(span.start_time), + timestamp=to_timestamp(span.end_time), + trace={ + "span_id": str(context.span_id), + "trace_id": str(context.trace_id), + "parent_id": str(parent.span_id if parent else ""), + }, + ) + + class TruLensDBSpanExporter(SpanExporter): """ - Implementation of `SpanExporter` that flushes the spans to the database in the TruLens session. + Implementation of `SpanExporter` that flushes the spans in the TruLens session to the connector. """ connector: core_connector.DBConnector + """ + The database connector used to export the spans. + """ def __init__(self, connector: core_connector.DBConnector): self.connector = connector - def _construct_event(self, span: ReadableSpan) -> event_schema.Event: - context = span.get_span_context() - parent = span.parent - - if context is None: - raise ValueError("Span context is None") - - return event_schema.Event( - event_id=str(context.span_id), - record={ - "name": span.name, - "kind": "SPAN_KIND_TRULENS", - "parent_span_id": str(parent.span_id if parent else ""), - "status": "STATUS_CODE_ERROR" - if span.status.status_code == StatusCode.ERROR - else "STATUS_CODE_UNSET", - }, - record_attributes=span.attributes, - record_type=event_schema.EventRecordType.SPAN, - resource_attributes=span.resource.attributes, - start_timestamp=to_timestamp(span.start_time), - timestamp=to_timestamp(span.end_time), - trace={ - "span_id": str(context.span_id), - "trace_id": str(context.trace_id), - "parent_id": str(parent.span_id if parent else ""), - }, - ) - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: try: - events = list(map(self._construct_event, spans)) + events = list(map(construct_event, spans)) self.connector.add_events(events) except Exception as e: diff --git a/tests/unit/test_exporter.py b/tests/unit/test_exporter.py new file mode 100644 index 000000000..d1ff8ec8b --- /dev/null +++ b/tests/unit/test_exporter.py @@ -0,0 +1,83 @@ +from opentelemetry.proto.common.v1.common_pb2 import AnyValue +from opentelemetry.proto.common.v1.common_pb2 import ArrayValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValue +from opentelemetry.proto.common.v1.common_pb2 import KeyValueList +import pytest +from trulens.experimental.otel_tracing.core.exporter import convert_to_any_value + + +def test_convert_to_any_value_string(): + value = "test_string" + any_value = convert_to_any_value(value) + assert any_value.string_value == value + + +def test_convert_to_any_value_bool(): + value = True + any_value = convert_to_any_value(value) + assert any_value.bool_value == value + + +def test_convert_to_any_value_int(): + value = 123 + any_value = convert_to_any_value(value) + assert any_value.int_value == value + + +def test_convert_to_any_value_float(): + value = 123.45 + any_value = convert_to_any_value(value) + assert any_value.double_value == pytest.approx(value) + + +def test_convert_to_any_value_bytes(): + value = b"test_bytes" + any_value = convert_to_any_value(value) + assert any_value.bytes_value == value + + +def test_convert_to_any_value_list(): + value = ["test_string", 123, 123.45, True] + any_value = convert_to_any_value(value) + assert any_value.array_value == ArrayValue( + values=[ + AnyValue(string_value="test_string"), + AnyValue(int_value=123), + AnyValue(double_value=123.45), + AnyValue(bool_value=True), + ] + ) + + +def test_convert_to_any_value_dict(): + value = {"key1": "value1", "key2": 123, "key3": 123.45, "key4": True} + any_value = convert_to_any_value(value) + assert any_value.kvlist_value == KeyValueList( + values=[ + KeyValue(key="key1", value=AnyValue(string_value="value1")), + KeyValue(key="key2", value=AnyValue(int_value=123)), + KeyValue(key="key3", value=AnyValue(double_value=123.45)), + KeyValue(key="key4", value=AnyValue(bool_value=True)), + ] + ) + + +def test_convert_to_any_value_unsupported_type(): + value = set([1, 2, 3]) + with pytest.raises( + ValueError, match="Unsupported value type: " + ): + convert_to_any_value(value) + + +def test_convert_to_any_value_tuple(): + value = ("test_string", 123, 123.45, True) + any_value = convert_to_any_value(value) + assert any_value.array_value == ArrayValue( + values=[ + AnyValue(string_value="test_string"), + AnyValue(int_value=123), + AnyValue(double_value=123.45), + AnyValue(bool_value=True), + ] + ) From 8bae5fba769a52c52742adcd4391136221751a1f Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 10 Jan 2025 21:39:44 -0800 Subject: [PATCH 58/68] nits --- .../trulens/experimental/otel_tracing/core/exporter.py | 10 ++++++++++ .../experimental/otel_tracing/core/instrument.py | 6 ------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 7fcec3ab6..c38120186 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -109,6 +109,16 @@ def to_timestamp(timestamp: Optional[int]) -> datetime: def check_if_trulens_span(span: ReadableSpan) -> bool: + """ + Check if a given span is a TruLens span. + This function checks the attributes of the provided span to determine if it + contains a TruLens-specific attribute, identified by the presence of + `SpanAttributes.RECORD_ID`. + Args: + span (ReadableSpan): The span to be checked. + Returns: + bool: True if the span contains the TruLens-specific attribute, False otherwise. + """ if not span.attributes: return False diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 5b16892c1..bf9a2af0d 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -104,12 +104,6 @@ class OTELRecordingContext(core_app.App): The ID of the input that the recording context is currently processing. """ - def _set_run_information(self, *, run_name: str, input_id: str): - self.run_name = run_name - self.input_id = input_id - - return self - # For use as a context manager. def __enter__(self): logger.debug("Entering the OTEL app context.") From c11b1c9b694d1b6ebf4cf2ff30c3a378ba2164ef Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Sat, 11 Jan 2025 16:46:47 -0800 Subject: [PATCH 59/68] update golden --- ..._instrument__test_instrument_decorator.csv | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index 7ebb1c133..079c0fcae 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,11 +1,11 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'root', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",13399253994103099667,"{'name': 'root', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'bb886c30-9624-467b-9645-7885f7917ca8', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': 'bb886c30-9624-467b-9645-7885f7917ca8'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.917597,2025-01-09 13:35:17.921345,"{'trace_id': '118553564708937541911122609198692448037', 'parent_id': '', 'span_id': '13399253994103099667'}" -1,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '13399253994103099667', 'status': 'STATUS_CODE_UNSET'}",18321528166075233933,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'bb886c30-9624-467b-9645-7885f7917ca8', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.917646,2025-01-09 13:35:17.920579,"{'trace_id': '118553564708937541911122609198692448037', 'parent_id': '13399253994103099667', 'span_id': '18321528166075233933'}" -2,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '18321528166075233933', 'status': 'STATUS_CODE_UNSET'}",12462238791380145685,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'bb886c30-9624-467b-9645-7885f7917ca8', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.917671,2025-01-09 13:35:17.919659,"{'trace_id': '118553564708937541911122609198692448037', 'parent_id': '18321528166075233933', 'span_id': '12462238791380145685'}" -3,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '12462238791380145685', 'status': 'STATUS_CODE_UNSET'}",5024909565452546849,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'bb886c30-9624-467b-9645-7885f7917ca8', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.917687,2025-01-09 13:35:17.918905,"{'trace_id': '118553564708937541911122609198692448037', 'parent_id': '12462238791380145685', 'span_id': '5024909565452546849'}" -4,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5024909565452546849', 'status': 'STATUS_CODE_UNSET'}",6019952548000960337,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'bb886c30-9624-467b-9645-7885f7917ca8', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.917704,2025-01-09 13:35:17.917732,"{'trace_id': '118553564708937541911122609198692448037', 'parent_id': '5024909565452546849', 'span_id': '6019952548000960337'}" -5,"{'name': 'root', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",5883416414011309074,"{'name': 'root', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '3096d1a6-d0e1-40a9-8a71-bd55be38cd84', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': '3096d1a6-d0e1-40a9-8a71-bd55be38cd84'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.922103,2025-01-09 13:35:17.925999,"{'trace_id': '304376211394925378896410901445450714789', 'parent_id': '', 'span_id': '5883416414011309074'}" -6,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '5883416414011309074', 'status': 'STATUS_CODE_UNSET'}",9333993101900153465,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '3096d1a6-d0e1-40a9-8a71-bd55be38cd84', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.922135,2025-01-09 13:35:17.925295,"{'trace_id': '304376211394925378896410901445450714789', 'parent_id': '5883416414011309074', 'span_id': '9333993101900153465'}" -7,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '9333993101900153465', 'status': 'STATUS_CODE_UNSET'}",3023113116731715287,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '3096d1a6-d0e1-40a9-8a71-bd55be38cd84', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.922165,2025-01-09 13:35:17.924525,"{'trace_id': '304376211394925378896410901445450714789', 'parent_id': '9333993101900153465', 'span_id': '3023113116731715287'}" -8,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '3023113116731715287', 'status': 'STATUS_CODE_UNSET'}",2493409196027493177,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '3096d1a6-d0e1-40a9-8a71-bd55be38cd84', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.922183,2025-01-09 13:35:17.923704,"{'trace_id': '304376211394925378896410901445450714789', 'parent_id': '3023113116731715287', 'span_id': '2493409196027493177'}" -9,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'parent_span_id': '2493409196027493177', 'status': 'STATUS_CODE_ERROR'}",3128651505026044765,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '3096d1a6-d0e1-40a9-8a71-bd55be38cd84', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-09 13:35:17.922200,2025-01-09 13:35:17.922718,"{'trace_id': '304376211394925378896410901445450714789', 'parent_id': '2493409196027493177', 'span_id': '3128651505026044765'}" +0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",10171214145140022378,"{'name': 'root', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'b6708522-36c0-4229-bb99-8cb6ce034ae6', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': 'b6708522-36c0-4229-bb99-8cb6ce034ae6'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.479061,2025-01-11 16:45:54.482640,"{'trace_id': '183708988068224948480846288953380522832', 'parent_id': '', 'span_id': '10171214145140022378'}" +1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '10171214145140022378', 'status': 'STATUS_CODE_UNSET'}",12693686145818269980,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'b6708522-36c0-4229-bb99-8cb6ce034ae6', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.479105,2025-01-11 16:45:54.481937,"{'trace_id': '183708988068224948480846288953380522832', 'parent_id': '10171214145140022378', 'span_id': '12693686145818269980'}" +2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '12693686145818269980', 'status': 'STATUS_CODE_UNSET'}",14126750786796684516,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'b6708522-36c0-4229-bb99-8cb6ce034ae6', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.479125,2025-01-11 16:45:54.481128,"{'trace_id': '183708988068224948480846288953380522832', 'parent_id': '12693686145818269980', 'span_id': '14126750786796684516'}" +3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '14126750786796684516', 'status': 'STATUS_CODE_UNSET'}",16913326535362873820,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'b6708522-36c0-4229-bb99-8cb6ce034ae6', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.479143,2025-01-11 16:45:54.480371,"{'trace_id': '183708988068224948480846288953380522832', 'parent_id': '14126750786796684516', 'span_id': '16913326535362873820'}" +4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '16913326535362873820', 'status': 'STATUS_CODE_UNSET'}",13095442553143098739,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': 'b6708522-36c0-4229-bb99-8cb6ce034ae6', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.479158,2025-01-11 16:45:54.479189,"{'trace_id': '183708988068224948480846288953380522832', 'parent_id': '16913326535362873820', 'span_id': '13095442553143098739'}" +5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",10313173348376832091,"{'name': 'root', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'record_root', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '853f86c3-e3ff-4bc7-bf84-3ea2a4652235', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_root.record_id': '853f86c3-e3ff-4bc7-bf84-3ea2a4652235'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.483449,2025-01-11 16:45:54.488175,"{'trace_id': '291242587496290935239848857119532985705', 'parent_id': '', 'span_id': '10313173348376832091'}" +6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '10313173348376832091', 'status': 'STATUS_CODE_UNSET'}",2117052901681467046,"{'name': 'respond_to_query', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'main', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '853f86c3-e3ff-4bc7-bf84-3ea2a4652235', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.483479,2025-01-11 16:45:54.487525,"{'trace_id': '291242587496290935239848857119532985705', 'parent_id': '10313173348376832091', 'span_id': '2117052901681467046'}" +7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '2117052901681467046', 'status': 'STATUS_CODE_UNSET'}",8161924188318385316,"{'name': 'nested', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '853f86c3-e3ff-4bc7-bf84-3ea2a4652235', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.483509,2025-01-11 16:45:54.486739,"{'trace_id': '291242587496290935239848857119532985705', 'parent_id': '2117052901681467046', 'span_id': '8161924188318385316'}" +8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '8161924188318385316', 'status': 'STATUS_CODE_UNSET'}",14428269894233492734,"{'name': 'nested2', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '853f86c3-e3ff-4bc7-bf84-3ea2a4652235', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.483526,2025-01-11 16:45:54.485951,"{'trace_id': '291242587496290935239848857119532985705', 'parent_id': '8161924188318385316', 'span_id': '14428269894233492734'}" +9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '14428269894233492734', 'status': 'STATUS_CODE_ERROR'}",10885632621132130757,"{'name': 'nested3', 'kind': 'SPAN_KIND_TRULENS', 'trulens.span_type': 'unknown', 'trulens.app_id': 'app_hash_baf7b2cb6402e84fa3b0b3a028d4bf65', 'trulens.record_id': '853f86c3-e3ff-4bc7-bf84-3ea2a4652235', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:45:54.483544,2025-01-11 16:45:54.484929,"{'trace_id': '291242587496290935239848857119532985705', 'parent_id': '14428269894233492734', 'span_id': '10885632621132130757'}" From 61d87a8f5cbde0e599da3d3c580f1aba0d066a5b Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 13 Jan 2025 18:56:57 -0800 Subject: [PATCH 60/68] pr feedback --- .../otel_tracing/core/exporter.py | 14 +- tests/unit/test_exporter.py | 169 ++++++++++-------- 2 files changed, 97 insertions(+), 86 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter.py index 98ba4360e..02d1b81ae 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter.py @@ -65,22 +65,16 @@ def convert_to_any_value(value: Any) -> AnyValue: def convert_readable_span_to_proto(span: ReadableSpan) -> SpanProto: """ - Converts a ReadableSpan object to a the protobuf object for a Span. + Converts a ReadableSpan object to a protobuf object for a Span. Args: span (ReadableSpan): The span to be converted. Returns: SpanProto: The converted span in SpanProto format. """ span_proto = SpanProto( - trace_id=span.context.trace_id.to_bytes(16, byteorder="big") - if span.context - else b"", - span_id=span.context.span_id.to_bytes(8, byteorder="big") - if span.context - else b"", - parent_span_id=span.parent.span_id.to_bytes(8, byteorder="big") - if span.parent - else b"", + trace_id=span.context.trace_id.to_bytes(16) if span.context else b"", + span_id=span.context.span_id.to_bytes(8) if span.context else b"", + parent_span_id=span.parent.span_id.to_bytes(8) if span.parent else b"", name=span.name, kind=SpanProto.SpanKind.SPAN_KIND_INTERNAL, start_time_unix_nano=span.start_time if span.start_time else 0, diff --git a/tests/unit/test_exporter.py b/tests/unit/test_exporter.py index d1ff8ec8b..0f011f751 100644 --- a/tests/unit/test_exporter.py +++ b/tests/unit/test_exporter.py @@ -1,83 +1,100 @@ +import unittest + from opentelemetry.proto.common.v1.common_pb2 import AnyValue from opentelemetry.proto.common.v1.common_pb2 import ArrayValue from opentelemetry.proto.common.v1.common_pb2 import KeyValue from opentelemetry.proto.common.v1.common_pb2 import KeyValueList -import pytest from trulens.experimental.otel_tracing.core.exporter import convert_to_any_value -def test_convert_to_any_value_string(): - value = "test_string" - any_value = convert_to_any_value(value) - assert any_value.string_value == value - - -def test_convert_to_any_value_bool(): - value = True - any_value = convert_to_any_value(value) - assert any_value.bool_value == value - - -def test_convert_to_any_value_int(): - value = 123 - any_value = convert_to_any_value(value) - assert any_value.int_value == value - - -def test_convert_to_any_value_float(): - value = 123.45 - any_value = convert_to_any_value(value) - assert any_value.double_value == pytest.approx(value) - - -def test_convert_to_any_value_bytes(): - value = b"test_bytes" - any_value = convert_to_any_value(value) - assert any_value.bytes_value == value - - -def test_convert_to_any_value_list(): - value = ["test_string", 123, 123.45, True] - any_value = convert_to_any_value(value) - assert any_value.array_value == ArrayValue( - values=[ - AnyValue(string_value="test_string"), - AnyValue(int_value=123), - AnyValue(double_value=123.45), - AnyValue(bool_value=True), - ] - ) - - -def test_convert_to_any_value_dict(): - value = {"key1": "value1", "key2": 123, "key3": 123.45, "key4": True} - any_value = convert_to_any_value(value) - assert any_value.kvlist_value == KeyValueList( - values=[ - KeyValue(key="key1", value=AnyValue(string_value="value1")), - KeyValue(key="key2", value=AnyValue(int_value=123)), - KeyValue(key="key3", value=AnyValue(double_value=123.45)), - KeyValue(key="key4", value=AnyValue(bool_value=True)), - ] - ) - - -def test_convert_to_any_value_unsupported_type(): - value = set([1, 2, 3]) - with pytest.raises( - ValueError, match="Unsupported value type: " - ): - convert_to_any_value(value) - - -def test_convert_to_any_value_tuple(): - value = ("test_string", 123, 123.45, True) - any_value = convert_to_any_value(value) - assert any_value.array_value == ArrayValue( - values=[ - AnyValue(string_value="test_string"), - AnyValue(int_value=123), - AnyValue(double_value=123.45), - AnyValue(bool_value=True), - ] - ) +class TestExporterUtils(unittest.TestCase): + def test_convert_to_any_value(self): + with self.subTest("String value"): + value = "test_string" + any_value = convert_to_any_value(value) + self.assertEqual(any_value.string_value, value) + + with self.subTest("Boolean value"): + value = True + any_value = convert_to_any_value(value) + self.assertEqual(any_value.bool_value, value) + + with self.subTest("Integer value"): + value = 123 + any_value = convert_to_any_value(value) + self.assertEqual(any_value.int_value, value) + + with self.subTest("Float value"): + value = 123.45 + any_value = convert_to_any_value(value) + self.assertAlmostEqual(any_value.double_value, value) + + with self.subTest("Bytes value"): + value = b"test_bytes" + any_value = convert_to_any_value(value) + self.assertEqual(any_value.bytes_value, value) + + with self.subTest("List value"): + value = ["test_string", 123, 123.45, True] + any_value = convert_to_any_value(value) + self.assertEqual( + any_value.array_value, + ArrayValue( + values=[ + AnyValue(string_value="test_string"), + AnyValue(int_value=123), + AnyValue(double_value=123.45), + AnyValue(bool_value=True), + ] + ), + ) + + with self.subTest("Dictionary value"): + value = { + "key1": "value1", + "key2": 123, + "key3": 123.45, + "key4": True, + } + any_value = convert_to_any_value(value) + self.assertEqual( + any_value.kvlist_value, + KeyValueList( + values=[ + KeyValue( + key="key1", value=AnyValue(string_value="value1") + ), + KeyValue(key="key2", value=AnyValue(int_value=123)), + KeyValue( + key="key3", value=AnyValue(double_value=123.45) + ), + KeyValue(key="key4", value=AnyValue(bool_value=True)), + ] + ), + ) + + with self.subTest("Unsupported type"): + value = set([1, 2, 3]) + with self.assertRaises( + ValueError, msg="Unsupported value type: " + ): + convert_to_any_value(value) + + with self.subTest("Tuple value"): + value = ("test_string", 123, 123.45, True) + any_value = convert_to_any_value(value) + self.assertEqual( + any_value.array_value, + ArrayValue( + values=[ + AnyValue(string_value="test_string"), + AnyValue(int_value=123), + AnyValue(double_value=123.45), + AnyValue(bool_value=True), + ] + ), + ) + + +if __name__ == "__main__": + unittest.main() From a9f053727f180374e934b5d3bde0d795d8638d02 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Mon, 13 Jan 2025 19:45:51 -0800 Subject: [PATCH 61/68] pr feedback --- src/core/trulens/core/session.py | 13 +------------ .../experimental/otel_tracing/core/session.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/core/trulens/core/session.py b/src/core/trulens/core/session.py index 9fb3c436d..3d4562661 100644 --- a/src/core/trulens/core/session.py +++ b/src/core/trulens/core/session.py @@ -180,17 +180,7 @@ def experimental_otel_exporter( return self._experimental_otel_exporter - @experimental_otel_exporter.setter - def experimental_otel_exporter(self, value: Optional[SpanExporter]): - otel_tracing_feature._FeatureSetup.assert_optionals_installed() - - from trulens.experimental.otel_tracing.core.session import _TruSession - - _TruSession._setup_otel_exporter(self, value) - - def experimental_force_flush( - self, timeout_millis: Optional[int] = None - ) -> bool: + def experimental_force_flush(self, timeout_millis: int = 300000) -> bool: """ Force flush the OpenTelemetry exporters. @@ -201,7 +191,6 @@ def experimental_force_flush( Returns: False if the timeout is exceeded, feature is not enabled, or the provider doesn't exist, True otherwise. """ - timeout_millis = timeout_millis or 300000 if ( not self.experimental_feature( diff --git a/src/core/trulens/experimental/otel_tracing/core/session.py b/src/core/trulens/experimental/otel_tracing/core/session.py index b2c4568a8..7de74818a 100644 --- a/src/core/trulens/experimental/otel_tracing/core/session.py +++ b/src/core/trulens/experimental/otel_tracing/core/session.py @@ -39,10 +39,18 @@ def _setup_otel_exporter( resource = Resource.create({"service.name": TRULENS_SERVICE_NAME}) provider = TracerProvider(resource=resource) trace.set_tracer_provider(provider) - self._experimental_tracer_provider = provider + + # The opentelemetry.sdk.trace.TracerProvider class is what OTEL uses under the hood, + # even though OTEL only chooses to expose a subset of the class attributes in + # the opentelemetry.trace module. + # See : https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-python+get_tracer_provider&type=code + # for examples of how the TracerProvider class is used in the OTEL codebase. + # Hence, here we force the typing to ignore the error. + tracer_provider: TracerProvider = trace.get_tracer_provider() # type: ignore + self._experimental_tracer_provider = tracer_provider # Export to the connector provided. - provider.add_span_processor( + tracer_provider.add_span_processor( otel_export_sdk.BatchSpanProcessor( TruLensOTELSpanExporter(connector) ) @@ -57,4 +65,4 @@ def _setup_otel_exporter( # asynchronous processing of the spans that results in the database not # being updated in time for the tests. db_processor = otel_export_sdk.BatchSpanProcessor(exporter) - provider.add_span_processor(db_processor) + tracer_provider.add_span_processor(db_processor) From f46af3ccd0b67cec4a66d49615eaaeeb5120a96b Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 16 Jan 2025 12:48:20 -0800 Subject: [PATCH 62/68] save --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d2fe5177f..5e798ec00 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ env-tests: pytest-azurepipelines \ pytest-cov \ pytest-subtests \ - ruff + ruff \ env-tests-required: poetry install --only required \ From 2903b521c94212714af70447c5402ffedab19876 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 16 Jan 2025 19:59:17 -0800 Subject: [PATCH 63/68] draft --- examples/experimental/otel_exporter.ipynb | 30 ++------ src/core/trulens/core/app.py | 62 ++++------------ .../otel_tracing/core/instrument.py | 71 ++++++++++++------- .../experimental/otel_tracing/core/session.py | 36 ++++------ .../experimental/otel_tracing/core/span.py | 15 ++-- 5 files changed, 85 insertions(+), 129 deletions(-) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 475728d47..1c91e9b4c 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -58,7 +58,7 @@ " @instrument(\n", " attributes=lambda ret, exception, *args, **kwargs: {\n", " \"nested2_ret\": ret,\n", - " \"nested2_args[0]\": args[0],\n", + " \"nested2_args[1]\": args[1],\n", " }\n", " )\n", " def nested2(self, query: str) -> str:\n", @@ -94,32 +94,12 @@ "import os\n", "\n", "import dotenv\n", - "from trulens.connectors.snowflake import SnowflakeConnector\n", + "from trulens.core.experimental import Feature\n", "from trulens.core.session import TruSession\n", - "from trulens.experimental.otel_tracing.core.exporter import (\n", - " TruLensSnowflakeSpanExporter,\n", - ")\n", "\n", "dotenv.load_dotenv()\n", - "\n", - "connection_params = {\n", - " \"account\": os.environ[\"SNOWFLAKE_ACCOUNT\"],\n", - " \"user\": os.environ[\"SNOWFLAKE_USER\"],\n", - " \"password\": os.environ[\"SNOWFLAKE_USER_PASSWORD\"],\n", - " \"role\": os.environ.get(\"SNOWFLAKE_ROLE\", \"ENGINEER\"),\n", - " \"database\": os.environ.get(\"SNOWFLAKE_DATABASE\"),\n", - " \"schema\": os.environ.get(\"SNOWFLAKE_SCHEMA\"),\n", - " \"warehouse\": os.environ.get(\"SNOWFLAKE_WAREHOUSE\"),\n", - "}\n", - "\n", - "connector = SnowflakeConnector(\n", - " **connection_params,\n", - ")\n", - "\n", - "exporter = TruLensSnowflakeSpanExporter(connector=connector)\n", - "\n", - "# Note that the connector for the session can be different from the connector for the exporter.\n", - "session = TruSession(_experimental_otel_exporter=exporter)" + "os.environ[\"TRULENS_OTEL_TRACING\"] = \"1\"\n", + "session = TruSession(experimental_feature_flags={Feature.OTEL_TRACING: True})" ] }, { @@ -138,7 +118,7 @@ " session=session,\n", ")\n", "\n", - "with custom_app(run_name=\"test run\", input_id=\"123\") as recording:\n", + "with custom_app() as recording:\n", " test_app.respond_to_query(\"test\")\n", "\n", "with custom_app(run_name=\"test run\", input_id=\"456\") as recording:\n", diff --git a/src/core/trulens/core/app.py b/src/core/trulens/core/app.py index 78eb26c5e..a655d3c53 100644 --- a/src/core/trulens/core/app.py +++ b/src/core/trulens/core/app.py @@ -3,7 +3,6 @@ from abc import ABC from abc import ABCMeta from abc import abstractmethod -import contextlib import contextvars import datetime import inspect @@ -423,18 +422,6 @@ def tru(self) -> core_connector.DBConnector: pydantic.PrivateAttr(default_factory=dict) ) - tokens: List[object] = [] - """ - OTEL context tokens for the current context manager. These tokens are how the OTEL - context api keeps track of what is changed in the context, and used to undo the changes. - """ - - span_context: Optional[contextlib.AbstractContextManager] = None - """ - Span context manager. Required to help keep track of the appropriate span context - to enter/exit. - """ - def __init__( self, connector: Optional[core_connector.DBConnector] = None, @@ -889,20 +876,18 @@ def model_dump(self, *args, redact_keys: bool = False, **kwargs): **kwargs, ) - # For use as a context manager. - def __enter__(self): - if not core_instruments.Instrument._have_context(): - raise RuntimeError(core_endpoint._NO_CONTEXT_WARNING) - + def _prevent_invalid_otel_syntax(self): if self.session.experimental_feature( core_experimental.Feature.OTEL_TRACING ): - from trulens.experimental.otel_tracing.core.instrument import ( - OTELRecordingContext as OTELApp, - ) + raise RuntimeError("Invalid TruLens OTEL Tracing syntax.") - return OTELApp.__enter__(self) + # For use as a context manager. + def __enter__(self): + if not core_instruments.Instrument._have_context(): + raise RuntimeError(core_endpoint._NO_CONTEXT_WARNING) + self._prevent_invalid_otel_syntax() ctx = core_instruments._RecordingContext(app=self) token = self.recording_contexts.set(ctx) @@ -912,14 +897,7 @@ def __enter__(self): # For use as a context manager. def __exit__(self, exc_type, exc_value, exc_tb): - if self.session.experimental_feature( - core_experimental.Feature.OTEL_TRACING - ): - from trulens.experimental.otel_tracing.core.instrument import ( - OTELRecordingContext as OTELApp, - ) - - return OTELApp.__exit__(self, exc_type, exc_value, exc_tb) + self._prevent_invalid_otel_syntax() ctx = self.recording_contexts.get() self.recording_contexts.reset(ctx.token) @@ -931,14 +909,7 @@ def __exit__(self, exc_type, exc_value, exc_tb): # For use as a context manager. async def __aenter__(self): - if self.session.experimental_feature( - core_experimental.Feature.OTEL_TRACING - ): - from trulens.experimental.otel_tracing.core.instrument import ( - OTELRecordingContext as OTELApp, - ) - - return OTELApp.__enter__(self) + self._prevent_invalid_otel_syntax() ctx = core_instruments._RecordingContext(app=self) @@ -951,14 +922,7 @@ async def __aenter__(self): # For use as a context manager. async def __aexit__(self, exc_type, exc_value, exc_tb): - if self.session.experimental_feature( - core_experimental.Feature.OTEL_TRACING - ): - from trulens.experimental.otel_tracing.core.instrument import ( - OTELRecordingContext as OTELApp, - ) - - return OTELApp.__exit__(self, exc_type, exc_value, exc_tb) + self._prevent_invalid_otel_syntax() ctx = self.recording_contexts.get() self.recording_contexts.reset(ctx.token) @@ -970,7 +934,7 @@ async def __aexit__(self, exc_type, exc_value, exc_tb): return - def __call__(self, *, run_name: str, input_id: str): + def __call__(self, *, run_name: str = "", input_id: str = ""): if not self.session.experimental_feature( core_experimental.Feature.OTEL_TRACING ): @@ -982,9 +946,7 @@ def __call__(self, *, run_name: str, input_id: str): # Pylance shows an error here, but it is likely a false positive. due to the overriden # model dump returning json instead of a dict. - return OTELApp.model_construct( - **self.model_dump(), run_name=run_name, input_id=input_id - ) + return OTELApp(app=self, run_name=run_name, input_id=input_id) def _set_context_vars(self): # HACK: For debugging purposes, try setting/resetting all context vars diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index bf9a2af0d..164854717 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -1,10 +1,10 @@ from functools import wraps import logging -from typing import Callable, Optional +from typing import Callable, List, Optional import uuid from opentelemetry import trace -from opentelemetry.baggage import clear as clear_baggage +from opentelemetry.baggage import remove_baggage from opentelemetry.baggage import set_baggage import opentelemetry.context as context_api from trulens.core import app as core_app @@ -93,41 +93,53 @@ def wrapper(*args, **kwargs): return inner_decorator -class OTELRecordingContext(core_app.App): - run_name: Optional[str] +class OTELRecordingContext: + run_name: str """ The name of the run that the recording context is currently processing. """ - input_id: Optional[str] + input_id: str """ The ID of the input that the recording context is currently processing. """ + tokens: List[object] = [] + """ + OTEL context tokens for the current context manager. These tokens are how the OTEL + context api keeps track of what is changed in the context, and used to undo the changes. + """ + + def __init__(self, *, app: core_app.App, run_name: str, input_id: str): + self.app = app + self.run_name = run_name + self.input_id = input_id + self.tokens = [] + self.span_context = None + + # Calling set_baggage does not actually add the baggage to the current context, but returns a new one + # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. + def attach_to_context(self, key: str, value: object): + self.tokens.append(context_api.attach(set_baggage(key, value))) + # For use as a context manager. def __enter__(self): - logger.debug("Entering the OTEL app context.") - # Note: This is not the same as the record_id in the core app since the OTEL # tracing is currently separate from the old records behavior otel_record_id = str(uuid.uuid4()) tracer = trace.get_tracer_provider().get_tracer(TRULENS_SERVICE_NAME) - # Calling set_baggage does not actually add the baggage to the current context, but returns a new one - # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. - def attach_to_context(key: str, value: object): - self.tokens.append(context_api.attach(set_baggage(key, value))) + self.attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) + self.attach_to_context(SpanAttributes.APP_NAME, self.app.app_name) + self.attach_to_context(SpanAttributes.APP_VERSION, self.app.app_version) - attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) - attach_to_context(SpanAttributes.APP_NAME, self.app_name) - attach_to_context(SpanAttributes.APP_VERSION, self.app_version) + if self.run_name: + logger.debug(f"Setting run name: '{self.run_name}'") + self.attach_to_context(SpanAttributes.RUN_NAME, self.run_name) - if hasattr(self, "run_name") and self.run_name: - attach_to_context(SpanAttributes.RUN_NAME, self.run_name) - - if hasattr(self, "run_name") and self.input_id: - attach_to_context(SpanAttributes.INPUT_ID, self.input_id) + if self.input_id: + self.attach_to_context(SpanAttributes.INPUT_ID, self.input_id) # Use start_as_current_span as a context manager self.span_context = tracer.start_as_current_span("root") @@ -141,10 +153,10 @@ def attach_to_context(key: str, value: object): # Set record root specific attributes root_span.set_attribute( - SpanAttributes.RECORD_ROOT.APP_NAME, self.app_name + SpanAttributes.RECORD_ROOT.APP_NAME, self.app.app_name ) root_span.set_attribute( - SpanAttributes.RECORD_ROOT.APP_VERSION, self.app_version + SpanAttributes.RECORD_ROOT.APP_VERSION, self.app.app_version ) root_span.set_attribute( SpanAttributes.RECORD_ROOT.RECORD_ID, otel_record_id @@ -153,15 +165,22 @@ def attach_to_context(key: str, value: object): return root_span def __exit__(self, exc_type, exc_value, exc_tb): - clear_baggage() + if self.span_context: + # TODO[SNOW-1854360]: Add in feature function spans. + self.span_context.__exit__(exc_type, exc_value, exc_tb) + + remove_baggage(SpanAttributes.RECORD_ID) + remove_baggage(SpanAttributes.APP_NAME) + remove_baggage(SpanAttributes.APP_VERSION) + + # Safe to remove baggage keys even if we did not set them + remove_baggage(SpanAttributes.RUN_NAME) + remove_baggage(SpanAttributes.INPUT_ID) logger.debug("Exiting the OTEL app context.") while self.tokens: + logger.debug(self.tokens[-1]) # Clearing the context once we're done with this root span. # See https://github.com/open-telemetry/opentelemetry-python/issues/2432#issuecomment-1593458684 context_api.detach(self.tokens.pop()) - - if self.span_context: - # TODO[SNOW-1854360]: Add in feature function spans. - self.span_context.__exit__(exc_type, exc_value, exc_tb) diff --git a/src/core/trulens/experimental/otel_tracing/core/session.py b/src/core/trulens/experimental/otel_tracing/core/session.py index 7de74818a..fad6ab50d 100644 --- a/src/core/trulens/experimental/otel_tracing/core/session.py +++ b/src/core/trulens/experimental/otel_tracing/core/session.py @@ -40,29 +40,21 @@ def _setup_otel_exporter( provider = TracerProvider(resource=resource) trace.set_tracer_provider(provider) - # The opentelemetry.sdk.trace.TracerProvider class is what OTEL uses under the hood, - # even though OTEL only chooses to expose a subset of the class attributes in - # the opentelemetry.trace module. - # See : https://github.com/search?q=repo%3Aopen-telemetry%2Fopentelemetry-python+get_tracer_provider&type=code - # for examples of how the TracerProvider class is used in the OTEL codebase. - # Hence, here we force the typing to ignore the error. - tracer_provider: TracerProvider = trace.get_tracer_provider() # type: ignore + global_trace_provider = trace.get_tracer_provider() + if not isinstance(global_trace_provider, TracerProvider): + raise ValueError("Received a TracerProvider of an unexpected type!") + + tracer_provider: TracerProvider = global_trace_provider + + # Setting it here for easy access without having to assert the type every time self._experimental_tracer_provider = tracer_provider - # Export to the connector provided. - tracer_provider.add_span_processor( - otel_export_sdk.BatchSpanProcessor( - TruLensOTELSpanExporter(connector) + if exporter and not isinstance(exporter, otel_export_sdk.SpanExporter): + raise ValueError( + "Provided exporter must be an OpenTelemetry SpanExporter" ) - ) - if exporter: - assert isinstance( - exporter, otel_export_sdk.SpanExporter - ), "otel_exporter must be an OpenTelemetry SpanExporter." - - # When testing, use a simple span processor to avoid issues with batching/ - # asynchronous processing of the spans that results in the database not - # being updated in time for the tests. - db_processor = otel_export_sdk.BatchSpanProcessor(exporter) - tracer_provider.add_span_processor(db_processor) + db_processor = otel_export_sdk.BatchSpanProcessor( + exporter if exporter else TruLensOTELSpanExporter(connector) + ) + tracer_provider.add_span_processor(db_processor) diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index d561f663f..4cf8fa2fc 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -97,12 +97,15 @@ def set_general_span_attributes( span.set_attribute( SpanAttributes.RECORD_ID, str(get_baggage(SpanAttributes.RECORD_ID)) ) - span.set_attribute( - SpanAttributes.RUN_NAME, str(get_baggage(SpanAttributes.RUN_NAME)) - ) - span.set_attribute( - SpanAttributes.INPUT_ID, str(get_baggage(SpanAttributes.INPUT_ID)) - ) + + run_name_baggage: str = str(get_baggage(SpanAttributes.RUN_NAME)) + input_id_baggage: str = str(get_baggage(SpanAttributes.INPUT_ID)) + + if run_name_baggage: + span.set_attribute(SpanAttributes.RUN_NAME, run_name_baggage) + + if input_id_baggage: + span.set_attribute(SpanAttributes.INPUT_ID, input_id_baggage) return span From 1a5d455b112c0a2baf9cb37514891419b7f105de Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 16 Jan 2025 19:59:55 -0800 Subject: [PATCH 64/68] comments --- src/core/trulens/experimental/otel_tracing/core/instrument.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 164854717..0174975c4 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -165,6 +165,8 @@ def __enter__(self): return root_span def __exit__(self, exc_type, exc_value, exc_tb): + # Exiting the span context before updating the context to ensure nothing + # carries over unintentionally if self.span_context: # TODO[SNOW-1854360]: Add in feature function spans. self.span_context.__exit__(exc_type, exc_value, exc_tb) @@ -180,7 +182,6 @@ def __exit__(self, exc_type, exc_value, exc_tb): logger.debug("Exiting the OTEL app context.") while self.tokens: - logger.debug(self.tokens[-1]) # Clearing the context once we're done with this root span. # See https://github.com/open-telemetry/opentelemetry-python/issues/2432#issuecomment-1593458684 context_api.detach(self.tokens.pop()) From 035325858f3f97043d365d43ec8d9b28ea38f20d Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Thu, 16 Jan 2025 22:23:30 -0800 Subject: [PATCH 65/68] PR feedback --- examples/experimental/otel_exporter.ipynb | 4 +- .../otel_tracing/core/exporter/connector.py | 44 ++++++ .../otel_tracing/core/exporter/snowflake.py | 107 +++++++++++++++ .../core/{exporter.py => exporter/utils.py} | 126 +----------------- .../otel_tracing/core/instrument.py | 25 ++-- .../experimental/otel_tracing/core/session.py | 2 +- .../experimental/otel_tracing/core/span.py | 12 +- tests/unit/test_exporter.py | 4 +- 8 files changed, 180 insertions(+), 144 deletions(-) create mode 100644 src/core/trulens/experimental/otel_tracing/core/exporter/connector.py create mode 100644 src/core/trulens/experimental/otel_tracing/core/exporter/snowflake.py rename src/core/trulens/experimental/otel_tracing/core/{exporter.py => exporter/utils.py} (53%) diff --git a/examples/experimental/otel_exporter.ipynb b/examples/experimental/otel_exporter.ipynb index 1c91e9b4c..116f61fdc 100644 --- a/examples/experimental/otel_exporter.ipynb +++ b/examples/experimental/otel_exporter.ipynb @@ -118,10 +118,10 @@ " session=session,\n", ")\n", "\n", - "with custom_app() as recording:\n", + "with custom_app(run_name=\"test run\", input_id=\"456\") as recording:\n", " test_app.respond_to_query(\"test\")\n", "\n", - "with custom_app(run_name=\"test run\", input_id=\"456\") as recording:\n", + "with custom_app() as recording:\n", " test_app.respond_to_query(\"throw\")" ] }, diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter/connector.py b/src/core/trulens/experimental/otel_tracing/core/exporter/connector.py new file mode 100644 index 000000000..6ff82fcad --- /dev/null +++ b/src/core/trulens/experimental/otel_tracing/core/exporter/connector.py @@ -0,0 +1,44 @@ +import logging +from typing import Sequence + +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter +from opentelemetry.sdk.trace.export import SpanExportResult +from trulens.core.database import connector as core_connector +from trulens.experimental.otel_tracing.core.exporter.utils import ( + check_if_trulens_span, +) +from trulens.experimental.otel_tracing.core.exporter.utils import ( + construct_event, +) + +logger = logging.getLogger(__name__) + + +class TruLensOTELSpanExporter(SpanExporter): + """ + Implementation of `SpanExporter` that flushes the spans in the TruLens session to the connector. + """ + + connector: core_connector.DBConnector + """ + The database connector used to export the spans. + """ + + def __init__(self, connector: core_connector.DBConnector): + self.connector = connector + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + trulens_spans = list(filter(check_if_trulens_span, spans)) + + try: + events = list(map(construct_event, trulens_spans)) + self.connector.add_events(events) + + except Exception as e: + logger.error( + f"Error exporting spans to the database: {e}", + ) + return SpanExportResult.FAILURE + + return SpanExportResult.SUCCESS diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter/snowflake.py b/src/core/trulens/experimental/otel_tracing/core/exporter/snowflake.py new file mode 100644 index 000000000..567717ce7 --- /dev/null +++ b/src/core/trulens/experimental/otel_tracing/core/exporter/snowflake.py @@ -0,0 +1,107 @@ +import logging +import os +import tempfile +from typing import Sequence + +from opentelemetry.sdk.trace import ReadableSpan +from opentelemetry.sdk.trace.export import SpanExporter +from opentelemetry.sdk.trace.export import SpanExportResult +from trulens.connectors.snowflake import SnowflakeConnector +from trulens.core.database import connector as core_connector +from trulens.experimental.otel_tracing.core.exporter.utils import ( + check_if_trulens_span, +) +from trulens.experimental.otel_tracing.core.exporter.utils import ( + convert_readable_span_to_proto, +) + +logger = logging.getLogger(__name__) + + +class TruLensSnowflakeSpanExporter(SpanExporter): + """ + Implementation of `SpanExporter` that flushes the spans in the TruLens session to a Snowflake Stage. + """ + + connector: core_connector.DBConnector + """ + The database connector used to export the spans. + """ + + def __init__(self, connector: core_connector.DBConnector): + self.connector = connector + + def _export_to_snowflake_stage( + self, spans: Sequence[ReadableSpan] + ) -> SpanExportResult: + """ + Exports a list of spans to a Snowflake stage as a protobuf file. + This function performs the following steps: + 1. Writes the provided spans to a temporary protobuf file. + 2. Creates a Snowflake stage if it does not already exist. + 3. Uploads the temporary protobuf file to the Snowflake stage. + 4. Removes the temporary protobuf file. + Args: + spans (Sequence[ReadableSpan]): A sequence of spans to be exported. + Returns: + SpanExportResult: The result of the export operation, either SUCCESS or FAILURE. + """ + if not isinstance(self.connector, SnowflakeConnector): + return SpanExportResult.FAILURE + + # Avoid uploading empty files to the stage + if not spans: + return SpanExportResult.SUCCESS + + snowpark_session = self.connector.snowpark_session + tmp_file_path = "" + + try: + with tempfile.NamedTemporaryFile( + delete=False, suffix=".pb", mode="wb" + ) as tmp_file: + tmp_file_path = tmp_file.name + logger.debug( + f"Writing spans to the protobuf file: {tmp_file_path}" + ) + + for span in spans: + span_proto = convert_readable_span_to_proto(span) + tmp_file.write(span_proto.SerializeToString()) + logger.debug( + f"Spans written to the protobuf file: {tmp_file_path}" + ) + except Exception as e: + logger.error(f"Error writing spans to the protobuf file: {e}") + return SpanExportResult.FAILURE + + try: + logger.debug("Uploading file to Snowflake stage") + + logger.debug("Creating Snowflake stage if it does not exist") + snowpark_session.sql( + "CREATE TEMP STAGE IF NOT EXISTS trulens_spans" + ).collect() + + logger.debug("Uploading the protobuf file to the stage") + snowpark_session.sql( + f"PUT file://{tmp_file_path} @trulens_spans" + ).collect() + + except Exception as e: + logger.error(f"Error uploading the protobuf file to the stage: {e}") + return SpanExportResult.FAILURE + + try: + logger.debug("Removing the temporary protobuf file") + os.remove(tmp_file_path) + except Exception as e: + # Not returning failure here since the export was technically a success + logger.error(f"Error removing the temporary protobuf file: {e}") + + return SpanExportResult.SUCCESS + + def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: + trulens_spans = list(filter(check_if_trulens_span, spans)) + + return self._export_to_snowflake_stage(trulens_spans) diff --git a/src/core/trulens/experimental/otel_tracing/core/exporter.py b/src/core/trulens/experimental/otel_tracing/core/exporter/utils.py similarity index 53% rename from src/core/trulens/experimental/otel_tracing/core/exporter.py rename to src/core/trulens/experimental/otel_tracing/core/exporter/utils.py index 96d708925..c534aa978 100644 --- a/src/core/trulens/experimental/otel_tracing/core/exporter.py +++ b/src/core/trulens/experimental/otel_tracing/core/exporter/utils.py @@ -1,8 +1,6 @@ from datetime import datetime import logging -import os -import tempfile -from typing import Any, Optional, Sequence +from typing import Any, Optional from opentelemetry.proto.common.v1.common_pb2 import AnyValue from opentelemetry.proto.common.v1.common_pb2 import ArrayValue @@ -10,11 +8,7 @@ from opentelemetry.proto.common.v1.common_pb2 import KeyValueList from opentelemetry.proto.trace.v1.trace_pb2 import Span as SpanProto from opentelemetry.sdk.trace import ReadableSpan -from opentelemetry.sdk.trace.export import SpanExporter -from opentelemetry.sdk.trace.export import SpanExportResult from opentelemetry.trace import StatusCode -from trulens.connectors.snowflake import SnowflakeConnector -from trulens.core.database import connector as core_connector from trulens.core.schema import event as event_schema from trulens.otel.semconv.trace import SpanAttributes @@ -147,121 +141,3 @@ def construct_event(span: ReadableSpan) -> event_schema.Event: "parent_id": str(parent.span_id if parent else ""), }, ) - - -class TruLensSnowflakeSpanExporter(SpanExporter): - """ - Implementation of `SpanExporter` that flushes the spans in the TruLens session to a Snowflake Stage. - """ - - connector: core_connector.DBConnector - """ - The database connector used to export the spans. - """ - - def __init__(self, connector: core_connector.DBConnector): - self.connector = connector - - def _export_to_snowflake_stage( - self, spans: Sequence[ReadableSpan] - ) -> SpanExportResult: - """ - Exports a list of spans to a Snowflake stage as a protobuf file. - This function performs the following steps: - 1. Writes the provided spans to a temporary protobuf file. - 2. Creates a Snowflake stage if it does not already exist. - 3. Uploads the temporary protobuf file to the Snowflake stage. - 4. Removes the temporary protobuf file. - Args: - spans (Sequence[ReadableSpan]): A sequence of spans to be exported. - Returns: - SpanExportResult: The result of the export operation, either SUCCESS or FAILURE. - """ - if not isinstance(self.connector, SnowflakeConnector): - return SpanExportResult.FAILURE - - # Avoid uploading empty files to the stage - if not spans: - return SpanExportResult.SUCCESS - - snowpark_session = self.connector.snowpark_session - tmp_file_path = "" - - try: - with tempfile.NamedTemporaryFile( - delete=False, suffix=".pb", mode="wb" - ) as tmp_file: - tmp_file_path = tmp_file.name - logger.debug( - f"Writing spans to the protobuf file: {tmp_file_path}" - ) - - for span in spans: - span_proto = convert_readable_span_to_proto(span) - tmp_file.write(span_proto.SerializeToString()) - logger.debug( - f"Spans written to the protobuf file: {tmp_file_path}" - ) - except Exception as e: - logger.error(f"Error writing spans to the protobuf file: {e}") - return SpanExportResult.FAILURE - - try: - logger.debug("Uploading file to Snowflake stage") - - logger.debug("Creating Snowflake stage if it does not exist") - snowpark_session.sql( - "CREATE TEMP STAGE IF NOT EXISTS trulens_spans" - ).collect() - - logger.debug("Uploading the protobuf file to the stage") - snowpark_session.sql( - f"PUT file://{tmp_file_path} @trulens_spans" - ).collect() - - except Exception as e: - logger.error(f"Error uploading the protobuf file to the stage: {e}") - return SpanExportResult.FAILURE - - try: - logger.debug("Removing the temporary protobuf file") - os.remove(tmp_file_path) - except Exception as e: - # Not returning failure here since the export was technically a success - logger.error(f"Error removing the temporary protobuf file: {e}") - - return SpanExportResult.SUCCESS - - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - trulens_spans = list(filter(check_if_trulens_span, spans)) - - return self._export_to_snowflake_stage(trulens_spans) - - -class TruLensOTELSpanExporter(SpanExporter): - """ - Implementation of `SpanExporter` that flushes the spans in the TruLens session to the connector. - """ - - connector: core_connector.DBConnector - """ - The database connector used to export the spans. - """ - - def __init__(self, connector: core_connector.DBConnector): - self.connector = connector - - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: - trulens_spans = list(filter(check_if_trulens_span, spans)) - - try: - events = list(map(construct_event, trulens_spans)) - self.connector.add_events(events) - - except Exception as e: - logger.error( - f"Error exporting spans to the database: {e}", - ) - return SpanExportResult.FAILURE - - return SpanExportResult.SUCCESS diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 0174975c4..58adfbf56 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -4,6 +4,7 @@ import uuid from opentelemetry import trace +from opentelemetry.baggage import get_baggage from opentelemetry.baggage import remove_baggage from opentelemetry.baggage import set_baggage import opentelemetry.context as context_api @@ -110,17 +111,24 @@ class OTELRecordingContext: context api keeps track of what is changed in the context, and used to undo the changes. """ + context_keys_added: List[str] = [] + """ + Keys added to the OTEL context. + """ + def __init__(self, *, app: core_app.App, run_name: str, input_id: str): self.app = app self.run_name = run_name self.input_id = input_id self.tokens = [] + self.context_keys_added = [] self.span_context = None # Calling set_baggage does not actually add the baggage to the current context, but returns a new one # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. def attach_to_context(self, key: str, value: object): self.tokens.append(context_api.attach(set_baggage(key, value))) + self.context_keys_added.append(key) # For use as a context manager. def __enter__(self): @@ -130,7 +138,10 @@ def __enter__(self): tracer = trace.get_tracer_provider().get_tracer(TRULENS_SERVICE_NAME) - self.attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) + # There might be nested records - in those scenarios use the outer one. + if not get_baggage(SpanAttributes.RECORD_ID): + self.attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) + self.attach_to_context(SpanAttributes.APP_NAME, self.app.app_name) self.attach_to_context(SpanAttributes.APP_VERSION, self.app.app_version) @@ -171,16 +182,12 @@ def __exit__(self, exc_type, exc_value, exc_tb): # TODO[SNOW-1854360]: Add in feature function spans. self.span_context.__exit__(exc_type, exc_value, exc_tb) - remove_baggage(SpanAttributes.RECORD_ID) - remove_baggage(SpanAttributes.APP_NAME) - remove_baggage(SpanAttributes.APP_VERSION) - - # Safe to remove baggage keys even if we did not set them - remove_baggage(SpanAttributes.RUN_NAME) - remove_baggage(SpanAttributes.INPUT_ID) - logger.debug("Exiting the OTEL app context.") + # Clearing the context / baggage added. + while self.context_keys_added: + remove_baggage(self.context_keys_added.pop()) + while self.tokens: # Clearing the context once we're done with this root span. # See https://github.com/open-telemetry/opentelemetry-python/issues/2432#issuecomment-1593458684 diff --git a/src/core/trulens/experimental/otel_tracing/core/session.py b/src/core/trulens/experimental/otel_tracing/core/session.py index fad6ab50d..46e1f9579 100644 --- a/src/core/trulens/experimental/otel_tracing/core/session.py +++ b/src/core/trulens/experimental/otel_tracing/core/session.py @@ -10,7 +10,7 @@ from trulens.core.database.connector import DBConnector from trulens.core.utils import python as python_utils from trulens.core.utils import text as text_utils -from trulens.experimental.otel_tracing.core.exporter import ( +from trulens.experimental.otel_tracing.core.exporter.connector import ( TruLensOTELSpanExporter, ) diff --git a/src/core/trulens/experimental/otel_tracing/core/span.py b/src/core/trulens/experimental/otel_tracing/core/span.py index 4cf8fa2fc..6a49b6f62 100644 --- a/src/core/trulens/experimental/otel_tracing/core/span.py +++ b/src/core/trulens/experimental/otel_tracing/core/span.py @@ -98,14 +98,14 @@ def set_general_span_attributes( SpanAttributes.RECORD_ID, str(get_baggage(SpanAttributes.RECORD_ID)) ) - run_name_baggage: str = str(get_baggage(SpanAttributes.RUN_NAME)) - input_id_baggage: str = str(get_baggage(SpanAttributes.INPUT_ID)) + run_name_baggage = get_baggage(SpanAttributes.RUN_NAME) + input_id_baggage = get_baggage(SpanAttributes.INPUT_ID) if run_name_baggage: - span.set_attribute(SpanAttributes.RUN_NAME, run_name_baggage) + span.set_attribute(SpanAttributes.RUN_NAME, str(run_name_baggage)) if input_id_baggage: - span.set_attribute(SpanAttributes.INPUT_ID, input_id_baggage) + span.set_attribute(SpanAttributes.INPUT_ID, str(input_id_baggage)) return span @@ -133,7 +133,7 @@ def set_user_defined_attributes( final_attributes = validate_attributes(attributes_to_add) - prefix = f"trulens.{span_type.value}." + prefix = f"{SpanAttributes.BASE}{span_type.value}." for key, value in final_attributes.items(): span.set_attribute(prefix + key, value) @@ -143,7 +143,7 @@ def set_user_defined_attributes( and SpanAttributes.SELECTOR_NAME_KEY in final_attributes ): span.set_attribute( - f"trulens.{final_attributes[SpanAttributes.SELECTOR_NAME_KEY]}.{key}", + f"{SpanAttributes.BASE}{final_attributes[SpanAttributes.SELECTOR_NAME_KEY]}.{key}", value, ) diff --git a/tests/unit/test_exporter.py b/tests/unit/test_exporter.py index 0f011f751..e0eaf423d 100644 --- a/tests/unit/test_exporter.py +++ b/tests/unit/test_exporter.py @@ -4,7 +4,9 @@ from opentelemetry.proto.common.v1.common_pb2 import ArrayValue from opentelemetry.proto.common.v1.common_pb2 import KeyValue from opentelemetry.proto.common.v1.common_pb2 import KeyValueList -from trulens.experimental.otel_tracing.core.exporter import convert_to_any_value +from trulens.experimental.otel_tracing.core.exporter.utils import ( + convert_to_any_value, +) class TestExporterUtils(unittest.TestCase): From c28e10d0df50936aca554805319eb95ed07d09a0 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 17 Jan 2025 08:58:01 -0800 Subject: [PATCH 66/68] pr feedback --- ..._instrument__test_instrument_decorator.csv | 20 +++++++++---------- tests/unit/test_otel_instrument.py | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index d51c66366..c9401397b 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,11 +1,11 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",17676774584635777232,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '29ee4817-72ec-4855-9533-1fe399590301', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '29ee4817-72ec-4855-9533-1fe399590301'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052239,2025-01-11 16:48:22.052519,"{'trace_id': '149279050558218446414779811627724576968', 'parent_id': '', 'span_id': '17676774584635777232'}" -1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '17676774584635777232', 'status': 'STATUS_CODE_UNSET'}",11540413822527064179,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '29ee4817-72ec-4855-9533-1fe399590301', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052303,2025-01-11 16:48:22.052504,"{'trace_id': '149279050558218446414779811627724576968', 'parent_id': '17676774584635777232', 'span_id': '11540413822527064179'}" -2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '11540413822527064179', 'status': 'STATUS_CODE_UNSET'}",4037572683958769911,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '29ee4817-72ec-4855-9533-1fe399590301', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052325,2025-01-11 16:48:22.052448,"{'trace_id': '149279050558218446414779811627724576968', 'parent_id': '11540413822527064179', 'span_id': '4037572683958769911'}" -3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '4037572683958769911', 'status': 'STATUS_CODE_UNSET'}",6759580477746872628,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '29ee4817-72ec-4855-9533-1fe399590301', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052344,2025-01-11 16:48:22.052429,"{'trace_id': '149279050558218446414779811627724576968', 'parent_id': '4037572683958769911', 'span_id': '6759580477746872628'}" -4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '6759580477746872628', 'status': 'STATUS_CODE_UNSET'}",13340260111705114942,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '29ee4817-72ec-4855-9533-1fe399590301', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052371,2025-01-11 16:48:22.052409,"{'trace_id': '149279050558218446414779811627724576968', 'parent_id': '6759580477746872628', 'span_id': '13340260111705114942'}" -5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",2291773425110446948,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'fb9967b8-6e7b-44a6-94f7-ed65773fd0b0', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': 'fb9967b8-6e7b-44a6-94f7-ed65773fd0b0'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052573,2025-01-11 16:48:22.053553,"{'trace_id': '165730192727962430166279173525415334900', 'parent_id': '', 'span_id': '2291773425110446948'}" -6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '2291773425110446948', 'status': 'STATUS_CODE_UNSET'}",17586722182046355381,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'fb9967b8-6e7b-44a6-94f7-ed65773fd0b0', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052598,2025-01-11 16:48:22.053520,"{'trace_id': '165730192727962430166279173525415334900', 'parent_id': '2291773425110446948', 'span_id': '17586722182046355381'}" -7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '17586722182046355381', 'status': 'STATUS_CODE_UNSET'}",15587405225993562765,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'fb9967b8-6e7b-44a6-94f7-ed65773fd0b0', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052624,2025-01-11 16:48:22.053466,"{'trace_id': '165730192727962430166279173525415334900', 'parent_id': '17586722182046355381', 'span_id': '15587405225993562765'}" -8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '15587405225993562765', 'status': 'STATUS_CODE_UNSET'}",13785091492562709820,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'fb9967b8-6e7b-44a6-94f7-ed65773fd0b0', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052710,2025-01-11 16:48:22.053449,"{'trace_id': '165730192727962430166279173525415334900', 'parent_id': '15587405225993562765', 'span_id': '13785091492562709820'}" -9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '13785091492562709820', 'status': 'STATUS_CODE_ERROR'}",11120220871120224021,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'fb9967b8-6e7b-44a6-94f7-ed65773fd0b0', 'trulens.run_name': 'None', 'trulens.input_id': 'None', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-11 16:48:22.052725,2025-01-11 16:48:22.053398,"{'trace_id': '165730192727962430166279173525415334900', 'parent_id': '13785091492562709820', 'span_id': '11120220871120224021'}" +0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",12269466148167242934,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337629,2025-01-17 08:29:31.337859,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '', 'span_id': '12269466148167242934'}" +1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '12269466148167242934', 'status': 'STATUS_CODE_UNSET'}",5487251687324701636,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337679,2025-01-17 08:29:31.337855,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '12269466148167242934', 'span_id': '5487251687324701636'}" +2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '5487251687324701636', 'status': 'STATUS_CODE_UNSET'}",14881017561913179896,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337700,2025-01-17 08:29:31.337809,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '5487251687324701636', 'span_id': '14881017561913179896'}" +3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '14881017561913179896', 'status': 'STATUS_CODE_UNSET'}",11276744325636403591,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337721,2025-01-17 08:29:31.337793,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '14881017561913179896', 'span_id': '11276744325636403591'}" +4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '11276744325636403591', 'status': 'STATUS_CODE_UNSET'}",4737367276529506671,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337738,2025-01-17 08:29:31.337769,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '11276744325636403591', 'span_id': '4737367276529506671'}" +5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",13217322988179622548,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337909,2025-01-17 08:29:31.339835,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '', 'span_id': '13217322988179622548'}" +6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '13217322988179622548', 'status': 'STATUS_CODE_UNSET'}",12584259857373781582,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337936,2025-01-17 08:29:31.339832,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '13217322988179622548', 'span_id': '12584259857373781582'}" +7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '12584259857373781582', 'status': 'STATUS_CODE_UNSET'}",16969196265702745985,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.338030,2025-01-17 08:29:31.339781,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '12584259857373781582', 'span_id': '16969196265702745985'}" +8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '16969196265702745985', 'status': 'STATUS_CODE_UNSET'}",16087567105887777689,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.338049,2025-01-17 08:29:31.339768,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '16969196265702745985', 'span_id': '16087567105887777689'}" +9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '16087567105887777689', 'status': 'STATUS_CODE_ERROR'}",2459351220635848964,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.338069,2025-01-17 08:29:31.339740,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '16087567105887777689', 'span_id': '2459351220635848964'}" diff --git a/tests/unit/test_otel_instrument.py b/tests/unit/test_otel_instrument.py index beab77067..d70621a99 100644 --- a/tests/unit/test_otel_instrument.py +++ b/tests/unit/test_otel_instrument.py @@ -124,9 +124,9 @@ def test_instrument_decorator(self) -> None: # Create and run app. test_app = _TestApp() custom_app = TruCustomApp(test_app) - with custom_app: + with custom_app(run_name="test run", input_id="456"): test_app.respond_to_query("test") - with custom_app: + with custom_app(): test_app.respond_to_query("throw") # Compare results to expected. GOLDEN_FILENAME = "tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv" From 1c1d71859dab60473f5d4ac493bcba1d42a35a23 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 17 Jan 2025 09:16:45 -0800 Subject: [PATCH 67/68] update golden --- .../unit/static/golden/api.trulens.3.11.yaml | 10 +- .../static/golden/api.trulens_eval.3.11.yaml | 470 +++++++++--------- ..._instrument__test_instrument_decorator.csv | 20 +- 3 files changed, 236 insertions(+), 264 deletions(-) diff --git a/tests/unit/static/golden/api.trulens.3.11.yaml b/tests/unit/static/golden/api.trulens.3.11.yaml index a3c97c56f..f87980dd9 100644 --- a/tests/unit/static/golden/api.trulens.3.11.yaml +++ b/tests/unit/static/golden/api.trulens.3.11.yaml @@ -4,7 +4,7 @@ trulens: lows: {} trulens.benchmark: __class__: builtins.module - __version__: 1.3.1 + __version__: 1.3.2 highs: {} lows: __version__: builtins.str @@ -60,7 +60,7 @@ trulens.benchmark.test_cases: generate_summeval_groundedness_golden_set: builtins.function trulens.core: __class__: builtins.module - __version__: 1.3.1 + __version__: 1.3.2 highs: Feedback: pydantic._internal._model_construction.ModelMetaclass FeedbackMode: enum.EnumType @@ -141,9 +141,7 @@ trulens.core.app.App: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class wait_for_feedback_results: builtins.function @@ -1727,7 +1725,7 @@ trulens.core.utils.trulens.Other: attributes: {} trulens.dashboard: __class__: builtins.module - __version__: 1.3.1 + __version__: 1.3.2 highs: run_dashboard: builtins.function run_dashboard_sis: builtins.function @@ -2015,7 +2013,7 @@ trulens.dashboard.ux.styles.ResultCategoryType: value: enum.property trulens.feedback: __class__: builtins.module - __version__: 1.3.1 + __version__: 1.3.2 highs: Embeddings: pydantic._internal._model_construction.ModelMetaclass GroundTruthAggregator: pydantic._internal._model_construction.ModelMetaclass diff --git a/tests/unit/static/golden/api.trulens_eval.3.11.yaml b/tests/unit/static/golden/api.trulens_eval.3.11.yaml index d39f87dae..59d3c1441 100644 --- a/tests/unit/static/golden/api.trulens_eval.3.11.yaml +++ b/tests/unit/static/golden/api.trulens_eval.3.11.yaml @@ -78,7 +78,7 @@ trulens_eval.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -86,7 +86,7 @@ trulens_eval.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -170,7 +170,7 @@ trulens_eval.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -178,7 +178,7 @@ trulens_eval.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -256,7 +256,7 @@ trulens_eval.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -264,7 +264,7 @@ trulens_eval.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -332,14 +332,14 @@ trulens_eval.Feedback: load: builtins.staticmethod max_score_val: typing.Optional[builtins.int, builtins.NoneType] min_score_val: typing.Optional[builtins.int, builtins.NoneType] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -407,14 +407,14 @@ trulens_eval.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -457,14 +457,14 @@ trulens_eval.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -533,7 +533,7 @@ trulens_eval.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -541,7 +541,7 @@ trulens_eval.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -624,7 +624,7 @@ trulens_eval.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -632,7 +632,7 @@ trulens_eval.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -707,7 +707,7 @@ trulens_eval.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -715,7 +715,7 @@ trulens_eval.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -769,14 +769,14 @@ trulens_eval.Provider: get_class: builtins.staticmethod json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -869,14 +869,14 @@ trulens_eval.Tru: get_records_and_feedback: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -947,14 +947,14 @@ trulens_eval.TruBasicApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -989,9 +989,7 @@ trulens_eval.TruBasicApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1048,14 +1046,14 @@ trulens_eval.TruChain: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1090,9 +1088,7 @@ trulens_eval.TruChain: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1150,14 +1146,14 @@ trulens_eval.TruCustomApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1192,9 +1188,7 @@ trulens_eval.TruCustomApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1249,14 +1243,14 @@ trulens_eval.TruLlama: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1292,9 +1286,7 @@ trulens_eval.TruLlama: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1349,14 +1341,14 @@ trulens_eval.TruRails: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1391,9 +1383,7 @@ trulens_eval.TruRails: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1450,14 +1440,14 @@ trulens_eval.TruVirtual: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1492,9 +1482,7 @@ trulens_eval.TruVirtual: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1582,14 +1570,14 @@ trulens_eval.app.App: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -1623,9 +1611,7 @@ trulens_eval.app.App: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -1846,14 +1832,14 @@ trulens_eval.database.base.DB: insert_record: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2005,14 +1991,14 @@ trulens_eval.database.sqlalchemy.SQLAlchemyDB: insert_record: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2117,7 +2103,7 @@ trulens_eval.feedback.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2125,7 +2111,7 @@ trulens_eval.feedback.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2209,7 +2195,7 @@ trulens_eval.feedback.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2217,7 +2203,7 @@ trulens_eval.feedback.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -2295,7 +2281,7 @@ trulens_eval.feedback.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2303,7 +2289,7 @@ trulens_eval.feedback.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2351,14 +2337,14 @@ trulens_eval.feedback.Embeddings: json: builtins.function load: builtins.staticmethod manhattan_distance: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2416,14 +2402,14 @@ trulens_eval.feedback.Feedback: load: builtins.staticmethod max_score_val: typing.Optional[builtins.int, builtins.NoneType] min_score_val: typing.Optional[builtins.int, builtins.NoneType] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2485,14 +2471,14 @@ trulens_eval.feedback.GroundTruthAgreement: json: builtins.function load: builtins.staticmethod mae: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2537,14 +2523,14 @@ trulens_eval.feedback.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2587,14 +2573,14 @@ trulens_eval.feedback.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2663,7 +2649,7 @@ trulens_eval.feedback.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2671,7 +2657,7 @@ trulens_eval.feedback.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2747,7 +2733,7 @@ trulens_eval.feedback.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2755,7 +2741,7 @@ trulens_eval.feedback.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2830,7 +2816,7 @@ trulens_eval.feedback.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -2838,7 +2824,7 @@ trulens_eval.feedback.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2900,14 +2886,14 @@ trulens_eval.feedback.embeddings.Embeddings: json: builtins.function load: builtins.staticmethod manhattan_distance: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -2974,14 +2960,14 @@ trulens_eval.feedback.feedback.Feedback: load: builtins.staticmethod max_score_val: typing.Optional[builtins.int, builtins.NoneType] min_score_val: typing.Optional[builtins.int, builtins.NoneType] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3059,14 +3045,14 @@ trulens_eval.feedback.groundtruth.GroundTruthAgreement: json: builtins.function load: builtins.staticmethod mae: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3197,7 +3183,7 @@ trulens_eval.feedback.provider.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3205,7 +3191,7 @@ trulens_eval.feedback.provider.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3289,7 +3275,7 @@ trulens_eval.feedback.provider.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3297,7 +3283,7 @@ trulens_eval.feedback.provider.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -3375,7 +3361,7 @@ trulens_eval.feedback.provider.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3383,7 +3369,7 @@ trulens_eval.feedback.provider.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3432,14 +3418,14 @@ trulens_eval.feedback.provider.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3482,14 +3468,14 @@ trulens_eval.feedback.provider.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3558,7 +3544,7 @@ trulens_eval.feedback.provider.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3566,7 +3552,7 @@ trulens_eval.feedback.provider.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3642,7 +3628,7 @@ trulens_eval.feedback.provider.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3650,7 +3636,7 @@ trulens_eval.feedback.provider.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3725,7 +3711,7 @@ trulens_eval.feedback.provider.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3733,7 +3719,7 @@ trulens_eval.feedback.provider.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3787,14 +3773,14 @@ trulens_eval.feedback.provider.Provider: get_class: builtins.staticmethod json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3866,7 +3852,7 @@ trulens_eval.feedback.provider.base.LLMProvider: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -3874,7 +3860,7 @@ trulens_eval.feedback.provider.base.LLMProvider: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3919,14 +3905,14 @@ trulens_eval.feedback.provider.base.Provider: get_class: builtins.staticmethod json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -3998,7 +3984,7 @@ trulens_eval.feedback.provider.bedrock.Bedrock: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -4006,7 +3992,7 @@ trulens_eval.feedback.provider.bedrock.Bedrock: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_id: builtins.str model_json_schema: builtins.classmethod @@ -4090,7 +4076,7 @@ trulens_eval.feedback.provider.cortex.Cortex: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -4098,7 +4084,7 @@ trulens_eval.feedback.provider.cortex.Cortex: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4171,14 +4157,14 @@ trulens_eval.feedback.provider.endpoint.BedrockEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4237,14 +4223,14 @@ trulens_eval.feedback.provider.endpoint.CortexEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4312,14 +4298,14 @@ trulens_eval.feedback.provider.endpoint.DummyEndpoint: load: builtins.staticmethod loading_prob: builtins.property loading_time: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4382,14 +4368,14 @@ trulens_eval.feedback.provider.endpoint.Endpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4450,14 +4436,14 @@ trulens_eval.feedback.provider.endpoint.HuggingfaceEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4518,14 +4504,14 @@ trulens_eval.feedback.provider.endpoint.LangchainEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4584,14 +4570,14 @@ trulens_eval.feedback.provider.endpoint.LiteLLMEndpoint: json: builtins.function litellm_provider: builtins.str load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4640,14 +4626,14 @@ trulens_eval.feedback.provider.endpoint.OpenAIClient: formatted_objects: _contextvars.ContextVar from_orm: builtins.classmethod json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4690,14 +4676,14 @@ trulens_eval.feedback.provider.endpoint.OpenAIEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4776,14 +4762,14 @@ trulens_eval.feedback.provider.endpoint.base.DummyEndpoint: load: builtins.staticmethod loading_prob: builtins.property loading_time: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4846,14 +4832,14 @@ trulens_eval.feedback.provider.endpoint.base.Endpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4906,14 +4892,14 @@ trulens_eval.feedback.provider.endpoint.base.EndpointCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -4957,14 +4943,14 @@ trulens_eval.feedback.provider.endpoint.bedrock.BedrockCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5007,14 +4993,14 @@ trulens_eval.feedback.provider.endpoint.bedrock.BedrockEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5075,14 +5061,14 @@ trulens_eval.feedback.provider.endpoint.cortex.CortexCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5124,14 +5110,14 @@ trulens_eval.feedback.provider.endpoint.cortex.CortexEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5191,14 +5177,14 @@ trulens_eval.feedback.provider.endpoint.hugs.HuggingfaceCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5243,14 +5229,14 @@ trulens_eval.feedback.provider.endpoint.hugs.HuggingfaceEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5312,14 +5298,14 @@ trulens_eval.feedback.provider.endpoint.langchain.LangchainCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5362,14 +5348,14 @@ trulens_eval.feedback.provider.endpoint.langchain.LangchainEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5429,14 +5415,14 @@ trulens_eval.feedback.provider.endpoint.litellm.LiteLLMCallback: handle_generation: builtins.function handle_generation_chunk: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5479,14 +5465,14 @@ trulens_eval.feedback.provider.endpoint.litellm.LiteLLMEndpoint: json: builtins.function litellm_provider: builtins.str load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5549,14 +5535,14 @@ trulens_eval.feedback.provider.endpoint.openai.OpenAICallback: handle_generation_chunk: builtins.function json: builtins.function langchain_handler: langchain_community.callbacks.openai_info.OpenAICallbackHandler - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5589,14 +5575,14 @@ trulens_eval.feedback.provider.endpoint.openai.OpenAIClient: formatted_objects: _contextvars.ContextVar from_orm: builtins.classmethod json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5639,14 +5625,14 @@ trulens_eval.feedback.provider.endpoint.openai.OpenAIEndpoint: instrumented_methods: collections.defaultdict json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5716,14 +5702,14 @@ trulens_eval.feedback.provider.hugs.Dummy: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5766,14 +5752,14 @@ trulens_eval.feedback.provider.hugs.Huggingface: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5816,14 +5802,14 @@ trulens_eval.feedback.provider.hugs.HuggingfaceBase: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5866,14 +5852,14 @@ trulens_eval.feedback.provider.hugs.HuggingfaceLocal: json: builtins.function language_match: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -5948,7 +5934,7 @@ trulens_eval.feedback.provider.langchain.Langchain: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -5956,7 +5942,7 @@ trulens_eval.feedback.provider.langchain.Langchain: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6038,7 +6024,7 @@ trulens_eval.feedback.provider.litellm.LiteLLM: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -6046,7 +6032,7 @@ trulens_eval.feedback.provider.litellm.LiteLLM: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6129,7 +6115,7 @@ trulens_eval.feedback.provider.openai.AzureOpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -6137,7 +6123,7 @@ trulens_eval.feedback.provider.openai.AzureOpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6221,7 +6207,7 @@ trulens_eval.feedback.provider.openai.OpenAI: misogyny: builtins.function misogyny_with_cot_reasons: builtins.function model_agreement: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function @@ -6229,7 +6215,7 @@ trulens_eval.feedback.provider.openai.OpenAI: model_dump_json: builtins.function model_engine: builtins.str model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6459,14 +6445,14 @@ trulens_eval.schema.app.AppDefinition: jsonify_extra: builtins.function load: builtins.staticmethod metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6569,14 +6555,14 @@ trulens_eval.schema.feedback.FeedbackCall: from_orm: builtins.classmethod json: builtins.function meta: typing.Dict[builtins.str, typing.Any] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6634,14 +6620,14 @@ trulens_eval.schema.feedback.FeedbackDefinition: builtins.NoneType] json: builtins.function load: builtins.staticmethod - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6707,14 +6693,14 @@ trulens_eval.schema.feedback.FeedbackResult: from_orm: builtins.classmethod json: builtins.function last_ts: datetime.datetime - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6814,14 +6800,14 @@ trulens_eval.schema.record.Record: builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] meta: typing.Union[builtins.str, builtins.int, builtins.float, builtins.bytes, builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6860,14 +6846,14 @@ trulens_eval.schema.record.RecordAppCall: from_orm: builtins.classmethod json: builtins.function method: builtins.property - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -6904,14 +6890,14 @@ trulens_eval.schema.record.RecordAppCallMethod: from_orm: builtins.classmethod json: builtins.function method: trulens.core.utils.pyschema.Method - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7001,14 +6987,14 @@ trulens_eval.tru.Tru: get_records_and_feedback: builtins.function json: builtins.function migrate_database: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7087,14 +7073,14 @@ trulens_eval.tru_basic_app.TruBasicApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7129,9 +7115,7 @@ trulens_eval.tru_basic_app.TruBasicApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7232,14 +7216,14 @@ trulens_eval.tru_chain.TruChain: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7274,9 +7258,7 @@ trulens_eval.tru_chain.TruChain: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7342,14 +7324,14 @@ trulens_eval.tru_custom_app.TruCustomApp: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7384,9 +7366,7 @@ trulens_eval.tru_custom_app.TruCustomApp: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7472,14 +7452,14 @@ trulens_eval.tru_llama.TruLlama: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7515,9 +7495,7 @@ trulens_eval.tru_llama.TruLlama: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7640,14 +7618,14 @@ trulens_eval.tru_rails.TruRails: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7682,9 +7660,7 @@ trulens_eval.tru_rails.TruRails: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7749,14 +7725,14 @@ trulens_eval.tru_virtual.TruVirtual: manage_pending_feedback_results_thread: typing.Optional[trulens.core.utils.threading.Thread, builtins.NoneType] metadata: typing.Dict - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -7791,9 +7767,7 @@ trulens_eval.tru_virtual.TruVirtual: selector_check_warning: builtins.bool selector_nocheck: builtins.bool session: trulens.core.session.TruSession - span_context: typing.Optional[contextlib.AbstractContextManager, builtins.NoneType] tags: builtins.str - tokens: typing.List[builtins.object] tru: builtins.property tru_class_info: trulens.core.utils.pyschema.Class update: builtins.function @@ -7840,14 +7814,14 @@ trulens_eval.tru_virtual.VirtualRecord: builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] meta: typing.Union[builtins.str, builtins.int, builtins.float, builtins.bytes, builtins.NoneType, typing.Sequence[typing.Any], typing.Dict[builtins.str, typing.Any]] - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8181,14 +8155,14 @@ trulens_eval.utils.pyschema.Bindings: json: builtins.function kwargs: typing.Dict[builtins.str, typing.Any] load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8221,14 +8195,14 @@ trulens_eval.utils.pyschema.Class: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8265,14 +8239,14 @@ trulens_eval.utils.pyschema.Function: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8306,14 +8280,14 @@ trulens_eval.utils.pyschema.FunctionOrMethod: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8344,14 +8318,14 @@ trulens_eval.utils.pyschema.Method: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8385,14 +8359,14 @@ trulens_eval.utils.pyschema.Module: from_orm: builtins.classmethod json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8429,14 +8403,14 @@ trulens_eval.utils.pyschema.Obj: init_bindings: typing.Optional[trulens.core.utils.pyschema.Bindings, builtins.NoneType] json: builtins.function load: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8589,14 +8563,14 @@ trulens_eval.utils.serial.Collect: get: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8627,14 +8601,14 @@ trulens_eval.utils.serial.GetAttribute: get_item_or_attribute: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8664,14 +8638,14 @@ trulens_eval.utils.serial.GetIndex: get_sole_item: builtins.function index: builtins.int json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8701,14 +8675,14 @@ trulens_eval.utils.serial.GetIndices: get_sole_item: builtins.function indices: typing.Tuple[builtins.int, ...] json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8739,14 +8713,14 @@ trulens_eval.utils.serial.GetItem: get_sole_item: builtins.function item: builtins.str json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8777,14 +8751,14 @@ trulens_eval.utils.serial.GetItemOrAttribute: get_sole_item: builtins.function item_or_attribute: builtins.str json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8814,14 +8788,14 @@ trulens_eval.utils.serial.GetItems: get_sole_item: builtins.function items: typing.Tuple[builtins.str, ...] json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8850,14 +8824,14 @@ trulens_eval.utils.serial.GetSlice: get: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod @@ -8939,14 +8913,14 @@ trulens_eval.utils.serial.StepItemOrAttribute: get_item_or_attribute: builtins.function get_sole_item: builtins.function json: builtins.function - model_computed_fields: builtins.property + model_computed_fields: builtins.dict model_config: builtins.dict model_construct: builtins.classmethod model_copy: builtins.function model_dump: builtins.function model_dump_json: builtins.function model_extra: builtins.property - model_fields: builtins.property + model_fields: builtins.dict model_fields_set: builtins.property model_json_schema: builtins.classmethod model_parametrized_name: builtins.classmethod diff --git a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv index c9401397b..9b44edf4d 100644 --- a/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv +++ b/tests/unit/static/golden/test_otel_instrument__test_instrument_decorator.csv @@ -1,11 +1,11 @@ ,record,event_id,record_attributes,record_type,resource_attributes,start_timestamp,timestamp,trace -0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",12269466148167242934,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337629,2025-01-17 08:29:31.337859,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '', 'span_id': '12269466148167242934'}" -1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '12269466148167242934', 'status': 'STATUS_CODE_UNSET'}",5487251687324701636,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337679,2025-01-17 08:29:31.337855,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '12269466148167242934', 'span_id': '5487251687324701636'}" -2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '5487251687324701636', 'status': 'STATUS_CODE_UNSET'}",14881017561913179896,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337700,2025-01-17 08:29:31.337809,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '5487251687324701636', 'span_id': '14881017561913179896'}" -3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '14881017561913179896', 'status': 'STATUS_CODE_UNSET'}",11276744325636403591,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337721,2025-01-17 08:29:31.337793,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '14881017561913179896', 'span_id': '11276744325636403591'}" -4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '11276744325636403591', 'status': 'STATUS_CODE_UNSET'}",4737367276529506671,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '3ef7be96-ba1f-4736-8938-76b102dcb085', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337738,2025-01-17 08:29:31.337769,"{'trace_id': '275913292593599765173059402816087688189', 'parent_id': '11276744325636403591', 'span_id': '4737367276529506671'}" -5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",13217322988179622548,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337909,2025-01-17 08:29:31.339835,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '', 'span_id': '13217322988179622548'}" -6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '13217322988179622548', 'status': 'STATUS_CODE_UNSET'}",12584259857373781582,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.337936,2025-01-17 08:29:31.339832,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '13217322988179622548', 'span_id': '12584259857373781582'}" -7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '12584259857373781582', 'status': 'STATUS_CODE_UNSET'}",16969196265702745985,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.338030,2025-01-17 08:29:31.339781,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '12584259857373781582', 'span_id': '16969196265702745985'}" -8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '16969196265702745985', 'status': 'STATUS_CODE_UNSET'}",16087567105887777689,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.338049,2025-01-17 08:29:31.339768,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '16969196265702745985', 'span_id': '16087567105887777689'}" -9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '16087567105887777689', 'status': 'STATUS_CODE_ERROR'}",2459351220635848964,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': '734cf7f3-654f-4ce1-88f6-fe74b59e0ade', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 08:29:31.338069,2025-01-17 08:29:31.339740,"{'trace_id': '190256847933985164652421984941536640794', 'parent_id': '16087567105887777689', 'span_id': '2459351220635848964'}" +0,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",885011916882347755,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'f9435ad0-0aa8-4912-8577-1841c3fb9a3b', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': 'f9435ad0-0aa8-4912-8577-1841c3fb9a3b'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511397,2025-01-17 09:15:29.511639,"{'trace_id': '229793490399270739278186354077987963789', 'parent_id': '', 'span_id': '885011916882347755'}" +1,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '885011916882347755', 'status': 'STATUS_CODE_UNSET'}",13011168907639666049,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'f9435ad0-0aa8-4912-8577-1841c3fb9a3b', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.main.main_input': 'test', 'trulens.main.main_output': 'answer: nested: nested2: nested3'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511447,2025-01-17 09:15:29.511634,"{'trace_id': '229793490399270739278186354077987963789', 'parent_id': '885011916882347755', 'span_id': '13011168907639666049'}" +2,"{'name': 'nested', 'kind': 1, 'parent_span_id': '13011168907639666049', 'status': 'STATUS_CODE_UNSET'}",9906100531505755695,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'f9435ad0-0aa8-4912-8577-1841c3fb9a3b', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511469,2025-01-17 09:15:29.511589,"{'trace_id': '229793490399270739278186354077987963789', 'parent_id': '13011168907639666049', 'span_id': '9906100531505755695'}" +3,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '9906100531505755695', 'status': 'STATUS_CODE_UNSET'}",16110296682980220575,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'f9435ad0-0aa8-4912-8577-1841c3fb9a3b', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested2_ret': 'nested2: nested3', 'trulens.unknown.nested2_args[1]': 'test'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511496,2025-01-17 09:15:29.511575,"{'trace_id': '229793490399270739278186354077987963789', 'parent_id': '9906100531505755695', 'span_id': '16110296682980220575'}" +4,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '16110296682980220575', 'status': 'STATUS_CODE_UNSET'}",3442080712927737532,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'f9435ad0-0aa8-4912-8577-1841c3fb9a3b', 'trulens.run_name': 'test run', 'trulens.input_id': '456', 'trulens.unknown.nested3_ret': 'nested3', 'trulens.special.nested3_ret': 'nested3', 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511518,2025-01-17 09:15:29.511549,"{'trace_id': '229793490399270739278186354077987963789', 'parent_id': '16110296682980220575', 'span_id': '3442080712927737532'}" +5,"{'name': 'root', 'kind': 1, 'parent_span_id': '', 'status': 'STATUS_CODE_UNSET'}",15303401343977328208,"{'name': 'root', 'trulens.span_type': 'record_root', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'de41b86d-1d0a-4eff-b8d2-54dfed057c9c', 'trulens.record_root.app_name': 'default_app', 'trulens.record_root.app_version': 'base', 'trulens.record_root.record_id': 'de41b86d-1d0a-4eff-b8d2-54dfed057c9c'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511689,2025-01-17 09:15:29.513703,"{'trace_id': '45900063213956915984249090167208037512', 'parent_id': '', 'span_id': '15303401343977328208'}" +6,"{'name': 'respond_to_query', 'kind': 1, 'parent_span_id': '15303401343977328208', 'status': 'STATUS_CODE_UNSET'}",547417776814034864,"{'name': 'respond_to_query', 'trulens.span_type': 'main', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'de41b86d-1d0a-4eff-b8d2-54dfed057c9c', 'trulens.main.main_input': 'throw', 'trulens.main.main_output': 'answer: nested: nested2: '}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511724,2025-01-17 09:15:29.513694,"{'trace_id': '45900063213956915984249090167208037512', 'parent_id': '15303401343977328208', 'span_id': '547417776814034864'}" +7,"{'name': 'nested', 'kind': 1, 'parent_span_id': '547417776814034864', 'status': 'STATUS_CODE_UNSET'}",11517410471449394114,"{'name': 'nested', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'de41b86d-1d0a-4eff-b8d2-54dfed057c9c', 'trulens.unknown.nested_attr1': 'value1'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511818,2025-01-17 09:15:29.513600,"{'trace_id': '45900063213956915984249090167208037512', 'parent_id': '547417776814034864', 'span_id': '11517410471449394114'}" +8,"{'name': 'nested2', 'kind': 1, 'parent_span_id': '11517410471449394114', 'status': 'STATUS_CODE_UNSET'}",16753019682932711026,"{'name': 'nested2', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'de41b86d-1d0a-4eff-b8d2-54dfed057c9c', 'trulens.unknown.nested2_ret': 'nested2: ', 'trulens.unknown.nested2_args[1]': 'throw'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511848,2025-01-17 09:15:29.513581,"{'trace_id': '45900063213956915984249090167208037512', 'parent_id': '11517410471449394114', 'span_id': '16753019682932711026'}" +9,"{'name': 'nested3', 'kind': 1, 'parent_span_id': '16753019682932711026', 'status': 'STATUS_CODE_ERROR'}",11872166416056529822,"{'name': 'nested3', 'trulens.span_type': 'unknown', 'trulens.app_name': 'default_app', 'trulens.app_version': 'base', 'trulens.record_id': 'de41b86d-1d0a-4eff-b8d2-54dfed057c9c', 'trulens.unknown.nested3_ex': ['nested3 exception'], 'trulens.special.nested3_ex': ['nested3 exception'], 'trulens.unknown.selector_name': 'special', 'trulens.unknown.cows': 'moo', 'trulens.special.cows': 'moo'}",EventRecordType.SPAN,"{'telemetry.sdk.language': 'python', 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': '1.29.0', 'service.name': 'trulens'}",2025-01-17 09:15:29.511866,2025-01-17 09:15:29.513523,"{'trace_id': '45900063213956915984249090167208037512', 'parent_id': '16753019682932711026', 'span_id': '11872166416056529822'}" From 4e27038e87a57349c0409fe4fe4e58650713fae9 Mon Sep 17 00:00:00 2001 From: Garett Tok Ern Liang Date: Fri, 17 Jan 2025 09:28:29 -0800 Subject: [PATCH 68/68] PR feedback --- .../experimental/otel_tracing/core/instrument.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/core/trulens/experimental/otel_tracing/core/instrument.py b/src/core/trulens/experimental/otel_tracing/core/instrument.py index 58adfbf56..c51417b7e 100644 --- a/src/core/trulens/experimental/otel_tracing/core/instrument.py +++ b/src/core/trulens/experimental/otel_tracing/core/instrument.py @@ -127,6 +127,9 @@ def __init__(self, *, app: core_app.App, run_name: str, input_id: str): # Calling set_baggage does not actually add the baggage to the current context, but returns a new one # To avoid issues with remembering to add/remove the baggage, we attach it to the runtime context. def attach_to_context(self, key: str, value: object): + if get_baggage(key) or value is None: + return + self.tokens.append(context_api.attach(set_baggage(key, value))) self.context_keys_added.append(key) @@ -138,19 +141,12 @@ def __enter__(self): tracer = trace.get_tracer_provider().get_tracer(TRULENS_SERVICE_NAME) - # There might be nested records - in those scenarios use the outer one. - if not get_baggage(SpanAttributes.RECORD_ID): - self.attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) - + self.attach_to_context(SpanAttributes.RECORD_ID, otel_record_id) self.attach_to_context(SpanAttributes.APP_NAME, self.app.app_name) self.attach_to_context(SpanAttributes.APP_VERSION, self.app.app_version) - if self.run_name: - logger.debug(f"Setting run name: '{self.run_name}'") - self.attach_to_context(SpanAttributes.RUN_NAME, self.run_name) - - if self.input_id: - self.attach_to_context(SpanAttributes.INPUT_ID, self.input_id) + self.attach_to_context(SpanAttributes.RUN_NAME, self.run_name) + self.attach_to_context(SpanAttributes.INPUT_ID, self.input_id) # Use start_as_current_span as a context manager self.span_context = tracer.start_as_current_span("root")