1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use std::fs;

use itertools::Itertools;
use time::UtcOffset;
use tracing::{Level, Subscriber};
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{reload::Handle, EnvFilter};

pub fn init_logging() -> Result<LoggingHandle<EnvFilter, impl Subscriber>, String> {
    init_tracing_logger()
}

pub enum ReloadLogLevelError {
    InvalidFilter(String),
    ReloadFailed(tracing_subscriber::reload::Error),
}

pub fn reload_log_level(
    logging_handle: &LoggingHandle<EnvFilter, impl Subscriber>,
) -> Result<String, ReloadLogLevelError> {
    let new_filter = read_env_filter().map_err(|e| ReloadLogLevelError::InvalidFilter(e))?;

    let filter_string = format!("{}", new_filter);

    logging_handle
        .handle
        .reload(new_filter)
        .map_err(|e| ReloadLogLevelError::ReloadFailed(e))?;

    Ok(filter_string)
}

fn init_tracing_logger() -> Result<LoggingHandle<EnvFilter, impl Subscriber>, String> {
    let timer = tracing_subscriber::fmt::time::OffsetTime::new(
        UtcOffset::current_local_offset().unwrap_or_else(|err| {
            eprintln!("Failed to get timezone: {}", err);
            UtcOffset::UTC
        }),
        time::macros::format_description!(
            "[year]-[month]-[day] [hour]:[minute]:[second] +[offset_hour]"
        ),
    );
    let (non_blocking, guard) = tracing_appender::non_blocking(std::io::stdout());

    let env_filter = read_env_filter().unwrap_or_else(|err| {
        eprintln!(
            "Failed to read env filter, using environment variable or default: {}",
            err
        );
        EnvFilter::builder()
            .with_default_directive(Level::DEBUG.into())
            .from_env_lossy()
    });

    println!("Env Filter: {}", env_filter);

    //let (layer, handle) = reload::Layer::new(env_filter);

    let builder = tracing_subscriber::fmt()
        .with_timer(timer)
        .with_writer(non_blocking)
        .with_env_filter(env_filter)
        .with_filter_reloading();

    //let layered = subscriber.with(env_filter);
    let handle = builder.reload_handle();

    tracing::subscriber::set_global_default(builder.finish())
        .map_err(|err| format!("failed to initialize logger: {}", err))?;

    Ok(LoggingHandle {
        non_blocking_guard: guard,
        handle,
    })
}

fn read_env_filter() -> Result<EnvFilter, String> {
    let s = fs::read_to_string("logging.env")
        .map_err(|err| format!("Failed to read file logging.env file: {}", err))?;

    parse_env_filter(&s)
}

/// Parses a multi-line env-filter with # comments.
fn parse_env_filter(filter_string: &str) -> Result<EnvFilter, String> {
    let filter = filter_string
        .lines()
        .map(|line| line.split("#").next().unwrap_or(""))
        .map(|part| part.trim())
        .filter(|part| !part.is_empty())
        .join(",");

    EnvFilter::builder()
        .with_default_directive(tracing::Level::DEBUG.into())
        .parse(filter)
        .map_err(|err| format!("Failed to parse env filter: {}", err))
}

pub struct LoggingHandle<L, S> {
    non_blocking_guard: WorkerGuard,
    handle: Handle<L, S>,
}

#[cfg(test)]
mod tests {
    use tracing_subscriber::EnvFilter;

    use crate::logging::parse_env_filter;

    const FILTER: &str = "info,sqlx=warn,follow_heating::brain::modes=debug,follow_heating::brain::boost_active_rooms=info";

    const MULTI_LINE_FILTER: &str = "
        # This is a multi line env filter where things can be commented out.
        info
        sqlx=warn
        #follow_heating::unused=info
        follow_heating::brain::modes=debug # Inline comment.
        follow_heating::brain::boost_active_rooms=info
    ";

    #[test]
    fn test_parse_logging_simple() {
        let actual = parse_env_filter(FILTER).unwrap();
        let expected = EnvFilter::builder().parse(FILTER).unwrap();
        assert_eq!(format!("{}", actual), format!("{}", expected));
    }

    #[test]
    fn test_parse_logging_multiline() {
        let actual = parse_env_filter(MULTI_LINE_FILTER).unwrap();
        let expected = EnvFilter::builder().parse(FILTER).unwrap();
        assert_eq!(format!("{}", actual), format!("{}", expected));
    }
}