Skip to main content

rust_template_foundation/
logging.rs

1//! Log level and format types shared across CLI and server crates.
2//!
3//! The initialisation functions (`init_cli_logging`, `init_server_logging`)
4//! are feature-gated so both can coexist when a crate enables `cli` and
5//! `server` simultaneously.
6
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9use thiserror::Error;
10
11/// Severity threshold for log output.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "lowercase")]
14pub enum LogLevel {
15  Trace,
16  Debug,
17  Info,
18  Warn,
19  Error,
20}
21
22impl FromStr for LogLevel {
23  type Err = LogLevelParseError;
24
25  fn from_str(s: &str) -> Result<Self, Self::Err> {
26    match s.to_lowercase().as_str() {
27      "trace" => Ok(LogLevel::Trace),
28      "debug" => Ok(LogLevel::Debug),
29      "info" => Ok(LogLevel::Info),
30      "warn" | "warning" => Ok(LogLevel::Warn),
31      "error" => Ok(LogLevel::Error),
32      _ => Err(LogLevelParseError::InvalidLevel(s.to_string())),
33    }
34  }
35}
36
37impl From<LogLevel> for tracing::Level {
38  fn from(level: LogLevel) -> Self {
39    match level {
40      LogLevel::Trace => tracing::Level::TRACE,
41      LogLevel::Debug => tracing::Level::DEBUG,
42      LogLevel::Info => tracing::Level::INFO,
43      LogLevel::Warn => tracing::Level::WARN,
44      LogLevel::Error => tracing::Level::ERROR,
45    }
46  }
47}
48
49impl std::fmt::Display for LogLevel {
50  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51    match self {
52      LogLevel::Trace => write!(f, "trace"),
53      LogLevel::Debug => write!(f, "debug"),
54      LogLevel::Info => write!(f, "info"),
55      LogLevel::Warn => write!(f, "warn"),
56      LogLevel::Error => write!(f, "error"),
57    }
58  }
59}
60
61/// Structured log encoding.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum LogFormat {
65  Text,
66  Json,
67}
68
69impl FromStr for LogFormat {
70  type Err = LogFormatParseError;
71
72  fn from_str(s: &str) -> Result<Self, Self::Err> {
73    match s.to_lowercase().as_str() {
74      "text" | "pretty" => Ok(LogFormat::Text),
75      "json" => Ok(LogFormat::Json),
76      _ => Err(LogFormatParseError::InvalidFormat(s.to_string())),
77    }
78  }
79}
80
81impl std::fmt::Display for LogFormat {
82  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83    match self {
84      LogFormat::Text => write!(f, "text"),
85      LogFormat::Json => write!(f, "json"),
86    }
87  }
88}
89
90#[derive(Debug, Error)]
91pub enum LogLevelParseError {
92  #[error(
93    "Invalid log level: {0}. Valid values are: trace, debug, info, warn, error"
94  )]
95  InvalidLevel(String),
96}
97
98#[derive(Debug, Error)]
99pub enum LogFormatParseError {
100  #[error("Invalid log format: {0}. Valid values are: text, json")]
101  InvalidFormat(String),
102}
103
104/// Initialise CLI logging — writes to stderr so program output on stdout
105/// remains clean for piping.
106#[cfg(feature = "cli")]
107pub fn init_cli_logging(level: LogLevel, format: LogFormat) {
108  use tracing_subscriber::{
109    fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
110  };
111
112  let env_filter = EnvFilter::try_from_default_env()
113    .unwrap_or_else(|_| EnvFilter::new(level.to_string()));
114
115  match format {
116    LogFormat::Text => {
117      tracing_subscriber::registry()
118        .with(
119          fmt::layer()
120            .with_writer(std::io::stderr)
121            .with_target(true)
122            .with_line_number(true)
123            .with_filter(env_filter),
124        )
125        .init();
126    }
127    LogFormat::Json => {
128      tracing_subscriber::registry()
129        .with(
130          fmt::layer()
131            .json()
132            .with_writer(std::io::stderr)
133            .with_target(true)
134            .with_line_number(true)
135            .with_filter(env_filter),
136        )
137        .init();
138    }
139  }
140}
141
142/// Initialise server logging — tries journald on Unix first, falls back
143/// to stderr with the requested format.
144#[cfg(feature = "server")]
145pub fn init_server_logging(level: LogLevel, format: LogFormat) {
146  use tracing_subscriber::{
147    fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
148  };
149
150  let env_filter = EnvFilter::try_from_default_env()
151    .unwrap_or_else(|_| EnvFilter::new(level.to_string()));
152
153  #[cfg(unix)]
154  if let Ok(journald) = tracing_journald::layer() {
155    tracing_subscriber::registry()
156      .with(tracing_subscriber::Layer::with_filter(journald, env_filter))
157      .init();
158    return;
159  }
160
161  match format {
162    LogFormat::Text => {
163      tracing_subscriber::registry()
164        .with(
165          fmt::layer()
166            .with_writer(std::io::stderr)
167            .with_target(true)
168            .with_line_number(true)
169            .with_filter(env_filter),
170        )
171        .init();
172    }
173    LogFormat::Json => {
174      tracing_subscriber::registry()
175        .with(
176          fmt::layer()
177            .json()
178            .with_writer(std::io::stderr)
179            .with_target(true)
180            .with_line_number(true)
181            .with_filter(env_filter),
182        )
183        .init();
184    }
185  }
186}