Skip to main content

wire_framework/vlog/logs/
mod.rs

1use std::{backtrace::Backtrace, str::FromStr};
2
3use serde::Deserialize;
4use tracing_subscriber::{EnvFilter, Layer, fmt, registry::LookupSpan};
5
6mod layer;
7
8/// Specifies the format of the logs in stdout.
9#[derive(Debug, Clone, Copy, Default, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum LogFormat {
12    #[default]
13    Plain,
14    Json,
15}
16
17impl FromStr for LogFormat {
18    type Err = LogFormatError;
19
20    fn from_str(s: &str) -> Result<Self, Self::Err> {
21        match s {
22            "plain" => Ok(Self::Plain),
23            "json" => Ok(Self::Json),
24            _ => Err(LogFormatError::InvalidFormat),
25        }
26    }
27}
28
29#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum LogFormatError {
32    #[error("Invalid log format")]
33    InvalidFormat,
34}
35
36#[derive(Debug, Default)]
37pub struct Logs {
38    format: LogFormat,
39    log_directives: Option<String>,
40    disable_default_logs: bool,
41}
42
43impl From<LogFormat> for Logs {
44    fn from(format: LogFormat) -> Self {
45        Self {
46            format,
47            log_directives: None,
48            disable_default_logs: false,
49        }
50    }
51}
52
53impl Logs {
54    pub fn new(format: &str) -> Result<Self, LogFormatError> {
55        Ok(Self {
56            format: format.parse()?,
57            log_directives: None,
58            disable_default_logs: false,
59        })
60    }
61
62    /// Builds a filter for the logs.
63    ///
64    /// Unless `disable_default_logs` was set, uses `wire=info` as a default which is then merged
65    /// with user-defined directives. Provided directives can extend/override the default value.
66    ///
67    /// The provided default convers all the crates with a name starting with `wire` (per `tracing`
68    /// [documentation][1]), which is a good enough default for any project.
69    ///
70    /// If `log_directives` are provided via `with_log_directives`, they will be used.
71    /// Otherwise, the value will be parsed from the environment variable `RUST_LOG`.
72    ///
73    /// [1]: https://docs.rs/tracing-subscriber/0.3.18/tracing_subscriber/filter/targets/struct.Targets.html#filtering-with-targets
74    pub(super) fn build_filter(&self) -> EnvFilter {
75        let mut directives = if self.disable_default_logs {
76            String::new()
77        } else {
78            format!("info,wire_framework=warn,")
79        };
80        if let Some(log_directives) = &self.log_directives {
81            directives.push_str(log_directives);
82        } else if let Ok(env_directives) = std::env::var(EnvFilter::DEFAULT_ENV) {
83            directives.push_str(&env_directives);
84        };
85        EnvFilter::new(directives)
86    }
87
88    pub fn with_log_directives(mut self, log_directives: Option<String>) -> Self {
89        self.log_directives = log_directives;
90        self
91    }
92
93    pub fn install_panic_hook(&self) {
94        // Check whether we need to change the default panic handler.
95        // Note that this must happen before we initialize Sentry, since otherwise
96        // Sentry's panic handler will also invoke the default one, resulting in unformatted
97        // panic info being output to stderr.
98        if matches!(self.format, LogFormat::Json) {
99            // Remove any existing hook. We expect that no hook is set by default.
100            let _ = std::panic::take_hook();
101            // Override the default panic handler to print the panic in JSON format.
102            std::panic::set_hook(Box::new(json_panic_handler));
103        };
104    }
105
106    pub fn into_layer<S>(self) -> impl Layer<S>
107    where
108        S: tracing::Subscriber + for<'span> LookupSpan<'span> + Send + Sync,
109    {
110        let filter = self.build_filter();
111        let layer = match self.format {
112            LogFormat::Plain => layer::LogsLayer::Plain(fmt::Layer::new()),
113            LogFormat::Json => {
114                let timer = tracing_subscriber::fmt::time::UtcTime::rfc_3339();
115                let json_layer = fmt::Layer::default()
116                    .with_file(true)
117                    .with_line_number(true)
118                    .with_timer(timer)
119                    .json();
120                layer::LogsLayer::Json(json_layer)
121            }
122        };
123        layer.with_filter(filter)
124    }
125}
126
127#[allow(deprecated)] // Not available yet on stable, so we can't switch right now.
128fn json_panic_handler(panic_info: &std::panic::PanicInfo) {
129    let backtrace = Backtrace::force_capture();
130    let timestamp = chrono::Utc::now();
131    let panic_message = if let Some(s) = panic_info.payload().downcast_ref::<String>() {
132        s.as_str()
133    } else if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
134        s
135    } else {
136        "Panic occurred without additional info"
137    };
138
139    let panic_location = panic_info
140        .location()
141        .map(|val| val.to_string())
142        .unwrap_or_else(|| "Unknown location".to_owned());
143
144    let backtrace_str = backtrace.to_string();
145    let timestamp_str = timestamp.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string();
146
147    println!(
148        "{}",
149        serde_json::json!({
150            "timestamp": timestamp_str,
151            "level": "CRITICAL",
152            "fields": {
153                "message": panic_message,
154                "location": panic_location,
155                "backtrace": backtrace_str,
156            }
157        })
158    );
159}