1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ import datetime
16+ import io
1517import json
1618import os
1719import unittest
1820from typing import Any , Mapping , Optional
19- from unittest .mock import patch
21+ from unittest .mock import Mock , patch
22+
23+ from pydantic import BaseModel
2024
2125from opentelemetry import trace
2226from opentelemetry .instrumentation ._semconv import (
4751 OutputMessage ,
4852 Text ,
4953)
50- from opentelemetry .util .genai .utils import get_content_capturing_mode
54+ from opentelemetry .util .genai .utils import (
55+ gen_ai_json_dump ,
56+ gen_ai_json_dumps ,
57+ get_content_capturing_mode ,
58+ )
5159
5260
5361def patch_env_vars (stability_mode , content_capturing ):
@@ -145,12 +153,12 @@ class TestVersion(unittest.TestCase):
145153 stability_mode = "gen_ai_latest_experimental" ,
146154 content_capturing = "SPAN_ONLY" ,
147155 )
148- def test_get_content_capturing_mode_parses_valid_envvar (self ): # pylint: disable=no-self-use
156+ def test_get_content_capturing_mode_parses_valid_envvar (
157+ self ,
158+ ): # pylint: disable=no-self-use
149159 assert get_content_capturing_mode () == ContentCapturingMode .SPAN_ONLY
150160
151- @patch_env_vars (
152- stability_mode = "gen_ai_latest_experimental" , content_capturing = ""
153- )
161+ @patch_env_vars (stability_mode = "gen_ai_latest_experimental" , content_capturing = "" )
154162 def test_empty_content_capturing_envvar (self ): # pylint: disable=no-self-use
155163 assert get_content_capturing_mode () == ContentCapturingMode .NO_CONTENT
156164
@@ -169,9 +177,7 @@ def test_get_content_capturing_mode_raises_exception_on_invalid_envvar(
169177 self ,
170178 ): # pylint: disable=no-self-use
171179 with self .assertLogs (level = "WARNING" ) as cm :
172- assert (
173- get_content_capturing_mode () == ContentCapturingMode .NO_CONTENT
174- )
180+ assert get_content_capturing_mode () == ContentCapturingMode .NO_CONTENT
175181 self .assertEqual (len (cm .output ), 1 )
176182 self .assertIn ("INVALID_VALUE is not a valid option for " , cm .output [0 ])
177183
@@ -180,12 +186,8 @@ class TestTelemetryHandler(unittest.TestCase):
180186 def setUp (self ):
181187 self .span_exporter = InMemorySpanExporter ()
182188 tracer_provider = TracerProvider ()
183- tracer_provider .add_span_processor (
184- SimpleSpanProcessor (self .span_exporter )
185- )
186- self .telemetry_handler = get_telemetry_handler (
187- tracer_provider = tracer_provider
188- )
189+ tracer_provider .add_span_processor (SimpleSpanProcessor (self .span_exporter ))
190+ self .telemetry_handler = get_telemetry_handler (tracer_provider = tracer_provider )
189191
190192 def tearDown (self ):
191193 # Clear spans and reset the singleton telemetry handler so each test starts clean
@@ -245,12 +247,8 @@ def test_llm_start_and_stop_creates_span(self): # pylint: disable=no-self-use
245247 },
246248 )
247249
248- input_message = _get_single_message (
249- span_attrs , "gen_ai.input.messages"
250- )
251- output_message = _get_single_message (
252- span_attrs , "gen_ai.output.messages"
253- )
250+ input_message = _get_single_message (span_attrs , "gen_ai.input.messages" )
251+ output_message = _get_single_message (span_attrs , "gen_ai.output.messages" )
254252 _assert_text_message (input_message , "Human" , "hello world" )
255253 _assert_text_message (output_message , "AI" , "hello back" , "stop" )
256254 self .assertEqual (invocation .attributes .get ("custom_attr" ), "value" )
@@ -376,10 +374,7 @@ def test_llm_span_uses_expected_schema_url(self):
376374 instrumentation = getattr (span , "instrumentation_info" , None )
377375
378376 assert instrumentation is not None
379- assert (
380- getattr (instrumentation , "schema_url" , None )
381- == Schemas .V1_37_0 .value
382- )
377+ assert getattr (instrumentation , "schema_url" , None ) == Schemas .V1_37_0 .value
383378
384379 @patch_env_vars (
385380 stability_mode = "gen_ai_latest_experimental" ,
@@ -467,3 +462,68 @@ class BoomError(RuntimeError):
467462 GenAI .GEN_AI_USAGE_OUTPUT_TOKENS : 22 ,
468463 },
469464 )
465+
466+
467+ class TestGenAiJson (unittest .TestCase ):
468+ def test_gen_ai_json_dumps (self ):
469+ class Unserializable :
470+ # pylint: disable=no-self-use
471+ def __str__ (self ):
472+ return "unserializable"
473+
474+ obj = {
475+ "bytes" : b"test" ,
476+ "datetime" : datetime .datetime (2023 , 1 , 1 , 12 , 0 , 0 ),
477+ "date" : datetime .date (2023 , 1 , 1 ),
478+ "unserializable" : Unserializable (),
479+ }
480+ result = gen_ai_json_dumps (obj )
481+ expected = '{"bytes":"dGVzdA==","datetime":"2023-01-01T12:00:00","date":"2023-01-01","unserializable":"unserializable"}'
482+ self .assertEqual (result , expected )
483+
484+ def test_gen_ai_json_dumps_pydantic (self ):
485+ class ExamplePydanticModel (BaseModel ):
486+ datetime_field : datetime .datetime
487+ string_field : str
488+
489+ pydantic_model = ExamplePydanticModel (
490+ datetime_field = datetime .datetime (2023 , 1 , 1 , 12 , 0 , 0 ),
491+ string_field = "test" ,
492+ )
493+ # spy the model
494+ pydantic_model = Mock (wraps = pydantic_model )
495+
496+ result = gen_ai_json_dumps ({"some" : {"pydantic" : [pydantic_model ]}})
497+ self .assertEqual (result , '{"key":"pydantic"}' )
498+
499+ pydantic_model .model_dump_json .assert_called_once ()
500+
501+ def test_gen_ai_json_dump (self ):
502+ class Unserializable :
503+ # pylint: disable=no-self-use
504+ def __str__ (self ):
505+ return "unserializable"
506+
507+ obj = {
508+ "bytes" : b"test" ,
509+ "datetime" : datetime .datetime (2023 , 1 , 1 , 12 , 0 , 0 ),
510+ "date" : datetime .date (2023 , 1 , 1 ),
511+ "unserializable" : Unserializable (),
512+ }
513+ string_io = io .StringIO ()
514+ gen_ai_json_dump (obj , string_io )
515+ result = string_io .getvalue ()
516+ expected = '{"bytes":"dGVzdA==","datetime":"2023-01-01T12:00:00","date":"2023-01-01","unserializable":"unserializable"}'
517+ self .assertEqual (result , expected )
518+
519+ def test_gen_ai_json_dump_pydantic (self ):
520+ class PydanticModel :
521+ # pylint: disable=no-self-use
522+ def model_dump_json (self , ** kwargs ):
523+ return '{"key":"pydantic"}'
524+
525+ obj = PydanticModel ()
526+ string_io = io .StringIO ()
527+ gen_ai_json_dump (obj , string_io )
528+ result = string_io .getvalue ()
529+ self .assertEqual (result , '{"key":"pydantic"}' )
0 commit comments