Skip to main content

systemprompt_logging/
lib.rs

1//! # systemprompt-logging
2//!
3//! Tracing and audit infrastructure for systemprompt.io. Owns the
4//! structured-event pipeline, the database-backed `tracing` layer,
5//! log/analytics repositories, and the CLI display
6//! sinks. The trace-assembly queries over agent, AI and MCP tables live in
7//! `systemprompt_runtime::trace`.
8//!
9//! ## Feature flags
10//!
11//! | Feature   | Description                                                               |
12//! |-----------|---------------------------------------------------------------------------|
13//! | (default) | Database layer, repositories                                              |
14//! | `cli`     | CLI display helpers (`CliService`, tables, banners) — pulls in `console`, `indicatif` |
15//!
16//! ## Top-level entry points
17//!
18//! - [`init_logging`] / [`init_console_logging`] /
19//!   [`init_console_logging_with_level`] — install the global `tracing`
20//!   subscriber (with optional database sink).
21//! - [`set_console_format`] — switch stderr between text and JSON lines
22//!   (`observability.log_format`); both redact the same fields.
23//! - [`shutdown_database_logging`] — flush the database sink's buffered and
24//!   queued entries and stop its writer task; awaited on server shutdown.
25//! - [`LoggingExtension`] — schema/extension registration via the `inventory`
26//!   framework.
27//! - [`LoggingRepository`], [`AnalyticsRepository`] — direct repository access.
28//!
29//! Copyright (c) systemprompt.io — Business Source License 1.1.
30//! See <https://systemprompt.io> for licensing details.
31
32pub 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}