ralph_workflow/json_parser/health/
parser_health.rs1use crate::logger::Colors;
6
7#[derive(Debug, Default, Clone, Copy)]
9pub struct ParserHealth {
10 pub total_events: u64,
12 pub parsed_events: u64,
14 pub partial_events: u64,
16 pub ignored_events: u64,
18 pub control_events: u64,
20 pub unknown_events: u64,
22 pub parse_errors: u64,
24}
25
26impl ParserHealth {
27 #[must_use]
29 pub fn new() -> Self {
30 Self::default()
31 }
32
33 pub const fn record_parsed(&mut self) {
35 self.total_events = self.total_events.saturating_add(1);
36 self.parsed_events = self.parsed_events.saturating_add(1);
37 }
38
39 pub const fn record_ignored(&mut self) {
41 self.total_events = self.total_events.saturating_add(1);
42 self.ignored_events = self.ignored_events.saturating_add(1);
43 }
44
45 pub const fn record_unknown_event(&mut self) {
51 self.total_events = self.total_events.saturating_add(1);
52 self.unknown_events = self.unknown_events.saturating_add(1);
53 self.ignored_events = self.ignored_events.saturating_add(1);
54 }
55
56 pub const fn record_parse_error(&mut self) {
58 self.total_events = self.total_events.saturating_add(1);
59 self.parse_errors = self.parse_errors.saturating_add(1);
60 self.ignored_events = self.ignored_events.saturating_add(1);
61 }
62
63 pub const fn record_control_event(&mut self) {
69 self.total_events = self.total_events.saturating_add(1);
70 self.control_events = self.control_events.saturating_add(1);
71 }
72
73 pub const fn record_partial_event(&mut self) {
79 self.total_events = self.total_events.saturating_add(1);
80 self.partial_events = self.partial_events.saturating_add(1);
81 }
82
83 #[must_use]
87 pub fn parse_error_percentage(&self) -> f64 {
88 if self.total_events == 0 {
89 return 0.0;
90 }
91 let percent_hundredths = self
94 .parse_errors
95 .saturating_mul(10000)
96 .checked_div(self.total_events)
97 .unwrap_or(0);
98 let scaled: u32 = u32::try_from(percent_hundredths)
101 .unwrap_or(u32::MAX)
102 .min(10000);
103 f64::from(scaled) / 100.0
104 }
105
106 #[must_use]
110 pub fn parse_error_percentage_int(&self) -> u32 {
111 if self.total_events == 0 {
112 return 0;
113 }
114 self.parse_errors
116 .saturating_mul(100)
117 .checked_div(self.total_events)
118 .and_then(|v| u32::try_from(v).ok())
119 .unwrap_or(0)
120 .min(100)
121 }
122
123 #[must_use]
129 pub fn is_concerning(&self) -> bool {
130 self.total_events > 10 && self.parse_error_percentage() > 50.0
131 }
132
133 #[must_use]
135 pub fn warning(&self, parser_name: &str, colors: Colors) -> Option<String> {
136 if !self.is_concerning() {
137 return None;
138 }
139 Some(self.format_warning_message(parser_name, colors))
140 }
141
142 fn format_warning_message(&self, parser_name: &str, colors: Colors) -> String {
143 let has_extra_events =
144 self.unknown_events > 0 || self.control_events > 0 || self.partial_events > 0;
145 if has_extra_events {
146 self.format_warning_with_extra_events(parser_name, colors)
147 } else {
148 format_basic_warning(
149 parser_name,
150 colors,
151 self.parse_errors,
152 self.parse_error_percentage_int(),
153 self.total_events,
154 )
155 }
156 }
157
158 fn format_warning_with_extra_events(&self, parser_name: &str, colors: Colors) -> String {
159 format!(
160 "{}[Parser Health Warning]{} {} parser has {} parse errors ({}% of {} events). \
161 Also encountered {} unknown event types (valid JSON but unhandled), \
162 {} control events (state management), \
163 and {} partial events (streaming deltas). \
164 This may indicate a parser mismatch. Consider using a different json_parser in your agent config.",
165 colors.yellow(),
166 colors.reset(),
167 parser_name,
168 self.parse_errors,
169 self.parse_error_percentage_int(),
170 self.total_events,
171 self.unknown_events,
172 self.control_events,
173 self.partial_events
174 )
175 }
176}
177
178fn format_basic_warning(
179 parser_name: &str,
180 colors: Colors,
181 parse_errors: u64,
182 error_pct: u32,
183 total_events: u64,
184) -> String {
185 format!(
186 "{}[Parser Health Warning]{} {} parser has {} parse errors ({}% of {} events). \
187 This may indicate malformed JSON output. Consider using a different json_parser in your agent config.",
188 colors.yellow(),
189 colors.reset(),
190 parser_name,
191 parse_errors,
192 error_pct,
193 total_events
194 )
195}