diff --git a/Cargo.lock b/Cargo.lock index 1c828c4..102b075 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -128,6 +128,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "example-logtail-facade" +version = "0.1.0" +dependencies = [ + "log", + "logtail-facade", + "tokio", +] + +[[package]] +name = "example-logtail-poster" +version = "0.1.0" +dependencies = [ + "logtail", + "logtail-poster", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "fnv" version = "1.0.7" diff --git a/Cargo.toml b/Cargo.toml index c66a4d7..af8ef17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["crates/*"] +members = [ "crates/*", "examples/*" ] diff --git a/crates/logtail-facade/Cargo.toml b/crates/logtail-facade/Cargo.toml index faef095..97873b4 100644 --- a/crates/logtail-facade/Cargo.toml +++ b/crates/logtail-facade/Cargo.toml @@ -10,7 +10,7 @@ default = [ "log-facade" ] log-facade = [ "log" ] [dependencies] -log = { version = "0.4", optional = true } +log = { version = "0.4", optional = true, features = [ "std" ] } serde = { version = "1", features = [ "derive" ] } serde_json = "1" diff --git a/crates/logtail-facade/src/lib.rs b/crates/logtail-facade/src/lib.rs index 48b6a29..65fd079 100644 --- a/crates/logtail-facade/src/lib.rs +++ b/crates/logtail-facade/src/lib.rs @@ -1,109 +1,90 @@ -use serde::{Deserialize, Serialize}; -use std::{env, ffi::OsString, fs, io, path::PathBuf}; +use log::{Level, Metadata, Record}; +use logtail_poster::Egress; +use std::{ + env, + sync::{Arc, Mutex}, +}; -#[cfg(feature = "log-facade")] -mod log; -#[cfg(feature = "log-facade")] -pub use self::log::*; - -pub(crate) fn cache_dir() -> Option { - let dir = env::var_os("STATE_DIRECTORY").or_else(|| { - env::var_os("HOME").and_then(|dir| { - let mut dir: PathBuf = dir.into(); - dir.push(".cache"); - dir.push("rebterlai"); - Some(OsString::from(dir)) - }) - }); - dir +pub struct LogtailLogger { + ing: Arc>, + threshold: Level, } -pub(crate) fn state_file(name: &str) -> Option { - let mut dir: PathBuf = cache_dir()?.into(); - dir.push(&format!("{}.json", name)); - Some(dir) +#[derive(serde::Serialize)] +struct LogData<'a> { + level: &'a str, + target: &'a str, + module_path: &'a str, + file: &'a str, + line: u32, + message: String, } -#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] -pub struct Config { - private_id: String, - public_id: String, -} - -impl Config { - pub fn new() -> Self { - let private_id = logtail::PrivateID::new(); - - Self { - private_id: private_id.as_hex(), - public_id: private_id.as_public().as_hex(), - } +impl log::Log for LogtailLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= self.threshold } - pub fn load(collection: S) -> io::Result - where - S: Into, - { - let dir = cache_dir() - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "can't find directory"))?; - if let Err(_) = fs::metadata(&dir) { - fs::create_dir_all(&dir)?; - } - println!("{:?}", dir); + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + let ld = LogData { + level: record.level().as_str(), + target: record.target(), + module_path: record.module_path().unwrap_or("???"), + file: record.file().unwrap_or("???"), + line: record.line().unwrap_or(0), + message: format!("{}", record.args()), + }; - let fname = state_file(&collection.into()).ok_or_else(|| { - io::Error::new( - io::ErrorKind::InvalidInput, - "can't derive logtail config filename", - ) - })?; - match fs::metadata(&fname) { - Ok(_) => { - let fin = fs::File::open(&fname)?; - let cfg = serde_json::from_reader(io::BufReader::new(fin)) - .or_else(|why| Err(io::Error::new(io::ErrorKind::Other, why)))?; - Ok(cfg) - } - Err(_) => { - let cfg = Self::new(); - let mut fout = fs::File::create(&fname)?; - serde_json::to_writer(&mut fout, &cfg) - .or_else(|why| Err(io::Error::new(io::ErrorKind::Other, why)))?; - Ok(cfg) + if let Ok(val) = serde_json::to_value(&ld) { + if let Ok(mut ing) = self.ing.lock() { + if let Err(why) = ing.send(val) { + eprintln!("logtail_facade::LogtailLogger::log: can't send json value to buffer: {}", why); + } + } } } } + + fn flush(&self) {} } -#[cfg(test)] -mod tests { - use super::*; - use tempdir::TempDir; +pub fn init(collection: String) -> Result> { + let cfg = logtail_poster::Config::load(collection.clone())?; + let target = env::var("TS_LOG_TARGET") + .unwrap_or(logtail_poster::DEFAULT_HOST.to_string()) + .to_string(); - #[test] - #[serial_test::serial] - fn cache_dir() { - let home = TempDir::new("cache").unwrap(); - std::env::set_var("STATE_DIRECTORY", home.path().to_str().unwrap()); - let dir = super::cache_dir(); + let (mut ing, eg) = logtail_poster::Builder::default() + .collection(collection) + .base_url(target) + .buffer_size(256) + .private_id(cfg.private_id()) + .build()?; - assert!(dir.is_some()); - assert_eq!(dir.unwrap(), home.path().to_str().unwrap()); + let threshold = if cfg!(debug_assertions) { + Level::Debug + } else { + Level::Info + }; - home.close().unwrap(); - } + ing.send(serde_json::to_value(&ProgramStarted { + msg: "Program started".to_string(), + })?)?; + let ing = Arc::new(Mutex::new(ing.clone())); - #[test] - #[serial_test::serial] - fn new_and_load() { - let home = TempDir::new("cache").unwrap(); - std::env::set_var("STATE_DIRECTORY", home.path().to_str().unwrap()); + let logger = LogtailLogger { + ing: ing.clone(), + threshold: threshold.clone(), + }; - let cfg = Config::load("foo.bar").unwrap(); - let cfg2 = Config::load("foo.bar").unwrap(); + log::set_boxed_logger(Box::new(logger)) + .map(|()| log::set_max_level(threshold.to_level_filter()))?; - assert_eq!(cfg, cfg2); - - home.close().unwrap(); - } + Ok(eg) +} + +#[derive(serde::Serialize)] +struct ProgramStarted { + msg: String, } diff --git a/crates/logtail-facade/src/log.rs b/crates/logtail-facade/src/log.rs deleted file mode 100644 index 2b64fb4..0000000 --- a/crates/logtail-facade/src/log.rs +++ /dev/null @@ -1,46 +0,0 @@ -use log::{Level, Metadata, Record}; -use std::sync::{Arc, Mutex}; - -pub struct LogtailLogger { - ing: Arc>, - threshold: Level, -} - -#[derive(serde::Serialize)] -struct LogData<'a> { - level: &'a str, - target: &'a str, - module_path: &'a str, - file: &'a str, - line: u32, - message: String, -} - -impl log::Log for LogtailLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= self.threshold - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - let ld = LogData { - level: record.level().as_str(), - target: record.target(), - module_path: record.module_path().unwrap_or("???"), - file: record.file().unwrap_or("???"), - line: record.line().unwrap_or(0), - message: format!("{}", record.args()), - }; - - if let Ok(val) = serde_json::to_value(&ld) { - if let Ok(mut ing) = self.ing.lock() { - if let Err(why) = ing.send(val) { - eprintln!("logtail_facade::jog::LogtailLogger::log: can't send json value to buffer: {}", why); - } - } - } - } - } - - fn flush(&self) {} -} diff --git a/crates/logtail-poster/src/config.rs b/crates/logtail-poster/src/config.rs new file mode 100644 index 0000000..100466a --- /dev/null +++ b/crates/logtail-poster/src/config.rs @@ -0,0 +1,112 @@ +use serde::{Deserialize, Serialize}; +use std::{env, ffi::OsString, fs, io, path::PathBuf}; + +#[cfg(feature = "log-facade")] +mod log; +#[cfg(feature = "log-facade")] +pub use self::log::*; + +pub(crate) fn cache_dir() -> Option { + let dir = env::var_os("STATE_DIRECTORY").or_else(|| { + env::var_os("HOME").and_then(|dir| { + let mut dir: PathBuf = dir.into(); + dir.push(".cache"); + dir.push("rebterlai"); + Some(OsString::from(dir)) + }) + }); + dir +} + +pub(crate) fn state_file(name: &str) -> Option { + let mut dir: PathBuf = cache_dir()?.into(); + dir.push(&format!("{}.json", name)); + Some(dir) +} + +#[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] +pub struct Config { + private_id: String, + public_id: String, +} + +impl Config { + pub fn new() -> Self { + let private_id = logtail::PrivateID::new(); + + Self { + private_id: private_id.as_hex(), + public_id: private_id.as_public().as_hex(), + } + } + + pub fn private_id(self) -> logtail::PrivateID { + logtail::PrivateID::from_hex(self.private_id).unwrap() + } + + pub fn load(collection: S) -> io::Result + where + S: Into, + { + let dir = cache_dir() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "can't find directory"))?; + if let Err(_) = fs::metadata(&dir) { + fs::create_dir_all(&dir)?; + } + + let fname = state_file(&collection.into()).ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "can't derive logtail config filename", + ) + })?; + match fs::metadata(&fname) { + Ok(_) => { + let fin = fs::File::open(&fname)?; + let cfg = serde_json::from_reader(io::BufReader::new(fin)) + .or_else(|why| Err(io::Error::new(io::ErrorKind::Other, why)))?; + Ok(cfg) + } + Err(_) => { + let cfg = Self::new(); + let mut fout = fs::File::create(&fname)?; + serde_json::to_writer(&mut fout, &cfg) + .or_else(|why| Err(io::Error::new(io::ErrorKind::Other, why)))?; + Ok(cfg) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + + #[test] + #[serial_test::serial] + fn cache_dir() { + let home = TempDir::new("cache").unwrap(); + std::env::set_var("STATE_DIRECTORY", home.path().to_str().unwrap()); + let dir = super::cache_dir(); + + assert!(dir.is_some()); + assert_eq!(dir.unwrap(), home.path().to_str().unwrap()); + + home.close().unwrap(); + } + + #[test] + #[serial_test::serial] + fn new_and_load() { + let home = TempDir::new("cache").unwrap(); + std::env::set_var("STATE_DIRECTORY", home.path().to_str().unwrap()); + + let cfg = Config::load("foo.bar").unwrap(); + let cfg2 = Config::load("foo.bar").unwrap(); + + assert_eq!(cfg, cfg2); + + home.close().unwrap(); + } +} diff --git a/crates/logtail-poster/src/lib.rs b/crates/logtail-poster/src/lib.rs index 728d4cc..4fdf395 100644 --- a/crates/logtail-poster/src/lib.rs +++ b/crates/logtail-poster/src/lib.rs @@ -7,8 +7,11 @@ This facilitates writing logs to a logtail server. This is a port of use reqwest::Client; use std::num::NonZeroUsize; +mod config; +pub use self::config::*; + /// DefaultHost is the default URL to upload logs to when Builder.base_url isn't provided. -const DEFAULT_HOST: &'static str = "https://log.tailscale.io"; +pub const DEFAULT_HOST: &'static str = "https://log.tailscale.io"; /** Builds a send/recv pair for the logtail service. Create a new Builder with the [Builder::default] @@ -120,8 +123,8 @@ pub enum Error { #[error("no collection defined")] NoCollection, - #[error("can't put to in-memory buffer")] - TXFail, + #[error("can't put to in-memory buffer: {0}")] + TXFail(String), #[error("can't get from in-memory buffer: {0}")] RXFail(#[from] ring_channel::TryRecvError), @@ -169,7 +172,7 @@ impl Ingress { match self.tx.send(val) { Ok(_) => Ok(()), - Err(_) => Err(Error::TXFail), + Err(why) => Err(Error::TXFail(format!("{}", why))), } } } diff --git a/examples/example-logtail-facade/Cargo.toml b/examples/example-logtail-facade/Cargo.toml new file mode 100644 index 0000000..b013b65 --- /dev/null +++ b/examples/example-logtail-facade/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example-logtail-facade" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4" +logtail-facade = { path = "../../crates/logtail-facade" } +tokio = { version = "1", features = [ "full" ] } diff --git a/examples/example-logtail-facade/src/main.rs b/examples/example-logtail-facade/src/main.rs new file mode 100644 index 0000000..d23094c --- /dev/null +++ b/examples/example-logtail-facade/src/main.rs @@ -0,0 +1,13 @@ +use log::{debug, error, info, warn}; + +#[tokio::main] +async fn main() { + let mut eg = logtail_facade::init("rebterlai.example-logtail-facade".to_string()).unwrap(); + + error!("error"); + warn!("warn"); + info!("info"); + debug!("debug"); + + eg.post().await.unwrap() +} diff --git a/examples/example-logtail-poster/Cargo.toml b/examples/example-logtail-poster/Cargo.toml new file mode 100644 index 0000000..d1cc0de --- /dev/null +++ b/examples/example-logtail-poster/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example-logtail-poster" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1", features = [ "derive" ] } +serde_json = "1" +tokio = { version = "1", features = [ "full" ] } + +logtail = { path = "../../crates/logtail" } +logtail-poster = { path = "../../crates/logtail-poster" } diff --git a/examples/example-logtail-poster/src/main.rs b/examples/example-logtail-poster/src/main.rs new file mode 100644 index 0000000..e11f951 --- /dev/null +++ b/examples/example-logtail-poster/src/main.rs @@ -0,0 +1,29 @@ +use logtail_poster::*; + +#[derive(Clone, serde::Serialize)] +struct Data { + pub msg: String, +} + +#[tokio::main] +async fn main() { + let collection = "rebterlai.example.logtail-poster".to_string(); + let cfg = logtail_poster::Config::load(collection.clone()).unwrap(); + let (mut ing, mut eg) = Builder::default() + .collection(collection) + .private_id(cfg.private_id()) + .user_agent("rebterlai/test".to_string()) + .base_url("http://127.0.0.1:48283".to_string()) + .build() + .unwrap(); + + ing.send( + serde_json::to_value(Data { + msg: "Hello, world!".to_string(), + }) + .unwrap(), + ) + .unwrap(); + + eg.post().await.unwrap(); +}