1use rlg::log::Log;
25use rlg::log_format::LogFormat;
26use rlg::log_level::LogLevel;
27
28pub fn parse_record(line: &str) -> Result<Log, serde_json::Error> {
41 serde_json::from_str(line.trim())
42}
43
44#[derive(Debug, Default, Clone)]
46pub struct Filter {
47 pub min_level: Option<LogLevel>,
50 pub component: Option<String>,
53 pub attribute: Option<(String, serde_json::Value)>,
56}
57
58impl Filter {
59 #[must_use]
61 pub const fn new() -> Self {
62 Self {
63 min_level: None,
64 component: None,
65 attribute: None,
66 }
67 }
68
69 #[must_use]
71 pub const fn min_level(mut self, level: LogLevel) -> Self {
72 self.min_level = Some(level);
73 self
74 }
75
76 #[must_use]
78 pub fn component(mut self, component: impl Into<String>) -> Self {
79 self.component = Some(component.into());
80 self
81 }
82
83 #[must_use]
85 pub fn attribute(
86 mut self,
87 key: impl Into<String>,
88 value: serde_json::Value,
89 ) -> Self {
90 self.attribute = Some((key.into(), value));
91 self
92 }
93
94 #[must_use]
96 pub fn matches(&self, record: &Log) -> bool {
97 if let Some(min) = self.min_level
98 && record.level.to_numeric() < min.to_numeric()
99 {
100 return false;
101 }
102 if let Some(comp) = &self.component
103 && record.component.as_ref() != comp.as_str()
104 {
105 return false;
106 }
107 if let Some((key, val)) = &self.attribute
108 && record.attributes.get(key) != Some(val)
109 {
110 return false;
111 }
112 true
113 }
114}
115
116#[must_use]
121pub fn render(mut record: Log, format: LogFormat) -> String {
122 record.format = format;
123 format!("{record}")
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 fn sample_json() -> &'static str {
131 r#"{
132 "session_id": 7,
133 "time": "2026-05-30T00:00:00.000000000Z",
134 "level": "INFO",
135 "component": "svc",
136 "description": "hi",
137 "format": "JSON",
138 "attributes": {"user_id": 42, "region": "eu-west-1"}
139 }"#
140 }
141
142 #[test]
143 fn parse_returns_log_struct() {
144 let log = parse_record(sample_json()).unwrap();
145 assert_eq!(log.session_id, 7);
146 assert_eq!(log.component.as_ref(), "svc");
147 assert_eq!(log.level, LogLevel::INFO);
148 }
149
150 #[test]
151 fn filter_default_passes_everything() {
152 let log = parse_record(sample_json()).unwrap();
153 assert!(Filter::new().matches(&log));
154 }
155
156 #[test]
157 fn filter_min_level_drops_below() {
158 let log = parse_record(sample_json()).unwrap();
159 assert!(!Filter::new().min_level(LogLevel::WARN).matches(&log));
160 assert!(Filter::new().min_level(LogLevel::INFO).matches(&log));
161 assert!(Filter::new().min_level(LogLevel::DEBUG).matches(&log));
162 }
163
164 #[test]
165 fn filter_component_exact_match() {
166 let log = parse_record(sample_json()).unwrap();
167 assert!(Filter::new().component("svc").matches(&log));
168 assert!(!Filter::new().component("other").matches(&log));
169 }
170
171 #[test]
172 fn filter_attribute_exact_match() {
173 let log = parse_record(sample_json()).unwrap();
174 assert!(
175 Filter::new()
176 .attribute("user_id", serde_json::json!(42))
177 .matches(&log)
178 );
179 assert!(
180 !Filter::new()
181 .attribute("user_id", serde_json::json!(99))
182 .matches(&log)
183 );
184 assert!(
185 !Filter::new()
186 .attribute("missing", serde_json::json!(true))
187 .matches(&log)
188 );
189 }
190
191 #[test]
192 fn render_overrides_format() {
193 let log = parse_record(sample_json()).unwrap();
194 let out = render(log, LogFormat::Logfmt);
195 assert!(out.contains("level=info"));
196 assert!(out.contains("session_id=7"));
197 }
198
199 #[test]
200 fn render_to_mcp_wraps_in_jsonrpc() {
201 let log = parse_record(sample_json()).unwrap();
202 let out = render(log, LogFormat::MCP);
203 assert!(out.contains("\"jsonrpc\":\"2.0\""));
204 assert!(out.contains("notifications/log"));
205 }
206
207 #[test]
208 fn parse_rejects_garbage() {
209 assert!(parse_record("not json at all").is_err());
210 }
211}