1use std::future::Future;
2use std::pin::Pin;
3use std::sync::{Arc, Mutex};
4
5use serde::{Deserialize, Serialize};
6
7use crate::event_log::{AnyEventLog, EventLog, LogEvent as EventLogRecord, Topic};
8
9use super::AgentEvent;
10
11pub trait AgentEventSink: Send + Sync {
14 fn handle_event(&self, event: &AgentEvent);
15
16 fn flush(&self) -> AgentEventSinkFlush<'_> {
20 Box::pin(async { Ok(()) })
21 }
22}
23
24pub type AgentEventSinkFlush<'a> =
25 Pin<Box<dyn Future<Output = Result<(), AgentEventSinkError>> + Send + 'a>>;
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct AgentEventSinkError {
29 sink: &'static str,
30 message: String,
31 dropped_events: u64,
32}
33
34impl AgentEventSinkError {
35 pub fn new(sink: &'static str, error: impl std::fmt::Display) -> Self {
36 Self {
37 sink,
38 message: error.to_string(),
39 dropped_events: 0,
40 }
41 }
42
43 fn dropped_event(sink: &'static str, error: impl std::fmt::Display) -> Self {
44 Self {
45 sink,
46 message: error.to_string(),
47 dropped_events: 1,
48 }
49 }
50
51 pub fn sink(&self) -> &'static str {
52 self.sink
53 }
54
55 pub fn message(&self) -> &str {
56 &self.message
57 }
58
59 pub fn dropped_events(&self) -> u64 {
61 self.dropped_events
62 }
63}
64
65impl std::fmt::Display for AgentEventSinkError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 write!(f, "{} sink flush failed: {}", self.sink, self.message)?;
68 if self.dropped_events > 0 {
69 write!(f, " ({} dropped events)", self.dropped_events)?;
70 }
71 Ok(())
72 }
73}
74
75impl std::error::Error for AgentEventSinkError {}
76
77fn record_sink_failure(first_error: &mut Option<AgentEventSinkError>, error: AgentEventSinkError) {
78 if let Some(first) = first_error {
79 first.dropped_events = first.dropped_events.saturating_add(error.dropped_events);
80 } else {
81 *first_error = Some(error);
82 }
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize)]
92pub struct PersistedAgentEvent {
93 pub index: u64,
97 pub emitted_at_ms: i64,
101 pub frame_depth: Option<u32>,
105 #[serde(flatten)]
107 pub event: AgentEvent,
108}
109
110pub struct JsonlEventSink {
115 state: Mutex<JsonlEventSinkState>,
116 base_path: std::path::PathBuf,
117}
118
119struct JsonlEventSinkState {
120 writer: std::io::BufWriter<std::fs::File>,
121 index: u64,
122 bytes_written: u64,
123 rotation: u32,
124 first_error: Option<AgentEventSinkError>,
125}
126
127impl JsonlEventSink {
128 pub const ROTATE_BYTES: u64 = 100 * 1024 * 1024;
132
133 pub fn open(base_path: impl Into<std::path::PathBuf>) -> std::io::Result<Arc<Self>> {
137 let base_path = base_path.into();
138 if let Some(parent) = base_path.parent() {
139 std::fs::create_dir_all(parent)?;
140 }
141 let file = std::fs::OpenOptions::new()
142 .create(true)
143 .truncate(true)
144 .write(true)
145 .open(&base_path)?;
146 Ok(Arc::new(Self {
147 state: Mutex::new(JsonlEventSinkState {
148 writer: std::io::BufWriter::new(file),
149 index: 0,
150 bytes_written: 0,
151 rotation: 0,
152 first_error: None,
153 }),
154 base_path,
155 }))
156 }
157
158 pub fn flush(&self) -> Result<(), AgentEventSinkError> {
161 use std::io::Write as _;
162 let mut state = self.state.lock().expect("jsonl sink mutex poisoned");
163 if let Err(error) = state.writer.flush() {
164 record_sink_failure(
165 &mut state.first_error,
166 AgentEventSinkError::new("jsonl_event", error),
167 );
168 }
169 state.first_error.clone().map_or(Ok(()), Err)
170 }
171
172 pub fn event_count(&self) -> u64 {
175 self.state.lock().expect("jsonl sink mutex poisoned").index
176 }
177
178 fn rotate_if_needed(&self, state: &mut JsonlEventSinkState) -> std::io::Result<()> {
179 use std::io::Write as _;
180 if state.bytes_written < Self::ROTATE_BYTES {
181 return Ok(());
182 }
183 state.writer.flush()?;
184 state.rotation += 1;
185 let suffix = format!("-{:06}", state.rotation);
186 let rotated = self.base_path.with_file_name({
187 let stem = self
188 .base_path
189 .file_stem()
190 .and_then(|s| s.to_str())
191 .unwrap_or("event_log");
192 let ext = self
193 .base_path
194 .extension()
195 .and_then(|e| e.to_str())
196 .unwrap_or("jsonl");
197 format!("{stem}{suffix}.{ext}")
198 });
199 let file = std::fs::OpenOptions::new()
200 .create(true)
201 .truncate(true)
202 .write(true)
203 .open(&rotated)?;
204 state.writer = std::io::BufWriter::new(file);
205 state.bytes_written = 0;
206 Ok(())
207 }
208}
209
210pub struct EventLogSink {
215 dispatch: EventLogSinkDispatch,
216 session_id: String,
217 first_error: Arc<Mutex<Option<AgentEventSinkError>>>,
218}
219
220enum EventLogSinkDispatch {
221 Async(tokio::sync::mpsc::UnboundedSender<EventLogSinkCommand>),
222 Blocking { log: Arc<AnyEventLog>, topic: Topic },
223}
224
225enum EventLogSinkCommand {
226 Append(EventLogRecord),
227 Flush(tokio::sync::oneshot::Sender<Result<(), AgentEventSinkError>>),
228}
229
230impl EventLogSink {
231 pub fn new(log: Arc<AnyEventLog>, session_id: impl Into<String>) -> Arc<Self> {
232 let session_id = session_id.into();
233 let topic = Topic::new(format!(
234 "observability.agent_events.{}",
235 crate::event_log::sanitize_topic_component(&session_id)
236 ))
237 .expect("session id should sanitize to a valid topic");
238 let first_error = Arc::new(Mutex::new(None));
239 let dispatch = if let Ok(handle) = tokio::runtime::Handle::try_current() {
240 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
241 handle.spawn(run_event_log_sink_worker(
242 log,
243 topic,
244 receiver,
245 first_error.clone(),
246 ));
247 EventLogSinkDispatch::Async(sender)
248 } else {
249 EventLogSinkDispatch::Blocking { log, topic }
250 };
251 Arc::new(Self {
252 dispatch,
253 session_id,
254 first_error,
255 })
256 }
257
258 pub async fn flush(&self) -> Result<(), AgentEventSinkError> {
259 match &self.dispatch {
260 EventLogSinkDispatch::Async(sender) => {
261 let (reply, response) = tokio::sync::oneshot::channel();
262 if sender.send(EventLogSinkCommand::Flush(reply)).is_err() {
263 return Err(self.latched_error().unwrap_or_else(|| {
264 AgentEventSinkError::new("event_log", "append worker is unavailable")
265 }));
266 }
267 response.await.unwrap_or_else(|_| {
268 Err(self.latched_error().unwrap_or_else(|| {
269 AgentEventSinkError::new("event_log", "append worker stopped before flush")
270 }))
271 })
272 }
273 EventLogSinkDispatch::Blocking { log, .. } => {
274 let append_error = self.latched_error();
275 let flush_result = flush_event_log(log.clone()).await;
276 append_error.map_or(flush_result, Err)
277 }
278 }
279 }
280
281 fn latched_error(&self) -> Option<AgentEventSinkError> {
282 self.first_error
283 .lock()
284 .expect("event-log sink error mutex poisoned")
285 .clone()
286 }
287
288 #[cfg(test)]
289 pub(crate) fn enqueue_flush_for_test(
290 &self,
291 ) -> tokio::sync::oneshot::Receiver<Result<(), AgentEventSinkError>> {
292 let EventLogSinkDispatch::Async(sender) = &self.dispatch else {
293 panic!("test flush enqueue requires an async event-log sink");
294 };
295 let (reply, response) = tokio::sync::oneshot::channel();
296 sender
297 .send(EventLogSinkCommand::Flush(reply))
298 .expect("event-log sink worker should accept test flush");
299 response
300 }
301}
302
303async fn run_event_log_sink_worker(
304 log: Arc<AnyEventLog>,
305 topic: Topic,
306 mut receiver: tokio::sync::mpsc::UnboundedReceiver<EventLogSinkCommand>,
307 first_error: Arc<Mutex<Option<AgentEventSinkError>>>,
308) {
309 while let Some(command) = receiver.recv().await {
310 match command {
311 EventLogSinkCommand::Append(record) => {
312 if let Err(error) = log.append(&topic, record).await {
313 record_sink_failure(
314 &mut first_error
315 .lock()
316 .expect("event-log sink error mutex poisoned"),
317 AgentEventSinkError::dropped_event("event_log", error),
318 );
319 }
320 }
321 EventLogSinkCommand::Flush(reply) => {
322 let flush_result = flush_event_log(log.clone()).await;
323 let append_error = first_error
324 .lock()
325 .expect("event-log sink error mutex poisoned")
326 .clone();
327 let result = append_error.map_or(flush_result, Err);
328 let _ = reply.send(result);
329 }
330 }
331 }
332}
333
334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335pub(super) enum EventLogFlushSchedule {
336 AsyncExecutor,
337 BlockingPool,
338}
339
340pub(super) fn event_log_flush_schedule(log: &AnyEventLog) -> EventLogFlushSchedule {
341 if matches!(log, AnyEventLog::Sqlite(_)) {
342 EventLogFlushSchedule::BlockingPool
343 } else {
344 EventLogFlushSchedule::AsyncExecutor
345 }
346}
347
348async fn flush_event_log(log: Arc<AnyEventLog>) -> Result<(), AgentEventSinkError> {
349 let result = if event_log_flush_schedule(&log) == EventLogFlushSchedule::BlockingPool
350 && tokio::runtime::Handle::try_current().is_ok()
351 {
352 tokio::task::spawn_blocking(move || futures::executor::block_on(log.flush()))
353 .await
354 .map_err(|error| {
355 AgentEventSinkError::new("event_log", format!("flush task failed: {error}"))
356 })?
357 } else {
358 log.flush().await
359 };
360 result.map_err(|error| AgentEventSinkError::new("event_log", error))
361}
362
363impl AgentEventSink for JsonlEventSink {
364 fn handle_event(&self, event: &AgentEvent) {
365 use std::io::Write as _;
366 let mut state = self.state.lock().expect("jsonl sink mutex poisoned");
367 if state.first_error.is_some() {
368 let error = AgentEventSinkError::dropped_event(
369 "jsonl_event",
370 "sink unavailable after an earlier persistence failure",
371 );
372 record_sink_failure(&mut state.first_error, error);
373 return;
374 }
375 let index = state.index;
376 let emitted_at_ms = std::time::SystemTime::now()
377 .duration_since(std::time::UNIX_EPOCH)
378 .map(|d| d.as_millis() as i64)
379 .unwrap_or(0);
380 let envelope = PersistedAgentEvent {
381 index,
382 emitted_at_ms,
383 frame_depth: None,
384 event: event.clone(),
385 };
386 let mut envelope_json = match serde_json::to_value(&envelope) {
387 Ok(value) => value,
388 Err(error) => {
389 record_sink_failure(
390 &mut state.first_error,
391 AgentEventSinkError::dropped_event("jsonl_event", error),
392 );
393 return;
394 }
395 };
396 crate::redact::current_policy().redact_json_in_place(&mut envelope_json);
397 let mut line = match serde_json::to_vec(&envelope_json) {
398 Ok(line) => line,
399 Err(error) => {
400 record_sink_failure(
401 &mut state.first_error,
402 AgentEventSinkError::dropped_event("jsonl_event", error),
403 );
404 return;
405 }
406 };
407 line.push(b'\n');
408 if let Err(error) = state
409 .writer
410 .write_all(&line)
411 .and_then(|_| state.writer.flush())
412 {
413 record_sink_failure(
414 &mut state.first_error,
415 AgentEventSinkError::dropped_event("jsonl_event", error),
416 );
417 return;
418 }
419 state.index += 1;
420 state.bytes_written += line.len() as u64;
421 if let Err(error) = self.rotate_if_needed(&mut state) {
422 record_sink_failure(
423 &mut state.first_error,
424 AgentEventSinkError::new("jsonl_event", error),
425 );
426 }
427 }
428
429 fn flush(&self) -> AgentEventSinkFlush<'_> {
430 Box::pin(async move { JsonlEventSink::flush(self) })
431 }
432}
433
434impl AgentEventSink for EventLogSink {
435 fn handle_event(&self, event: &AgentEvent) {
436 let event_json = match serde_json::to_value(event) {
437 Ok(value) => value,
438 Err(error) => {
439 record_sink_failure(
440 &mut self
441 .first_error
442 .lock()
443 .expect("event-log sink error mutex poisoned"),
444 AgentEventSinkError::dropped_event("event_log", error),
445 );
446 return;
447 }
448 };
449 let event_kind = event_json
450 .get("type")
451 .and_then(|value| value.as_str())
452 .unwrap_or("agent_event")
453 .to_string();
454 let payload = serde_json::json!({
455 "index_hint": now_ms(),
456 "session_id": self.session_id,
457 "event": event_json,
458 });
459 let mut headers = std::collections::BTreeMap::new();
460 headers.insert("session_id".to_string(), self.session_id.clone());
461 let mut record = EventLogRecord::new(event_kind, payload).with_headers(headers);
462 record.redact_in_place(&crate::redact::current_policy());
463 match &self.dispatch {
464 EventLogSinkDispatch::Async(sender) => {
465 if sender.send(EventLogSinkCommand::Append(record)).is_err() {
466 record_sink_failure(
467 &mut self
468 .first_error
469 .lock()
470 .expect("event-log sink error mutex poisoned"),
471 AgentEventSinkError::dropped_event(
472 "event_log",
473 "append worker is unavailable",
474 ),
475 );
476 }
477 }
478 EventLogSinkDispatch::Blocking { log, topic } => {
479 if let Err(error) = futures::executor::block_on(log.append(topic, record)) {
480 record_sink_failure(
481 &mut self
482 .first_error
483 .lock()
484 .expect("event-log sink error mutex poisoned"),
485 AgentEventSinkError::dropped_event("event_log", error),
486 );
487 }
488 }
489 }
490 }
491
492 fn flush(&self) -> AgentEventSinkFlush<'_> {
493 Box::pin(EventLogSink::flush(self))
494 }
495}
496
497impl Drop for JsonlEventSink {
498 fn drop(&mut self) {
499 if let Ok(mut state) = self.state.lock() {
500 use std::io::Write as _;
501 let _ = state.writer.flush();
502 }
503 }
504}
505
506pub struct MultiSink {
508 sinks: Mutex<Vec<Arc<dyn AgentEventSink>>>,
509}
510
511impl MultiSink {
512 pub fn new() -> Self {
513 Self {
514 sinks: Mutex::new(Vec::new()),
515 }
516 }
517 pub fn push(&self, sink: Arc<dyn AgentEventSink>) {
518 self.sinks.lock().expect("sink mutex poisoned").push(sink);
519 }
520 pub fn len(&self) -> usize {
521 self.sinks.lock().expect("sink mutex poisoned").len()
522 }
523 pub fn is_empty(&self) -> bool {
524 self.len() == 0
525 }
526}
527
528impl Default for MultiSink {
529 fn default() -> Self {
530 Self::new()
531 }
532}
533
534impl AgentEventSink for MultiSink {
535 fn handle_event(&self, event: &AgentEvent) {
536 let sinks = self.sinks.lock().expect("sink mutex poisoned").clone();
542 for sink in sinks {
543 sink.handle_event(event);
544 }
545 }
546
547 fn flush(&self) -> AgentEventSinkFlush<'_> {
548 let sinks = self.sinks.lock().expect("sink mutex poisoned").clone();
549 Box::pin(flush_all_sinks(sinks))
550 }
551}
552
553pub(super) async fn flush_all_sinks(
554 sinks: impl IntoIterator<Item = Arc<dyn AgentEventSink>>,
555) -> Result<(), AgentEventSinkError> {
556 let mut first_error = None;
557 for sink in sinks {
558 if let Err(error) = sink.flush().await {
559 first_error.get_or_insert(error);
560 }
561 }
562 first_error.map_or(Ok(()), Err)
563}
564
565pub(super) fn now_ms() -> i64 {
566 std::time::SystemTime::now()
567 .duration_since(std::time::UNIX_EPOCH)
568 .map(|duration| duration.as_millis() as i64)
569 .unwrap_or(0)
570}