celp_sdk/logger/
log.rs

1use std::io::{self, Write};
2use std::path::Path;
3
4use chrono::Utc;
5use colored::Colorize;
6use env_logger::fmt::{style, Formatter};
7use log::{
8    kv::{Error, Key, Value, VisitSource},
9    Level, Record,
10};
11
12pub use log::{debug, error, info, trace, warn as warning};
13
14/// SDK Log Level Enum
15#[derive(
16    Clone,
17    Copy,
18    Debug,
19    Default,
20    strum_macros::Display,
21    strum_macros::EnumString,
22    strum_macros::FromRepr,
23)]
24pub enum SDKLogLevel {
25    Error = 1,
26    Warning,
27    #[default]
28    Info,
29    Debug,
30    Trace,
31}
32static LOG_ENV_VAR: &str = "CELP_LOG";
33
34/// SDKLogLevel implementation
35impl SDKLogLevel {
36    /// Most verbose logging level available.
37    pub const MAX: Self = SDKLogLevel::Trace;
38
39    /// Least verbose logging level available.
40    pub const MIN: Self = SDKLogLevel::Error;
41}
42
43/// Initialise the logging functionality to a certain level.
44///
45/// # Arguments
46///
47/// * `module_name` - A string to denote the calling application. It can be any string. It does not have to match the exact application name.
48/// * `log_level` - The level of logging to use. If not given, will use default logging level.
49///
50/// # Example
51/// ```rust,no_run
52/// use celp_sdk::{SDKLogLevel, init_logger};
53///
54/// let app_name = "test";
55/// let level = SDKLogLevel::default();
56///
57/// init_logger(app_name, Some(level));
58/// ```
59pub fn init_logger(module_name: &str, log_level: Option<SDKLogLevel>) {
60    let log_level = match log_level.unwrap_or_default() {
61        SDKLogLevel::Error => Level::Error,
62        SDKLogLevel::Warning => Level::Warn,
63        SDKLogLevel::Info => Level::Info,
64        SDKLogLevel::Debug => Level::Debug,
65        SDKLogLevel::Trace => Level::Trace,
66    }
67    .to_level_filter();
68
69    env_logger::Builder::from_env(LOG_ENV_VAR)
70        // NOTE: `filter_level` must be first before `filter_module`,
71        //  otherwise the level filter is not set correctly.
72        .filter_level(log_level)
73        .filter_module(module_name, log_level)
74        .target(env_logger::Target::Stdout)
75        .format(format_log_str)
76        .init();
77}
78
79/// Format the log string in a similar manner to C++ SDK logger.
80/// Will output in the following format:
81/// "[1970-01-01T12:34:56.012345Z] [main.rs:123] [---D---] log message key=value"
82fn format_log_str(buf: &mut Formatter, record: &Record) -> Result<(), io::Error> {
83    let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
84
85    let filename = record.file().unwrap_or("Unknown file path");
86    let path = Path::new(filename);
87    let file = path
88        .file_name()
89        .and_then(|s| s.to_str())
90        .unwrap_or("<unknown>");
91
92    let line = record.line().unwrap_or(0);
93
94    let sdk_log_level = record.level();
95    // TODO: Confirm color choice with C++ version.
96    let level = match sdk_log_level {
97        Level::Error => String::from("E").red(),
98        Level::Warn => String::from("W").yellow(),
99        Level::Info => String::from("I").green(),
100        Level::Debug => String::from("D").blue(),
101        Level::Trace => String::from("T").white(),
102    };
103
104    let args = record.args();
105
106    // NOTE: Will not implement thread ID printing for now. Rust doesn't have any nice ways for this library to get the thread ID of the calling application.
107    //       We would have to pss the thread ID directly from the calling application. That would just be messy.
108    write!(buf, "[{now}] [{file}:{line}] [---{level}---] {args:?}")?;
109
110    record
111        .key_values()
112        .visit(&mut KvSourceVisitor(buf))
113        .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
114
115    writeln!(buf)?;
116
117    Ok(())
118}
119
120struct KvSourceVisitor<'a>(&'a mut Formatter);
121
122impl<'kvs> VisitSource<'kvs> for KvSourceVisitor<'_> {
123    fn visit_pair(&mut self, key: Key<'_>, value: Value<'kvs>) -> Result<(), Error> {
124        let style = style::Style::new().italic();
125
126        write!(self.0, "{style}")?;
127        write!(self.0, " {}={}", key, value)?;
128        write!(self.0, "{style:#}")?;
129        Ok(())
130    }
131}