use std::path::PathBuf;
use tracing_appender::non_blocking::{NonBlockingBuilder, WorkerGuard};
use tracing_subscriber::fmt::writer::MakeWriterExt;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, EnvFilter, Registry};
const LOG_BUFFER_LINES: usize = 1_000_000;
pub mod admin_intent_log;
pub mod janitor;
pub mod operator_event;
pub mod operator_event_router;
#[cfg(feature = "otel")]
pub mod otel;
pub mod slow_query_logger;
pub mod span;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogFormat {
Pretty,
Json,
}
impl LogFormat {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"pretty" | "text" | "human" => Some(Self::Pretty),
"json" | "ndjson" => Some(Self::Json),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
pub log_dir: Option<PathBuf>,
pub file_prefix: String,
pub level_filter: String,
pub format: LogFormat,
pub rotation_keep_days: u16,
pub service_name: &'static str,
pub level_explicit: bool,
pub format_explicit: bool,
pub rotation_keep_days_explicit: bool,
pub file_prefix_explicit: bool,
pub log_dir_explicit: bool,
pub log_file_disabled: bool,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
log_dir: None,
file_prefix: "reddb.log".to_string(),
level_filter: "info".to_string(),
format: LogFormat::Pretty,
rotation_keep_days: 14,
service_name: "reddb",
level_explicit: false,
format_explicit: false,
rotation_keep_days_explicit: false,
file_prefix_explicit: false,
log_dir_explicit: false,
log_file_disabled: false,
}
}
}
pub struct TelemetryGuard {
_stderr_worker: Option<WorkerGuard>,
_file_worker: Option<WorkerGuard>,
}
pub fn init(cfg: TelemetryConfig) -> Option<TelemetryGuard> {
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&cfg.level_filter));
let (stderr_writer, stderr_worker) = NonBlockingBuilder::default()
.buffered_lines_limit(LOG_BUFFER_LINES)
.lossy(true)
.finish(std::io::stderr());
let (file_writer_opt, file_worker) = match cfg.log_dir.as_ref() {
Some(dir) => {
if let Err(err) = std::fs::create_dir_all(dir) {
eprintln!(
"telemetry: failed to create log dir {}: {err}",
dir.display()
);
(None, None)
} else {
let file_appender = tracing_appender::rolling::daily(dir, &cfg.file_prefix);
let (writer, guard) = NonBlockingBuilder::default()
.buffered_lines_limit(LOG_BUFFER_LINES)
.lossy(true)
.finish(file_appender);
if cfg.rotation_keep_days > 0 {
janitor::spawn(dir.clone(), cfg.file_prefix.clone(), cfg.rotation_keep_days);
}
(Some(writer), Some(guard))
}
}
None => (None, None),
};
let result = match cfg.format {
LogFormat::Pretty => {
let stderr_layer = fmt::layer()
.with_writer(stderr_writer.clone())
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false);
let base = Registry::default().with(env_filter).with(stderr_layer);
if let Some(writer) = file_writer_opt.clone() {
let file_layer = fmt::layer()
.with_writer(writer.with_max_level(tracing::Level::TRACE))
.with_target(true)
.with_ansi(false);
base.with(file_layer).try_init()
} else {
base.try_init()
}
}
LogFormat::Json => {
let stderr_json = fmt::layer()
.with_writer(stderr_writer.clone())
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false)
.json()
.with_current_span(true)
.with_span_list(false);
let base = Registry::default().with(env_filter).with(stderr_json);
if let Some(writer) = file_writer_opt {
let file_json = fmt::layer()
.with_writer(writer.with_max_level(tracing::Level::TRACE))
.with_target(true)
.json()
.with_current_span(true)
.with_span_list(false);
base.with(file_json).try_init()
} else {
base.try_init()
}
}
};
if result.is_err() {
return None;
}
tracing::info!(
service = cfg.service_name,
log_dir = cfg.log_dir.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "<none>".into()),
format = ?cfg.format,
"telemetry initialised"
);
Some(TelemetryGuard {
_stderr_worker: Some(stderr_worker),
_file_worker: file_worker,
})
}