afch_logger/
lib.rs

1//! Logger for Azure Function Custom Handler,
2//! abusing the undocumented (at least I don't know where) rule of Azure Function
3//! "infering" the log level from stderr.
4//! 
5//! For custom handler, if you print a message to stdout, it will be considered as a `Information` 
6//! level log by Azure Function runtime.
7//! 
8//! If you print a message to stderr, then it will be consider `Error` if it does not contain `warn` (case insensitive),
9//! otherwise it will be `Warning`.
10//! 
11//! So the strategy is, for error-level log, if `warn` occurs, base64-encode it, if the encoded string still contains `warn`,
12//! base-encode again, and if the twice-encoded string still contains `warn` (which should be impossible), log an error explain that the
13//! following warning is error, then log it as a warning. For warning-level log, if `warn` does not occur, add a `warning:` prefix.
14//! 
15//! You can initialize the log by [init]. You can also implement your own transform logic by implementing
16//! [Transform] trait and passing it to [init_transform].
17const WARN: [char; 4] = ['w', 'a', 'r', 'n'];
18
19pub trait Transform {
20    /// Transform the error log message that contains `warn` (case insensitive).
21    fn transform_error(&self, msg: String) -> String;
22    /// Transform the warning log message that does not contain `warn` (case insensitive).
23    fn transform_warning(&self, msg: String) -> String;
24}
25struct Logger<T>(T);
26impl<T: Transform + Send + Sync> log::Log for Logger<T> {
27    fn enabled(&self, m: &log::Metadata) -> bool {
28        m.level() <= log::Level::Info
29    }
30
31    fn log(&self, record: &log::Record) {
32        match record.level() {
33            log::Level::Error => {
34                let mut log = record.args().to_string();
35                if contains_warn(&log) {
36                    log = self.0.transform_error(log);
37                }
38
39                eprintln!("{}", log);
40            },
41            log::Level::Warn => {
42                let mut log = record.args().to_string();
43                if !contains_warn(&log) {
44                    log = self.0.transform_warning(log);
45                }
46
47                eprintln!("{}", log);
48            }
49            log::Level::Info => println!("{}", record.args()),
50            _ => {}
51        }
52        
53    }
54
55    fn flush(&self) {}
56}
57/// Returns true if the message contains `warn` (case insensitive).
58pub fn contains_warn(s: &str) -> bool {
59    let mut warn_ptr = 0;
60    for ch in s.chars() {
61        if ch.eq_ignore_ascii_case(&WARN[warn_ptr]) {
62            if warn_ptr == 3 {
63                return true
64            }
65            warn_ptr += 1;
66        } else {
67            warn_ptr = 0;
68        }
69    }
70    false
71}
72
73pub struct DefaultTransform;
74impl Transform for DefaultTransform {
75    fn transform_error(&self, msg: String) -> String {
76        let mut transformed = base64::encode(&msg);
77
78        if !contains_warn(&transformed) {
79            "base64-encoded log: ".to_string() + &transformed
80        } else {
81            transformed = base64::encode(transformed);
82            if !contains_warn(&transformed) {
83                "base64-encoded-twice log: ".to_string() + &transformed
84            } else {
85                // Should be impossible.
86                "The following error log has to be logged as Warning: \n".to_string() + &msg
87            }
88        }
89    }
90
91    fn transform_warning(&self, msg: String) -> String {
92        "warning: ".to_string() + &msg
93    }
94}
95
96pub fn init() {
97    init_transform(DefaultTransform);
98}
99
100pub fn init_transform<T: Transform + 'static + Send + Sync>(transform: T) {
101    log::set_logger(Box::leak(Box::new(Logger(transform))))
102        .expect("Failed to initialize logger");
103    log::set_max_level(log::LevelFilter::Info);
104}
105
106
107#[cfg(test)]
108mod tests {
109    use crate::contains_warn;
110
111    #[test]
112    fn test_no_warn() {
113        assert!(!contains_warn("abcdefg"));
114    }
115    #[test]
116    fn has_warn() {
117        assert!(contains_warn("awarng"));
118    }
119    #[test]
120    fn differnt_case_warn() {
121        assert!(contains_warn("awARng"));
122    }
123    #[test]
124    fn suffix_warn() {
125        assert!(contains_warn("awARn"));
126    }
127    #[test]
128    fn split_warn() {
129        assert!(!contains_warn("wa#rn"));
130    }
131}
132