#![doc = include_str!("../README.md")] // TODO(important): Replace tokio runtime handlers with tasks and LocalSet. #[cfg(test)] mod tests; use backtrace::Backtrace; use codectrl_protobuf_bindings::{ data::{BacktraceData, Log}, logs_service::{LoggerClient, RequestResult, RequestStatus}, }; use futures_util::stream; use hashbag::HashBag; use serde::{Deserialize, Serialize}; use std::{ cell::RefCell, collections::{BTreeMap, VecDeque}, env, fmt::Debug, fs, fs::File, io::{self, prelude::*, BufReader}, }; use tokio::runtime::{Handle, Runtime}; use tonic::Request; /// The Error type used by [`Logger`] and [`LogBatch`] whenever something can /// potentially fail. #[derive(thiserror::Error, Debug)] pub enum LoggerError { /// An error that has been generated by Tonic during transportation to/from /// the server. #[error("Tonic reported an error during transport: {0}")] TonicTransportError(#[from] tonic::transport::Error), /// An status code generated by Tonic as a result of a request. #[error("Tonic request resulted in status code: {0}")] TonicStatusCode(#[from] tonic::Status), /// Any error that is invoked with [`std::error::Error`], typically a Tokio /// error or a file read error. #[error("IO error occurred: {0}")] IOError(#[from] io::Error), /// A possible error returned by the gRPC server itself. #[error("gRPC server reported an error: status code {status_code}: {message} ")] LogServerError { message: String, status_code: String, }, /// An error generated by either [`Logger`] or [`LogBatch`]. #[error("This logger encountered an error: {0}")] LoggerError(String), /// Any other error with unknown origins. #[error("An unknown error occured: {0}")] Other(#[from] anyhow::Error), } impl From for LoggerError { fn from(res: RequestResult) -> Self { Self::LogServerError { message: res.message, status_code: format!("{:?}", res.status), } } } type LoggerResult = Result; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] enum Warning { CompiledWithoutDebugInfo, } impl ToString for Warning { fn to_string(&self) -> String { match self { Self::CompiledWithoutDebugInfo => "File was compiled without debug info, meaning information was lost", } .into() } } fn create_log( message: T, surround: Option, function_name: Option<&str>, function_name_occurences: Option<&HashBag<&'static str>>, ) -> Log { let function_name = function_name.unwrap_or_default(); let mut log = Log { uuid: "".to_string(), stack: Vec::new(), line_number: 0, file_name: String::new(), code_snippet: BTreeMap::new(), message: format!("{:#?}", &message), message_type: std::any::type_name::().to_string(), address: String::new(), warnings: Vec::new(), language: "Rust".into(), }; #[cfg(not(debug_assertions))] eprintln!( "Unfortunately, using this function without debug_assertions enabled will \ produce limited information. The stack trace, file path and line number will \ be missing from the final message that is sent to the server. Please consider \ guarding this function using #[cfg(debug_assertions)] so that this message \ does not re-appear." ); #[cfg(not(debug_assertions))] log.warnings .push(Warning::CompiledWithoutDebugInfo.to_string()); let surround = surround.unwrap_or(3); Logger::get_stack_trace(&mut log); if let Some(last) = log.stack.last() { log.line_number = last.line_number; log.code_snippet = Logger::get_code_snippet( &last.file_path, &mut log.line_number, surround, function_name, function_name_occurences, ); log.file_name = last.file_path.clone(); } log } /// Type used for generating batch logs to be sent by [`Logger`]. pub struct LogBatch<'a> { logger: Logger<'a>, log_batch: VecDeque, tokio_runtime: Option<&'a Handle>, host: &'static str, port: &'static str, surround: u32, function_name_occurences: HashBag<&'static str>, } impl<'a> LogBatch<'a> { fn new(logger: Logger<'a>) -> Self { Self { logger, log_batch: VecDeque::new(), tokio_runtime: None, host: "127.0.0.1", port: "3002", surround: 3, function_name_occurences: HashBag::new(), } } /// Sets the host IP address of the gRPC server to connect to. pub fn host(mut self, host: &'static str) -> Self { self.host = host; self } /// Sets the port of the `host` gRPC server to connect to. pub fn port(mut self, port: &'static str) -> Self { self.port = port; self } /// If a tokio runtime is already present in the parent scope, you can pass /// it here so that a new tokio runtime is not created when the batch is /// sent. pub fn tokio_runtime(mut self, rt: &'a Handle) -> Self { self.tokio_runtime = Some(rt); self } /// Sets the surround for the generated code snippet. This value will be /// used where a value isn't manually passed into each `add_X` function, /// otherwise the value passed into those functions will take /// precedence. pub fn surround(mut self, surround: u32) -> Self { self.surround = surround; self } /// Batch equivelent of [`Logger::log`]. See [`Logger::log`] for relevant /// documentation. pub fn add_log(mut self, message: T, surround: Option) -> Self { let surround = Some(surround.unwrap_or(self.surround)); self.function_name_occurences.insert("add_log"); self.log_batch.push_back(create_log( message, surround, Some("add_log"), Some(&self.function_name_occurences), )); self } /// Batch equivelent of [`Logger::log_if`]. See [`Logger::log_if`] for /// relevant documentation. pub fn add_log_if( mut self, condition: fn() -> bool, message: T, surround: Option, ) -> Self { let surround = Some(surround.unwrap_or(self.surround)); self.function_name_occurences.insert("add_log_if"); if condition() { self.log_batch.push_back(create_log( message, surround, Some("add_log_if"), Some(&self.function_name_occurences), )); } self } /// Batch equivelent of [`Logger::boxed_log_if`]. See /// [`Logger::boxed_log_if`] for relevant documentation. pub fn add_boxed_log_if( mut self, condition: Box bool>, message: T, surround: Option, ) -> Self { let surround = Some(surround.unwrap_or(self.surround)); self.function_name_occurences.insert("add_boxed_log_if"); if condition() { self.log_batch.push_back(create_log( message, surround, Some("add_boxed_log_if"), Some(&self.function_name_occurences), )); } self } /// Batch equivelent of [`Logger::log_when_env`]. See /// [`Logger::log_when_env`] for relevant documentation. pub fn add_log_when_env( mut self, message: T, surround: Option, ) -> Self { let surround = Some(surround.unwrap_or(self.surround)); self.function_name_occurences.insert("add_log_when_env"); if env::var("CODECTRL_DEBUG").ok().is_some() { self.log_batch.push_back(create_log( message, surround, Some("add_log_when_env"), Some(&self.function_name_occurences), )); } else { #[cfg(debug_assertions)] println!("add_log_when_env not called: envvar CODECTRL_DEBUG not present"); } self } /// Consumes `self` and returns a [`Logger`] that can be used to send /// multiple [`Log`]s with one gRPC connection. /// /// [`Log`]: codectrl_protobuf_bindings::data::Log pub fn build(mut self) -> Logger<'a> { self.logger = Logger { log_batch: self.log_batch, batch_host: self.host, batch_port: self.port, batch_tokio_runtime: self.tokio_runtime, }; self.logger } } /// The main type to be used to create and send [`Log`]s to a specified gRPC /// server. This is the main "entrypoint" for any usage of this crate. /// /// [`Log`]: codectrl_protobuf_bindings::data::Log #[derive(Debug, Clone, Default)] pub struct Logger<'a> { log_batch: VecDeque, batch_host: &'static str, batch_port: &'static str, batch_tokio_runtime: Option<&'a Handle>, } impl<'a> Logger<'a> { /// Returns a [`LogBatch`], which can be used to start the process of /// generating multiple logs to be sent in a single connection. Should /// be preferred over sending one-time [`Log`]s if possile. /// /// [`Log`]: codectrl_protobuf_bindings::data::Log pub fn start_batch() -> LogBatch<'a> { LogBatch::new(Self::default()) } /// Sends the configured batch in `log_batch` to the configured `batch_host` /// and `batch_port`. This _should_ be the preferred way of sending /// multiple logs. /// /// If given a pre-existing tokio runtime, it _will_ block the executor /// while it waits for the log to complete. pub fn send_batch(&mut self) -> LoggerResult<()> { if self.log_batch.is_empty() { return Err(LoggerError::LoggerError( "Can't send batch: Log batch is empty".to_string(), )); } let mut ret = Ok(()); async fn send_batch( host: &str, port: &str, logs: &mut VecDeque, ) -> LoggerResult<()> { let mut log_client = LoggerClient::connect(format!("http://{host}:{port}")).await?; let request = Request::new(stream::iter(logs.clone())); let response = log_client.send_logs(request).await?; match response.into_inner() { RequestResult { status, .. } if status == RequestStatus::Confirmed.into() => Ok(()), RequestResult { message, status, auth_status, } if status == RequestStatus::Error.into() => Err(RequestResult { message, status, auth_status, } .into()), RequestResult { .. } => unreachable!(), } } if let Some(handle) = self.batch_tokio_runtime { handle.block_on(async { ret = send_batch(self.batch_host, self.batch_port, &mut self.log_batch) .await; }); } else { let rt = Runtime::new()?; rt.block_on(async { ret = send_batch(self.batch_host, self.batch_port, &mut self.log_batch) .await; }) } ret } /// The main log function that is called from Rust code. /// /// This function will print a warning to stderr if this crate is compiled /// with debug_assertions disabled as it will produce a much less /// informative log for codeCTRL. /// /// If given a pre-existing tokio runtime, it _will_ block the executor /// while it waits for the log to complete. pub fn log( message: T, surround: Option, host: Option<&str>, port: Option<&str>, tokio_runtime: Option<&Handle>, ) -> LoggerResult<()> { let host = host.unwrap_or("127.0.0.1"); let port = port.unwrap_or("3002"); let mut log = create_log(message, surround, None, None); let mut ret = Ok(()); if let Some(handle) = tokio_runtime { handle.block_on(async { ret = Self::_log(&mut log, host, port).await; }); } else { let rt = Runtime::new()?; rt.block_on(async { ret = Self::_log(&mut log, host, port).await; }) } ret } /// A log function that takes a closure and only logs out if that function /// returns `true`. Essentially a conditional wrapper over /// [`Self::log`]. See [`Self::boxed_log_if`] for a variation that /// allows for closures that take can take from values in scope. /// /// If given a pre-existing tokio runtime, it _will_ block the executor /// while it waits for the log to complete. pub fn log_if( condition: fn() -> bool, message: T, surround: Option, host: Option<&str>, port: Option<&str>, tokio_runtime: Option<&Handle>, ) -> LoggerResult { if condition() { Self::log(message, surround, host, port, tokio_runtime)?; return Ok(true); } Ok(false) } /// A log function, similar to [`Self::log_if`] that takes a boxed closure /// or function that can take in parameters from the outer scope. /// /// If given a pre-existing tokio runtime, it _will_ block the executor /// while it waits for the log to complete. pub fn boxed_log_if( condition: Box bool>, message: T, surround: Option, host: Option<&str>, port: Option<&str>, tokio_runtime: Option<&Handle>, ) -> LoggerResult { if condition() { Self::log(message, surround, host, port, tokio_runtime)?; return Ok(true); } Ok(false) } /// A log function, similar to [`Self::log_if`] and [`Self::boxed_log_if`], /// that only takes effect if the environment variable `CODECTRL_DEBUG` /// is present or not. /// /// If given a pre-existing tokio runtime, it _will_ block the executor /// while it waits for the log to complete. pub fn log_when_env( message: T, surround: Option, host: Option<&str>, port: Option<&str>, tokio_runtime: Option<&Handle>, ) -> LoggerResult { if env::var("CODECTRL_DEBUG").ok().is_some() { Self::log(message, surround, host, port, tokio_runtime)?; Ok(true) } else { #[cfg(debug_assertions)] println!("log_when_env not called: envvar CODECTRL_DEBUG not present"); Ok(false) } } // We have a non-async wrapper over _log so that we can log from non-async // scopes. // // TODO: Provide a direct wrapper so that async environments do not need to call // a non-async wrapper, just for that to call an async wrapper. async fn _log(log: &mut Log, host: &str, port: &str) -> LoggerResult<()> { let mut log_client = LoggerClient::connect(format!("http://{host}:{port}")).await?; let request = Request::new(log.clone()); let response = log_client.send_log(request).await?; match response.into_inner() { RequestResult { status, .. } if status == RequestStatus::Confirmed.into() => Ok(()), RequestResult { message, status, auth_status, } if status == RequestStatus::Error.into() => Err(RequestResult { message, status, auth_status, } .into()), RequestResult { .. } => unreachable!(), } } fn get_stack_trace(log: &mut Log) { let backtrace = Backtrace::new(); for frame in backtrace.frames() { backtrace::resolve(frame.ip(), |symbol| { let name = if let Some(symbol) = symbol.name() { let mut symbol = symbol.to_string(); let mut split = symbol.split("::").collect::>(); if split.len() > 1 { split.remove(split.len() - 1); } symbol = split.join("::"); symbol } else { "".into() }; if let (Some(file_name), Some(line_number), Some(column_number)) = (symbol.filename(), symbol.lineno(), symbol.colno()) { let file_path: String = if let Ok(path) = fs::canonicalize(file_name) { path.as_os_str().to_str().unwrap().to_string() } else { file_name.as_os_str().to_str().unwrap().to_string() }; if !(name.contains("Logger::") || name.contains("LogBatch::") || name.ends_with("create_log") || file_path.contains(".cargo") || file_path.starts_with("/rustc/")) && file_path.contains(".rs") { let code = Self::get_code(&file_path, line_number); log.stack.insert( 0, BacktraceData { name, file_path, line_number, column_number, code, }, ); } } }); } } fn get_code(file_path: &str, line_number: u32) -> String { let mut code = String::new(); let file = File::open(file_path).unwrap_or_else(|_| { panic!("Unexpected error: could not open file: {}", file_path) }); let reader = BufReader::new(file); if let Some(Ok(line)) = reader.lines().nth(line_number.saturating_sub(1) as usize) { code = line.trim().to_string(); } code } fn get_code_snippet( file_path: &str, line_number: &mut u32, surround: u32, function_name: &str, function_name_occurences: Option<&HashBag<&'static str>>, ) -> BTreeMap { let file = File::open(file_path).unwrap_or_else(|_| { panic!("Unexpected error: could not open file: {}", file_path) }); let reader = BufReader::new(file); let lines: BTreeMap = reader .lines() .enumerate() .filter(|(_, line)| line.is_ok()) .map(|(n, line)| ((n + 1) as u32, line.unwrap())) .collect(); if let Some(function_name_occurences) = function_name_occurences { if !function_name.is_empty() { let offset = RefCell::new(1); let occurences = function_name_occurences.contains(function_name); // In the case of batch commands, this attempts to find the line number // for each `add_x` command, rather than the `start_batch` // command. This does this by skipping up to N lines where // N is the line number of the `start_batch` command and filtering out any // lines that don't at least start with the // `function_name`. // // TODO: Find a way to account for multiple of the same function on the // same line. let item = lines .iter() .skip(*line_number as usize) .filter(|(_, line)| { line.replace('.', "").split('(').next().unwrap().trim() == function_name }) .map(|(line_number, line)| { (line_number, { let mut v = line.trim().split('.').collect::>(); if v[0].is_empty() { v.remove(0); } if v.len() > 1 { *offset.borrow_mut() += v.len(); } }) }) .nth({ if occurences > 1 { occurences.saturating_sub(*offset.borrow()) } else { 0 } }); if let Some((i, _)) = item { *line_number = *i; } } } let offset = line_number.saturating_sub(surround); let end = line_number.saturating_add(surround); lines .range(offset..=end) .map(|(key, value)| (*key, value.clone())) .collect() } }