stackdriver_logger/
lib.rs

1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::{env, fmt};
5
6use log::{Level, SetLoggerError};
7
8#[cfg(any(test, not(all(feature = "pretty_env_logger", debug_assertions))))]
9use serde_json::{json, Value};
10
11#[cfg(feature = "cargo")]
12#[doc(hidden)]
13#[macro_use]
14pub mod macros;
15
16#[cfg(feature = "customfields")]
17use log::kv;
18
19#[cfg(feature = "customfields")]
20use std::collections::HashMap;
21
22// Wrap Level from the log crate so we can implement standard traits for it
23struct LogLevel(Level);
24
25// Wrap a Hashmap so we can implement log::kv traits for structured logging of custom fields
26// See https://cloud.google.com/logging/docs/view/overview#custom-fields
27#[cfg(feature = "customfields")]
28struct CustomFields<'kvs>(HashMap<kv::Key<'kvs>, kv::Value<'kvs>>);
29
30#[cfg(feature = "customfields")]
31impl<'kvs> CustomFields<'kvs> {
32    fn new() -> Self {
33        Self(HashMap::new())
34    }
35
36    fn inner(&self) -> &HashMap<kv::Key, kv::Value> {
37        &self.0
38    }
39}
40
41/// Parameters expected by the logger, used for manual initialization.
42#[derive(Clone)]
43pub struct Service {
44    /// Name of your service as it will be reported by Stackdriver
45    pub name: String,
46
47    /// Version of your service as it will be reported by Stackdriver
48    pub version: String,
49}
50
51impl Service {
52    pub fn from_env() -> Option<Service> {
53        let name = env::var("SERVICE_NAME")
54            .or_else(|_| env::var("CARGO_PKG_NAME"))
55            .unwrap_or_else(|_| String::new());
56
57        let version = env::var("SERVICE_VERSION")
58            .or_else(|_| env::var("CARGO_PKG_VERSION"))
59            .unwrap_or_else(|_| String::new());
60
61        if name.is_empty() && version.is_empty() {
62            return None;
63        }
64
65        Some(Service { name, version })
66    }
67}
68
69/// Basic initializer, expects SERVICE_NAME and SERVICE_VERSION env variables
70/// to be defined, otherwise you won't have much context available in Stackdriver.
71/// ## Usage
72/// ```rust
73/// use log::info;
74///
75/// stackdriver_logger::init();
76/// info!("Make sur you don't forget the env variables !");
77/// ```
78pub fn init() {
79    try_init(None, true).expect("Could not initialize stackdriver_logger");
80}
81
82/// Initialize the logger manually.
83/// ## Usage
84/// With everything manually specified :
85/// ```rust
86/// use log::info;
87/// use stackdriver_logger::Service;
88///
89/// let params = Service {
90///     name: "My Service".to_owned(),
91///     version: "2.3.1".to_owned(),
92/// };
93///
94/// stackdriver_logger::init_with(Some(params), true);
95/// info!("We're all set here !");
96/// ```
97/// You can also pass a `None` instead of `Some(Service{ ... })` and define the `SERVICE_NAME`
98/// and `SERVICE_VERSION` env variables :
99/// ```rust
100/// use log::info;
101///
102/// stackdriver_logger::init_with(None, false);
103/// info!("Make sur you don't forget the env variables !");
104/// ```
105pub fn init_with(service: Option<Service>, report_location: bool) {
106    try_init(service, report_location).expect("Could not initialize stackdriver_logger");
107}
108
109// Initialize the logger, defaults to pretty_env_logger in debug mode
110// Allow unused variables for convenience when toggling feature flags
111#[allow(unused_variables)]
112pub(crate) fn try_init(
113    service: Option<Service>,
114    report_location: bool,
115) -> Result<(), SetLoggerError> {
116    #[cfg(all(feature = "pretty_env_logger", debug_assertions))]
117    {
118        #[cfg(feature = "customfields")]
119        {
120            use std::io::Write;
121            let mut builder = env_logger::Builder::new();
122            builder.format(move |f, record| writeln!(f, "{}", format_record_pretty(record)));
123        }
124
125        pretty_env_logger::try_init()
126    }
127
128    #[cfg(not(all(feature = "pretty_env_logger", debug_assertions)))]
129    {
130        use std::io::Write;
131        let mut builder = env_logger::Builder::new();
132        builder.format(move |f, record| {
133            writeln!(
134                f,
135                "{}",
136                format_record(record, service.as_ref(), report_location)
137            )
138        });
139
140        if let Ok(s) = ::std::env::var("RUST_LOG") {
141            builder.parse_filters(&s);
142        }
143
144        builder.try_init()
145    }
146}
147
148// Format log level for Stackdriver
149impl fmt::Display for LogLevel {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.write_str(match self {
152            LogLevel(Level::Error) => "ERROR",
153            LogLevel(Level::Warn) => "WARNING",
154            LogLevel(Level::Info) => "INFO",
155
156            // Debug and Trace are caught here. Stackdriver doesn't have Trace, we map it to Debug instead
157            LogLevel(_) => "DEBUG",
158        })
159    }
160}
161
162#[cfg(feature = "customfields")]
163impl<'kvs> kv::Visitor<'kvs> for CustomFields<'kvs> {
164    fn visit_pair(&mut self, key: kv::Key<'kvs>, value: kv::Value<'kvs>) -> Result<(), kv::Error> {
165        self.0.insert(key, value);
166        Ok(())
167    }
168}
169
170// Message structure is documented here: https://cloud.google.com/error-reporting/docs/formatting-error-messages
171#[cfg(any(test, not(all(feature = "pretty_env_logger", debug_assertions))))]
172fn format_record(
173    record: &log::Record<'_>,
174    service: Option<&Service>,
175    report_location: bool,
176) -> Value {
177    let json_payload = json!({
178        "eventTime": chrono::Utc::now().to_rfc3339(),
179        "severity": LogLevel(record.level()).to_string(),
180
181        // Error messages also have a pseudo stack trace
182        "message": match record.level() {
183            Level::Error => format!(
184                "{} \n at {}:{}",
185                record.args(),
186                record.file().unwrap_or("unknown_file"),
187                record.line().unwrap_or(0)
188            ),
189            _ => format!("{}", record.args()),
190        },
191
192        // Service context may or may not be defined
193        "serviceContext": service.map(|s| json!({
194                "service": s.name,
195                "version": s.version
196            }))
197            .unwrap_or_else(|| json!({
198                "service": "unknown_service"
199            })),
200
201        // Report location may or may not be available
202        "reportLocation": if report_location {
203            json!({
204                "filePath": record.file(),
205                "modulePath": record.module_path(),
206                "lineNumber": record.line(),
207            })
208        } else {
209            Value::Null
210        }
211    });
212
213    #[cfg(not(feature = "customfields"))]
214    return json_payload;
215
216    #[cfg(feature = "customfields")]
217    {
218        let mut json_payload = json_payload;
219        let mut custom_fields = CustomFields::new();
220        if record.key_values().visit(&mut custom_fields).is_ok() {
221            for (key, val) in custom_fields.inner().iter() {
222                json_payload[key.as_str()] = Value::String(val.to_string());
223            }
224        }
225        return json_payload;
226    }
227}
228
229#[cfg(all(
230    feature = "pretty_env_logger",
231    feature = "customfields",
232    debug_assertions
233))]
234fn format_record_pretty(record: &log::Record<'_>) -> String {
235    let mut message = format!("{}", record.args());
236    let mut custom_fields = CustomFields::new();
237    let mut kv_message_parts = vec![];
238    if record.key_values().visit(&mut custom_fields).is_ok() {
239        for (key, val) in custom_fields.inner().iter() {
240            kv_message_parts.push(format!("{}={}", key, val));
241        }
242    }
243
244    if !kv_message_parts.is_empty() {
245        kv_message_parts.sort();
246        message = format!("{} {}", message, kv_message_parts.join(", "))
247    }
248
249    message
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn info_formatter() {
258        let svc = Service {
259            name: String::from("test"),
260            version: String::from("0.0.0"),
261        };
262
263        let record = log::Record::builder()
264            .args(format_args!("Info!"))
265            .level(Level::Info)
266            .target("test_app")
267            .file(Some("my_file.rs"))
268            .line(Some(1337))
269            .module_path(Some("my_module"))
270            .build();
271
272        let mut output = format_record(&record, Some(&svc), false);
273        let expected = include_str!("../test_snapshots/info_svc.json");
274        let expected: Value = serde_json::from_str(expected).unwrap();
275
276        // Make sure eventTime is set then overwrite generated timestamp with a known value
277        assert!(output["eventTime"].as_str().is_some());
278        *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00");
279        assert_eq!(output, expected);
280    }
281
282    #[test]
283    fn error_formatter() {
284        let svc = Service {
285            name: String::from("test"),
286            version: String::from("0.0.0"),
287        };
288
289        let record = log::Record::builder()
290            .args(format_args!("Error!"))
291            .level(Level::Error)
292            .target("test_app")
293            .file(Some("my_file.rs"))
294            .line(Some(1337))
295            .module_path(Some("my_module"))
296            .build();
297
298        let mut output = format_record(&record, None, false);
299        let expected = include_str!("../test_snapshots/no_scv_no_loc.json");
300        let expected: Value = serde_json::from_str(expected).unwrap();
301        assert!(output["eventTime"].as_str().is_some());
302        *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00");
303        assert_eq!(output, expected);
304
305        let mut output = format_record(&record, Some(&svc), true);
306        let expected = include_str!("../test_snapshots/svc_and_loc.json");
307        let expected: Value = serde_json::from_str(expected).unwrap();
308        assert!(output["eventTime"].as_str().is_some());
309        *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00");
310        assert_eq!(output, expected);
311    }
312
313    #[test]
314    #[cfg(feature = "customfields")]
315    fn custom_fields_formatter() {
316        let svc = Service {
317            name: String::from("test"),
318            version: String::from("0.0.0"),
319        };
320
321        let mut map = std::collections::HashMap::new();
322        map.insert("a", "a value");
323        map.insert("b", "b value");
324
325        let record = log::Record::builder()
326            .args(format_args!("Info!"))
327            .level(Level::Info)
328            .target("test_app")
329            .file(Some("my_file.rs"))
330            .line(Some(1337))
331            .module_path(Some("my_module"))
332            .key_values(&mut map)
333            .build();
334
335        let mut output = format_record(&record, Some(&svc), false);
336        let expected = include_str!("../test_snapshots/custom_fields.json");
337        let expected: Value = serde_json::from_str(expected).unwrap();
338
339        // Make sure eventTime is set then overwrite generated timestamp with a known value
340        assert!(output["eventTime"].as_str().is_some());
341        *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00");
342        assert_eq!(output, expected);
343    }
344
345    #[test]
346    #[cfg(feature = "customfields")]
347    fn custom_fields_formatter_pretty() {
348        let mut map = std::collections::HashMap::new();
349        map.insert("a", "a value");
350        map.insert("b", "b value");
351
352        let record = log::Record::builder()
353            .args(format_args!("Info!"))
354            .level(Level::Info)
355            .target("test_app")
356            .file(Some("my_file.rs"))
357            .line(Some(1337))
358            .module_path(Some("my_module"))
359            .key_values(&mut map)
360            .build();
361
362        let output = format_record_pretty(&record);
363        let expected = "Info! a=a value, b=b value";
364
365        assert_eq!(output, expected);
366    }
367}