afch_logger/lib.rs
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
//! Logger for Azure Function Custom Handler,
//! abusing the undocumented (at least I don't know where) rule of Azure Function
//! "infering" the log level from stderr.
//!
//! For custom handler, if you print a message to stdout, it will be considered as a `Information`
//! level log by Azure Function runtime.
//!
//! If you print a message to stderr, then it will be consider `Error` if it does not contain `warn` (case insensitive),
//! otherwise it will be `Warning`.
//!
//! So the strategy is, for error-level log, if `warn` occurs, base64-encode it, if the encoded string still contains `warn`,
//! base-encode again, and if the twice-encoded string still contains `warn` (which should be impossible), log an error explain that the
//! following warning is error, then log it as a warning. For warning-level log, if `warn` does not occur, add a `warning:` prefix.
//!
//! You can initialize the log by [init]. You can also implement your own transform logic by implementing
//! [Transform] trait and passing it to [init_transform].
const WARN: [char; 4] = ['w', 'a', 'r', 'n'];
pub trait Transform {
/// Transform the error log message that contains `warn` (case insensitive).
fn transform_error(&self, msg: String) -> String;
/// Transform the warning log message that does not contain `warn` (case insensitive).
fn transform_warning(&self, msg: String) -> String;
}
struct Logger<T>(T);
impl<T: Transform + Send + Sync> log::Log for Logger<T> {
fn enabled(&self, m: &log::Metadata) -> bool {
m.level() <= log::Level::Info
}
fn log(&self, record: &log::Record) {
match record.level() {
log::Level::Error => {
let mut log = record.args().to_string();
if contains_warn(&log) {
log = self.0.transform_error(log);
}
eprintln!("{}", log);
},
log::Level::Warn => {
let mut log = record.args().to_string();
if !contains_warn(&log) {
log = self.0.transform_warning(log);
}
eprintln!("{}", log);
}
log::Level::Info => println!("{}", record.args()),
_ => {}
}
}
fn flush(&self) {}
}
/// Returns true if the message contains `warn` (case insensitive).
pub fn contains_warn(s: &str) -> bool {
let mut warn_ptr = 0;
for ch in s.chars() {
if ch.eq_ignore_ascii_case(&WARN[warn_ptr]) {
if warn_ptr == 3 {
return true
}
warn_ptr += 1;
} else {
warn_ptr = 0;
}
}
false
}
pub struct DefaultTransform;
impl Transform for DefaultTransform {
fn transform_error(&self, msg: String) -> String {
let mut transformed = base64::encode(&msg);
if !contains_warn(&transformed) {
"base64-encoded log: ".to_string() + &transformed
} else {
transformed = base64::encode(transformed);
if !contains_warn(&transformed) {
"base64-encoded-twice log: ".to_string() + &transformed
} else {
// Should be impossible.
"The following error log has to be logged as Warning: \n".to_string() + &msg
}
}
}
fn transform_warning(&self, msg: String) -> String {
"warning: ".to_string() + &msg
}
}
pub fn init() {
init_transform(DefaultTransform);
}
pub fn init_transform<T: Transform + 'static + Send + Sync>(transform: T) {
log::set_logger(Box::leak(Box::new(Logger(transform))))
.expect("Failed to initialize logger");
log::set_max_level(log::LevelFilter::Info);
}
#[cfg(test)]
mod tests {
use crate::contains_warn;
#[test]
fn test_no_warn() {
assert!(!contains_warn("abcdefg"));
}
#[test]
fn has_warn() {
assert!(contains_warn("awarng"));
}
#[test]
fn differnt_case_warn() {
assert!(contains_warn("awARng"));
}
#[test]
fn suffix_warn() {
assert!(contains_warn("awARn"));
}
#[test]
fn split_warn() {
assert!(!contains_warn("wa#rn"));
}
}