ralph_workflow/json_parser/health/parser_health.rs
1// Parser health statistics.
2//
3// Contains the ParserHealth struct for tracking event processing statistics.
4
5/// Parser health statistics
6#[derive(Debug, Default, Clone, Copy)]
7pub struct ParserHealth {
8 /// Total number of events processed
9 pub total_events: u64,
10 /// Number of events successfully parsed and displayed
11 pub parsed_events: u64,
12 /// Number of partial/delta events (streaming content displayed incrementally)
13 pub partial_events: u64,
14 /// Number of events ignored (malformed JSON, unknown events, etc.)
15 pub ignored_events: u64,
16 /// Number of control events (state management, no user output)
17 pub control_events: u64,
18 /// Number of unknown event types (valid JSON but unhandled)
19 pub unknown_events: u64,
20 /// Number of JSON parse errors (malformed JSON)
21 pub parse_errors: u64,
22}
23
24impl ParserHealth {
25 /// Create a new health tracker
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 /// Record a parsed event
31 pub const fn record_parsed(&mut self) {
32 self.total_events += 1;
33 self.parsed_events += 1;
34 }
35
36 /// Record an ignored event
37 pub const fn record_ignored(&mut self) {
38 self.total_events += 1;
39 self.ignored_events += 1;
40 }
41
42 /// Record an unknown event type (valid JSON but unhandled)
43 ///
44 /// Unknown events are valid JSON that the parser deserialized successfully
45 /// but doesn't have specific handling for. These should not trigger health
46 /// warnings as they represent future/new event types, not parser errors.
47 pub const fn record_unknown_event(&mut self) {
48 self.total_events += 1;
49 self.unknown_events += 1;
50 self.ignored_events += 1;
51 }
52
53 /// Record a parse error (malformed JSON)
54 pub const fn record_parse_error(&mut self) {
55 self.total_events += 1;
56 self.parse_errors += 1;
57 self.ignored_events += 1;
58 }
59
60 /// Record a control event (state management with no user-facing output)
61 ///
62 /// Control events are valid JSON that represent state transitions
63 /// rather than user-facing content. They should not be counted as
64 /// "ignored" for health monitoring purposes.
65 pub const fn record_control_event(&mut self) {
66 self.total_events += 1;
67 self.control_events += 1;
68 }
69
70 /// Record a partial/delta event (streaming content displayed incrementally)
71 ///
72 /// Partial events represent streaming content that is shown to the user
73 /// in real-time as deltas. These are NOT errors and should not trigger
74 /// health warnings. They are tracked separately to show streaming activity.
75 pub const fn record_partial_event(&mut self) {
76 self.total_events += 1;
77 self.partial_events += 1;
78 }
79
80 /// Get the percentage of parse errors (excluding unknown events)
81 ///
82 /// Returns percentage using integer-safe arithmetic to avoid precision loss warnings.
83 pub fn parse_error_percentage(&self) -> f64 {
84 if self.total_events == 0 {
85 return 0.0;
86 }
87 // Use integer arithmetic: (errors * 10000) / total, then divide by 100.0
88 // This gives two decimal places of precision without casting u64 to f64
89 let percent_hundredths = self
90 .parse_errors
91 .saturating_mul(10000)
92 .checked_div(self.total_events)
93 .unwrap_or(0);
94 // Convert to f64 only after scaling down to a reasonable range
95 // percent_hundredths is at most 10000 (100% * 100), which fits precisely in f64
96 let scaled: u32 = u32::try_from(percent_hundredths)
97 .unwrap_or(u32::MAX)
98 .min(10000);
99 f64::from(scaled) / 100.0
100 }
101
102 /// Get the percentage of parse errors as a rounded integer.
103 ///
104 /// This is for display purposes where a whole number is sufficient.
105 pub fn parse_error_percentage_int(&self) -> u32 {
106 if self.total_events == 0 {
107 return 0;
108 }
109 // (errors * 100) / total gives us the integer percentage
110 self.parse_errors
111 .saturating_mul(100)
112 .checked_div(self.total_events)
113 .and_then(|v| u32::try_from(v).ok())
114 .unwrap_or(0)
115 .min(100)
116 }
117
118 /// Check if the parser health is concerning
119 ///
120 /// Only returns true if there are actual parse errors (malformed JSON),
121 /// not just unknown event types. Unknown events are valid JSON that we
122 /// don't have specific handling for, which is not a health concern.
123 pub fn is_concerning(&self) -> bool {
124 self.total_events > 10 && self.parse_error_percentage() > 50.0
125 }
126
127 /// Get a warning message if health is concerning
128 pub fn warning(&self, parser_name: &str, colors: Colors) -> Option<String> {
129 if !self.is_concerning() {
130 return None;
131 }
132
133 let msg = if self.unknown_events > 0 || self.control_events > 0 || self.partial_events > 0 {
134 format!(
135 "{}[Parser Health Warning]{} {} parser has {} parse errors ({}% of {} events). \
136 Also encountered {} unknown event types (valid JSON but unhandled), \
137 {} control events (state management), \
138 and {} partial events (streaming deltas). \
139 This may indicate a parser mismatch. Consider using a different json_parser in your agent config.",
140 colors.yellow(),
141 colors.reset(),
142 parser_name,
143 self.parse_errors,
144 self.parse_error_percentage_int(),
145 self.total_events,
146 self.unknown_events,
147 self.control_events,
148 self.partial_events
149 )
150 } else {
151 format!(
152 "{}[Parser Health Warning]{} {} parser has {} parse errors ({}% of {} events). \
153 This may indicate malformed JSON output. Consider using a different json_parser in your agent config.",
154 colors.yellow(),
155 colors.reset(),
156 parser_name,
157 self.parse_errors,
158 self.parse_error_percentage_int(),
159 self.total_events
160 )
161 };
162
163 Some(msg)
164 }
165}