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