1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
//! In-memory structured system log events for the current Agentty process.
use std::collections::VecDeque;
/// Maximum number of system log entries retained for the current process.
pub const SYSTEM_LOG_LIMIT: usize = 1000;
/// Source area associated with a system log entry.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemLogCategory {
/// Forge operations such as pull-request or merge-request refreshes.
Forge,
/// Git synchronization and branch status operations.
Git,
/// Session lifecycle and status changes.
Session,
/// Manual or background project synchronization.
Sync,
/// Process-level startup and housekeeping events.
System,
/// Version-check and self-update operations.
Update,
}
impl SystemLogCategory {
/// Returns the compact label rendered in the logs page.
pub const fn label(self) -> &'static str {
match self {
Self::Forge => "forge",
Self::Git => "git",
Self::Session => "session",
Self::Sync => "sync",
Self::System => "system",
Self::Update => "update",
}
}
}
/// Severity used to highlight system log entries.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SystemLogLevel {
/// Informational lifecycle or background-work event.
Info,
/// Successful completion of a notable operation.
Success,
/// Recoverable failure or degraded workflow.
Warning,
/// Operation failure that needs user attention.
Error,
}
impl SystemLogLevel {
/// Returns the compact uppercase label rendered in the logs page.
pub const fn label(self) -> &'static str {
match self {
Self::Info => "INFO",
Self::Success => "OK",
Self::Warning => "WARN",
Self::Error => "ERR",
}
}
}
/// Untimestamped system log payload emitted by app and background producers.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SystemLogEvent {
/// Source area associated with this event.
pub category: SystemLogCategory,
/// Optional secondary detail rendered after the primary message.
pub detail: Option<String>,
/// Highlight severity for this event.
pub level: SystemLogLevel,
/// Primary human-readable event text.
pub message: String,
}
impl SystemLogEvent {
/// Creates a structured system log event.
pub fn new(
level: SystemLogLevel,
category: SystemLogCategory,
message: impl Into<String>,
) -> Self {
Self {
category,
detail: None,
level,
message: message.into(),
}
}
/// Attaches secondary detail to the event.
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
/// Timestamped system log entry stored in the process-local buffer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SystemLogEntry {
/// Source area associated with this entry.
pub category: SystemLogCategory,
/// Optional secondary detail rendered after the primary message.
pub detail: Option<String>,
/// Highlight severity for this entry.
pub level: SystemLogLevel,
/// Primary human-readable entry text.
pub message: String,
/// Process-local monotonically increasing sequence number.
pub sequence: u64,
/// Wall-clock Unix timestamp in seconds recorded by the reducer.
pub timestamp_unix_seconds: i64,
}
/// Process-local bounded system log buffer.
pub struct SystemLogBuffer {
entries: VecDeque<SystemLogEntry>,
next_sequence: u64,
}
impl SystemLogBuffer {
/// Returns the entries retained in chronological order.
pub fn entries(&self) -> &VecDeque<SystemLogEntry> {
&self.entries
}
/// Returns whether the buffer currently has no entries.
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// Returns the number of retained entries.
pub fn len(&self) -> usize {
self.entries.len()
}
/// Records one event with a reducer-supplied timestamp and trims the
/// oldest entries when the process-local limit is exceeded.
pub fn push(&mut self, timestamp_unix_seconds: i64, event: SystemLogEvent) {
self.entries.push_back(SystemLogEntry {
category: event.category,
detail: event.detail,
level: event.level,
message: event.message,
sequence: self.next_sequence,
timestamp_unix_seconds,
});
self.next_sequence = self.next_sequence.saturating_add(1);
while self.entries.len() > SYSTEM_LOG_LIMIT {
self.entries.pop_front();
}
}
}
impl Default for SystemLogBuffer {
/// Creates an empty process-local system log buffer.
fn default() -> Self {
Self {
entries: VecDeque::with_capacity(SYSTEM_LOG_LIMIT),
next_sequence: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Verifies pushed events are timestamped and sequenced in insertion
/// order.
#[test]
fn push_records_timestamped_entries_in_order() {
// Arrange
let mut buffer = SystemLogBuffer::default();
// Act
buffer.push(
10,
SystemLogEvent::new(
SystemLogLevel::Info,
SystemLogCategory::System,
"Agentty started",
),
);
buffer.push(
11,
SystemLogEvent::new(
SystemLogLevel::Success,
SystemLogCategory::Sync,
"Sync complete",
)
.with_detail("main"),
);
// Assert
let entries = buffer.entries().iter().collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].sequence, 0);
assert_eq!(entries[0].timestamp_unix_seconds, 10);
assert_eq!(entries[0].message, "Agentty started");
assert_eq!(entries[1].sequence, 1);
assert_eq!(entries[1].detail.as_deref(), Some("main"));
}
/// Verifies the bounded buffer drops oldest entries past the retention
/// limit.
#[test]
fn push_purges_oldest_entries_after_limit() {
// Arrange
let mut buffer = SystemLogBuffer::default();
// Act
for index in 0..=SYSTEM_LOG_LIMIT {
let timestamp = i64::try_from(index).expect("test index should fit in i64");
buffer.push(
timestamp,
SystemLogEvent::new(
SystemLogLevel::Info,
SystemLogCategory::System,
format!("event {index}"),
),
);
}
// Assert
assert_eq!(buffer.len(), SYSTEM_LOG_LIMIT);
assert_eq!(
buffer.entries().front().map(|entry| entry.sequence),
Some(1)
);
assert_eq!(
buffer.entries().back().map(|entry| entry.sequence),
Some(SYSTEM_LOG_LIMIT as u64)
);
}
}