yggdrasil/tasks/
log_writer.rs1use std::time::Duration;
15
16use crate::api::logs::capture::{self, LogRecord};
17use crate::db::pool::get_conn;
18
19const BATCH_SIZE: usize = 200;
21const FLUSH_WINDOW: Duration = Duration::from_millis(500);
23const CONN_RETRY_INTERVAL: Duration = Duration::from_secs(2);
25
26pub async fn run_writer() {
28 let mut rx = match capture::take_db_receiver() {
29 Some(rx) => rx,
30 None => {
31 tracing::error!("log writer: capture receiver already taken; task exiting");
33 return;
34 }
35 };
36 tracing::info!(
37 batch_size = BATCH_SIZE,
38 flush_window_ms = FLUSH_WINDOW.as_millis() as u64,
39 "log writer started"
40 );
41
42 let mut batch: Vec<LogRecord> = Vec::with_capacity(BATCH_SIZE);
43 loop {
44 let first = match rx.recv().await {
46 Some(r) => r,
47 None => {
48 flush(&mut batch).await;
49 return;
50 }
51 };
52 batch.push(first);
53
54 let deadline = tokio::time::Instant::now() + FLUSH_WINDOW;
56 while batch.len() < BATCH_SIZE {
57 match tokio::time::timeout_at(deadline, rx.recv()).await {
58 Ok(Some(r)) => batch.push(r),
59 Ok(None) | Err(_) => break,
60 }
61 }
62 flush(&mut batch).await;
63 }
64}
65
66async fn flush(batch: &mut Vec<LogRecord>) {
68 if batch.is_empty() {
69 return;
70 }
71
72 let client = loop {
75 match get_conn().await {
76 Ok(c) => break c,
77 Err(e) => {
78 tracing::error!(error = %e, "log writer: failed to get DB connection; retrying");
79 tokio::time::sleep(CONN_RETRY_INTERVAL).await;
80 }
81 }
82 };
83
84 let ts: Vec<chrono::DateTime<chrono::Utc>> = batch.iter().map(|r| r.ts).collect();
85 let levels: Vec<&str> = batch.iter().map(|r| r.level.as_str()).collect();
86 let targets: Vec<&str> = batch.iter().map(|r| r.target.as_str()).collect();
87 let messages: Vec<&str> = batch.iter().map(|r| r.message.as_str()).collect();
88
89 let result = client
90 .execute(
91 "INSERT INTO logs (ts, level, target, message) \
92 SELECT * FROM UNNEST($1::timestamptz[], $2::text[], $3::text[], $4::text[])",
93 &[&ts, &levels, &targets, &messages],
94 )
95 .await;
96
97 if let Err(e) = result {
98 tracing::error!(
100 error = %e,
101 dropped = batch.len() as u64,
102 "log writer: batch insert failed; dropping batch"
103 );
104 capture::record_dropped(batch.len() as u64);
105 }
106 batch.clear();
107}