systemprompt_logging/layer/
mod.rs1mod proxy;
16mod visitor;
17mod writer;
18
19use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{Arc, OnceLock};
21
22use tokio::sync::mpsc;
23use tracing::{Event, Subscriber};
24use tracing_subscriber::Layer;
25use tracing_subscriber::layer::Context;
26use tracing_subscriber::registry::LookupSpan;
27
28pub use proxy::ProxyDatabaseLayer;
29use proxy::{build_log_entry, record_span_fields, update_span_fields};
30pub use writer::{LogWriterHandle, LogWriterShutdownError};
31
32use self::writer::LogCommand;
33use crate::models::{LogEntry, LogLevel};
34use crate::repository::LoggingRepository;
35use systemprompt_database::DbPool;
36use systemprompt_identifiers::InstanceId;
37
38const CHANNEL_CAPACITY: usize = 8192;
39
40#[derive(Debug, Clone, Default)]
41pub(crate) struct InstanceLabel(Arc<OnceLock<InstanceId>>);
42
43impl InstanceLabel {
44 pub(crate) fn get(&self) -> Option<&InstanceId> {
45 self.0.get()
46 }
47
48 pub(crate) fn set(&self, instance_id: InstanceId) {
49 if let Err(rejected) = self.0.set(instance_id) {
50 tracing::debug!(instance_id = %rejected, "logging instance label already set; ignoring");
51 }
52 }
53}
54
55#[derive(Debug, Clone, Default)]
56pub(crate) struct LayerShared {
57 pub(crate) instance: InstanceLabel,
58 dropped: Arc<AtomicU64>,
59}
60
61impl LayerShared {
62 pub(crate) fn dropped(&self) -> u64 {
63 self.dropped.load(Ordering::Relaxed)
64 }
65
66 pub(crate) fn record_drop(&self) {
67 self.dropped.fetch_add(1, Ordering::Relaxed);
68 }
69}
70
71pub struct DatabaseLayer {
72 sender: mpsc::Sender<LogCommand>,
73 shared: LayerShared,
74}
75
76impl std::fmt::Debug for DatabaseLayer {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.debug_struct("DatabaseLayer")
79 .field("dropped", &self.dropped_entries())
80 .finish_non_exhaustive()
81 }
82}
83
84impl DatabaseLayer {
85 pub fn new(db_pool: &DbPool) -> (Self, LogWriterHandle) {
86 Self::with_shared(db_pool, LayerShared::default())
87 }
88
89 pub(crate) fn with_shared(db_pool: &DbPool, shared: LayerShared) -> (Self, LogWriterHandle) {
90 let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
91 let writer =
92 LogWriterHandle::spawn(LoggingRepository::new(db_pool), sender.clone(), receiver);
93 (Self { sender, shared }, writer)
94 }
95
96 #[must_use]
97 pub fn dropped_entries(&self) -> u64 {
98 self.shared.dropped()
99 }
100
101 pub fn enqueue(&self, mut entry: LogEntry) {
102 if entry.instance_id.is_none() {
103 entry.instance_id = self.shared.instance.get().cloned();
104 }
105 let is_error = entry.level == LogLevel::Error;
106 if self
107 .sender
108 .try_send(LogCommand::Entry(Box::new(entry)))
109 .is_err()
110 {
111 self.shared.record_drop();
112 return;
113 }
114 if is_error {
115 let _flush_requested = self.sender.try_send(LogCommand::FlushNow).is_ok();
118 }
119 }
120}
121
122impl<S> Layer<S> for DatabaseLayer
123where
124 S: Subscriber + for<'a> LookupSpan<'a>,
125{
126 fn on_new_span(
127 &self,
128 attrs: &tracing::span::Attributes<'_>,
129 id: &tracing::span::Id,
130 ctx: Context<'_, S>,
131 ) {
132 record_span_fields(attrs, id, &ctx);
133 }
134
135 fn on_record(
136 &self,
137 id: &tracing::span::Id,
138 values: &tracing::span::Record<'_>,
139 ctx: Context<'_, S>,
140 ) {
141 update_span_fields(id, values, &ctx);
142 }
143
144 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
145 if let Some(entry) = build_log_entry(event, &ctx) {
146 self.enqueue(entry);
147 }
148 }
149}