1616
1717package com .google .adk .plugins .agentanalytics ;
1818
19+ import static java .util .Collections .newSetFromMap ;
20+
1921import com .fasterxml .jackson .databind .JsonNode ;
2022import com .fasterxml .jackson .databind .ObjectMapper ;
2123import com .fasterxml .jackson .databind .node .ArrayNode ;
2224import com .fasterxml .jackson .databind .node .ObjectNode ;
2325import com .google .auto .value .AutoValue ;
2426import com .google .common .base .Utf8 ;
27+ import java .util .IdentityHashMap ;
2528import java .util .Map ;
2629import java .util .Set ;
30+ import java .util .logging .Logger ;
2731import org .jspecify .annotations .Nullable ;
2832
2933/** Utility for parsing, formatting and truncating content for BigQuery logging. */
3034final class JsonFormatter {
35+ private static final Logger logger = Logger .getLogger (JsonFormatter .class .getName ());
3136 static final ObjectMapper mapper = new ObjectMapper ().findAndRegisterModules ();
3237 static final String TRUNCATION_SUFFIX = "...[truncated]" ;
38+ static final String CYCLE_DETECTED_MESSAGE = "[cycle detected]" ;
3339
3440 @ AutoValue
3541 abstract static class TruncationResult {
@@ -47,11 +53,15 @@ static TruncationResult smartTruncate(Object obj, int maxLength) {
4753 if (obj == null ) {
4854 return TruncationResult .create (mapper .nullNode (), false );
4955 }
56+ if (obj instanceof JsonNode jsonNode ) {
57+ return recursiveSmartTruncate (jsonNode , maxLength , newSetFromMap (new IdentityHashMap <>()));
58+ }
5059 try {
51- return recursiveSmartTruncate (mapper .valueToTree (obj ), maxLength );
52- } catch (IllegalArgumentException e ) {
60+ return recursiveSmartTruncate (
61+ mapper .valueToTree (obj ), maxLength , newSetFromMap (new IdentityHashMap <>()));
62+ } catch (IllegalArgumentException | StackOverflowError e ) {
5363 // Fallback for types that mapper can't handle directly as a tree
54- return truncateWithStatus (String . valueOf (obj ), maxLength );
64+ return truncateWithStatus (safeToString (obj ), maxLength );
5565 }
5666 }
5767
@@ -61,39 +71,64 @@ static JsonNode convertToJsonNode(Object obj) {
6171 }
6272 try {
6373 return mapper .valueToTree (obj );
64- } catch (IllegalArgumentException e ) {
65- // Fallback for types that mapper can't handle directly as a tree
66- return mapper .valueToTree (String . valueOf (obj ));
74+ } catch (IllegalArgumentException | StackOverflowError e ) {
75+ // Fallback for types that mapper can't handle directly as a tree.
76+ return mapper .valueToTree (safeToString (obj ));
6777 }
6878 }
6979
70- private static TruncationResult recursiveSmartTruncate (JsonNode node , int maxLength ) {
71- boolean isTruncated = false ;
72- if (node .isTextual ()) {
73- String text = node .asText ();
74- if (Utf8 .encodedLength (text ) > maxLength ) {
75- return TruncationResult .create (mapper .valueToTree (truncate (text , maxLength )), true );
80+ static String safeToString (Object obj ) {
81+ try {
82+ return String .valueOf (obj );
83+ } catch (StackOverflowError e ) {
84+ logger .warning ("StackOverflowError when converting object to string" );
85+ return "[STACK OVERFLOW ERROR CONVERTING TO STRING]" ;
86+ } catch (RuntimeException e ) {
87+ logger .warning ("RuntimeException when converting object to string" );
88+ return "[ERROR CONVERTING TO STRING]" ;
89+ }
90+ }
91+
92+ private static TruncationResult recursiveSmartTruncate (
93+ JsonNode node , int maxLength , Set <JsonNode > visited ) {
94+ if (node .isContainerNode ()) {
95+ if (visited .contains (node )) {
96+ return TruncationResult .create (mapper .valueToTree (CYCLE_DETECTED_MESSAGE ), true );
7697 }
77- return TruncationResult .create (node , false );
78- } else if (node .isObject ()) {
79- ObjectNode newNode = mapper .createObjectNode ();
80- Set <Map .Entry <String , JsonNode >> properties = node .properties ();
81- for (Map .Entry <String , JsonNode > entry : properties ) {
82- TruncationResult res = recursiveSmartTruncate (entry .getValue (), maxLength );
83- newNode .set (entry .getKey (), res .node ());
84- isTruncated = isTruncated || res .isTruncated ();
98+ visited .add (node );
99+ }
100+ try {
101+ boolean isTruncated = false ;
102+ if (node .isTextual ()) {
103+ String text = node .asText ();
104+ if (Utf8 .encodedLength (text ) > maxLength ) {
105+ return TruncationResult .create (mapper .valueToTree (truncate (text , maxLength )), true );
106+ }
107+ return TruncationResult .create (node , false );
108+ } else if (node .isObject ()) {
109+ ObjectNode newNode = mapper .createObjectNode ();
110+ Set <Map .Entry <String , JsonNode >> properties = node .properties ();
111+ for (Map .Entry <String , JsonNode > entry : properties ) {
112+ TruncationResult res = recursiveSmartTruncate (entry .getValue (), maxLength , visited );
113+ newNode .set (entry .getKey (), res .node ());
114+ isTruncated = isTruncated || res .isTruncated ();
115+ }
116+ return TruncationResult .create (newNode , isTruncated );
117+ } else if (node .isArray ()) {
118+ ArrayNode newNode = mapper .createArrayNode ();
119+ for (JsonNode element : node ) {
120+ TruncationResult res = recursiveSmartTruncate (element , maxLength , visited );
121+ newNode .add (res .node ());
122+ isTruncated = isTruncated || res .isTruncated ();
123+ }
124+ return TruncationResult .create (newNode , isTruncated );
85125 }
86- return TruncationResult .create (newNode , isTruncated );
87- } else if (node .isArray ()) {
88- ArrayNode newNode = mapper .createArrayNode ();
89- for (JsonNode element : node ) {
90- TruncationResult res = recursiveSmartTruncate (element , maxLength );
91- newNode .add (res .node ());
92- isTruncated = isTruncated || res .isTruncated ();
126+ return TruncationResult .create (node , false );
127+ } finally {
128+ if (node .isContainerNode ()) {
129+ visited .remove (node );
93130 }
94- return TruncationResult .create (newNode , isTruncated );
95131 }
96- return TruncationResult .create (node , false );
97132 }
98133
99134 static TruncationResult truncateWithStatus (String s , int maxLength ) {
0 commit comments