handsome_logger-0.8.0/.cargo_vcs_info.json0000644000000001360000000000100141760ustar { "git": { "sha1": "756f1385132e563fcf508a9a0b47207c6f74aa1c" }, "path_in_vcs": "" }handsome_logger-0.8.0/.github/workflows/ci.yml000064400000000000000000000022561046102023000175060ustar 00000000000000name: Build and test on: push: pull_request: schedule: - cron: '0 0 * * 1' jobs: full_ci: strategy: matrix: os: [ ubuntu-latest, macos-latest, windows-latest ] toolchain: [ stable, 1.67.0 ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 - name: Setup rust version run: rustup default ${{ matrix.toolchain }} - name: Clippy fmt run: | cargo fmt -- --check if: ${{ matrix.type == 'stable' }} - name: Cargo check run: | cargo check cargo check --all-features - name: Cargo check examples run: | cargo check --examples cargo check --examples --all-features - name: Clippy pedantic run: | cargo clippy -- -W clippy::pedantic -A clippy::module-name-repetitions -A clippy::missing-errors-doc -A clippy::inline_always -A clippy::wildcard-imports if: ${{ matrix.type == 'stable' }} - name: Test run: | cargo test --no-fail-fast -- --nocapture cargo test --all-features --no-fail-fast -- --nocapture - name: Doc run: | cargo doc handsome_logger-0.8.0/.gitignore000064400000000000000000000000561046102023000147570ustar 00000000000000.vscode target Cargo.lock *.log *.log.* .idea/handsome_logger-0.8.0/.rustfmt.toml000064400000000000000000000002361046102023000154460ustar 00000000000000newline_style = "Unix" max_width = 180 remove_nested_parens = true # Enable only with nightly channel via - cargo +nightly fmt imports_granularity = "Module"handsome_logger-0.8.0/CHANGELOG.md000064400000000000000000000022251046102023000146000ustar 00000000000000## 0.8.0 - 29.09.2023 - By default local time offset is used instead of UTC - Chrono dependency is completelly removed and tz-rs is used instead ## 0.7.1 - 27.09.2023 - Added ability to use `handsome_logger::init();` for really simple initialization - Added ability to use non-lowercased log levels in `RUST_LOG` environment variable ## 0.7.0 - 19.09.2023 - Added ability to set log level by environment variable ## 0.6.0 - 15.09.2023 - Added by default showing microseconds - it is quite usable when testing quite fast operations - Increased minimum rust version from 1.65 to 1.67 ## 0.5.0 - 14.07.2023 - Added custom user function formatting ## 0.4.0 - 09.07.2023 - Added ability to filter messages by content ## 0.3.0 - 04.07.2023 - Added example with file rotating - Added ability to write all items at once to writer object(useful for file rotating) - New type of items - `[_thread_name]` and `[_process_id]` - Renamed `[_thread]` to `[_thread_id]` ## 0.2.0 - 02.07.2023 - Added examples - Fixed problem with additional and missing new lines in the output - Added support for using different text and background colors ## 0.1.0 - 02.07.2023 - Initial Release handsome_logger-0.8.0/Cargo.toml0000644000000021520000000000100121740ustar # 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" rust-version = "1.67.0" name = "handsome_logger" version = "0.8.0" authors = ["Rafał Mikrut "] description = "A fast, handsom and quite easy to use logger" documentation = "https://docs.rs/handsome_logger/" readme = "README.md" keywords = [ "log", "filelog", "logging", ] license = "MIT OR Apache-2.0" repository = "https://github.com/qarmin/handsome_logger" [dependencies.log] version = "0.4" features = ["std"] [dependencies.termcolor] version = "1.3" [dependencies.time] version = "0.3" features = [ "formatting", "macros", "local-offset", ] [dependencies.tz-rs] version = "0.6.14" handsome_logger-0.8.0/Cargo.toml.orig000064400000000000000000000011251046102023000156540ustar 00000000000000[package] name = "handsome_logger" version = "0.8.0" edition = "2021" authors = ["Rafał Mikrut "] description = "A fast, handsom and quite easy to use logger" documentation = "https://docs.rs/handsome_logger/" repository = "https://github.com/qarmin/handsome_logger" readme = "README.md" keywords = ["log", "filelog", "logging"] license = "MIT OR Apache-2.0" rust-version = "1.67.0" [dependencies] log = { version = "0.4", features = ["std"] } termcolor = { version = "1.3" } time = { version = "0.3", features = ["formatting", "macros", "local-offset"] } tz-rs = "0.6.14" handsome_logger-0.8.0/LICENSE.APACHE2000064400000000000000000000240571046102023000150050ustar 00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright (c) 2023 Rafał Mikrut Copyright (c) 2015-2023 Victor Brekenfeld Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. handsome_logger-0.8.0/LICENSE.MIT000064400000000000000000000021421046102023000144220ustar 00000000000000The MIT License (MIT) Copyright (c) 2023 Rafał Mikrut Copyright (c) 2015-2023 Victor Brekenfeld Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. handsome_logger-0.8.0/README.md000064400000000000000000000061221046102023000142460ustar 00000000000000# Handsome Logger Handsome logger aims to be fast, easy to use and configurable logger. This is a fork of [simplelog.rs](https://github.com/Drakulix/simplelog.rs) from commit `70f4dcb6c20de819b68a4c52988e850403f779db` and is available under same license as the original project. I created it because the formatting abilities of this library were insufficient for me and the changes that would have to be made to it to "fix" it were too big. ![Example](https://github.com/qarmin/handsome_logger/assets/41945903/f409c771-abb5-47dd-acfe-0aa385475591) ## Features - Multiple loggers - SimpleLogger(simplest and the stablest), TermLogger(SimpleLogger + colored output), WriteLogger(can save logs e.g. to file), CombinedLogger(can combine multiple loggers and save logs, both to file and to terminal) - Uses by default local time offset instead of UTC - this can be easily disabled - Customizable format - each element, like timestamp or module name, log level, can be customized - Colored output - you can colorize any part of log message - Simple to use - library can be easily configured in few lines of code for most use cases - Ability to set log level by environment variable e.g. `RUST_LOG=error ./app` - Filtering messages - you can ignore any message basing on your own function - Multiple log message formatters(you can use them more than once - nobody can stop you): ``` [_line] - prints line of code where log was called or 0 if cannot read line [_file] - prints full project path to file where log was called if is inside repository of full path if is outside, or "" if cannot read file path [_file_name] - prints file name where log was called or "" if cannot read file name [_module] - prints module name where log was called or "" if cannot read module name [_msg] - prints user log message [_time] - prints time of logged message [_level] - prints log level (INFO, DEBUG, etc.) [_thread_id] - prints thread id [_thread_name] - prints thread name [_process_id] - prints process id [_color_start], [_color_end] - starts and ends colorization of log message ``` ## Example Usage First add to Cargo.toml, this two lines ``` handsome_logger = "0.8" log = "0.4" ``` ```rust use handsome_logger::{ColorChoice, Config, TermLogger, TerminalMode}; use log::*; fn main() { handsome_logger::init().unwrap(); // which is simpler alternative to // TermLogger::init(Config::default(), TerminalMode::Mixed, ColorChoice::Auto).unwrap(); trace!("Got TRACE"); debug!("Got DEBUG"); info!("Got INFO"); warn!("Got WARNING"); error!("Got ERROR"); } ``` should print ``` 21:20:22 [INFO] terminal_logging: Got INFO 21:20:22 [WARN] terminal_logging: Got WARNING 21:20:22 [ERROR] terminal_logging: Got ERROR ``` examples folder contains examples of - formatting logs - saving logs to file and rotating it - using multiple loggers - colouring terminal output - filtering messages ## License Apache 2.0 or MIT, at your option. Copyright (c) 2023 Rafał Mikrut Copyright (c) 2015-2023 Victor Brekenfeld and contributors(for full list see https://github.com/Drakulix/simplelog.rs/graphs/contributors) handsome_logger-0.8.0/src/common.rs000064400000000000000000000006361046102023000154200ustar 00000000000000use log::LevelFilter; use std::env; pub fn get_env_log() -> Option { match env::var("RUST_LOG").ok()?.to_lowercase().as_str() { "err" | "error" => Some(LevelFilter::Error), "warn" | "warning" => Some(LevelFilter::Warn), "info" => Some(LevelFilter::Info), "debug" => Some(LevelFilter::Debug), "trace" => Some(LevelFilter::Trace), _ => None, } } handsome_logger-0.8.0/src/config.rs000064400000000000000000000416501046102023000153760ustar 00000000000000use core::fmt::{Debug, Formatter}; use log::{LevelFilter, Record}; use std::io::{Error, Write}; use std::sync::Arc; use termcolor::{BufferedStandardStream, Color, ColorSpec}; pub use time::format_description::FormatItem; pub use time::macros::format_description; pub use time::UtcOffset; const LEVEL_NUMBER: usize = 6; #[derive(Debug, Clone, Copy)] pub enum TimeFormat { Rfc2822, Rfc3339, Custom(&'static [FormatItem<'static>]), } #[derive(Debug, Clone, Eq, PartialEq)] pub enum Token { Text(&'static str), Time, Level, ColorStart, ColorEnd, ThreadId, ThreadName, ProcessId, Module, FileName, File, Line, Message, } type FilterFunction = dyn Fn(&Record) -> bool + Send + Sync; type WriteFunction = dyn Fn(&Record, &mut dyn Write) -> Result<(), Error> + Send + Sync; type TerminalWriteFunction = dyn Fn(&Record, &mut BufferedStandardStream) -> Result<(), Error> + Send + Sync; #[derive(Clone)] pub struct Config { pub(crate) level: LevelFilter, pub(crate) time_offset: UtcOffset, pub(crate) write_once: bool, pub(crate) time_format: [TimeFormat; LEVEL_NUMBER], pub(crate) format_text: [&'static str; LEVEL_NUMBER], pub(crate) tokens: [Vec; LEVEL_NUMBER], // Colors pub(crate) colored_text_color: [Option; LEVEL_NUMBER], pub(crate) background_color: [Option; LEVEL_NUMBER], pub(crate) compiled_colors: [ColorSpec; LEVEL_NUMBER], pub(crate) enabled_colors: bool, pub(crate) message_filtering: Option>, pub(crate) write_formatter: Option>, pub(crate) terminal_formatter: Option>, } const DEFAULT_FORMAT_TEXT: &str = "[_time] [_color_start][[_level]][_color_end] [_module]: [_msg]"; const CONF_FULL_FORMAT_TEST: &str = "[_time] [_color_start][[_level]][_color_end] [[_module]] [_file_name]:[_line] - [_msg]"; impl Config { /// Internal function to calculate all required data from user input /// this is done only once to avoid unnecessary computations pub(crate) fn calculate_data(&mut self) { self.calculate_tokens(); self.calculate_colors(); } /// Creating `ColorSpec` from user colors fn calculate_colors(&mut self) { for (idx, color_spec) in self.compiled_colors.iter_mut().enumerate() { *color_spec = ColorSpec::new().set_bg(self.background_color[idx]).set_fg(self.colored_text_color[idx]).clone(); } } /// Calculate tokens from format text fn calculate_tokens(&mut self) { let allowed_tokens = [ ("[_time]", Token::Time), ("[_level]", Token::Level), ("[_color_start]", Token::ColorStart), ("[_color_end]", Token::ColorEnd), ("[_thread_id]", Token::ThreadId), ("[_thread_name]", Token::ThreadName), ("[_process_id]", Token::ProcessId), ("[_module]", Token::Module), ("[_file]", Token::File), ("[_file_name]", Token::FileName), ("[_line]", Token::Line), ("[_msg]", Token::Message), ]; for (idx, format_text) in self.format_text.into_iter().enumerate() { let mut collected_tokens = Vec::new(); let mut current_index = 0; loop { let mut minimum_index = usize::MAX; let mut choose_token_text = None; for (token_txt, token) in &allowed_tokens { if let Some(find_idx) = format_text[current_index..].find(token_txt) { if minimum_index > find_idx + current_index { minimum_index = find_idx + current_index; choose_token_text = Some((token_txt, token)); } } } // No more tokens if let Some((token_txt, token)) = choose_token_text { if minimum_index > current_index { let text = &format_text[current_index..minimum_index]; collected_tokens.push(Token::Text(text)); } collected_tokens.push(token.clone()); current_index = minimum_index + token_txt.len() - 1; } else { if current_index != format_text.len() { let text = &format_text[current_index..]; collected_tokens.push(Token::Text(text)); } break; // No more to check } current_index += 1; } self.tokens[idx] = collected_tokens; } } } #[derive(Debug, Clone)] pub struct ConfigBuilder(Config); impl ConfigBuilder { #[must_use] pub fn new() -> ConfigBuilder { ConfigBuilder(Config::default()) } /// Preset for saving bigger amount of information than default preset #[must_use] pub fn new_preset_config_full() -> ConfigBuilder { let mut builder = ConfigBuilder::default(); builder.set_time_format( TimeFormat::Custom(format_description!( "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3] [offset_hour sign:mandatory]" )), None, ); builder.set_format_text(CONF_FULL_FORMAT_TEST, None); builder } #[must_use] pub fn new_preset_no_micros_in_time() -> ConfigBuilder { let mut builder = ConfigBuilder::default(); builder.set_time_format(TimeFormat::Custom(format_description!("[hour]:[minute]:[second].[subsecond digits:3]")), None); builder } /// Sets format of logged message /// E.g. "\[_time\] \[\[_level\]\] \[_module\] \"\[_msg\]\"" /// depending on other settings, may print something like: /// 14:21:15 \[INFO\] main: "Hello world!" /// If level is none, it will set all levels pub fn set_format_text(&mut self, format_text: &'static str, level: Option) -> &mut ConfigBuilder { if let Some(level) = level { self.0.format_text[level as usize] = format_text; } else { self.0.format_text = [format_text; LEVEL_NUMBER]; } self } /// Sets background color /// If color is none, background will not be colored /// If level is none, it will set all levels /// If level is some, it will set only that level /// Background color is used only if `enabled_colors` is true pub fn set_background_color(&mut self, background_color: Option, level: Option) -> &mut ConfigBuilder { if let Some(level) = level { self.0.background_color[level as usize] = background_color; } else { self.0.background_color = [background_color; LEVEL_NUMBER]; } self } /// Sets text color /// If color is none, text will be invisible /// If level is none, it will set all levels /// If level is some, it will set only that level /// Background color is used only if `enabled_colors` is true pub fn set_colored_text_color(&mut self, colored_text_color: Option, level: Option) -> &mut ConfigBuilder { if let Some(level) = level { self.0.colored_text_color[level as usize] = colored_text_color; } else { self.0.colored_text_color = [colored_text_color; LEVEL_NUMBER]; } self } /// Enables colouring of text - only works with `TermLogger` pub fn set_enabled_colours(&mut self, enabled_colours: bool) -> &mut ConfigBuilder { self.0.enabled_colors = enabled_colours; self } /// Sets the level of the logger. /// E.g. using `LevelFilter::Info` will print all logs with level `Info`, `Warn`, `Error`, /// but not `Debug` or `Trace`. pub fn set_level(&mut self, level: LevelFilter) -> &mut ConfigBuilder { self.0.level = level; self } /// Instead of writing multiple times to target, creates a buffer, writes to memory and /// at the end writes only once to target /// This is useful when saving to file, because allows to not split one log into multiple /// files if rotating is used. /// Works only with `WriteLogger` pub fn set_write_once(&mut self, write_once: bool) -> &mut ConfigBuilder { self.0.write_once = write_once; self } /// Set time format used in logger /// If level is none, it will set all levels /// Time format can be predefined(Rfc2822 or Rfc3339) or custom pub fn set_time_format(&mut self, time_format: TimeFormat, level: Option) -> &mut ConfigBuilder { if let Some(level) = level { self.0.time_format[level as usize] = time_format; } else { self.0.time_format = [time_format; LEVEL_NUMBER]; } self } /// Manually sets the offset used for the time. pub fn set_time_offset(&mut self, offset: UtcOffset) -> &mut ConfigBuilder { self.0.time_offset = offset; self } /// Sets the offset used to the current local time offset pub fn set_time_offset_to_local(&mut self) -> Result<&mut ConfigBuilder, &mut ConfigBuilder> { match Self::get_local_time_offset() { Some(offset) => { self.0.time_offset = offset; Ok(self) } None => Err(self), } } /// Reset the offset used to UTC pub fn set_remove_time_offset(&mut self) -> &mut ConfigBuilder { self.0.time_offset = UtcOffset::UTC; self } /// Sets function that will be used to filter messages /// If function returns true, message will be logged, otherwise it will be ignored /// Function takes as argument function that will be filtered allowed results /// If `message_filtering` is none, all messages will be logged /// ``` /// use log::{info, Record}; /// use handsome_logger::{Config, ConfigBuilder}; /// /// fn filtering_messages(record: &Record) -> bool { /// if let Some(arg) = record.args().as_str() { /// !arg.contains("E") /// } else { /// true /// } /// } /// /// let logger = ConfigBuilder::new().set_message_filtering(Some(filtering_messages)).build(); /// info!("Got BED"); // This will be ignored /// info!("Got ANANAS"); // This will be printed /// ``` pub fn set_message_filtering(&mut self, message_filtering: Option) -> &mut ConfigBuilder where F: Fn(&Record) -> bool + Send + Sync + 'static, { if let Some(message_filtering) = message_filtering { self.0.message_filtering = Some(Arc::new(message_filtering)); } else { self.0.message_filtering = None; } self } /// Sets custom formatter for `WriteLogger` /// If you don't want to use default formatter, you can set your own /// Setting `write_formatter` to None will use default formatter /// Function takes as argument function that will be filtered allowed results pub fn set_custom_write_formatter(&mut self, write_formatter: Option) -> &mut ConfigBuilder where F: Fn(&Record, &mut dyn Write) -> Result<(), Error> + Send + Sync + 'static, { if let Some(write_formatter) = write_formatter { self.0.write_formatter = Some(Arc::new(write_formatter)); } else { self.0.write_formatter = None; } self } /// Sets custom formatter for `TermLogger` /// If you don't want to use default formatter, you can set your own /// Setting `terminal_formatter` to None will use default formatter /// Function takes as argument function that will be filtered allowed results pub fn set_custom_terminal_formatter(&mut self, terminal_formatter: Option) -> &mut ConfigBuilder where F: Fn(&Record, &mut BufferedStandardStream) -> Result<(), Error> + Send + Sync + 'static, { if let Some(terminal_formatter) = terminal_formatter { self.0.terminal_formatter = Some(Arc::new(terminal_formatter)); } else { self.0.terminal_formatter = None; } self } /// Gets the local time offset /// On unix will this use tz-rs crate, /// otherwise it will use time crate fn get_local_time_offset() -> Option { #[cfg(target_family = "unix")] { let Ok(timezone) = tz::TimeZone::local() else { return None; }; let Ok(time_type) = timezone.find_current_local_time_type() else { return None; }; UtcOffset::from_whole_seconds(time_type.ut_offset()).ok() } #[cfg(not(target_family = "unix"))] { time::UtcOffset::current_local_offset().ok() } } /// Builds the config pub fn build(&mut self) -> Config { self.0.clone() } } impl Default for ConfigBuilder { fn default() -> Self { ConfigBuilder::new() } } impl Default for Config { fn default() -> Config { let tz_offset = ConfigBuilder::get_local_time_offset().unwrap_or(UtcOffset::UTC); Config { level: LevelFilter::Info, write_once: false, time_format: [TimeFormat::Custom(format_description!("[hour]:[minute]:[second].[subsecond digits:3]")); LEVEL_NUMBER], time_offset: tz_offset, tokens: [vec![], vec![], vec![], vec![], vec![], vec![]], colored_text_color: [ None, Some(Color::Red), // Error Some(Color::Yellow), // Warn Some(Color::Blue), // Info Some(Color::Cyan), // Debug Some(Color::White), // Trace ], background_color: [None, None, None, None, None, None], enabled_colors: true, format_text: [DEFAULT_FORMAT_TEXT; LEVEL_NUMBER], compiled_colors: [ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new()], message_filtering: None, write_formatter: None, terminal_formatter: None, } } } impl Debug for Config { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { f.debug_struct("Config") .field("level", &self.level) .field("write_once", &self.write_once) .field("time_format", &self.time_format) .field("time_offset", &self.time_offset) .field("tokens", &self.tokens) .field("colored_text_color", &self.colored_text_color) .field("background_color", &self.background_color) .field("enabled_colors", &self.enabled_colors) .field("format_text", &self.format_text) .field("compiled_colors", &self.compiled_colors) .finish_non_exhaustive() } } #[cfg(test)] mod tests { use super::*; #[test] fn test() { let text = "[_time] [_level] [_thread_id] [_thread_name] [_process_id] [_module] [_file][_file_name] [_line] [_color_start][_msg][_color_end] [RAR]"; let mut config = ConfigBuilder::new().set_format_text(text, None).build(); config.calculate_data(); assert_eq!( config.tokens[0], vec![ Token::Time, Token::Text(" "), Token::Level, Token::Text(" "), Token::ThreadId, Token::Text(" "), Token::ThreadName, Token::Text(" "), Token::ProcessId, Token::Text(" "), Token::Module, Token::Text(" "), Token::File, Token::FileName, Token::Text(" "), Token::Line, Token::Text(" "), Token::ColorStart, Token::Message, Token::ColorEnd, Token::Text(" [RAR]"), ] ); let text = "]]][["; let mut config = ConfigBuilder::new().set_format_text(text, None).build(); config.calculate_data(); assert_eq!(config.tokens[0], vec![Token::Text("]]][[")]); let text = " [_time]"; let mut config = ConfigBuilder::new().set_format_text(text, None).build(); config.calculate_data(); assert_eq!(config.tokens[0], vec![Token::Text(" "), Token::Time]); let text = "[_time]"; let mut config = ConfigBuilder::new().set_format_text(text, None).build(); config.calculate_data(); assert_eq!(config.tokens[0], vec![Token::Time]); let text = "[_time] "; let mut config = ConfigBuilder::new().set_format_text(text, None).build(); config.calculate_data(); assert_eq!(config.tokens[0], vec![Token::Time, Token::Text(" ")]); let text = ""; let mut config = ConfigBuilder::new().set_format_text(text, None).build(); config.calculate_data(); assert_eq!(config.tokens[0], vec![]); } } handsome_logger-0.8.0/src/lib.rs000064400000000000000000000014571046102023000147000ustar 00000000000000pub use log::{Level, LevelFilter, Log}; pub use termcolor::{Color, ColorChoice}; pub use self::config::{format_description, Config, ConfigBuilder, FormatItem, TimeFormat}; pub use self::loggers::{CombinedLogger, SimpleLogger, TermLogger, TerminalMode, WriteLogger}; mod common; mod config; mod loggers; pub fn init() -> Result<(), log::SetLoggerError> { TermLogger::init(Config::default(), TerminalMode::Mixed, ColorChoice::Auto) } pub fn init_without_local_time() -> Result<(), log::SetLoggerError> { let config = ConfigBuilder::default().set_remove_time_offset().build(); TermLogger::init(config, TerminalMode::Mixed, ColorChoice::Auto) } pub trait SharedLogger: Log { fn level(&self) -> LevelFilter; fn config(&self) -> Option<&Config>; fn as_log(self: Box) -> Box; } handsome_logger-0.8.0/src/loggers/combine_logger.rs000064400000000000000000000031251046102023000205410ustar 00000000000000use log::{set_boxed_logger, set_max_level, LevelFilter, Log, Metadata, Record, SetLoggerError}; use crate::common::get_env_log; use crate::{Config, SharedLogger}; pub struct CombinedLogger { level: LevelFilter, logger: Vec>, } impl CombinedLogger { pub fn init(logger: Vec>) -> Result<(), SetLoggerError> { let comblog = CombinedLogger::new(logger); let log_level = get_env_log().unwrap_or(comblog.level()); set_max_level(log_level); set_boxed_logger(comblog) } #[must_use] pub fn new(logger: Vec>) -> Box { let mut log_level = LevelFilter::Off; for log in &logger { if log_level < log.level() { log_level = log.level(); } } let log_level = get_env_log().unwrap_or(log_level); Box::new(CombinedLogger { level: log_level, logger }) } } impl Log for CombinedLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= self.level } fn log(&self, record: &Record) { if self.enabled(record.metadata()) { for log in &self.logger { log.log(record); } } } fn flush(&self) { for log in &self.logger { log.flush(); } } } impl SharedLogger for CombinedLogger { fn level(&self) -> LevelFilter { self.level } fn config(&self) -> Option<&Config> { None } fn as_log(self: Box) -> Box { Box::new(*self) } } handsome_logger-0.8.0/src/loggers/logging.rs000064400000000000000000000225621046102023000172220ustar 00000000000000use std::io::{Error, Write}; use std::{process, thread}; use log::Record; use termcolor::{BufferedStandardStream, WriteColor}; use crate::config::{TimeFormat, Token}; use crate::Config; /// Logging functionality for `WriteLogger` that can be used for any write target, even console. /// Operate on tokens, which allow to easily change position of printing item #[inline(always)] pub fn try_log(config: &Config, record: &Record, write: &mut W) -> Result<(), Error> where W: Write + Sized, { for token in &config.tokens[record.level() as usize] { match token { Token::Time => write_time(write, config, record)?, Token::Level => write!(write, "{}", record.level())?, Token::ThreadId => write_thread_id(write)?, Token::ThreadName => write_thread_name(write)?, Token::ProcessId => write!(write, "{}", process::id())?, Token::Module => write!(write, "{}", record.module_path().unwrap_or(""))?, Token::File => write!(write, "{}", record.file().unwrap_or(""))?, Token::FileName => write_file_name(record, write)?, Token::Line => write!(write, "{}", record.line().unwrap_or(0))?, Token::Text(text) => write!(write, "{text}")?, Token::Message => write_args(record, write)?, Token::ColorStart | Token::ColorEnd => {} } } writeln!(write)?; Ok(()) } /// Terminal logging functionality, that can only be used by `TermLogger` /// The only difference between this function and casual `try_log` is flushing at the end and /// using tokens `ColorStart` and `ColorEnd`, that allows to write colors into terminal #[inline(always)] pub fn try_log_term(config: &Config, record: &Record, write: &mut BufferedStandardStream) -> Result<(), Error> { for token in &config.tokens[record.level() as usize] { match token { Token::Time => write_time(write, config, record)?, Token::Level => write!(write, "{}", record.level())?, Token::ThreadId => write_thread_id(write)?, Token::ThreadName => write_thread_name(write)?, Token::ProcessId => write!(write, "{}", process::id())?, Token::Module => write!(write, "{}", record.module_path().unwrap_or(""))?, Token::File => write!(write, "{}", record.file().unwrap_or(""))?, Token::FileName => write_file_name(record, write)?, Token::Line => write!(write, "{}", record.line().unwrap_or(0))?, Token::Text(text) => write!(write, "{text}")?, Token::Message => write_args(record, write)?, Token::ColorStart => set_color(write, config, record, true)?, Token::ColorEnd => set_color(write, config, record, false)?, } } writeln!(write)?; // The log crate holds the logger as a `static mut`, which isn't dropped // at program exit: https://doc.rust-lang.org/reference/items/static-items.html // Sadly, this means we can't rely on the BufferedStandardStreams flushing // themselves on the way out, so to avoid the Case of the Missing 8k, // flush each entry. write.flush() } /// Writes local thread id and starts from 1 #[inline(always)] pub fn write_thread_id(write: &mut W) -> Result<(), Error> where W: Write + Sized, { // TODO, change this to simple `thread::current().id().as_u64()` when will be stabilized let thread_id_string = format!("{:?}", thread::current().id()).replace("ThreadId(", "").replace(')', ""); write!(write, "{thread_id_string}")?; Ok(()) } /// Writes thread name #[inline(always)] pub fn write_thread_name(write: &mut W) -> Result<(), Error> where W: Write + Sized, { match thread::current().name() { Some(thread_name) => write!(write, "{thread_name}")?, None => write!(write, "")?, } Ok(()) } /// Writes file name with its extension /// E.g. `file.rs` #[inline(always)] pub fn write_file_name(record: &Record, write: &mut W) -> Result<(), Error> where W: Write + Sized, { match record.file() { Some(file) => { let mut file = file.rsplitn(2, '/'); if let Some(file) = file.next() { write!(write, "{file}")?; } else { write!(write, "")?; } } None => write!(write, "")?, } Ok(()) } /// Writes time in choosen time format #[inline(always)] pub fn write_time(write: &mut W, config: &Config, record: &Record) -> Result<(), Error> where W: Write + Sized, { use time::error::Format; use time::format_description::well_known::*; let time = time::OffsetDateTime::now_utc().to_offset(config.time_offset); let res = match config.time_format[record.level() as usize] { TimeFormat::Rfc2822 => time.format_into(write, &Rfc2822), TimeFormat::Rfc3339 => time.format_into(write, &Rfc3339), TimeFormat::Custom(format) => time.format_into(write, &format), }; match res { Err(Format::StdIo(err)) => return Err(err), Err(err) => panic!("Invalid time format: {err}"), _ => {} }; write!(write, "")?; Ok(()) } /// Writes color to terminal output #[inline(always)] pub fn set_color(write: &mut BufferedStandardStream, config: &Config, record: &Record, color_start: bool) -> Result<(), Error> { if config.enabled_colors { if color_start { let color = &config.compiled_colors[record.level() as usize]; write.set_color(color)?; } else { write.reset()?; } } Ok(()) } /// Writes args provided in time macro /// E.g. record.args() in info!("Print This") will contain one argument "Print This" #[inline(always)] pub fn write_args(record: &Record, write: &mut W) -> Result<(), Error> where W: Write + Sized, { write!(write, "{}", record.args())?; Ok(()) } #[cfg(test)] mod tests { use log::Level; use termcolor::ColorChoice; use crate::loggers::term_logger::OutputStreams; use crate::ConfigBuilder; use super::*; #[test] fn test_tokens_output() { let mut config = ConfigBuilder::new().build(); let record = Record::builder().build(); let i = vec![ Token::File, Token::Text(" "), Token::Level, Token::Text(" "), Token::Module, Token::Text(" "), Token::Line, Token::Text(" "), Token::FileName, Token::Text(" "), Token::ColorEnd, Token::Text(" "), Token::ColorStart, Token::Text(" "), Token::ThreadName, Token::Text(" "), Token::Message, Token::Text("test"), ]; config.tokens = [i.clone(), i.clone(), i.clone(), i.clone(), i.clone(), i.clone()]; let mut res_vec = Vec::new(); let res = try_log(&config, &record, &mut res_vec); assert!(res.is_ok()); assert_eq!( String::from_utf8(res_vec).unwrap(), " INFO 0 loggers::logging::tests::test_tokens_output test\n".to_string() ); let mut config = ConfigBuilder::new().build(); let record = Record::builder().build(); let i = vec![Token::ThreadId]; config.tokens = [i.clone(), i.clone(), i.clone(), i.clone(), i.clone(), i.clone()]; let mut res_vec = Vec::new(); let res = try_log(&config, &record, &mut res_vec); assert!(res.is_ok()); let mut ret = String::from_utf8(res_vec.clone()).unwrap(); ret.pop().unwrap(); assert!(ret.parse::().is_ok()); let mut config = ConfigBuilder::new().build(); let record = Record::builder().build(); let i = vec![Token::ProcessId]; config.tokens = [i.clone(), i.clone(), i.clone(), i.clone(), i.clone(), i.clone()]; let mut res_vec = Vec::new(); let res = try_log(&config, &record, &mut res_vec); assert!(res.is_ok()); let mut ret = String::from_utf8(res_vec.clone()).unwrap(); ret.pop().unwrap(); assert!(ret.parse::().is_ok()); let mut config = ConfigBuilder::new().build(); let record = Record::builder().build(); let i = vec![Token::Time]; config.tokens = [i.clone(), i.clone(), i.clone(), i.clone(), i.clone(), i.clone()]; let mut res_vec = Vec::new(); let res = try_log(&config, &record, &mut res_vec); assert!(res.is_ok()); assert_eq!(String::from_utf8(res_vec).unwrap().len(), "20:24:46.123\n".len()); } #[test] fn test_colours() { for level_filter in &[Level::Info, Level::Warn, Level::Error, Level::Debug, Level::Trace] { test_colour_level(*level_filter); } } fn test_colour_level(level: Level) { let mut config = ConfigBuilder::new().set_enabled_colours(true).build(); let record = Record::builder().level(level).build(); let i = vec![Token::Text("RAR"), Token::Level, Token::Text("RAR")]; config.tokens = [i.clone(), i.clone(), i.clone(), i.clone(), i.clone(), i.clone()]; let mut streams = OutputStreams { err: BufferedStandardStream::stderr(ColorChoice::Always), out: BufferedStandardStream::stdout(ColorChoice::Always), }; let res = try_log_term(&config, &record, &mut streams.err); assert!(res.is_ok()); } } handsome_logger-0.8.0/src/loggers/mod.rs000064400000000000000000000004251046102023000163450ustar 00000000000000pub use self::combine_logger::CombinedLogger; pub use self::simple_logger::SimpleLogger; pub use self::term_logger::{TermLogger, TerminalMode}; pub use self::write_logger::WriteLogger; mod combine_logger; pub mod logging; mod simple_logger; mod term_logger; mod write_logger; handsome_logger-0.8.0/src/loggers/simple_logger.rs000064400000000000000000000042141046102023000204160ustar 00000000000000use std::io::{stderr, stdout}; use std::sync::Mutex; use log::{set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; use crate::{Config, SharedLogger}; use super::logging::try_log; use crate::common::get_env_log; pub struct SimpleLogger { level: LevelFilter, config: Config, output_lock: Mutex<()>, } impl SimpleLogger { pub fn init(config: Config) -> Result<(), SetLoggerError> { let log_level = get_env_log().unwrap_or(config.level); set_max_level(log_level); let logger = SimpleLogger::new(log_level, config); set_boxed_logger(logger) } #[must_use] pub fn new(log_level: LevelFilter, mut config: Config) -> Box { config.calculate_data(); let log_level = get_env_log().unwrap_or(log_level); Box::new(SimpleLogger { level: log_level, config, output_lock: Mutex::new(()), }) } } impl Log for SimpleLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= self.level } fn log(&self, record: &Record) { if let Some(message_filtering) = &self.config.message_filtering { if !message_filtering(record) { return; } } if self.enabled(record.metadata()) { let _lock = self.output_lock.lock().unwrap(); if record.level() == Level::Error { let stderr = stderr(); let mut stderr_lock = stderr.lock(); let _ = try_log(&self.config, record, &mut stderr_lock); } else { let stdout = stdout(); let mut stdout_lock = stdout.lock(); let _ = try_log(&self.config, record, &mut stdout_lock); } } } fn flush(&self) { use std::io::Write; let _ = stdout().flush(); } } impl SharedLogger for SimpleLogger { fn level(&self) -> LevelFilter { self.level } fn config(&self) -> Option<&Config> { Some(&self.config) } fn as_log(self: Box) -> Box { Box::new(*self) } } handsome_logger-0.8.0/src/loggers/term_logger.rs000064400000000000000000000067221046102023000201020ustar 00000000000000use std::io::{Error, Write}; use std::sync::Mutex; use log::{set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record, SetLoggerError}; use termcolor::{BufferedStandardStream, ColorChoice}; use crate::common::get_env_log; use crate::{Config, SharedLogger}; use super::logging::*; pub struct OutputStreams { pub(crate) err: BufferedStandardStream, pub(crate) out: BufferedStandardStream, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Default)] pub enum TerminalMode { Stdout, Stderr, #[default] Mixed, } pub struct TermLogger { level: LevelFilter, config: Config, streams: Mutex, } impl TermLogger { pub fn init(config: Config, mode: TerminalMode, color_choice: ColorChoice) -> Result<(), SetLoggerError> { let log_level = get_env_log().unwrap_or(config.level); set_max_level(log_level); let logger = TermLogger::new(config, mode, color_choice); set_boxed_logger(logger) } #[must_use] pub fn new(mut config: Config, mode: TerminalMode, color_choice: ColorChoice) -> Box { let streams = match mode { TerminalMode::Stdout => OutputStreams { err: BufferedStandardStream::stdout(color_choice), out: BufferedStandardStream::stdout(color_choice), }, TerminalMode::Stderr => OutputStreams { err: BufferedStandardStream::stderr(color_choice), out: BufferedStandardStream::stderr(color_choice), }, TerminalMode::Mixed => OutputStreams { err: BufferedStandardStream::stderr(color_choice), out: BufferedStandardStream::stdout(color_choice), }, }; config.calculate_data(); let log_level = get_env_log().unwrap_or(config.level); Box::new(TermLogger { level: log_level, config, streams: Mutex::new(streams), }) } fn try_log(&self, record: &Record) -> Result<(), Error> { if self.enabled(record.metadata()) { let mut streams = self.streams.lock().unwrap(); if let Some(terminal_logger) = &self.config.terminal_formatter { if record.level() == Level::Error { terminal_logger(record, &mut streams.err) } else { terminal_logger(record, &mut streams.out) } } else if record.level() == Level::Error { try_log_term(&self.config, record, &mut streams.err) } else { try_log_term(&self.config, record, &mut streams.out) } } else { Ok(()) } } } impl Log for TermLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= self.level } fn log(&self, record: &Record) { if let Some(message_filtering) = &self.config.message_filtering { if !message_filtering(record) { return; } } let _ = self.try_log(record); } fn flush(&self) { let mut streams = self.streams.lock().unwrap(); let _ = streams.out.flush(); let _ = streams.err.flush(); } } impl SharedLogger for TermLogger { fn level(&self) -> LevelFilter { self.level } fn config(&self) -> Option<&Config> { Some(&self.config) } fn as_log(self: Box) -> Box { Box::new(*self) } } handsome_logger-0.8.0/src/loggers/write_logger.rs000064400000000000000000000051161046102023000202610ustar 00000000000000use std::io::Write; use std::sync::Mutex; use log::{set_boxed_logger, set_max_level, LevelFilter, Log, Metadata, Record, SetLoggerError}; use crate::common::get_env_log; use crate::{Config, SharedLogger}; use super::logging::try_log; pub struct WriteLogger { level: LevelFilter, config: Config, writable: Mutex, } impl WriteLogger { pub fn init(config: Config, writable: W) -> Result<(), SetLoggerError> { let log_level = get_env_log().unwrap_or(config.level); set_max_level(log_level); let logger = WriteLogger::new(config, writable); set_boxed_logger(logger) } #[must_use] pub fn new(mut config: Config, writable: W) -> Box> { config.calculate_data(); let log_level = get_env_log().unwrap_or(config.level); Box::new(WriteLogger { level: log_level, config, writable: Mutex::new(writable), }) } } impl Log for WriteLogger { fn enabled(&self, metadata: &Metadata) -> bool { metadata.level() <= self.level } fn log(&self, record: &Record) { if let Some(message_filtering) = &self.config.message_filtering { if !message_filtering(record) { return; } } if self.enabled(record.metadata()) { let mut write_lock = self.writable.lock().unwrap(); if let Some(write_formatter) = &self.config.write_formatter { if self.config.write_once { let mut buffer: Vec = Vec::new(); let _ = write_formatter(record, &mut buffer); let _ = write_lock.write_all(buffer.as_slice()); } else { let _ = write_formatter(record, &mut *write_lock); } } else if self.config.write_once { let mut buffer: Vec = Vec::new(); let _ = try_log(&self.config, record, &mut buffer); let _ = write_lock.write_all(buffer.as_slice()); } else { let _ = try_log(&self.config, record, &mut *write_lock); } } } fn flush(&self) { let _ = self.writable.lock().unwrap().flush(); } } impl SharedLogger for WriteLogger { fn level(&self) -> LevelFilter { self.level } fn config(&self) -> Option<&Config> { Some(&self.config) } fn as_log(self: Box) -> Box { Box::new(*self) } }