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 a typed query surface
6//! over the audit trail (traces, AI requests, MCP tool executions).
7//!
8//! ## Feature flags
9//!
10//! | Feature   | Description                                                               |
11//! |-----------|---------------------------------------------------------------------------|
12//! | (default) | Database layer, repositories, trace queries, retention scheduler          |
13//! | `cli`     | CLI display helpers (`CliService`, tables, banners) — pulls in `console`, `indicatif` |
14//!
15//! ## Top-level entry points
16//!
17//! - [`init_logging`] / [`init_console_logging`] /
18//!   [`init_console_logging_with_level`] — install the global `tracing`
19//!   subscriber (with optional database sink).
20//! - [`LoggingExtension`] — schema/extension registration via the `inventory`
21//!   framework.
22//! - [`LoggingRepository`], [`AnalyticsRepository`] — direct repository access.
23//! - [`TraceQueryService`], [`AiTraceService`] — typed audit/trace queries.
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;
35pub mod trace;
36
37pub use attribution::{LogAttributionUnset, install_log_attribution, platform_attribution};
38pub use extension::LoggingExtension;
39
40pub use layer::{DatabaseLayer, enqueue_background};
41pub use models::{LogActor, LogEntry, LogFilter, LogLevel};
42pub use repository::{AnalyticsEvent, AnalyticsRepository, LoggingRepository};
43#[cfg(feature = "cli")]
44pub use services::CliService;
45pub use services::{
46    BufferedNotice, DatabaseLogService, FilterSystemFields, LogThrottle, LoggingMaintenanceService,
47    SystemSpan, buffer_notice, drain_notices, is_startup_mode, is_structured_output,
48    mark_structured_emitted, publish_log, reset_structured_emitted, set_log_publisher,
49    set_startup_mode, set_structured_output, structured_was_emitted,
50};
51pub use trace::{
52    AiRequestDetail, AiRequestFilter, AiRequestInfo, AiRequestListItem, AiRequestStats,
53    AiRequestSummary, AiTraceService, AuditLookupResult, AuditToolCallRow, ConversationMessage,
54    ExecutionStep, ExecutionStepSummary, LevelCount, LinkedMcpCall, LogSearchFilter, LogSearchItem,
55    LogTimeRange, McpExecutionSummary, McpToolExecution, ModelStatsRow, ModuleCount,
56    ProviderStatsRow, TaskArtifact, TaskInfo, ToolExecutionFilter, ToolExecutionItem, ToolLogEntry,
57    TraceEvent, TraceListFilter, TraceListItem, TraceQueryService,
58};
59
60use std::sync::OnceLock;
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();
85
86const NOISE_FILTERS: &[&str] = &[
87    "tokio_cron_scheduler=warn",
88    "sqlx::postgres::notice=warn",
89    "sqlx::query=warn",
90    "handlebars=warn",
91    "systemprompt_database::lifecycle=info",
92    "systemprompt_templates=info",
93    "systemprompt_extension::registry=info",
94    "systemprompt_api::services::middleware::session=info",
95    "rmcp=warn",
96    "rmcp::transport=warn",
97];
98
99fn build_filter(base: &str) -> EnvFilter {
100    let filter_str = std::iter::once(base.to_owned())
101        .chain(NOISE_FILTERS.iter().map(ToString::to_string))
102        .collect::<Vec<_>>()
103        .join(",");
104    EnvFilter::new(filter_str)
105}
106
107fn ensure_subscriber(level_override: Option<&str>) {
108    if SUBSCRIBER_INITIALIZED.set(()).is_err() {
109        return;
110    }
111
112    let base_filter = level_override.map_or_else(
113        || EnvFilter::try_from_default_env().unwrap_or_else(|_| build_filter("info")),
114        |level| EnvFilter::try_from_default_env().unwrap_or_else(|_| build_filter(level)),
115    );
116
117    let gate_active = level_override.is_none();
118    let startup_gate = FilterFn::new(move |meta| {
119        !(gate_active && is_startup_mode()) || *meta.level() <= Level::WARN
120    });
121
122    let fmt_layer = tracing_subscriber::fmt::layer()
123        .fmt_fields(FilterSystemFields::new())
124        .with_target(true)
125        .with_writer(std::io::stderr)
126        .log_internal_errors(true)
127        .with_filter(base_filter)
128        .with_filter(startup_gate);
129
130    let proxy = DB_PROXY.get_or_init(ProxyDatabaseLayer::new).clone();
131    let db_layer = proxy.with_filter(build_filter("info"));
132
133    tracing_subscriber::registry()
134        .with(fmt_layer)
135        .with(db_layer)
136        .init();
137}
138
139pub fn init_logging(db_pool: DbPool) {
140    ensure_subscriber(None);
141
142    let proxy = DB_PROXY.get_or_init(ProxyDatabaseLayer::new);
143    proxy.attach(db_pool);
144}
145
146pub fn init_console_logging() {
147    init_console_logging_with_level(None);
148}
149
150pub fn init_console_logging_with_level(level: Option<&str>) {
151    ensure_subscriber(level);
152}