systemprompt_logging/
lib.rs1pub mod attribution;
33pub mod extension;
34pub mod layer;
35pub mod models;
36pub mod repository;
37pub mod sanitize;
38pub mod services;
39
40pub use attribution::{LogAttributionUnset, install_log_attribution, platform_attribution};
41pub use extension::LoggingExtension;
42
43pub use layer::{
44 DatabaseLayer, LogWriterHandle, LogWriterShutdownError, dropped_log_entries, enqueue_background,
45};
46pub use models::{LogActor, LogEntry, LogFilter, LogLevel};
47pub use repository::{
48 AnalyticsEvent, AnalyticsRepository, LoggingOwnerReassignment, LoggingRepository,
49};
50#[cfg(feature = "cli")]
51pub use services::CliService;
52pub use services::{
53 BufferedNotice, ConsoleEventFormat, DatabaseLogService, FilterSystemFields, JsonEventFormat,
54 LogThrottle, LoggingMaintenanceService, SYSTEM_LOG_OWNER, SystemSpan, buffer_notice,
55 console_format, drain_notices, is_startup_mode, is_structured_output, mark_structured_emitted,
56 reset_structured_emitted, set_console_format, set_startup_mode, set_structured_output,
57 structured_was_emitted,
58};
59
60use std::sync::{Mutex, OnceLock, PoisonError};
61
62use layer::ProxyDatabaseLayer;
63use systemprompt_database::DbPool;
64use systemprompt_identifiers::InstanceId;
65use tracing::Level;
66use tracing_subscriber::filter::FilterFn;
67use tracing_subscriber::layer::SubscriberExt;
68use tracing_subscriber::util::SubscriberInitExt;
69use tracing_subscriber::{EnvFilter, Layer};
70
71static SUBSCRIBER_INITIALIZED: OnceLock<()> = OnceLock::new();
72static INSTANCE_ID: OnceLock<InstanceId> = OnceLock::new();
73
74pub fn set_instance_id(instance_id: InstanceId) {
75 if INSTANCE_ID.set(instance_id).is_err() {
76 tracing::debug!("logging instance id already set; ignoring");
77 }
78}
79
80#[must_use]
81pub fn instance_id() -> Option<&'static InstanceId> {
82 INSTANCE_ID.get()
83}
84static DB_PROXY: OnceLock<ProxyDatabaseLayer> = OnceLock::new();
85static DB_WRITER: Mutex<Option<LogWriterHandle>> = Mutex::new(None);
86
87const NOISE_FILTERS: &[&str] = &[
88 "tokio_cron_scheduler=warn",
89 "sqlx::postgres::notice=warn",
90 "sqlx::query=warn",
91 "handlebars=warn",
92 "systemprompt_database::lifecycle=info",
93 "systemprompt_templates=info",
94 "systemprompt_extension::registry=info",
95 "systemprompt_api::services::middleware::session=info",
96 "rmcp=warn",
97 "rmcp::transport=warn",
98];
99
100fn build_filter(base: &str) -> EnvFilter {
101 let filter_str = std::iter::once(base.to_owned())
102 .chain(NOISE_FILTERS.iter().map(ToString::to_string))
103 .collect::<Vec<_>>()
104 .join(",");
105 EnvFilter::new(filter_str)
106}
107
108fn ensure_subscriber(level_override: Option<&str>) {
109 if SUBSCRIBER_INITIALIZED.set(()).is_err() {
110 return;
111 }
112
113 let base_filter = level_override.map_or_else(
114 || EnvFilter::try_from_default_env().unwrap_or_else(|_| build_filter("info")),
115 |level| EnvFilter::try_from_default_env().unwrap_or_else(|_| build_filter(level)),
116 );
117
118 let gate_active = level_override.is_none();
119 let startup_gate = FilterFn::new(move |meta| {
120 !(gate_active && is_startup_mode()) || *meta.level() <= Level::WARN
121 });
122
123 let fmt_layer = tracing_subscriber::fmt::layer()
124 .fmt_fields(FilterSystemFields::new())
125 .event_format(ConsoleEventFormat::default())
126 .with_writer(std::io::stderr)
127 .log_internal_errors(true)
128 .with_filter(base_filter)
129 .with_filter(startup_gate);
130
131 let proxy = DB_PROXY.get_or_init(ProxyDatabaseLayer::new).clone();
132 let db_layer = proxy.with_filter(build_filter("info"));
133
134 tracing_subscriber::registry()
135 .with(fmt_layer)
136 .with(db_layer)
137 .init();
138}
139
140pub fn init_logging(db_pool: &DbPool) {
141 ensure_subscriber(None);
142
143 let proxy = DB_PROXY.get_or_init(ProxyDatabaseLayer::new);
144 if let Some(writer) = proxy.attach(db_pool) {
145 *DB_WRITER.lock().unwrap_or_else(PoisonError::into_inner) = Some(writer);
146 }
147}
148
149pub async fn shutdown_database_logging() -> Result<(), LogWriterShutdownError> {
150 let writer = DB_WRITER
151 .lock()
152 .unwrap_or_else(PoisonError::into_inner)
153 .take();
154 let Some(writer) = writer else {
155 return Ok(());
156 };
157 writer.shutdown().await
158}
159
160pub fn init_console_logging() {
161 init_console_logging_with_level(None);
162}
163
164pub fn init_console_logging_with_level(level: Option<&str>) {
165 ensure_subscriber(level);
166}