use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use tonic::service::Interceptor;
use tonic::{Request, Status};
use tracing::{debug, error, info, trace, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LogLevel {
Trace,
Debug,
#[default]
Info,
Warn,
Error,
Off,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LogLevel::Trace => write!(f, "TRACE"),
LogLevel::Debug => write!(f, "DEBUG"),
LogLevel::Info => write!(f, "INFO"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
LogLevel::Off => write!(f, "OFF"),
}
}
}
#[derive(Debug, Clone)]
pub struct LoggingConfig {
pub success_level: LogLevel,
pub error_level: LogLevel,
pub log_metadata: bool,
pub log_path: bool,
pub redact_sensitive: bool,
pub sensitive_headers: Vec<String>,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
success_level: LogLevel::Info,
error_level: LogLevel::Error,
log_metadata: true,
log_path: true,
redact_sensitive: true,
sensitive_headers: vec![
"authorization".to_string(),
"x-api-key".to_string(),
"x-auth-token".to_string(),
],
}
}
}
impl LoggingConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_success_level(mut self, level: LogLevel) -> Self {
self.success_level = level;
self
}
#[must_use]
pub fn with_error_level(mut self, level: LogLevel) -> Self {
self.error_level = level;
self
}
#[must_use]
pub fn with_metadata(mut self, enabled: bool) -> Self {
self.log_metadata = enabled;
self
}
#[must_use]
pub fn with_path(mut self, enabled: bool) -> Self {
self.log_path = enabled;
self
}
#[must_use]
pub fn with_redaction(mut self, enabled: bool) -> Self {
self.redact_sensitive = enabled;
self
}
#[must_use]
pub fn with_sensitive_header(mut self, header: impl Into<String>) -> Self {
self.sensitive_headers.push(header.into());
self
}
#[must_use]
pub fn verbose() -> Self {
Self {
success_level: LogLevel::Debug,
error_level: LogLevel::Error,
log_metadata: true,
log_path: true,
redact_sensitive: true,
sensitive_headers: vec![
"authorization".to_string(),
"x-api-key".to_string(),
"x-auth-token".to_string(),
],
}
}
#[must_use]
pub fn quiet() -> Self {
Self {
success_level: LogLevel::Off,
error_level: LogLevel::Warn,
log_metadata: false,
log_path: true,
redact_sensitive: true,
sensitive_headers: vec![
"authorization".to_string(),
"x-api-key".to_string(),
"x-auth-token".to_string(),
],
}
}
}
#[derive(Debug, Default)]
pub struct InterceptorMetrics {
total_requests: AtomicU64,
successful_requests: AtomicU64,
failed_requests: AtomicU64,
}
impl InterceptorMetrics {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record_success(&self) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
self.successful_requests.fetch_add(1, Ordering::Relaxed);
}
pub fn record_failure(&self) {
self.total_requests.fetch_add(1, Ordering::Relaxed);
self.failed_requests.fetch_add(1, Ordering::Relaxed);
}
#[must_use]
pub fn total_requests(&self) -> u64 {
self.total_requests.load(Ordering::Relaxed)
}
#[must_use]
pub fn successful_requests(&self) -> u64 {
self.successful_requests.load(Ordering::Relaxed)
}
#[must_use]
pub fn failed_requests(&self) -> u64 {
self.failed_requests.load(Ordering::Relaxed)
}
#[must_use]
pub fn success_rate(&self) -> f64 {
let total = self.total_requests.load(Ordering::Relaxed);
if total == 0 {
return 1.0;
}
let successful = self.successful_requests.load(Ordering::Relaxed);
successful as f64 / total as f64
}
pub fn reset(&self) {
self.total_requests.store(0, Ordering::Relaxed);
self.successful_requests.store(0, Ordering::Relaxed);
self.failed_requests.store(0, Ordering::Relaxed);
}
}
#[derive(Clone)]
pub struct LoggingInterceptor {
config: LoggingConfig,
}
impl LoggingInterceptor {
#[must_use]
pub fn new() -> Self {
Self {
config: LoggingConfig::default(),
}
}
#[must_use]
pub fn with_config(config: LoggingConfig) -> Self {
Self { config }
}
#[must_use]
pub fn config(&self) -> &LoggingConfig {
&self.config
}
fn log_request<T>(&self, request: &Request<T>) {
if self.config.success_level == LogLevel::Off {
return;
}
let metadata_str = if self.config.log_metadata {
let mut parts = Vec::new();
for key_and_value in request.metadata().iter() {
match key_and_value {
tonic::metadata::KeyAndValueRef::Ascii(key, value) => {
let key_str = key.as_str();
if self.config.redact_sensitive
&& self
.config
.sensitive_headers
.iter()
.any(|h| h.eq_ignore_ascii_case(key_str))
{
parts.push(format!("{}=[REDACTED]", key_str));
} else {
parts.push(format!("{}={:?}", key_str, value));
}
}
tonic::metadata::KeyAndValueRef::Binary(key, value) => {
let key_str = key.as_str();
if self.config.redact_sensitive
&& self
.config
.sensitive_headers
.iter()
.any(|h| h.eq_ignore_ascii_case(key_str))
{
parts.push(format!("{}=[REDACTED]", key_str));
} else {
parts.push(format!("{}={:?}", key_str, value));
}
}
}
}
if parts.is_empty() {
String::new()
} else {
format!(" metadata=[{}]", parts.join(", "))
}
} else {
String::new()
};
match self.config.success_level {
LogLevel::Trace => {
trace!(target: "talos_api::grpc", "gRPC request{}", metadata_str);
}
LogLevel::Debug => {
debug!(target: "talos_api::grpc", "gRPC request{}", metadata_str);
}
LogLevel::Info => {
info!(target: "talos_api::grpc", "gRPC request{}", metadata_str);
}
LogLevel::Warn => {
warn!(target: "talos_api::grpc", "gRPC request{}", metadata_str);
}
LogLevel::Error => {
error!(target: "talos_api::grpc", "gRPC request{}", metadata_str);
}
LogLevel::Off => {}
}
}
}
impl Default for LoggingInterceptor {
fn default() -> Self {
Self::new()
}
}
impl Interceptor for LoggingInterceptor {
fn call(&mut self, request: Request<()>) -> std::result::Result<Request<()>, Status> {
self.log_request(&request);
Ok(request)
}
}
#[derive(Debug)]
pub struct RequestLogger {
config: LoggingConfig,
metrics: InterceptorMetrics,
}
impl RequestLogger {
#[must_use]
pub fn new() -> Self {
Self {
config: LoggingConfig::default(),
metrics: InterceptorMetrics::new(),
}
}
#[must_use]
pub fn with_config(config: LoggingConfig) -> Self {
Self {
config,
metrics: InterceptorMetrics::new(),
}
}
#[must_use]
pub fn metrics(&self) -> &InterceptorMetrics {
&self.metrics
}
#[must_use]
pub fn start(&self, method: &str) -> RequestSpan {
RequestSpan {
method: method.to_string(),
start: Instant::now(),
}
}
pub fn finish_success(&self, span: RequestSpan) {
self.metrics.record_success();
let elapsed = span.start.elapsed();
if self.config.success_level == LogLevel::Off {
return;
}
let msg = format!("gRPC response: {} completed in {:?}", span.method, elapsed);
match self.config.success_level {
LogLevel::Trace => trace!(target: "talos_api::grpc", "{}", msg),
LogLevel::Debug => debug!(target: "talos_api::grpc", "{}", msg),
LogLevel::Info => info!(target: "talos_api::grpc", "{}", msg),
LogLevel::Warn => warn!(target: "talos_api::grpc", "{}", msg),
LogLevel::Error => error!(target: "talos_api::grpc", "{}", msg),
LogLevel::Off => {}
}
}
pub fn finish_error(&self, span: RequestSpan, error: &str) {
self.metrics.record_failure();
let elapsed = span.start.elapsed();
if self.config.error_level == LogLevel::Off {
return;
}
let msg = format!(
"gRPC error: {} failed in {:?}: {}",
span.method, elapsed, error
);
match self.config.error_level {
LogLevel::Trace => trace!(target: "talos_api::grpc", "{}", msg),
LogLevel::Debug => debug!(target: "talos_api::grpc", "{}", msg),
LogLevel::Info => info!(target: "talos_api::grpc", "{}", msg),
LogLevel::Warn => warn!(target: "talos_api::grpc", "{}", msg),
LogLevel::Error => error!(target: "talos_api::grpc", "{}", msg),
LogLevel::Off => {}
}
}
}
impl Default for RequestLogger {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct RequestSpan {
method: String,
start: Instant,
}
impl RequestSpan {
#[must_use]
pub fn method(&self) -> &str {
&self.method
}
#[must_use]
pub fn elapsed(&self) -> std::time::Duration {
self.start.elapsed()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_level_display() {
assert_eq!(LogLevel::Trace.to_string(), "TRACE");
assert_eq!(LogLevel::Debug.to_string(), "DEBUG");
assert_eq!(LogLevel::Info.to_string(), "INFO");
assert_eq!(LogLevel::Warn.to_string(), "WARN");
assert_eq!(LogLevel::Error.to_string(), "ERROR");
assert_eq!(LogLevel::Off.to_string(), "OFF");
}
#[test]
fn test_log_level_default() {
assert_eq!(LogLevel::default(), LogLevel::Info);
}
#[test]
fn test_logging_config_default() {
let config = LoggingConfig::default();
assert_eq!(config.success_level, LogLevel::Info);
assert_eq!(config.error_level, LogLevel::Error);
assert!(config.log_metadata);
assert!(config.log_path);
assert!(config.redact_sensitive);
}
#[test]
fn test_logging_config_builder() {
let config = LoggingConfig::new()
.with_success_level(LogLevel::Debug)
.with_error_level(LogLevel::Warn)
.with_metadata(false)
.with_path(false)
.with_redaction(false)
.with_sensitive_header("x-custom-auth");
assert_eq!(config.success_level, LogLevel::Debug);
assert_eq!(config.error_level, LogLevel::Warn);
assert!(!config.log_metadata);
assert!(!config.log_path);
assert!(!config.redact_sensitive);
assert!(config
.sensitive_headers
.contains(&"x-custom-auth".to_string()));
}
#[test]
fn test_logging_config_verbose() {
let config = LoggingConfig::verbose();
assert_eq!(config.success_level, LogLevel::Debug);
assert!(config.log_metadata);
}
#[test]
fn test_logging_config_quiet() {
let config = LoggingConfig::quiet();
assert_eq!(config.success_level, LogLevel::Off);
assert_eq!(config.error_level, LogLevel::Warn);
assert!(!config.log_metadata);
}
#[test]
fn test_interceptor_metrics() {
let metrics = InterceptorMetrics::new();
assert_eq!(metrics.total_requests(), 0);
assert_eq!(metrics.successful_requests(), 0);
assert_eq!(metrics.failed_requests(), 0);
assert_eq!(metrics.success_rate(), 1.0);
metrics.record_success();
metrics.record_success();
metrics.record_failure();
assert_eq!(metrics.total_requests(), 3);
assert_eq!(metrics.successful_requests(), 2);
assert_eq!(metrics.failed_requests(), 1);
assert!((metrics.success_rate() - 0.666_666_666_666_666_6).abs() < 0.001);
}
#[test]
fn test_interceptor_metrics_reset() {
let metrics = InterceptorMetrics::new();
metrics.record_success();
metrics.record_failure();
metrics.reset();
assert_eq!(metrics.total_requests(), 0);
assert_eq!(metrics.successful_requests(), 0);
assert_eq!(metrics.failed_requests(), 0);
}
#[test]
fn test_logging_interceptor_default() {
let interceptor = LoggingInterceptor::default();
assert_eq!(interceptor.config().success_level, LogLevel::Info);
}
#[test]
fn test_request_logger() {
let logger = RequestLogger::new();
let span = logger.start("Version");
assert_eq!(span.method(), "Version");
assert!(span.elapsed() < std::time::Duration::from_secs(1));
logger.finish_success(span);
assert_eq!(logger.metrics().total_requests(), 1);
assert_eq!(logger.metrics().successful_requests(), 1);
}
#[test]
fn test_request_logger_error() {
let logger = RequestLogger::with_config(LoggingConfig::quiet());
let span = logger.start("ApplyConfiguration");
logger.finish_error(span, "Permission denied");
assert_eq!(logger.metrics().total_requests(), 1);
assert_eq!(logger.metrics().failed_requests(), 1);
}
#[test]
fn test_request_span() {
let span = RequestSpan {
method: "test".to_string(),
start: Instant::now(),
};
assert_eq!(span.method(), "test");
std::thread::sleep(std::time::Duration::from_millis(1));
assert!(span.elapsed() >= std::time::Duration::from_millis(1));
}
}