diff --git a/.editorconfig b/.editorconfig index c8e4a9c9..5b74c262 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,5 +14,17 @@ insert_final_newline = true indent_style = space indent_size = 4 +[*.rs] +indent_size = false + [*.md] trim_trailing_whitespace = false +indent_size = 2 + +[*.{yml,yaml}] +indent_size = 2 + +# Ignore License files +[LICENSE-*] +indent_size = unset +indent_style = unset diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..1c7caab1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# Required for all files +* @estk @gadunga + diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 00000000..f8aefd2a --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,23 @@ +--- +name: Security audit +on: + push: + paths: + - '**/Cargo.toml' + - '**/Cargo.lock' + - '.github/workflows/audit.yml' +jobs: + cargo-audit: + name: Audit the baseline for CVEs + runs-on: ubuntu-latest + steps: + - name: Checkout the source code + uses: actions/checkout@v4 + + - uses: cargo-bins/cargo-binstall@main + + - name: Install cargo-audit + run: cargo binstall -y cargo-audit + + - name: Audit + run: cargo audit diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..2ca0f327 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,35 @@ +--- +name: coverage + +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +jobs: + test: + # https://github.com/xd009642/tarpaulin#github-actions + name: coverage + runs-on: ubuntu-latest + container: + image: xd009642/tarpaulin:develop-nightly + options: --security-opt seccomp=unconfined + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Generate code coverage + run: | + cargo +nightly tarpaulin --verbose --all-features --workspace --timeout 120 --out xml + env: + USER: "test-user" + + - name: Upload to codecov.io + uses: codecov/codecov-action@v3 + with: + fail_ci_if_error: true diff --git a/.github/workflows/editor-check.yml b/.github/workflows/editor-check.yml new file mode 100644 index 00000000..3d20538a --- /dev/null +++ b/.github/workflows/editor-check.yml @@ -0,0 +1,20 @@ +--- +name: EditorConfig Checker + +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +jobs: + editorconfig: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: editorconfig-checker/action-editorconfig-checker@main + - run: editorconfig-checker diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..7571ef9d --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,45 @@ +--- +name: Lint Jobs + +on: + push: + branches: + - main + - devel + pull_request: + branches: + - main + - devel + +jobs: + rust-lint: + name: Lint + runs-on: ubuntu-latest + steps: + - name: Checkout the source code + uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@nightly + with: + components: rustfmt, clippy + + - name: Run cargo check + run: cargo check + + - name: Run rustfmt + run: cargo fmt -- --check + + - name: Run Clippy + run: cargo clippy --all-features + env: + RUSTFLAGS: -D warnings + + docs-lint: + name: Docs Lint + runs-on: ubuntu-latest + steps: + - name: Markdown Linting Action + uses: avto-dev/markdown-lint@v1 + with: + config: .markdownlint.yml + args: docs/Configuration.md README.md diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 35f9e752..342c785e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,37 +1,17 @@ -name: CI +--- +name: Build CI on: push: branches: - - master + - main - devel pull_request: branches: - - master + - main - devel jobs: - rust-lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Checkout the source code - uses: actions/checkout@master - - - name: Install Rust stable - run: | - rustup toolchain update --no-self-update stable - rustup default stable - rustup component add clippy rustfmt - - - name: Run rustfmt - run: cargo fmt -- --check - - - name: Run clippy - run: cargo clippy --all-features - env: - RUSTFLAGS: -D warnings - test: name: Test runs-on: ${{ matrix.os }} @@ -40,41 +20,43 @@ jobs: rust_versions: ["stable", "1.67"] os: [ubuntu-latest, windows-latest] steps: - - name: Checkout the source code - uses: actions/checkout@master + - name: Checkout the source code + uses: actions/checkout@v4 - - name: Install Rust Versions - run: | - rustup toolchain install --no-self-update ${{ matrix.rust_versions }} - rustup default stable + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust_versions }} - - name: Build lib - run: rustup run ${{ matrix.rust_versions }} cargo build ${{ matrix.cargo_build_flags }} + - name: Build lib + run: cargo build - - name: test lib - run: cargo test --all-features - env: - RUSTFLAGS: -D warnings + - name: Test lib + run: | + cargo test --all-features + # This test needs another run under the release mode + cargo test --release --features simple_writer encode::pattern::tests::debug_release + env: + RUSTFLAGS: -D warnings features: name: Test Individual Features runs-on: ubuntu-latest steps: - - name: Checkout the source code - uses: actions/checkout@master + - name: Checkout the source code + uses: actions/checkout@v4 - - name: Install Rust toolchain and jq - run: | - rustup toolchain install --no-self-update stable - rustup default stable - sudo apt-get install -y --no-install-recommends jq + - uses: dtolnay/rust-toolchain@stable - - name: Test lib - run: | - for feature in $(cargo read-manifest | jq -r '.features|keys|join("\n")'); do + - name: Install jq + run: | + sudo apt-get install -y --no-install-recommends jq + + - name: Test lib + run: | + for feature in $(cargo read-manifest | jq -r '.features|keys|join("\n")'); do echo building with feature "$feature" RUSTFLAGS='-D warnings' cargo test --no-default-features --features "$feature" - done + done bench: name: Benchmark the background_rotation feature @@ -83,41 +65,12 @@ jobs: matrix: rust_versions: ["stable", "1.67"] steps: - - name: Checkout the source code - uses: actions/checkout@master - - - name: Install Rust toolchain - run: | - rustup toolchain install --no-self-update stable - rustup default stable + - name: Checkout the source code + uses: actions/checkout@v4 - - name: Rotation with backgrounded rotation - run: cargo bench --features gzip,background_rotation + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust_versions }} - cargo-audit: - name: Audit the baseline for CVEs - runs-on: ubuntu-latest - steps: - - name: Checkout the source code - uses: actions/checkout@master - - - name: Install Rust toolchain - run: | - rustup toolchain install --no-self-update stable - rustup default stable - - - name: Install Auditting Tools - run: cargo install cargo-audit - - - name: Audit project - run: cargo audit - - docs-lint: - name: Lint - runs-on: ubuntu-latest - steps: - - name: Markdown Linting Action - uses: avto-dev/markdown-lint@v1.5.0 - with: - config: .markdownlint.yml - args: docs/Configuration.md README.md + - name: Bench features + run: cargo bench --all-features diff --git a/.rustfmt.toml b/.rustfmt.toml index 7d2cf549..e86028b1 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1 +1 @@ -merge_imports = true +imports_granularity="Crate" diff --git a/README.md b/README.md index 1559c3d2..53219341 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/log4rs.svg)](https://crates.io/crates/log4rs) [![License: MIT OR Apache-2.0](https://img.shields.io/crates/l/clippy.svg)](#license) ![CI](https://github.com/estk/log4rs/workflows/CI/badge.svg) -[![Minimum rustc version](https://img.shields.io/badge/rustc-1.56+-green.svg)](https://github.com/estk/log4rs#rust-version-requirements) +[![Minimum rustc version](https://img.shields.io/badge/rustc-1.67+-green.svg)](https://github.com/estk/log4rs#rust-version-requirements) log4rs is a highly configurable logging framework modeled after Java's Logback and log4j libraries. diff --git a/benches/rotation.rs b/benches/rotation.rs index de108dab..4167331f 100644 --- a/benches/rotation.rs +++ b/benches/rotation.rs @@ -1,5 +1,7 @@ -use std::thread; -use std::time::{Duration, Instant}; +use std::{ + thread, + time::{Duration, Instant}, +}; use lazy_static::lazy_static; use tempfile::{tempdir, TempDir}; diff --git a/docs/Configuration.md b/docs/Configuration.md index 50b208a5..d4120e5a 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -10,7 +10,7 @@ The `init_file` function takes the path to a config file as well as a objects specified by the config file. The following section covers the exact configuration syntax. Examples of both the programatic and configuration files can be found in the -[examples directory](https://github.com/estk/log4rs/tree/master/examples). +[examples directory](https://github.com/estk/log4rs/tree/main/examples). ## Common Fields @@ -32,8 +32,8 @@ i.e. ```yml filters: - - kind: threshold - level: info + - kind: threshold + level: info ``` ### Encoder @@ -51,8 +51,8 @@ i.e. ```yml encoder: - kind: pattern - pattern: "{h({d(%+)(utc)} [{f}:{L}] {l:<6} {M}:{m})}{n}" + kind: pattern + pattern: "{h({d(%+)(utc)} [{f}:{L}] {l:<6} {M}:{m})}{n}" ``` ## Loggers @@ -76,11 +76,11 @@ i.e. ```yml loggers: - my_logger: - level: info - appenders: - - my_appender - additive: true + my_logger: + level: info + appenders: + - my_appender + additive: true ``` ## The Root Logger @@ -93,9 +93,9 @@ field does not apply. ```yml root: - level: info - appenders: - - my_appender + level: info + appenders: + - my_appender ``` ## Appenders @@ -121,9 +121,9 @@ the [encoder](#encoder) documention. ```yml my_console_appender: - kind: console - target: stdout - tty_only: false + kind: console + target: stdout + tty_only: false ``` #### The File Appender @@ -139,9 +139,9 @@ append to the log file if it exists, false will truncate the existing file. ```yml my_file_appender: - kind: file - path: $ENV{PWD}/log/test.log - append: true + kind: file + path: $ENV{PWD}/log/test.log + append: true ``` #### The Rolling File Appender @@ -154,18 +154,18 @@ i.e. ```yml my_rolling_appender: - kind: rolling_file - path: "logs/test.log" - policy: - kind: compound - trigger: - kind: size - limit: 1mb - roller: - kind: fixed_window - base: 1 - count: 5 - pattern: "logs/test.{}.log" + kind: rolling_file + path: "logs/test.log" + policy: + kind: compound + trigger: + kind: size + limit: 1mb + roller: + kind: fixed_window + base: 1 + count: 5 + pattern: "logs/test.{}.log" ``` The new component is the _policy_ field. A policy must have `kind` like most @@ -186,8 +186,8 @@ i.e. ```yml trigger: - kind: size - limit: 10 mb + kind: size + limit: 10 mb ``` The _roller_ field supports two types: delete, and fixed_window. The delete @@ -214,17 +214,17 @@ i.e. ```yml roller: - kind: fixed_window - base: 1 - count: 5 - pattern: "archive/journey-service.{}.log" + kind: fixed_window + base: 1 + count: 5 + pattern: "archive/journey-service.{}.log" ``` or ```yml roller: - kind: delete + kind: delete ``` ## Refresh Rate diff --git a/examples/sample_config.yml b/examples/sample_config.yml index 85e31936..4a0d69cd 100644 --- a/examples/sample_config.yml +++ b/examples/sample_config.yml @@ -1,12 +1,12 @@ appenders: - stdout: - kind: console - encoder: - pattern: "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}" - filters: - - kind: threshold - level: info + stdout: + kind: console + encoder: + pattern: "{d(%+)(utc)} [{f}:{L}] {h({l})} {M}:{m}{n}" + filters: + - kind: threshold + level: info root: - level: info - appenders: - - stdout + level: info + appenders: + - stdout diff --git a/src/append/rolling_file/mod.rs b/src/append/rolling_file/mod.rs index 6f1f5072..e14e4ad8 100644 --- a/src/append/rolling_file/mod.rs +++ b/src/append/rolling_file/mod.rs @@ -366,28 +366,28 @@ mod test { let config = format!( " appenders: - foo: - kind: rolling_file - path: {0}/foo.log - policy: - trigger: - kind: size - limit: 1024 - roller: - kind: delete - bar: - kind: rolling_file - path: {0}/foo.log - policy: - kind: compound - trigger: - kind: size - limit: 5 mb - roller: - kind: fixed_window - pattern: '{0}/foo.log.{{}}' - base: 1 - count: 5 + foo: + kind: rolling_file + path: {0}/foo.log + policy: + trigger: + kind: size + limit: 1024 + roller: + kind: delete + bar: + kind: rolling_file + path: {0}/foo.log + policy: + kind: compound + trigger: + kind: size + limit: 5 mb + roller: + kind: fixed_window + pattern: '{0}/foo.log.{{}}' + base: 1 + count: 5 ", dir.path().display() ); diff --git a/src/append/rolling_file/policy/compound/roll/fixed_window.rs b/src/append/rolling_file/policy/compound/roll/fixed_window.rs index 43611617..340ddd05 100644 --- a/src/append/rolling_file/policy/compound/roll/fixed_window.rs +++ b/src/append/rolling_file/policy/compound/roll/fixed_window.rs @@ -12,8 +12,7 @@ use std::{ path::{Path, PathBuf}, }; -use crate::append::env_util::expand_env_vars; -use crate::append::rolling_file::policy::compound::roll::Roll; +use crate::append::{env_util::expand_env_vars, rolling_file::policy::compound::roll::Roll}; #[cfg(feature = "config_parsing")] use crate::config::{Deserialize, Deserializers}; diff --git a/src/config/raw.rs b/src/config/raw.rs index f04350f4..77963ed3 100644 --- a/src/config/raw.rs +++ b/src/config/raw.rs @@ -457,6 +457,8 @@ fn logger_additive_default() -> bool { #[cfg(test)] #[allow(unused_imports)] mod test { + use std::fs; + use super::*; #[test] @@ -466,28 +468,28 @@ mod test { refresh_rate: 60 seconds appenders: - console: - kind: console - filters: - - kind: threshold - level: debug - baz: - kind: file - path: /tmp/baz.log - encoder: - pattern: "%m" + console: + kind: console + filters: + - kind: threshold + level: debug + baz: + kind: file + path: /tmp/baz.log + encoder: + pattern: "%m" root: - appenders: - - console - level: info + appenders: + - console + level: info loggers: - foo::bar::baz: - level: warn - appenders: - - baz - additive: false + foo::bar::baz: + level: warn + appenders: + - baz + additive: false "#; let config = ::serde_yaml::from_str::(cfg).unwrap(); let errors = config.appenders_lossy(&Deserializers::new()).1; @@ -500,4 +502,29 @@ loggers: fn empty() { ::serde_yaml::from_str::("{}").unwrap(); } + + #[cfg(windows)] + #[allow(dead_code)] + const LINE_ENDING: &'static str = "\r\n"; + #[cfg(not(windows))] + #[allow(dead_code)] + const LINE_ENDING: &'static str = "\n"; + + #[test] + #[cfg(feature = "yaml_format")] + fn readme_sample_file_is_ok() { + let readme = fs::read_to_string("./README.md").expect("README file exists"); + let sample_file = &readme[readme + .find("log4rs.yaml:") + .expect("Sample file exists and is called log4rs.yaml")..]; + let config_start_string = format!("{}```yaml{}", LINE_ENDING, LINE_ENDING); + let config_end_string = format!("{}```{}", LINE_ENDING, LINE_ENDING); + let config_start = + sample_file.find(&config_start_string).unwrap() + config_start_string.len(); + let config_end = sample_file.find(&config_end_string).unwrap(); + let config_str = sample_file[config_start..config_end].trim(); + + let config = ::serde_yaml::from_str::(config_str); + assert!(config.is_ok()) + } } diff --git a/src/encode/json.rs b/src/encode/json.rs index dc23fbdc..784a8739 100644 --- a/src/encode/json.rs +++ b/src/encode/json.rs @@ -67,8 +67,8 @@ impl JsonEncoder { let thread = thread::current(); let message = Message { time: time.format_with_items(Some(Item::Fixed(Fixed::RFC3339)).into_iter()), - message: record.args(), level: record.level(), + message: record.args(), module_path: record.module_path(), file: record.file(), line: record.line(), @@ -93,6 +93,7 @@ impl Encode for JsonEncoder { struct Message<'a> { #[serde(serialize_with = "ser_display")] time: DelayedFormat>>, + level: Level, #[serde(serialize_with = "ser_display")] message: &'a fmt::Arguments<'a>, #[serde(skip_serializing_if = "Option::is_none")] @@ -101,7 +102,6 @@ struct Message<'a> { file: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] line: Option, - level: Level, target: &'a str, thread: Option<&'a str>, thread_id: usize, @@ -206,15 +206,15 @@ mod test { .unwrap(); let expected = format!( - "{{\"time\":\"{}\",\"message\":\"{}\",\"module_path\":\"{}\",\ - \"file\":\"{}\",\"line\":{},\"level\":\"{}\",\"target\":\"{}\",\ - \"thread\":\"{}\",\"thread_id\":{},\"mdc\":{{\"foo\":\"bar\"}}}}", + "{{\"time\":\"{}\",\"level\":\"{}\",\"message\":\"{}\",\"module_path\":\"{}\",\ + \"file\":\"{}\",\"line\":{},\"target\":\"{}\",\ + \"thread\":\"{}\",\"thread_id\":{},\"mdc\":{{\"foo\":\"bar\"}}}}", time.to_rfc3339(), + level, message, module_path, file, line, - level, target, thread, thread_id::get(), diff --git a/src/encode/pattern/mod.rs b/src/encode/pattern/mod.rs index 9316ecc8..5215f2ec 100644 --- a/src/encode/pattern/mod.rs +++ b/src/encode/pattern/mod.rs @@ -53,6 +53,8 @@ //! the default style for all other levels. //! * `{h(the level is {l})}` - //! the level is ERROR +//! * `D`, `debug` - Outputs its arguments ONLY in debug build. +//! * `R`, `release` - Outputs its arguments ONLY in release build. //! * `l`, `level` - The log level. //! * `L`, `line` - The line that the log message came from, or `???` if not //! provided. @@ -388,7 +390,7 @@ impl<'a> From> for Chunk { return Chunk::Error("expected at most two arguments".to_owned()); } - let format = match formatter.args.get(0) { + let format = match formatter.args.first() { Some(arg) => { let mut format = String::new(); for piece in arg { @@ -411,7 +413,7 @@ impl<'a> From> for Chunk { let timezone = match formatter.args.get(1) { Some(arg) => { - if let Some(arg) = arg.get(0) { + if let Some(arg) = arg.first() { match *arg { Piece::Text("utc") => Timezone::Utc, Piece::Text("local") => Timezone::Local, @@ -449,6 +451,40 @@ impl<'a> From> for Chunk { params: parameters, } } + "D" | "debug" => { + if formatter.args.len() != 1 { + return Chunk::Error("expected exactly one argument".to_owned()); + } + + let chunks = formatter + .args + .pop() + .unwrap() + .into_iter() + .map(From::from) + .collect(); + Chunk::Formatted { + chunk: FormattedChunk::Debug(chunks), + params: parameters, + } + } + "R" | "release" => { + if formatter.args.len() != 1 { + return Chunk::Error("expected exactly one argument".to_owned()); + } + + let chunks = formatter + .args + .pop() + .unwrap() + .into_iter() + .map(From::from) + .collect(); + Chunk::Formatted { + chunk: FormattedChunk::Release(chunks), + params: parameters, + } + } "l" | "level" => no_args(&formatter.args, parameters, FormattedChunk::Level), "m" | "message" => no_args(&formatter.args, parameters, FormattedChunk::Message), "M" | "module" => no_args(&formatter.args, parameters, FormattedChunk::Module), @@ -465,9 +501,9 @@ impl<'a> From> for Chunk { return Chunk::Error("expected at most two arguments".to_owned()); } - let key = match formatter.args.get(0) { + let key = match formatter.args.first() { Some(arg) => { - if let Some(arg) = arg.get(0) { + if let Some(arg) = arg.first() { match arg { Piece::Text(key) => key.to_owned(), Piece::Error(ref e) => return Chunk::Error(e.clone()), @@ -482,7 +518,7 @@ impl<'a> From> for Chunk { let default = match formatter.args.get(1) { Some(arg) => { - if let Some(arg) = arg.get(0) { + if let Some(arg) = arg.first() { match arg { Piece::Text(key) => key.to_owned(), Piece::Error(ref e) => return Chunk::Error(e.clone()), @@ -554,6 +590,8 @@ enum FormattedChunk { Newline, Align(Vec), Highlight(Vec), + Debug(Vec), + Release(Vec), Mdc(String, String), } @@ -609,6 +647,22 @@ impl FormattedChunk { } Ok(()) } + FormattedChunk::Debug(ref chunks) => { + if cfg!(debug_assertions) { + for chunk in chunks { + chunk.encode(w, record)?; + } + } + Ok(()) + } + FormattedChunk::Release(ref chunks) => { + if !cfg!(debug_assertions) { + for chunk in chunks { + chunk.encode(w, record)?; + } + } + Ok(()) + } FormattedChunk::Mdc(ref key, ref default) => { log_mdc::get(key, |v| write!(w, "{}", v.unwrap_or(default))) } @@ -976,4 +1030,37 @@ mod tests { assert_eq!(buf, b"missing value"); } + + #[test] + #[cfg(feature = "simple_writer")] + fn debug_release() { + let debug_pat = "{D({l})}"; + let release_pat = "{R({l})}"; + + let debug_encoder = PatternEncoder::new(debug_pat); + let release_encoder = PatternEncoder::new(release_pat); + let mut debug_buf = vec![]; + let mut release_buf = vec![]; + + debug_encoder + .encode( + &mut SimpleWriter(&mut debug_buf), + &Record::builder().level(Level::Info).build(), + ) + .unwrap(); + release_encoder + .encode( + &mut SimpleWriter(&mut release_buf), + &Record::builder().level(Level::Info).build(), + ) + .unwrap(); + + if cfg!(debug_assertions) { + assert_eq!(debug_buf, b"INFO"); + assert!(release_buf.is_empty()); + } else { + assert_eq!(release_buf, b"INFO"); + assert!(debug_buf.is_empty()); + } + } } diff --git a/src/lib.rs b/src/lib.rs index cbe8a687..ecd7e354 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,7 +172,7 @@ //! # fn main() {} //! ``` //! -//! For more examples see the [examples](https://github.com/estk/log4rs/tree/master/examples). +//! For more examples see the [examples](https://github.com/estk/log4rs/tree/main/examples). //! #![allow(where_clauses_object_safety, clippy::manual_non_exhaustive)] diff --git a/test/log.yml b/test/log.yml index b8c5aee3..f51edcc1 100644 --- a/test/log.yml +++ b/test/log.yml @@ -6,8 +6,8 @@ appenders: encoder: pattern: "{d(%+)(local)} [{t}] {h({l})} {M}:{m}{n}" filters: - - kind: threshold - level: error + - kind: threshold + level: error file: kind: file path: error.log