micrologger/
lib.rs

1/*! A Tiny Structured Logging Utility
2
3This is a lightweight logging utility that does little more than structure messages consistently and print them to standard out.
4
5This logging facility is a simple object that should be passed around to anything that needs to do logging. I recommend making it a part of your application context so that it is simply passed along with the rest of your application information. Begin by instantiating the object:
6
7```text
8let logger = micrologger::Logger::new(micrologger::json_stdout, "a-host", "an-app")
9```
10
11This API is fairly low-level and works best if clients wrap helper functions to build the log entries around the basic logging function.
12
13```text
14fn log_app_start(logger: micrologger::Logger) {
15    logger.log("app-start", HashMap::new());
16}
17
18fn log_method_start(logger: micrologger::Logger, method_name: str) {
19    let mut msg = Hashmap::new();
20    msg.insert("method-name", method_name);
21    logger.log("method-start", msg);
22}
23```
24*/
25
26#[macro_use]
27extern crate serde_derive;
28#[allow(unused_imports)]
29#[macro_use]
30extern crate serde_json;
31
32extern crate chrono;
33extern crate serde;
34
35use chrono::prelude::*;
36use serde_json::Value;
37use std::collections::HashMap;
38
39/// This defines the structured of an entry on disk. You will never create one of these directly,
40/// however you can always deserialize one from a line in the log file:
41/// ```text
42/// let res: Result<Entry, serde_json::Error> = serde_json::from_str(&js_str);
43/// ```
44
45#[derive(Serialize, Deserialize)]
46pub struct Entry {
47    #[serde(rename = "timestamp")]
48    time: DateTime<Utc>,
49    host: String,
50    app: String,
51    #[serde(rename = "type")]
52    msgtype: String,
53    msg: HashMap<String, Value>,
54}
55
56/// This allows the definition of a custom format closure. The trait provides both immutable and
57/// mutable logging methods for different scenarios. For instance, Iron middleware does not allow
58/// mutability in the handlers, and so a logging middleware must be immutable. This effectively
59/// means that for Iron, logging can only go to stdout.
60///
61/// Other systems may allow mutable logging, which allows for a Formatter that records to a file or
62/// to an internal data structure.
63pub trait Formatter {
64    fn format(&self, _: Entry) {
65        panic!("No formatter specified");
66    }
67
68    fn format_mut(&mut self, entry: Entry) {
69        self.format(entry);
70    }
71}
72
73/// The logger class
74pub struct Logger<F>
75where
76    F: Formatter,
77{
78    formatter: F,
79    app: String,
80    host: String,
81}
82
83impl<F> Logger<F>
84where
85    F: Formatter,
86{
87    /// Create a logger class with the specified command, the name of the host, and the name of the
88    /// application. Note that the logger takes ownership of the formatter. If you will later need
89    /// to examine the internals of the formatter, it is available at `logger.formatter`.
90    pub fn new(formatter: F, host: &str, app: &str) -> Logger<F> {
91        Logger {
92            formatter: formatter,
93            app: app.to_string(),
94            host: host.to_string(),
95        }
96    }
97
98    /// Write a message to the log. msgtype is any arbitrary string, but it should be meaningful to
99    /// the data provided in msg so that downstream parsers know how to interpret the message.
100    pub fn log(&self, msgtype: &str, msg: HashMap<String, Value>) {
101        let entry = Entry {
102            time: Utc::now(),
103            host: self.host.clone(),
104            app: self.app.clone(),
105            msgtype: msgtype.to_string(),
106            msg,
107        };
108        self.formatter.format(entry);
109    }
110
111    /// A mutable version of `log`. This uses the `format_mut` method on the formatter.
112    pub fn log_mut(&mut self, msgtype: &str, msg: HashMap<String, Value>) {
113        let entry = Entry {
114            time: Utc::now(),
115            host: self.host.clone(),
116            app: self.app.clone(),
117            msgtype: msgtype.to_string(),
118            msg,
119        };
120        self.formatter.format_mut(entry);
121    }
122}
123
124impl<F> Formatter for F
125where
126    F: Fn(Entry),
127{
128    fn format(&self, entry: Entry) {
129        (*self)(entry);
130    }
131}
132
133/// Standard formatter which converts entry into a JSON object and prints it to standard out.
134pub fn json_stdout(entry: Entry) {
135    match serde_json::to_string(&entry) {
136        Ok(s) => println!("{}", s),
137        Err(err) => println!("{{ error: \"{}\" }}", err.to_string()),
138    }
139}
140
141#[cfg(test)]
142mod test {
143    use super::*;
144
145    #[test]
146    fn it_logs_a_message() {
147        let logger = Logger::new(json_stdout, "my-host", "my-app");
148        let mut msg: HashMap<String, Value> = HashMap::new();
149        msg.insert("field1".to_string(), json!("value1"));
150        msg.insert("field2".to_string(), json!(12.5));
151        logger.log("test-message", msg);
152    }
153
154    #[test]
155    fn it_formats_log_messages_correctly() {
156        let mut msg: HashMap<String, Value> = HashMap::new();
157        msg.insert("field1".to_string(), json!("value1"));
158        msg.insert("field2".to_string(), json!(12.5));
159
160        let time = Utc::now();
161
162        let entry = Entry {
163            time,
164            host: "a-host".to_string(),
165            app: "an-app".to_string(),
166            msgtype: "a-message".to_string(),
167            msg,
168        };
169
170        let js_str = serde_json::to_string(&entry).expect("how can json encoding fail?");
171        println!("[it_formats] {}", js_str);
172        //let js: Result<Value, serde_json::Error> = serde_json::from_str(&js_str);
173        match serde_json::from_str(&js_str) {
174            Err(err) => panic!(err),
175            Ok(Value::Object(obj)) => {
176                // TODO: get this back at some point, but circle does nanoseconds whereas my
177                // machine does microseconds and *ugh* wtf.
178                //assert_eq!(obj.get("@timestamp"), Some(&json!(time.to_rfc3339_opts(SecondsFormat::Micros, true))));
179                assert_eq!(obj.get("host"), Some(&json!("a-host")));
180                assert_eq!(obj.get("app"), Some(&json!("an-app")));
181                assert_eq!(obj.get("type"), Some(&json!("a-message")));
182                assert_eq!(
183                    obj.get("msg").and_then(|o| o.get("field1")),
184                    Some(&json!("value1"))
185                );
186                assert_eq!(
187                    obj.get("msg").and_then(|o| o.get("field2")),
188                    Some(&json!(12.5))
189                );
190            }
191            Ok(_) => panic!("json deserialized incorrectly"),
192        }
193    }
194
195    #[test]
196    fn it_can_decode_log_entries() {
197        let mut msg: HashMap<String, Value> = HashMap::new();
198        msg.insert("field1".to_string(), json!("value1"));
199        msg.insert("field2".to_string(), json!(12.5));
200
201        let time = Utc::now();
202
203        let entry = Entry {
204            time,
205            host: "a-host".to_string(),
206            app: "an-app".to_string(),
207            msgtype: "a-message".to_string(),
208            msg,
209        };
210
211        let js_str = serde_json::to_string(&entry).expect("how can json encoding fail?");
212
213        let res: Result<Entry, serde_json::Error> = serde_json::from_str(&js_str);
214        match res {
215            Ok(entry_) => {
216                assert_eq!(entry_.time, time);
217                assert_eq!(entry_.host, "a-host");
218                assert_eq!(entry_.app, "an-app");
219                assert_eq!(entry_.msgtype, "a-message");
220                assert_eq!(entry_.msg.get("field1"), Some(&json!("value1")));
221                assert_eq!(entry_.msg.get("field1"), Some(&json!("value1")));
222                assert_eq!(entry_.msg.get("field2"), Some(&json!(12.5)));
223            }
224            Err(error) => panic!(error),
225        }
226    }
227
228    struct CustomFormatter {
229        prefix: String,
230        output: Vec<String>,
231    }
232
233    impl Formatter for CustomFormatter {
234        fn format_mut(&mut self, entry: Entry) {
235            self.output
236                .push(format!("{}, {}", self.prefix, entry.time.to_rfc3339()));
237        }
238    }
239
240    #[test]
241    fn it_can_handle_a_custom_formatter() {
242        let msg: HashMap<String, Value> = HashMap::new();
243
244        let formatter = CustomFormatter {
245            prefix: String::from("a-prefix"),
246            output: Vec::new(),
247        };
248        let mut logger = Logger::new(formatter, "my-host", "my-app");
249        logger.log_mut("generic", msg);
250        println!("results: {:?}", logger.formatter.output);
251        assert_eq!(logger.formatter.output.len(), 1);
252        assert!(logger.formatter.output[0].starts_with("a-prefix,"));
253    }
254}