1use std::sync::Arc;
2
3use parking_lot::RwLock;
4use time::OffsetDateTime;
5
6use crate::{LogLevel, sink::SinkEvent};
7
8#[derive(Clone, Debug, Default)]
13pub struct LogStore {
14 inner: Arc<RwLock<Vec<LogEntry>>>,
15}
16
17impl LogStore {
18 #[must_use]
20 pub fn new() -> Self {
21 Self::default()
22 }
23
24 #[must_use]
26 pub fn len(&self) -> usize {
27 self.inner.read().len()
28 }
29
30 #[must_use]
32 pub fn is_empty(&self) -> bool {
33 self.inner.read().is_empty()
34 }
35
36 #[must_use]
38 pub fn entries(&self) -> Vec<LogEntry> {
39 self.inner.read().clone()
40 }
41
42 #[must_use]
46 pub fn query(&self, filter: &LogFilter) -> Vec<LogEntry> {
47 let entries = self.inner.read();
48 match filter.limit {
49 Some(0) => Vec::new(),
50 Some(limit) => {
51 let mut recent = Vec::with_capacity(limit.min(entries.len()));
52 for entry in entries.iter().rev() {
53 if filter.matches(entry) {
54 recent.push(entry.clone());
55 if recent.len() == limit {
56 break;
57 }
58 }
59 }
60 recent.reverse();
61 recent
62 }
63 None => entries
64 .iter()
65 .filter(|entry| filter.matches(entry))
66 .cloned()
67 .collect(),
68 }
69 }
70
71 pub(crate) fn push(&self, entry: LogEntry) {
72 self.inner.write().push(entry);
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct LogEntry {
79 timestamp: OffsetDateTime,
80 level: LogLevel,
81 target: String,
82 message: String,
83 fields: Vec<LogField>,
84 rendered: String,
85}
86
87impl LogEntry {
88 pub(crate) fn from_sink_event(event: &SinkEvent<'_>) -> Self {
89 Self {
90 timestamp: event.timestamp,
91 level: event.level,
92 target: event.target.to_owned(),
93 message: event.message.to_owned(),
94 fields: event.fields.iter().map(LogField::from).collect(),
95 rendered: event.rendered.to_owned(),
96 }
97 }
98
99 #[must_use]
101 pub fn timestamp(&self) -> OffsetDateTime {
102 self.timestamp
103 }
104
105 #[must_use]
107 pub fn level(&self) -> LogLevel {
108 self.level
109 }
110
111 #[must_use]
113 pub fn target(&self) -> &str {
114 &self.target
115 }
116
117 #[must_use]
119 pub fn message(&self) -> &str {
120 &self.message
121 }
122
123 #[must_use]
125 pub fn fields(&self) -> &[LogField] {
126 &self.fields
127 }
128
129 #[must_use]
131 pub fn rendered(&self) -> &str {
132 &self.rendered
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct LogField {
139 name: String,
140 value: String,
141}
142
143impl LogField {
144 #[must_use]
146 pub fn name(&self) -> &str {
147 &self.name
148 }
149
150 #[must_use]
152 pub fn value(&self) -> &str {
153 &self.value
154 }
155}
156
157impl From<&crate::format::CapturedField> for LogField {
158 fn from(field: &crate::format::CapturedField) -> Self {
159 Self {
160 name: field.name.clone(),
161 value: field.value.clone(),
162 }
163 }
164}
165
166#[derive(Debug, Clone, Default, PartialEq, Eq)]
168pub struct LogFilter {
169 max_level: Option<LogLevel>,
170 target: Option<String>,
171 target_prefix: Option<String>,
172 text: Option<String>,
173 limit: Option<usize>,
174}
175
176impl LogFilter {
177 #[must_use]
179 pub fn new() -> Self {
180 Self::default()
181 }
182
183 #[must_use]
185 pub fn with_max_level(mut self, level: LogLevel) -> Self {
186 self.max_level = Some(level);
187 self
188 }
189
190 #[must_use]
192 pub fn with_target(mut self, target: impl Into<String>) -> Self {
193 self.target = Some(target.into());
194 self
195 }
196
197 #[must_use]
199 pub fn with_target_prefix(mut self, prefix: impl Into<String>) -> Self {
200 self.target_prefix = Some(prefix.into());
201 self
202 }
203
204 #[must_use]
209 pub fn containing_text(mut self, text: impl Into<String>) -> Self {
210 self.text = Some(text.into());
211 self
212 }
213
214 #[must_use]
216 pub fn with_limit(mut self, limit: usize) -> Self {
217 self.limit = Some(limit);
218 self
219 }
220
221 fn matches(&self, entry: &LogEntry) -> bool {
222 if self
223 .max_level
224 .is_some_and(|max_level| entry.level > max_level)
225 {
226 return false;
227 }
228
229 if self
230 .target
231 .as_ref()
232 .is_some_and(|target| entry.target != *target)
233 {
234 return false;
235 }
236
237 if self
238 .target_prefix
239 .as_ref()
240 .is_some_and(|prefix| !entry.target.starts_with(prefix))
241 {
242 return false;
243 }
244
245 if self
246 .text
247 .as_ref()
248 .is_some_and(|text| !entry_contains_text(entry, text))
249 {
250 return false;
251 }
252
253 true
254 }
255}
256
257fn entry_contains_text(entry: &LogEntry, text: &str) -> bool {
258 entry.message.contains(text)
259 || entry.rendered.contains(text)
260 || entry
261 .fields
262 .iter()
263 .any(|field| field.name.contains(text) || field.value.contains(text))
264}