diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..0875727 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,39 @@ +# This is the clippy workflow, seperate from the main 'Rust' workflow +# +# This will fail if clippy fails (if we encounter any of its "correctness" lints) +# +# Clippy warnings won't fail the build. They may or may not turn into Github warnings. +# +# TODO: Test clippy with different feature combos? +# TODO: Should we fail on clippy warnings? +on: [push, pull_request] +name: Clippy + +env: + CARGO_TERM_COLOR: always + # has a history of occasional bugs (especially on old versions) + # + # the ci is free so we might as well use it ;) + CARGO_INCREMENTAL: 0 + + +jobs: + clippy: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: clippy + - shell: bash + run: | + cargo clippy diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml new file mode 100644 index 0000000..72659f5 --- /dev/null +++ b/.github/workflows/rustfmt.yml @@ -0,0 +1,31 @@ +# The file is the workflow for rustfmt +# +# It runs `cargo fmt --check` +# +# It will fail if there are formatting problems. +on: [push, pull_request] +name: rustfmt + +env: + CARGO_TERM_COLOR: always + +jobs: + rustfmt: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt + - shell: bash + run: | + cargo fmt -- --check diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..40e3474 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,63 @@ +# We use `actions-rs` for most of our actions +# +# This file is for the main tests. clippy & rustfmt are seperate workflows +# +# It is mostly copied from slog-rs/term +on: [push, pull_request] +name: Cargo Test + +env: + CARGO_TERM_COLOR: always + # has a history of occasional bugs (especially on old versions) + # + # the ci is free so we might as well use it ;) + CARGO_INCREMENTAL: 0 + + +# Tested versions: +# 1. stable +# 2. nightly +# 3. Minimum Supported Rust Version (MSRV) + +jobs: + test: + # Only run on PRs if the source branch is on someone else's repo + if: ${{ github.event_name != 'pull_request' || github.repository != github.event.pull_request.head.repo.full_name }} + + runs-on: ubuntu-latest + strategy: + fail-fast: false # Even if one job fails we still want to see the other ones + matrix: + # 1.39 is MSRV. Keep in sync with Cargo.toml + # + # There are no MSRV guarentees for the kv_unstable feature. + # See `Cargo.toml` for more details. + rust: [1.39, stable, nightly] + # NOTE: Features to test must be specified manually. They are applied to all versions seperately. + # + # This has the advantage of being more flexibile and thorough + # This has the disadvantage of being more vebrose + # + # Specific feature combos can be overriden per-version with 'include' and 'exclude' + features: ["", "kv_unstable"] + exclude: + - rust: 1.39 # MSRV (see above) + features: "kv_unstable" + + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + - name: Check + run: | + cargo check --verbose --features "${{ matrix.features }}" + # A failing `cargo check` always fails the build + continue-on-error: false + # NOTE: We only run `cargo test` if version > MSRV. Tests break on old MSRV due to + # a dev-dependency. + - name: Test + run: | + cargo test --verbose --features "${{ matrix.features }}" + if: "${{ matrix.rust == 'stable' || matrix.rust == 'nightly' }}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95139b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/target +/Cargo.lock +/tags +/.cargo +/rusty-tags.vi + +/.idea +*.iml \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index bd66197..0000000 --- a/.travis.yml +++ /dev/null @@ -1,30 +0,0 @@ -language: rust -rust: - - stable - - beta - - nightly - -before_install: - - sudo apt-get update -qq - - sudo apt-get install -qq graphviz - -script: - - make all - - make travistest - - if [ "$TRAVIS_RUST_VERSION" == "nightly" ]; then make bench ; fi - - if [ "$TRAVIS_RUST_VERSION" == "nightly" ]; then cargo build --no-default-features; fi - -env: - global: - - RUST_BACKTRACE=1 - matrix: - - - - RELEASE=true - -notifications: - webhooks: - urls: - - https://webhooks.gitter.im/e/87a331e1a21456b6e2ad - on_success: change # options: [always|never|change] default: always - on_failure: change # options: [always|never|change] default: always - on_start: false # default: false diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f89eb..2976911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,73 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 4.1.1 - 2022-03-22 +### Added + +* Support `feature="log/kv-unstable"` (PR #18) + * Requires `slog/dynamic-keys` + * This is unstable (obviously), so I don't consider it + a change that warants a minor-version bump + +### Changed + +* Switch to github actions (PR #20) + +### Fixed + +* Avoid using log's private API to construct records (PR #21) + * Fixes recent versions of `log` crate (`>=0.4.15`), resolving slog-rs/slog#309 +* Fix formatting & clippy lints (part of switch to GH actions) + +## 4.1.0 - 2020-10-21 +### Changed + +* Require `slog>=2.4` +* Require `log>=0.4.11 + +## Fixed + +* Remove unused dependency `crossbeam = 0.7.1` + +## 4.0.0 - 2018-08-13 +### Changed + +* Update to `log` 0.4 + +## 3.0.2 - 2017-05-29 +### Fixed + +* Documentation example for `init` + +## 3.0.0 - 2017-05-28 +### Changed + +* Update to slog-scope v4 wh default to panicking instead of discarding + messages. Be warned! +* Relicensed under MPL-2.0/MIT/Apache-2.0 + +## 2.0.0-4.0 - 2017-04-29 + +### Changed + +* Updated slog dependency + +## 2.0.0-3.0 - 2017-04-02 +### Changed + +* Updated slog dependency + +## 2.0.0-0.2 +### Fixed + +* Dependencies + +## 2.0.0-0.1 +### Changed + +* Port to slog v2 +* Base on `slog-scope` + ## 1.1.0 ### Changed diff --git a/Cargo.toml b/Cargo.toml index 5ce124d..783438b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,47 @@ [package] name = "slog-stdlog" -version = "1.1.0" +version = "4.1.1" authors = ["Dawid Ciężarkiewicz "] -description = "Standard Rust log crate adapter to slog-rs" +description = "`log` crate adapter for slog-rs" keywords = ["slog", "logging", "json", "log"] -license = "MPL-2.0" -documentation = "https://dpc.github.io/slog-rs/" -homepage = "https://github.com/dpc/slog-rs" -repository = "https://github.com/dpc/slog-rs" +license = "MPL-2.0 OR MIT OR Apache-2.0" +documentation = "https://docs.rs/slog-stdlog" +homepage = "https://github.com/slog-rs/stdlog" +repository = "https://github.com/slog-rs/stdlog" readme = "README.md" +edition = "2018" +# This is our Minimum Supported Rust Version (MSRV) +# +# To change this, you must also: +# 1. Update the README file +# 2. Update the github actions file +# 3. Have an appropriate change to the minor version +# +# See the Cargo.toml in the primary slog repo for more details +# +# The first version of Cargo that supports this field was in Rust 1.56.0. +# In older releases, the field will be ignored, and Cargo will display a warning. +# +# DISCLAIMER: +# The log/kv_unstable feature requires a recent (stable) compiler. +# It will not compile with this claimed MSRV and requires a recent (stable) compiler. +rust-version = "1.38" [lib] path = "lib.rs" +[features] +default = [] +kv_unstable = ["log/kv_unstable_std", "slog/dynamic-keys"] + [dependencies] -slog = "1.1" -slog-term = "1.3" -log = "0.3.6" -lazy_static = "0.2.1" -crossbeam = "0.2.9" +slog = "2.4" +slog-scope = "4" +log = { version = "0.4.11", features = ["std"] } + +[dev-dependencies] +slog-term = "2" +slog-async = "2" +# Used for tests +fragile = "2" + diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache 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 [yyyy] [name of copyright owner] + +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. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..39d4bdb --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2014 The Rust Project Developers + +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. diff --git a/README.md b/README.md index c411e63..78d825b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -## slog-stdlog - Standard Rust log crate adapter for [slog-rs] -

Travis CI Build Status @@ -12,6 +10,16 @@ slog-rs Gitter Chat + + + Minimum Rust Version 1.38 +

+## slog-stdlog - `log` crate adapter for [slog-rs] + +See [slog-stdlog documentation](https://docs.rs/slog-stdlog) for details. + +For more information, help, to report issues etc. see [slog-rs][slog-rs]. + [slog-rs]: //github.com/slog-rs/slog diff --git a/kv.rs b/kv.rs new file mode 100644 index 0000000..9854a9e --- /dev/null +++ b/kv.rs @@ -0,0 +1,82 @@ +use log::kv::value::Error as ValueError; +use slog::{Record, Serializer}; + +struct Visitor<'s> { + serializer: &'s mut dyn Serializer, +} + +impl<'s> Visitor<'s> { + pub fn new(serializer: &'s mut dyn Serializer) -> Self { + Self { serializer } + } +} + +pub(crate) struct SourceKV<'kvs>(pub &'kvs dyn log::kv::source::Source); + +struct KeyVisit<'s> { + serializer: &'s mut dyn Serializer, + key: &'s str, +} + +impl<'kvs> log::kv::Visitor<'kvs> for Visitor<'kvs> { + fn visit_pair( + &mut self, + key: log::kv::Key<'kvs>, + val: log::kv::Value<'kvs>, + ) -> Result<(), log::kv::Error> { + val.visit(KeyVisit { + serializer: self.serializer, + key: key.as_str(), + }) + } +} + +macro_rules! visit_to_emit { + ($t:ty : $vname:ident -> $ename:ident) => { + fn $vname(&mut self, value: $t) -> Result<(), ValueError> { + self.serializer + .$ename(self.key.to_string().into(), value) + .map_err(to_value_err) + } + }; +} + +impl<'s> log::kv::value::Visit<'s> for KeyVisit<'s> { + fn visit_any(&mut self, value: log::kv::Value<'_>) -> Result<(), ValueError> { + let key = self.key.to_string().into(); + self.serializer + .emit_arguments(key, &format_args!("{}", value)) + .map_err(to_value_err) + } + + visit_to_emit!(u64: visit_u64 -> emit_u64); + visit_to_emit!(i64: visit_i64 -> emit_i64); + visit_to_emit!(u128: visit_u128 -> emit_u128); + visit_to_emit!(i128: visit_i128 -> emit_i128); + visit_to_emit!(f64: visit_f64 -> emit_f64); + visit_to_emit!(bool: visit_bool -> emit_bool); + visit_to_emit!(&str: visit_str -> emit_str); + visit_to_emit!(char: visit_char -> emit_char); + visit_to_emit!(&(dyn std::error::Error + 'static): visit_error -> emit_error); +} + +impl slog::KV for SourceKV<'_> { + fn serialize(&self, _record: &Record, serializer: &mut dyn Serializer) -> slog::Result { + // Unfortunately, there isn't a way for use to pass the original error through. + self.0 + .visit(&mut Visitor::new(serializer)) + .map_err(|_| slog::Error::Other) + } +} + +fn to_value_err(err: slog::Error) -> ValueError { + use slog::Error::*; + + match err { + Io(e) => e.into(), + Fmt(e) => e.into(), + Other => ValueError::boxed(err), + } +} + +// TODO: support going the other way diff --git a/lib.rs b/lib.rs index 7cff4c3..0404371 100644 --- a/lib.rs +++ b/lib.rs @@ -1,254 +1,194 @@ -//! Standard Rust log crate adapter to slog-rs +//! `log` crate adapter for slog-rs //! -//! This crate allows using `slog` features with code -//! using legacy `log` statements. +//! This crate provides two way compatibility with Rust standard `log` crate. //! -//! `log` crate expects a global logger to be registered -//! (popular one is `env_logger`) as a handler for all -//! `info!(...)` and similar. +//! ### `log` -> `slog` //! -//! `slog-stdlog` will register itself as `log` global handler and forward all -//! legacy logging statements to `slog`'s `Logger`. That means existing logging -//! `debug!` (even in dependencies crates) work and utilize `slog` composable -//! drains. +//! After calling `init()` `slog-stdlog` will take a role of `log` crate +//! back-end, forwarding all the `log` logging to `slog_scope::logger()`. +//! In other words, any `log` crate logging statement will behave like it was `slog` +//! logging statement executed with logger returned by `slog_scope::logger()`. //! -//! See `init()` documentation for minimal working example. +//! See documentation of `slog-scope` for more information about logging scopes. //! -//! Note: While `slog-scope` provides a similiar functionality, the `slog-scope` and `slog-stdlog` -//! keep track of distinct logging scopes. +//! See [`init` documentation](fn.init.html) for an example. +//! +//! ### `slog` -> `log` +//! +//! `StdLog` is `slog::Drain` that will pass all `Record`s passing through it to +//! `log` crate just like they were crated with `log` crate logging macros in +//! the first place. +//! +//! ## `slog-scope` +//! +//! Since `log` does not have any form of context, and does not support `Logger` +//! `slog-stdlog` relies on "logging scopes" to establish it. +//! +//! You must set up logging context for `log` -> `slog` via `slog_scope::scope` +//! or `slog_scope::set_global_logger`. Setting a global logger upfront via +//! `slog_scope::set_global_logger` is highly recommended. +//! +//! Note: Since `slog-stdlog` v2, unlike previous releases, `slog-stdlog` uses +//! logging scopes provided by `slog-scope` crate instead of it's own. +//! +//! Refer to `slog-scope` crate documentation for more information. +//! +//! ### Warning +//! +//! Be careful when using both methods at the same time, as a loop can be easily +//! created: `log` -> `slog` -> `log` -> ... //! //! ## Compile-time log level filtering //! -//! For filtering `debug!` and other `log` statements at compile-time, configure the features on -//! the `log` crate in your `Cargo.toml`: +//! For filtering `debug!` and other `log` statements at compile-time, configure +//! the features on the `log` crate in your `Cargo.toml`: //! //! ```norust //! log = { version = "*", features = ["max_level_trace", "release_max_level_warn"] } //! ``` #![warn(missing_docs)] -#[macro_use] -extern crate slog; -extern crate slog_term; extern crate log; -#[macro_use] -extern crate lazy_static; -extern crate crossbeam; - -use slog::{DrainExt, ser}; - -use log::LogMetadata; -use std::sync::Arc; -use std::cell::RefCell; -use std::{io, fmt}; -use std::io::Write; - -use slog::Level; -use crossbeam::sync::ArcCell; - -thread_local! { - static TL_SCOPES: RefCell> = RefCell::new(Vec::with_capacity(8)) -} -lazy_static! { - static ref GLOBAL_LOGGER : ArcCell = ArcCell::new( - Arc::new( - slog::Logger::root(slog::Discard, o!()) - ) - ); -} +#[cfg(feature = "kv_unstable")] +mod kv; -fn set_global_logger(l: slog::Logger) { - let _ = GLOBAL_LOGGER.set(Arc::new(l)); -} +use slog::{b, Level, KV}; +use std::{fmt, io}; struct Logger; - -fn log_to_slog_level(level: log::LogLevel) -> Level { +fn log_to_slog_level(level: log::Level) -> Level { match level { - log::LogLevel::Trace => Level::Trace, - log::LogLevel::Debug => Level::Debug, - log::LogLevel::Info => Level::Info, - log::LogLevel::Warn => Level::Warning, - log::LogLevel::Error => Level::Error, + log::Level::Trace => Level::Trace, + log::Level::Debug => Level::Debug, + log::Level::Info => Level::Info, + log::Level::Warn => Level::Warning, + log::Level::Error => Level::Error, + } +} + +fn record_as_location(r: &log::Record) -> slog::RecordLocation { + let module = r.module_path_static().unwrap_or(""); + let file = r.file_static().unwrap_or(""); + let line = r.line().unwrap_or_default(); + + slog::RecordLocation { + file, + line, + column: 0, + function: "", + module, } } impl log::Log for Logger { - fn enabled(&self, _: &LogMetadata) -> bool { + fn enabled(&self, _: &log::Metadata) -> bool { true } - fn log(&self, r: &log::LogRecord) { + fn log(&self, r: &log::Record) { let level = log_to_slog_level(r.metadata().level()); let args = r.args(); let target = r.target(); - let module = r.location().__module_path; - let file = r.location().__file; - let line = r.location().line(); - with_current_logger(|l| { - let s = slog::RecordStatic { - level: level, - file: file, - line: line, - column: 0, - function: "", - module: module, - target: target, - }; - l.log(&slog::Record::new(&s, *args, &[])) - }) + let location = &record_as_location(r); + let s = slog::RecordStatic { + location, + level, + tag: target, + }; + #[cfg(feature = "kv_unstable")] + { + let key_values = kv::SourceKV(r.key_values()); + slog_scope::with_logger(|logger| { + logger.log(&slog::Record::new(&s, args, b!(key_values))) + }) + } + #[cfg(not(feature = "kv_unstable"))] + slog_scope::with_logger(|logger| logger.log(&slog::Record::new(&s, args, b!()))) } + + fn flush(&self) {} } -/// Set a `slog::Logger` as a global `log` create handler +/// Register `slog-stdlog` as `log` backend. /// -/// This will forward all `log` records to `slog` logger. +/// This will pass all logging statements crated with `log` +/// crate to current `slog-scope::logger()`. /// /// ``` -/// // only use `o` macro from `slog` crate -/// #[macro_use(o)] -/// extern crate slog; /// #[macro_use] /// extern crate log; +/// #[macro_use(slog_o, slog_kv)] +/// extern crate slog; /// extern crate slog_stdlog; +/// extern crate slog_scope; +/// extern crate slog_term; +/// extern crate slog_async; +/// +/// use slog::Drain; /// /// fn main() { -/// let root = slog::Logger::root( -/// slog::Discard, -/// o!("build-id" => "8dfljdf"), -/// ); -/// slog_stdlog::set_logger(root).unwrap(); +/// let decorator = slog_term::TermDecorator::new().build(); +/// let drain = slog_term::FullFormat::new(decorator).build().fuse(); +/// let drain = slog_async::Async::new(drain).build().fuse(); +/// let logger = slog::Logger::root(drain, slog_o!("version" => env!("CARGO_PKG_VERSION"))); +/// +/// let _scope_guard = slog_scope::set_global_logger(logger); +/// let _log_guard = slog_stdlog::init().unwrap(); /// // Note: this `info!(...)` macro comes from `log` crate /// info!("standard logging redirected to slog"); /// } /// ``` -pub fn set_logger(logger: slog::Logger) -> Result<(), log::SetLoggerError> { - log::set_logger(|max_log_level| { - max_log_level.set(log::LogLevelFilter::max()); - set_global_logger(logger); - Box::new(Logger) - }) -} - -/// Set a `slog::Logger` as a global `log` create handler -/// -/// This will forward `log` records that satisfy `log_level_filter` to `slog` logger. -pub fn set_logger_level(logger: slog::Logger, - log_level_filter: log::LogLevelFilter) - -> Result<(), log::SetLoggerError> { - log::set_logger(|max_log_level| { - max_log_level.set(log_level_filter); - set_global_logger(logger); - Box::new(Logger) - }) +pub fn init() -> Result<(), log::SetLoggerError> { + init_with_level(log::Level::max()) } -/// Minimal initialization with default drain +/// Register `slog-stdlog` as `log` backend. +/// Pass a log::Level to do the log filter explicitly. /// -/// The exact default drain is unspecified and will -/// change in future versions! Use `set_logger` instead -/// to build customized drain. +/// This will pass all logging statements crated with `log` +/// crate to current `slog-scope::logger()`. /// /// ``` /// #[macro_use] /// extern crate log; +/// #[macro_use(slog_o, slog_kv)] +/// extern crate slog; /// extern crate slog_stdlog; +/// extern crate slog_scope; +/// extern crate slog_term; +/// extern crate slog_async; +/// +/// use slog::Drain; /// /// fn main() { -/// slog_stdlog::init().unwrap(); +/// let decorator = slog_term::TermDecorator::new().build(); +/// let drain = slog_term::FullFormat::new(decorator).build().fuse(); +/// let drain = slog_async::Async::new(drain).build().fuse(); +/// let logger = slog::Logger::root(drain, slog_o!("version" => env!("CARGO_PKG_VERSION"))); +/// +/// let _scope_guard = slog_scope::set_global_logger(logger); +/// let _log_guard = slog_stdlog::init_with_level(log::Level::Error).unwrap(); /// // Note: this `info!(...)` macro comes from `log` crate /// info!("standard logging redirected to slog"); +/// error!("standard logging redirected to slog"); /// } /// ``` -pub fn init() -> Result<(), log::SetLoggerError> { - let drain = slog::level_filter(Level::Info, slog_term::streamer().compact().build()); - set_logger(slog::Logger::root(drain.fuse(), o!())) -} - -struct ScopeGuard; - - -impl ScopeGuard { - fn new(logger: slog::Logger) -> Self { - TL_SCOPES.with(|s| { - s.borrow_mut().push(logger); - }); - - ScopeGuard - } -} +pub fn init_with_level(level: log::Level) -> Result<(), log::SetLoggerError> { + log::set_boxed_logger(Box::new(Logger))?; + log::set_max_level(level.to_level_filter()); -impl Drop for ScopeGuard { - fn drop(&mut self) { - TL_SCOPES.with(|s| { - s.borrow_mut().pop().expect("TL_SCOPES should contain a logger"); - }) - } -} - - -/// Access the currently active logger -/// -/// The reference logger will be either: -/// * global logger, or -/// * currently active scope logger -/// -/// **Warning**: Calling `scope` inside `f` -/// will result in a panic. -pub fn with_current_logger(f: F) -> R - where F: FnOnce(&slog::Logger) -> R -{ - TL_SCOPES.with(|s| { - let s = s.borrow(); - if s.is_empty() { - f(&GLOBAL_LOGGER.get()) - } else { - f(&s[s.len() - 1]) - } - }) -} - -/// Access the `Logger` for the current logging scope -pub fn logger() -> slog::Logger { - TL_SCOPES.with(|s| { - let s = s.borrow(); - if s.is_empty() { - (*GLOBAL_LOGGER.get()).clone() - } else { - s[s.len() - 1].clone() - } - }) -} - -/// Execute code in a logging scope -/// -/// Logging scopes allow using different logger for legacy logging -/// statements in part of the code. -/// -/// Logging scopes can be nested and are panic safe. -/// -/// `logger` is the `Logger` to use during the duration of `f`. -/// `with_current_logger` can be used to build it as a child of currently active -/// logger. -/// -/// `f` is a code to be executed in the logging scope. -/// -/// Note: Thread scopes are thread-local. Each newly spawned thread starts -/// with a global logger, as a current logger. -pub fn scope(logger: slog::Logger, f: SF) -> R - where SF: FnOnce() -> R -{ - let _guard = ScopeGuard::new(logger); - f() + Ok(()) } /// Drain logging `Record`s into `log` crate /// -/// Using `StdLog` is effectively the same as using `log::info!(...)` and -/// other standard logging statements. +/// Any `Record` passing through this `Drain` will be forwarded +/// to `log` crate, just like it was created with `log` crate macros +/// in the first place. The message and key-value pairs will be formated +/// to be one string. /// /// Caution needs to be taken to prevent circular loop where `Logger` /// installed via `slog-stdlog::set_logger` would log things to a `StdLog` @@ -258,100 +198,97 @@ pub struct StdLog; struct LazyLogString<'a> { info: &'a slog::Record<'a>, - logger_values : &'a slog::OwnedKeyValueList + logger_values: &'a slog::OwnedKVList, } impl<'a> LazyLogString<'a> { - - fn new(info : &'a slog::Record, logger_values : &'a slog::OwnedKeyValueList) -> Self { - + fn new(info: &'a slog::Record, logger_values: &'a slog::OwnedKVList) -> Self { LazyLogString { - info: info, - logger_values: logger_values, + info, + logger_values, } } } impl<'a> fmt::Display for LazyLogString<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - - try!(write!(f, "{}", self.info.msg())); + write!(f, "{}", self.info.msg())?; let io = io::Cursor::new(Vec::new()); - let mut ser = KSV::new(io, ": ".into()); - - let res = { - || -> io::Result<()> { + let mut ser = Ksv::new(io); - for (ref k, ref v) in self.logger_values.iter() { - try!(ser.io().write_all(", ".as_bytes())); - try!(v.serialize(self.info, k, &mut ser)); - } - - for &(ref k, ref v) in self.info.values().iter() { - try!(ser.io().write_all(", ".as_bytes())); - try!(v.serialize(self.info, k, &mut ser)); - } - Ok(()) - } - }().map_err(|_| fmt::Error); - - try!(res); + self.logger_values + .serialize(self.info, &mut ser) + .map_err(|_| fmt::Error)?; + self.info + .kv() + .serialize(self.info, &mut ser) + .map_err(|_| fmt::Error)?; let values = ser.into_inner().into_inner(); - write!(f, "{}", String::from_utf8_lossy(&values)) - } } impl slog::Drain for StdLog { - type Error = io::Error; - fn log(&self, info: &slog::Record, logger_values : &slog::OwnedKeyValueList) -> io::Result<()> { + type Ok = (); + type Err = io::Error; + fn log(&self, info: &slog::Record, logger_values: &slog::OwnedKVList) -> io::Result<()> { let level = match info.level() { - slog::Level::Critical => log::LogLevel::Error, - slog::Level::Error => log::LogLevel::Error, - slog::Level::Warning => log::LogLevel::Warn, - slog::Level::Info => log::LogLevel::Info, - slog::Level::Debug => log::LogLevel::Debug, - slog::Level::Trace => log::LogLevel::Trace, + slog::Level::Critical | slog::Level::Error => log::Level::Error, + slog::Level::Warning => log::Level::Warn, + slog::Level::Info => log::Level::Info, + slog::Level::Debug => log::Level::Debug, + slog::Level::Trace => log::Level::Trace, }; - let target = info.target(); - - let location = log::LogLocation { - __module_path: info.module(), - __file: info.file(), - __line: info.line(), - }; + let mut target = info.tag(); + if target.is_empty() { + target = info.module(); + } let lazy = LazyLogString::new(info, logger_values); - // Please don't yell at me for this! :D - // https://github.com/rust-lang-nursery/log/issues/95 - log::__log(level, target, &location, format_args!("{}", lazy)); + /* + * TODO: Support `log` crate key_values here. + * + * This requires the log/kv_unstable feature here. + * + * Not supporting this feature is backwards compatible + * and it shouldn't break anything (because we've never had), + * but is undesirable from a feature-completeness point of view. + * + * However, this is most likely not as powerful as slog's own + * notion of key/value pairs, so I would humbly suggest using `slog` + * directly if this feature is important to you ;) + * + * This avoids using the private log::__private_api_log api function, + * which is just a thin wrapper around a `RecordBuilder`. + */ + log::logger().log( + &log::Record::builder() + .args(format_args!("{}", lazy)) + .level(level) + .target(target) + .module_path_static(Some(info.module())) + .file_static(Some(info.file())) + .line(Some(info.line())) + .build(), + ); Ok(()) } } /// Key-Separator-Value serializer -struct KSV { - separator: String, +struct Ksv { io: W, } -impl KSV { - fn new(io: W, separator: String) -> Self { - KSV { - io: io, - separator: separator, - } - } - - fn io(&mut self) -> &mut W { - &mut self.io +impl Ksv { + fn new(io: W) -> Self { + Ksv { io } } fn into_inner(self) -> W { @@ -359,81 +296,9 @@ impl KSV { } } -impl ser::Serializer for KSV { - fn emit_none(&mut self, key: &str) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, "None")); - Ok(()) - } - fn emit_unit(&mut self, key: &str) -> ser::Result { - try!(write!(self.io, "{}", key)); - Ok(()) - } - - fn emit_bool(&mut self, key: &str, val: bool) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - - fn emit_char(&mut self, key: &str, val: char) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - - fn emit_usize(&mut self, key: &str, val: usize) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_isize(&mut self, key: &str, val: isize) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - - fn emit_u8(&mut self, key: &str, val: u8) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_i8(&mut self, key: &str, val: i8) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_u16(&mut self, key: &str, val: u16) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_i16(&mut self, key: &str, val: i16) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_u32(&mut self, key: &str, val: u32) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_i32(&mut self, key: &str, val: i32) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_f32(&mut self, key: &str, val: f32) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_u64(&mut self, key: &str, val: u64) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_i64(&mut self, key: &str, val: i64) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_f64(&mut self, key: &str, val: f64) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_str(&mut self, key: &str, val: &str) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); - Ok(()) - } - fn emit_arguments(&mut self, key: &str, val: &fmt::Arguments) -> ser::Result { - try!(write!(self.io, "{}{}{}", key, self.separator, val)); +impl slog::Serializer for Ksv { + fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result { + write!(self.io, ", {}: {}", key, val)?; Ok(()) } } diff --git a/tests/log2slog.rs.disabled b/tests/log2slog.rs.disabled new file mode 100644 index 0000000..8cf5044 --- /dev/null +++ b/tests/log2slog.rs.disabled @@ -0,0 +1,61 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::fmt; +use std::collections::HashMap; + +struct ExpectedValue { + value: String, + seen: AtomicBool, +} +struct SlogExpectSerialize { + expected: HashMap, +} +impl slog::Serializer for SlogExpectSerialize { + fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result { + if let Some(expected_value) = self.expected.get(&key) { + let was_seen = expected_value.seen.compare_exchange( + false, true, + Ordering::SeqCst, Ordering::SeqCst + ).unwrap_or_else(std::convert::identity); + assert!(!was_seen, "Already saw {key:?}"); + assert_eq!(expected_value.value, fmt::format(*val)); + Ok(()) + } else { + panic!("Got unexpected key {key:?} = {val}"); + } + } +} +impl SlogExpectSerialize { + fn check_finished(&self) { + for (key, value) in self.expected { + if !value.seen.load(Ordering::SeqCst) { + panic!("Did not see value for key = {key:?}") + } + } + } +} +impl fmt::Debug for SlogExpectSerialize { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_map().entries(self.expected.iter().map(|(ref key, ref value)| { + let key: &str = key.as_ref(); + (key, &*value.value) + })).finish() + } +} + +struct SlogAssertExpected { + _fo: std::convert::Infallible, +} +impl slog::Drain for SlogAssertExpected { + type Ok = (); + type Err = slog::Error; + fn log(&self, record: &slog::Record<'_>, values: &slog::OwnedKVList) -> Result<(), slog::Error> { + todo!() + } +} +impl SlogAssertExpected { + fn assert_finished(&self) { + todo!() + } +} + +compile_error!("Not Yet Implemented"); diff --git a/tests/slog2log.rs b/tests/slog2log.rs new file mode 100644 index 0000000..722a977 --- /dev/null +++ b/tests/slog2log.rs @@ -0,0 +1,98 @@ +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Mutex; + +use fragile::Fragile; +use log::RecordBuilder; +use slog::Drain; + +struct StdLogAssertExpected<'a> { + expected: Mutex>>>, + current_index: AtomicUsize, +} +impl log::Log for StdLogAssertExpected<'_> { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + fn log(&self, actual: &log::Record<'_>) { + let expected = { + let expected = self.expected.lock().unwrap(); + // NOTE: I think load fence is implied by the lock + let old_index = self.current_index.load(Ordering::Relaxed); + match expected.get(old_index) { + Some(e) => { + assert_eq!( + old_index, + // Do we need a store fence, or is that implied as well? + self.current_index.fetch_add(1, Ordering::Acquire) + ); + e.get().clone() + } + None => panic!("Expected no more log records. but got {:?}", actual), + } + }; + assert_eq!(expected.metadata(), actual.metadata()); + assert_eq!(expected.args().to_string(), actual.args().to_string()); + assert_eq!(expected.level(), actual.level()); + assert_eq!(expected.target(), actual.target()); + assert_eq!(expected.module_path(), actual.module_path()); + assert_eq!(expected.file(), actual.file()); + // NOTE: Intentionally ignored `line` + if cfg!(feature = "kv_unstable") { + todo!("Structure not currently used. See PR #26"); + } + } + fn flush(&self) {} +} +impl StdLogAssertExpected<'_> { + fn assert_finished(&self) { + let expected = self.expected.lock().unwrap(); + // load fence implied (I think) + let remaining = expected.len() - self.current_index.load(Ordering::Relaxed); + assert!( + remaining == 0, + "Expected {remaining} more values (first={first:?}) {maybeLast}", + first = expected.first().unwrap(), + maybeLast = if remaining >= 2 { + format!("(last={:?})", expected.last().unwrap()) + } else { + String::new() + } + ); + } +} + +macro_rules! record { + ($level:ident, $fmt:expr) => { + RecordBuilder::new() + .args(format_args!($fmt)) + .level(log::Level::$level) + .file(Some(file!())) + .module_path(Some(module_path!())) + .target(module_path!()) + .build() + }; +} + +#[test] +#[cfg_attr( + feature = "kv_unstable", + ignore = "TODO: Support kv-unstable feature (See PR #26)" +)] +fn test_slog2log() { + let expected = vec![ + record!(Info, "Hello World!"), + record!(Debug, "Hello World, I am 100 years old"), + ] + .into_iter() + .map(Fragile::new) + .collect::>>(); + let std_logger = Box::leak(Box::new(StdLogAssertExpected { + expected: Mutex::new(expected), + current_index: 0.into(), + })); + log::set_logger(std_logger).unwrap(); + let sl = slog::Logger::root(slog_stdlog::StdLog.fuse(), slog::o!()); + slog::info!(sl, "Hello {}", "World!"); + slog::debug!(sl, "Hello {}, I am {} years old", "World", 100); + std_logger.assert_finished(); +}