systemprompt_logging/layer/
mod.rs1mod proxy;
11mod visitor;
12
13use std::io::Write;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, OnceLock};
16use std::time::Duration;
17
18use tokio::sync::mpsc;
19use tracing::{Event, Subscriber};
20use tracing_subscriber::Layer;
21use tracing_subscriber::layer::Context;
22use tracing_subscriber::registry::LookupSpan;
23
24pub use proxy::ProxyDatabaseLayer;
25use proxy::{build_log_entry, record_span_fields, update_span_fields};
26
27use crate::models::{LogEntry, LogLevel};
28use systemprompt_database::DbPool;
29use systemprompt_identifiers::{ClientId, ContextId, TaskId};
30
31const BUFFER_FLUSH_SIZE: usize = 100;
32const BUFFER_FLUSH_INTERVAL_SECS: u64 = 10;
33
34const CHANNEL_CAPACITY: usize = 8192;
38
39static BACKGROUND_SENDER: OnceLock<mpsc::Sender<LogCommand>> = OnceLock::new();
40static BACKGROUND_DROPPED: AtomicU64 = AtomicU64::new(0);
41
42pub fn enqueue_background(entry: LogEntry) {
46 let Some(sender) = BACKGROUND_SENDER.get() else {
47 BACKGROUND_DROPPED.fetch_add(1, Ordering::Relaxed);
48 return;
49 };
50 let is_error = entry.level == LogLevel::Error;
51 if sender.try_send(LogCommand::Entry(Box::new(entry))).is_err() {
52 BACKGROUND_DROPPED.fetch_add(1, Ordering::Relaxed);
53 return;
54 }
55 if is_error {
56 sender.try_send(LogCommand::FlushNow).ok();
58 }
59}
60
61enum LogCommand {
62 Entry(Box<LogEntry>),
63 FlushNow,
64}
65
66struct LogChannel {
70 sender: mpsc::Sender<LogCommand>,
71 dropped: Arc<AtomicU64>,
72}
73
74impl LogChannel {
75 fn new(capacity: usize) -> (Self, mpsc::Receiver<LogCommand>) {
76 let (sender, receiver) = mpsc::channel(capacity);
77 let channel = Self {
78 sender,
79 dropped: Arc::new(AtomicU64::new(0)),
80 };
81 (channel, receiver)
82 }
83
84 fn send(&self, command: LogCommand) {
85 if let Err(mpsc::error::TrySendError::Full(_)) = self.sender.try_send(command) {
86 self.dropped.fetch_add(1, Ordering::Relaxed);
87 }
88 }
89
90 fn dropped(&self) -> u64 {
91 self.dropped.load(Ordering::Relaxed)
92 }
93}
94
95pub struct DatabaseLayer {
96 channel: LogChannel,
97}
98
99impl std::fmt::Debug for DatabaseLayer {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("DatabaseLayer")
102 .field("dropped", &self.channel.dropped())
103 .finish_non_exhaustive()
104 }
105}
106
107impl DatabaseLayer {
108 pub fn new(db_pool: DbPool) -> Self {
109 let (channel, receiver) = LogChannel::new(CHANNEL_CAPACITY);
110
111 BACKGROUND_SENDER.get_or_init(|| channel.sender.clone());
112
113 tokio::spawn(Self::batch_writer(db_pool, receiver));
114
115 Self { channel }
116 }
117
118 async fn batch_writer(db_pool: DbPool, mut receiver: mpsc::Receiver<LogCommand>) {
119 let mut buffer = Vec::with_capacity(BUFFER_FLUSH_SIZE);
120 let mut interval = tokio::time::interval(Duration::from_secs(BUFFER_FLUSH_INTERVAL_SECS));
121 let mut failed_total: u64 = 0;
122
123 loop {
124 tokio::select! {
125 Some(command) = receiver.recv() => {
126 match command {
127 LogCommand::Entry(entry) => {
128 buffer.push(*entry);
129 if buffer.len() >= BUFFER_FLUSH_SIZE {
130 Self::flush(&db_pool, &mut buffer, &mut failed_total).await;
131 }
132 }
133 LogCommand::FlushNow => {
134 if !buffer.is_empty() {
135 Self::flush(&db_pool, &mut buffer, &mut failed_total).await;
136 }
137 }
138 }
139 }
140 _ = interval.tick() => {
141 if !buffer.is_empty() {
142 Self::flush(&db_pool, &mut buffer, &mut failed_total).await;
143 }
144 }
145 }
146 }
147 }
148
149 async fn flush(db_pool: &DbPool, buffer: &mut Vec<LogEntry>, failed_total: &mut u64) {
150 if let Err(e) = Self::batch_insert(db_pool, buffer).await {
151 let lost = u64::try_from(buffer.len()).unwrap_or(u64::MAX);
152 *failed_total = failed_total.saturating_add(lost);
153 writeln!(
154 std::io::stderr(),
155 "DATABASE LOG FLUSH FAILED ({lost} entries lost this flush, {failed_total} total lost since start): {e}"
156 )
157 .ok();
158 }
159 buffer.clear();
160 }
161
162 async fn batch_insert(
163 db_pool: &DbPool,
164 entries: &[LogEntry],
165 ) -> Result<(), crate::models::LoggingError> {
166 let pool = db_pool.write_pool_arc()?;
167
168 let mut tx = pool.begin().await?;
171 sqlx::query!("SET LOCAL synchronous_commit = off")
172 .execute(&mut *tx)
173 .await?;
174
175 for entry in entries {
176 let metadata_json: Option<String> = entry
177 .metadata
178 .as_ref()
179 .map(serde_json::to_string)
180 .transpose()?;
181
182 let entry_id = entry.id.as_str();
183 let level_str = entry.level.to_string();
184 let user_id = entry.user_id.as_str();
185 let session_id = entry.session_id.as_str();
186 let task_id = entry.task_id.as_ref().map(TaskId::as_str);
187 let trace_id = entry.trace_id.as_str();
188 let context_id = entry.context_id.as_ref().map(ContextId::as_str);
189 let client_id = entry.client_id.as_ref().map(ClientId::as_str);
190
191 sqlx::query!(
192 r"
193 INSERT INTO logs (id, timestamp, level, module, message, metadata, user_id, session_id, task_id, trace_id, context_id, client_id)
194 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
195 ",
196 entry_id,
197 entry.timestamp,
198 level_str,
199 entry.module,
200 entry.message,
201 metadata_json,
202 user_id,
203 session_id,
204 task_id,
205 trace_id,
206 context_id,
207 client_id
208 )
209 .execute(&mut *tx)
210 .await?;
211 }
212
213 tx.commit().await?;
214 Ok(())
215 }
216}
217
218impl DatabaseLayer {
219 fn send_entry(&self, entry: LogEntry) {
220 let is_error = entry.level == LogLevel::Error;
221 self.channel.send(LogCommand::Entry(Box::new(entry)));
222 if is_error {
223 self.channel.send(LogCommand::FlushNow);
224 }
225 }
226}
227
228impl<S> Layer<S> for DatabaseLayer
229where
230 S: Subscriber + for<'a> LookupSpan<'a>,
231{
232 fn on_new_span(
233 &self,
234 attrs: &tracing::span::Attributes<'_>,
235 id: &tracing::span::Id,
236 ctx: Context<'_, S>,
237 ) {
238 record_span_fields(attrs, id, &ctx);
239 }
240
241 fn on_record(
242 &self,
243 id: &tracing::span::Id,
244 values: &tracing::span::Record<'_>,
245 ctx: Context<'_, S>,
246 ) {
247 update_span_fields(id, values, &ctx);
248 }
249
250 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
251 if let Some(entry) = build_log_entry(event, &ctx) {
252 self.send_entry(entry);
253 }
254 }
255}