stackdriver_logger-0.8.2/.cargo_vcs_info.json0000644000000001360000000000100147230ustar { "git": { "sha1": "35b55a77e3b64e8c5849f7e3ff51f003211d52b7" }, "path_in_vcs": "" }stackdriver_logger-0.8.2/.gitignore000064400000000000000000000000371046102023000155030ustar 00000000000000/Cargo.lock /target **/*.rs.bk stackdriver_logger-0.8.2/Cargo.toml0000644000000031340000000000100127220ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "stackdriver_logger" version = "0.8.2" authors = ["Kamek "] description = "A logger for Google's Stackdriver with a cli-friendly fallback for local development" readme = "README.md" keywords = [ "log", "logging", "cloud", "google", "stackdriver", ] categories = ["development-tools::debugging"] license = "MIT/Apache-2.0" repository = "https://github.com/kamek-pf/stackdriver-logger/" resolver = "2" [dependencies.chrono] version = "0.4.23" features = ["clock"] default-features = false [dependencies.env_logger] version = "0.9.3" default-features = false [dependencies.log] version = "0.4.17" [dependencies.pretty_env_logger] version = "0.4.0" optional = true [dependencies.serde_json] version = "1.0.87" [dependencies.toml] version = "0.5.9" optional = true [features] atty = ["env_logger/atty"] cargo = ["toml"] customfields = ["log/kv_unstable"] default = [ "cargo", "termcolor", "atty", "humantime", "regex", "pretty_env_logger", ] humantime = ["env_logger/humantime"] prod = ["cargo"] regex = ["env_logger/regex"] termcolor = ["env_logger/termcolor"] stackdriver_logger-0.8.2/Cargo.toml.orig000064400000000000000000000021571046102023000164070ustar 00000000000000[package] name = "stackdriver_logger" version = "0.8.2" description = "A logger for Google's Stackdriver with a cli-friendly fallback for local development" authors = ["Kamek "] license = "MIT/Apache-2.0" categories = ["development-tools::debugging"] keywords = ["log", "logging", "cloud", "google", "stackdriver"] readme = "README.md" repository = "https://github.com/kamek-pf/stackdriver-logger/" edition = "2021" [features] default = [ "cargo", "termcolor", "atty", "humantime", "regex", "pretty_env_logger", ] prod = ["cargo"] # Used by the init macro cargo = ["toml"] # Toggle env logger features termcolor = ["env_logger/termcolor"] atty = ["env_logger/atty"] humantime = ["env_logger/humantime"] regex = ["env_logger/regex"] # Toggle log features customfields = ["log/kv_unstable"] [dependencies] env_logger = { version = "0.9.3", default-features = false } pretty_env_logger = { version = "0.4.0", optional = true } chrono = { version = "0.4.23", default-features = false, features = ["clock"] } serde_json = "1.0.87" log = "0.4.17" toml = { version = "0.5.9", optional = true } stackdriver_logger-0.8.2/README.md000064400000000000000000000042051046102023000147730ustar 00000000000000# Stackdriver logger A logger for Google's Stackdriver.\ By default, in debug mode, we fall back to [`pretty_env_logger`](https://github.com/seanmonstar/pretty-env-logger). \ In release mode, we output JSON formatted logs compatible with Stackdriver. ## Usage ```rust use log::{error, info, trace, debug, warn}; fn main() { stackdriver_logger::init_with_cargo!(); trace!("trace log"); debug!("debug log"); info!("info log"); warn!("warn log"); error!("error log"); } ``` Note that the `init_with_cargo!` macro will include your `Cargo.toml` in the resulting binary. If you don't want that, check out the docs, a few more initializers are available. ## Behavior When using the above macro, you don't have anything else to do. For other initializers, you may need to provide two environment variables : `SERVICE_NAME` and `SERVICE_VERSION`. We're using Cargo's `CARGO_PKG_NAME` and `CARGO_PKG_VERSION` as a fallback, but these are only available if you run your application via Cargo. \ Check out the docs to see which initializers require environment variables. ## Enabling logging This library accepts a `RUST_LOG` env variable, it works exactly like in [`env_logger`](https://github.com/sebasmagri/env_logger). \ By default, everything is disabled except for `error!`. To enable all logs for your application : ```sh RUST_LOG=your_application cargo run ``` For more details, take a look at the [`env_logger` docs](https://docs.rs/env_logger/0.7.0/env_logger/#enabling-logging). ## Feature flags By default, this crate enables all `env_logger` defaults features and always pulls `pretty_env_logger`. \ These crates have some heavy dependencies like `regex`. \ If you want smaller builds in production, and don't use fancy `env_logger` features, you can disable default features for `stackdriver_logger` like so : ```toml stackdriver_logger = { version = "*", default-features = false, features = ["prod"] } ``` ## License Licensed under either of - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) stackdriver_logger-0.8.2/src/lib.rs000064400000000000000000000264561046102023000154330ustar 00000000000000#![doc = include_str!("../README.md")] #![forbid(unsafe_code)] use std::{env, fmt}; use log::{Level, SetLoggerError}; #[cfg(any(test, not(all(feature = "pretty_env_logger", debug_assertions))))] use serde_json::{json, Value}; #[cfg(feature = "cargo")] #[doc(hidden)] #[macro_use] pub mod macros; #[cfg(feature = "customfields")] use log::kv; #[cfg(feature = "customfields")] use std::collections::HashMap; // Wrap Level from the log crate so we can implement standard traits for it struct LogLevel(Level); // Wrap a Hashmap so we can implement log::kv traits for structured logging of custom fields // See https://cloud.google.com/logging/docs/view/overview#custom-fields #[cfg(feature = "customfields")] struct CustomFields<'kvs>(HashMap, kv::Value<'kvs>>); #[cfg(feature = "customfields")] impl<'kvs> CustomFields<'kvs> { fn new() -> Self { Self(HashMap::new()) } fn inner(&self) -> &HashMap { &self.0 } } /// Parameters expected by the logger, used for manual initialization. #[derive(Clone)] pub struct Service { /// Name of your service as it will be reported by Stackdriver pub name: String, /// Version of your service as it will be reported by Stackdriver pub version: String, } impl Service { pub fn from_env() -> Option { let name = env::var("SERVICE_NAME") .or_else(|_| env::var("CARGO_PKG_NAME")) .unwrap_or_else(|_| String::new()); let version = env::var("SERVICE_VERSION") .or_else(|_| env::var("CARGO_PKG_VERSION")) .unwrap_or_else(|_| String::new()); if name.is_empty() && version.is_empty() { return None; } Some(Service { name, version }) } } /// Basic initializer, expects SERVICE_NAME and SERVICE_VERSION env variables /// to be defined, otherwise you won't have much context available in Stackdriver. /// ## Usage /// ```rust /// use log::info; /// /// stackdriver_logger::init(); /// info!("Make sur you don't forget the env variables !"); /// ``` pub fn init() { try_init(None, true).expect("Could not initialize stackdriver_logger"); } /// Initialize the logger manually. /// ## Usage /// With everything manually specified : /// ```rust /// use log::info; /// use stackdriver_logger::Service; /// /// let params = Service { /// name: "My Service".to_owned(), /// version: "2.3.1".to_owned(), /// }; /// /// stackdriver_logger::init_with(Some(params), true); /// info!("We're all set here !"); /// ``` /// You can also pass a `None` instead of `Some(Service{ ... })` and define the `SERVICE_NAME` /// and `SERVICE_VERSION` env variables : /// ```rust /// use log::info; /// /// stackdriver_logger::init_with(None, false); /// info!("Make sur you don't forget the env variables !"); /// ``` pub fn init_with(service: Option, report_location: bool) { try_init(service, report_location).expect("Could not initialize stackdriver_logger"); } // Initialize the logger, defaults to pretty_env_logger in debug mode // Allow unused variables for convenience when toggling feature flags #[allow(unused_variables)] pub(crate) fn try_init( service: Option, report_location: bool, ) -> Result<(), SetLoggerError> { #[cfg(all(feature = "pretty_env_logger", debug_assertions))] { #[cfg(feature = "customfields")] { use std::io::Write; let mut builder = env_logger::Builder::new(); builder.format(move |f, record| writeln!(f, "{}", format_record_pretty(record))); } pretty_env_logger::try_init() } #[cfg(not(all(feature = "pretty_env_logger", debug_assertions)))] { use std::io::Write; let mut builder = env_logger::Builder::new(); builder.format(move |f, record| { writeln!( f, "{}", format_record(record, service.as_ref(), report_location) ) }); if let Ok(s) = ::std::env::var("RUST_LOG") { builder.parse_filters(&s); } builder.try_init() } } // Format log level for Stackdriver impl fmt::Display for LogLevel { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { LogLevel(Level::Error) => "ERROR", LogLevel(Level::Warn) => "WARNING", LogLevel(Level::Info) => "INFO", // Debug and Trace are caught here. Stackdriver doesn't have Trace, we map it to Debug instead LogLevel(_) => "DEBUG", }) } } #[cfg(feature = "customfields")] impl<'kvs> kv::Visitor<'kvs> for CustomFields<'kvs> { fn visit_pair(&mut self, key: kv::Key<'kvs>, value: kv::Value<'kvs>) -> Result<(), kv::Error> { self.0.insert(key, value); Ok(()) } } // Message structure is documented here: https://cloud.google.com/error-reporting/docs/formatting-error-messages #[cfg(any(test, not(all(feature = "pretty_env_logger", debug_assertions))))] fn format_record( record: &log::Record<'_>, service: Option<&Service>, report_location: bool, ) -> Value { let json_payload = json!({ "eventTime": chrono::Utc::now().to_rfc3339(), "severity": LogLevel(record.level()).to_string(), // Error messages also have a pseudo stack trace "message": match record.level() { Level::Error => format!( "{} \n at {}:{}", record.args(), record.file().unwrap_or("unknown_file"), record.line().unwrap_or(0) ), _ => format!("{}", record.args()), }, // Service context may or may not be defined "serviceContext": service.map(|s| json!({ "service": s.name, "version": s.version })) .unwrap_or_else(|| json!({ "service": "unknown_service" })), // Report location may or may not be available "reportLocation": if report_location { json!({ "filePath": record.file(), "modulePath": record.module_path(), "lineNumber": record.line(), }) } else { Value::Null } }); #[cfg(not(feature = "customfields"))] return json_payload; #[cfg(feature = "customfields")] { let mut json_payload = json_payload; let mut custom_fields = CustomFields::new(); if record.key_values().visit(&mut custom_fields).is_ok() { for (key, val) in custom_fields.inner().iter() { json_payload[key.as_str()] = Value::String(val.to_string()); } } return json_payload; } } #[cfg(all( feature = "pretty_env_logger", feature = "customfields", debug_assertions ))] fn format_record_pretty(record: &log::Record<'_>) -> String { let mut message = format!("{}", record.args()); let mut custom_fields = CustomFields::new(); let mut kv_message_parts = vec![]; if record.key_values().visit(&mut custom_fields).is_ok() { for (key, val) in custom_fields.inner().iter() { kv_message_parts.push(format!("{}={}", key, val)); } } if !kv_message_parts.is_empty() { kv_message_parts.sort(); message = format!("{} {}", message, kv_message_parts.join(", ")) } message } #[cfg(test)] mod tests { use super::*; #[test] fn info_formatter() { let svc = Service { name: String::from("test"), version: String::from("0.0.0"), }; let record = log::Record::builder() .args(format_args!("Info!")) .level(Level::Info) .target("test_app") .file(Some("my_file.rs")) .line(Some(1337)) .module_path(Some("my_module")) .build(); let mut output = format_record(&record, Some(&svc), false); let expected = include_str!("../test_snapshots/info_svc.json"); let expected: Value = serde_json::from_str(expected).unwrap(); // Make sure eventTime is set then overwrite generated timestamp with a known value assert!(output["eventTime"].as_str().is_some()); *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00"); assert_eq!(output, expected); } #[test] fn error_formatter() { let svc = Service { name: String::from("test"), version: String::from("0.0.0"), }; let record = log::Record::builder() .args(format_args!("Error!")) .level(Level::Error) .target("test_app") .file(Some("my_file.rs")) .line(Some(1337)) .module_path(Some("my_module")) .build(); let mut output = format_record(&record, None, false); let expected = include_str!("../test_snapshots/no_scv_no_loc.json"); let expected: Value = serde_json::from_str(expected).unwrap(); assert!(output["eventTime"].as_str().is_some()); *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00"); assert_eq!(output, expected); let mut output = format_record(&record, Some(&svc), true); let expected = include_str!("../test_snapshots/svc_and_loc.json"); let expected: Value = serde_json::from_str(expected).unwrap(); assert!(output["eventTime"].as_str().is_some()); *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00"); assert_eq!(output, expected); } #[test] #[cfg(feature = "customfields")] fn custom_fields_formatter() { let svc = Service { name: String::from("test"), version: String::from("0.0.0"), }; let mut map = std::collections::HashMap::new(); map.insert("a", "a value"); map.insert("b", "b value"); let record = log::Record::builder() .args(format_args!("Info!")) .level(Level::Info) .target("test_app") .file(Some("my_file.rs")) .line(Some(1337)) .module_path(Some("my_module")) .key_values(&mut map) .build(); let mut output = format_record(&record, Some(&svc), false); let expected = include_str!("../test_snapshots/custom_fields.json"); let expected: Value = serde_json::from_str(expected).unwrap(); // Make sure eventTime is set then overwrite generated timestamp with a known value assert!(output["eventTime"].as_str().is_some()); *output.get_mut("eventTime").unwrap() = json!("2019-09-28T04:00:00.000000000+00:00"); assert_eq!(output, expected); } #[test] #[cfg(feature = "customfields")] fn custom_fields_formatter_pretty() { let mut map = std::collections::HashMap::new(); map.insert("a", "a value"); map.insert("b", "b value"); let record = log::Record::builder() .args(format_args!("Info!")) .level(Level::Info) .target("test_app") .file(Some("my_file.rs")) .line(Some(1337)) .module_path(Some("my_module")) .key_values(&mut map) .build(); let output = format_record_pretty(&record); let expected = "Info! a=a value, b=b value"; assert_eq!(output, expected); } } stackdriver_logger-0.8.2/src/macros.rs000064400000000000000000000032441046102023000161370ustar 00000000000000use crate::{try_init, Service}; use toml::Value; /// Initialize the logger using your project's TOML file. /// /// This initializer includes your Cargo.toml file at compile time and extract the /// service name and version at run time. /// ## Usage /// This is the basic form : /// ```rust /// use log::info; /// /// stackdriver_logger::init_with_cargo!(); /// info!("Default path used for Cargo.toml : ../Cargo.toml"); /// ``` /// You can also specify the path if you need to : /// ```rust /// use log::info; /// /// stackdriver_logger::init_with_cargo!("../Cargo.toml"); /// info!("Path was specified !"); /// ``` /// Note that the `init_with_cargo!` macro will include your `Cargo.toml` in the resulting binary. /// If you don't want that, take a look at the other initializers. #[macro_export] macro_rules! init_with_cargo { ($e:expr) => {{ let base = include_str!($e); $crate::macros::read_cargo(base); }}; () => {{ let base = include_str!("../Cargo.toml"); $crate::macros::read_cargo(base); }}; } #[doc(hidden)] pub fn read_cargo(input: &str) { input .parse::() .ok() .and_then(|toml: Value| -> Option<()> { let service = Service { name: read_package_key(&toml, "name")?, version: read_package_key(&toml, "version")?, }; try_init(Some(service), true).expect("Could not initialize stackdriver_logger"); None }); } fn read_package_key(toml: &Value, key: &str) -> Option { let key = toml .get("package")? .as_table()? .get(key)? .as_str()? .to_owned(); Some(key) } stackdriver_logger-0.8.2/test_snapshots/custom_fields.json000064400000000000000000000003401046102023000223240ustar 00000000000000{ "eventTime": "2019-09-28T04:00:00.000000000+00:00", "message": "Info!", "reportLocation": null, "serviceContext": { "service": "test", "version": "0.0.0" }, "severity": "INFO", "a": "a value", "b": "b value" } stackdriver_logger-0.8.2/test_snapshots/info_svc.json000064400000000000000000000002761046102023000213020ustar 00000000000000{ "eventTime": "2019-09-28T04:00:00.000000000+00:00", "message": "Info!", "reportLocation": null, "serviceContext": { "service": "test", "version": "0.0.0" }, "severity": "INFO" } stackdriver_logger-0.8.2/test_snapshots/no_scv_no_loc.json000064400000000000000000000003131046102023000223040ustar 00000000000000{ "eventTime": "2019-09-28T04:00:00.000000000+00:00", "message": "Error! \n at my_file.rs:1337", "reportLocation": null, "serviceContext": { "service": "unknown_service" }, "severity": "ERROR" } stackdriver_logger-0.8.2/test_snapshots/svc_and_loc.json000064400000000000000000000004441046102023000217430ustar 00000000000000{ "eventTime": "2019-09-28T04:00:00.000000000+00:00", "message": "Error! \n at my_file.rs:1337", "reportLocation": { "filePath": "my_file.rs", "lineNumber": 1337, "modulePath": "my_module" }, "serviceContext": { "service": "test", "version": "0.0.0" }, "severity": "ERROR" }