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, retention scheduling, 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, retention scheduler                         |
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//! - [`LoggingExtension`] — schema/extension registration via the `inventory`
22//!   framework.
23//! - [`LoggingRepository`], [`AnalyticsRepository`] — direct repository access.
24//!
25//! Copyright (c) systemprompt.io — Business Source License 1.1.
26//! See <https://systemprompt.io> for licensing details.
27
28pub mod attribution;
29pub mod extension;
30pub mod layer;
31pub mod models;
32pub mod repository;
33pub mod sanitize;
34pub mod services;
35
36pub use attribution::{LogAttributionUnset, install_log_attribution, platform_attribution};
37pub use extension::LoggingExtension;
38
39pub use layer::{DatabaseLayer, enqueue_background};
40pub use models::{LogActor, LogEntry, LogFilter, LogLevel};
41pub use repository::{AnalyticsEvent, AnalyticsRepository, LoggingRepository};
42#[cfg(feature = "cli")]
43pub use services::CliService;
44pub use services::{
45    BufferedNotice, DatabaseLogService, FilterSystemFields, LogThrottle, LoggingMaintenanceService,
46    SystemSpan, buffer_notice, drain_notices, is_startup_mode, is_structured_output,
47    mark_structured_emitted, reset_structured_emitted, set_startup_mode, set_structured_output,
48    structured_was_emitted,
49};
50
51use std::sync::OnceLock;
52
53use layer::ProxyDatabaseLayer;
54use systemprompt_database::DbPool;
55use systemprompt_identifiers::InstanceId;
56use tracing::Level;
57use tracing_subscriber::filter::FilterFn;
58use tracing_subscriber::layer::SubscriberExt;
59use tracing_subscriber::util::SubscriberInitExt;
60use tracing_subscriber::{EnvFilter, Layer};
61
62static SUBSCRIBER_INITIALIZED: OnceLock<()> = OnceLock::new();
63static INSTANCE_ID: OnceLock<InstanceId> = OnceLock::new();
64
65pub fn set_instance_id(instance_id: InstanceId) {
66    if INSTANCE_ID.set(instance_id).is_err() {
67        tracing::debug!("logging instance id already set; ignoring");
68    }
69}
70
71#[must_use]
72pub fn instance_id() -> Option<&'static InstanceId> {
73    INSTANCE_ID.get()
74}
75static DB_PROXY: OnceLock<ProxyDatabaseLayer> = OnceLock::new();
76
77const NOISE_FILTERS: &[&str] = &[
78    "tokio_cron_scheduler=warn",
79    "sqlx::postgres::notice=warn",
80    "sqlx::query=warn",
81    "handlebars=warn",
82    "systemprompt_database::lifecycle=info",
83    "systemprompt_templates=info",
84    "systemprompt_extension::registry=info",
85    "systemprompt_api::services::middleware::session=info",
86    "rmcp=warn",
87    "rmcp::transport=warn",
88];
89
90fn build_filter(base: &str) -> EnvFilter {
91    let filter_str = std::iter::once(base.to_owned())
92        .chain(NOISE_FILTERS.iter().map(ToString::to_string))
93        .collect::<Vec<_>>()
94        .join(",");
95    EnvFilter::new(filter_str)
96}
97
98fn ensure_subscriber(level_override: Option<&str>) {
99    if SUBSCRIBER_INITIALIZED.set(()).is_err() {
100        return;
101    }
102
103    let base_filter = level_override.map_or_else(
104        || EnvFilter::try_from_default_env().unwrap_or_else(|_| build_filter("info")),
105        |level| EnvFilter::try_from_default_env().unwrap_or_else(|_| build_filter(level)),
106    );
107
108    let gate_active = level_override.is_none();
109    let startup_gate = FilterFn::new(move |meta| {
110        !(gate_active && is_startup_mode()) || *meta.level() <= Level::WARN
111    });
112
113    let fmt_layer = tracing_subscriber::fmt::layer()
114        .fmt_fields(FilterSystemFields::new())
115        .with_target(true)
116        .with_writer(std::io::stderr)
117        .log_internal_errors(true)
118        .with_filter(base_filter)
119        .with_filter(startup_gate);
120
121    let proxy = DB_PROXY.get_or_init(ProxyDatabaseLayer::new).clone();
122    let db_layer = proxy.with_filter(build_filter("info"));
123
124    tracing_subscriber::registry()
125        .with(fmt_layer)
126        .with(db_layer)
127        .init();
128}
129
130pub fn init_logging(db_pool: DbPool) {
131    ensure_subscriber(None);
132
133    let proxy = DB_PROXY.get_or_init(ProxyDatabaseLayer::new);
134    proxy.attach(db_pool);
135}
136
137pub fn init_console_logging() {
138    init_console_logging_with_level(None);
139}
140
141pub fn init_console_logging_with_level(level: Option<&str>) {
142    ensure_subscriber(level);
143}