systemprompt_logging/layer/
proxy.rs1use std::sync::{Arc, Mutex, OnceLock, PoisonError};
17
18use chrono::Utc;
19use tracing::{Event, Subscriber};
20use tracing_subscriber::Layer;
21use tracing_subscriber::layer::Context;
22use tracing_subscriber::registry::LookupSpan;
23
24use super::visitor::{FieldVisitor, SpanContext, SpanFields, SpanVisitor, extract_span_context};
25use super::{DatabaseLayer, LayerShared, LogWriterHandle, LogWriterShutdownError};
26use crate::models::{LogEntry, LogLevel};
27use systemprompt_database::DbPool;
28use systemprompt_identifiers::{
29 ClientId, ContextId, InstanceId, LogId, SessionId, TaskId, TraceId, UserId,
30};
31
32#[derive(Default)]
33struct ProxyState {
34 db: OnceLock<DatabaseLayer>,
35 shared: LayerShared,
36 writer: Mutex<Option<LogWriterHandle>>,
37}
38
39#[derive(Clone, Default)]
40pub struct ProxyDatabaseLayer {
41 inner: Arc<ProxyState>,
42}
43
44impl std::fmt::Debug for ProxyDatabaseLayer {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.debug_struct("ProxyDatabaseLayer")
47 .field("attached", &self.inner.db.get().is_some())
48 .finish()
49 }
50}
51
52impl ProxyDatabaseLayer {
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 pub fn attach(&self, db_pool: &DbPool) -> Option<LogWriterHandle> {
58 let mut writer = None;
59 self.inner.db.get_or_init(|| {
60 let (layer, handle) = DatabaseLayer::with_shared(db_pool, self.inner.shared.clone());
61 writer = Some(handle);
62 layer
63 });
64 writer
65 }
66
67 pub(crate) fn attach_owned(&self, db_pool: &DbPool, instance_id: Option<InstanceId>) {
68 if let Some(instance_id) = instance_id {
69 self.inner.shared.instance.set(instance_id);
70 }
71 if let Some(writer) = self.attach(db_pool) {
72 *self
73 .inner
74 .writer
75 .lock()
76 .unwrap_or_else(PoisonError::into_inner) = Some(writer);
77 }
78 }
79
80 pub(crate) fn shared(&self) -> &LayerShared {
81 &self.inner.shared
82 }
83
84 pub(crate) fn enqueue(&self, entry: LogEntry) {
85 match self.inner.db.get() {
86 Some(db) => db.enqueue(entry),
87 None => self.inner.shared.record_drop(),
88 }
89 }
90
91 pub(crate) async fn shutdown(&self) -> Result<(), LogWriterShutdownError> {
92 let writer = self
93 .inner
94 .writer
95 .lock()
96 .unwrap_or_else(PoisonError::into_inner)
97 .take();
98 match writer {
99 Some(writer) => writer.shutdown().await,
100 None => Ok(()),
101 }
102 }
103}
104
105impl<S> Layer<S> for ProxyDatabaseLayer
106where
107 S: Subscriber + for<'a> LookupSpan<'a>,
108{
109 fn on_new_span(
110 &self,
111 attrs: &tracing::span::Attributes<'_>,
112 id: &tracing::span::Id,
113 ctx: Context<'_, S>,
114 ) {
115 if let Some(db) = self.inner.db.get() {
116 db.on_new_span(attrs, id, ctx);
117 } else {
118 record_span_fields(attrs, id, &ctx);
119 }
120 }
121
122 fn on_record(
123 &self,
124 id: &tracing::span::Id,
125 values: &tracing::span::Record<'_>,
126 ctx: Context<'_, S>,
127 ) {
128 if let Some(db) = self.inner.db.get() {
129 db.on_record(id, values, ctx);
130 } else {
131 update_span_fields(id, values, &ctx);
132 }
133 }
134
135 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
136 if let Some(db) = self.inner.db.get() {
137 db.on_event(event, ctx);
138 }
139 }
140}
141
142pub(super) fn record_span_fields<S>(
143 attrs: &tracing::span::Attributes<'_>,
144 id: &tracing::span::Id,
145 ctx: &Context<'_, S>,
146) where
147 S: Subscriber + for<'a> LookupSpan<'a>,
148{
149 let Some(span) = ctx.span(id) else {
150 return;
151 };
152 let mut fields = SpanFields::default();
153 let mut context = SpanContext::default();
154 let mut visitor = SpanVisitor {
155 context: &mut context,
156 };
157 attrs.record(&mut visitor);
158
159 fields.user = context.user;
160 fields.session = context.session;
161 fields.task = context.task;
162 fields.trace = context.trace;
163 fields.context = context.context;
164 fields.client = context.client;
165
166 let mut extensions = span.extensions_mut();
167 extensions.insert(fields);
168}
169
170pub(super) fn update_span_fields<S>(
171 id: &tracing::span::Id,
172 values: &tracing::span::Record<'_>,
173 ctx: &Context<'_, S>,
174) where
175 S: Subscriber + for<'a> LookupSpan<'a>,
176{
177 if let Some(span) = ctx.span(id) {
178 let mut extensions = span.extensions_mut();
179 if let Some(fields) = extensions.get_mut::<SpanFields>() {
180 let mut context = SpanContext {
181 user: fields.user.clone(),
182 session: fields.session.clone(),
183 task: fields.task.clone(),
184 trace: fields.trace.clone(),
185 context: fields.context.clone(),
186 client: fields.client.clone(),
187 };
188 let mut visitor = SpanVisitor {
189 context: &mut context,
190 };
191 values.record(&mut visitor);
192
193 fields.user = context.user;
194 fields.session = context.session;
195 fields.task = context.task;
196 fields.trace = context.trace;
197 fields.context = context.context;
198 fields.client = context.client;
199 }
200 }
201}
202
203pub(super) fn build_log_entry<S>(event: &Event<'_>, ctx: &Context<'_, S>) -> Option<LogEntry>
204where
205 S: Subscriber + for<'a> LookupSpan<'a>,
206{
207 let level = *event.metadata().level();
208 let module = event.metadata().target().to_owned();
209
210 let mut visitor = FieldVisitor::default();
211 event.record(&mut visitor);
212
213 let span_context = ctx
214 .current_span()
215 .id()
216 .and_then(|id| ctx.span(id))
217 .map(extract_span_context)?;
218
219 let log_level = match level {
220 tracing::Level::ERROR => LogLevel::Error,
221 tracing::Level::WARN => LogLevel::Warn,
222 tracing::Level::INFO => LogLevel::Info,
223 tracing::Level::DEBUG => LogLevel::Debug,
224 tracing::Level::TRACE => LogLevel::Trace,
225 };
226
227 let user_id = UserId::new(span_context.user.as_ref()?.clone());
228 let session_id = SessionId::new(span_context.session.as_ref()?.clone());
229 let trace_id = TraceId::new(span_context.trace.as_ref()?.clone());
230
231 Some(LogEntry {
232 id: LogId::generate(),
233 timestamp: Utc::now(),
234 level: log_level,
235 module,
236 message: visitor.message,
237 metadata: visitor.fields,
238 user_id,
239 session_id,
240 task_id: span_context.task.as_ref().map(|s| TaskId::new(s.clone())),
241 trace_id,
242 context_id: span_context.context.as_ref().and_then(|s| {
243 ContextId::try_new(s.clone())
244 .map_err(|e| {
245 tracing::warn!(error = %e, raw = %s, "Skipping non-UUID context_id from span context");
246 e
247 })
248 .ok()
249 }),
250 client_id: span_context
251 .client
252 .as_ref()
253 .map(|s| ClientId::new(s.clone())),
254 instance_id: None,
255 })
256}