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    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}