Skip to main content

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}