tracing-appender-0.2.2/.cargo_vcs_info.json0000644000000001560000000000100142620ustar { "git": { "sha1": "b37d0d35c7a2292f10da07fb2358571cc4e28686" }, "path_in_vcs": "tracing-appender" }tracing-appender-0.2.2/CHANGELOG.md000064400000000000000000000060710072674642500147150ustar 00000000000000# 0.2.2 (March 17, 2022) This release fixes a bug in `RollingFileAppender` that could result in a failure to rotate the log file, or in panics in debug mode. ### Fixed - **rolling**: Fixed a panic that prohibited rolling files over. ([#1989]) [#1989]: https://github.com/tokio-rs/tracing/pull/1989 # 0.2.1 (February 28, 2022) This release adds an implementation of the `MakeWriter` trait for `RollingFileAppender`, allowing it to be used without wrapping in a `NonBlocking` writer. This release increases the minimum supported Rust version to 1.53.0. ### Added - **rolling**: Added `MakeWriter` implementation for `RollingFileAppender` ([#1760]) ### Changed - Updated minimum supported Rust version (MSRV) to 1.53.0 ([#1851]) - `parking_lot`: updated to v0.12 ([#1878]) ### Fixed - Fixed several documentation typos and issues ([#1780], [d868054], [#1943]) [#1760]: https://github.com/tokio-rs/tracing/pull/1760 [#1851]: https://github.com/tokio-rs/tracing/pull/1851 [#1878]: https://github.com/tokio-rs/tracing/pull/1878 [#1943]: https://github.com/tokio-rs/tracing/pull/1943 [d868054]: https://github.com/tokio-rs/tracing/commit/d8680547b509978c7113c8f7e19e9b00c789c698 # 0.2.0 (October 22, 2021) This breaking change release adds support for the new v0.3.x series of `tracing-subscriber`. In addition, it resolves the security advisory for the `chrono` crate, [RUSTSEC-2020-0159]. This release increases the minimum supported Rust version to 1.51.0. ### Breaking Changes - Updated `tracing-subscriber` to v0.3.x ([#1677]) - Changed `NonBlocking::error_counter` to return an `ErrorCounter` type, rather than an `Arc` ([#1675]) ### Changed - Updated `tracing-subscriber` to v0.3.x ([#1677]) ### Fixed - **non-blocking**: Fixed compilation on 32-bit targets ([#1675]) - **rolling**: Replaced `chrono` dependency with `time` to resolve [RUSTSEC-2020-0159] ([#1652]) - **rolling**: Fixed an issue where `RollingFileAppender` would fail to print errors that occurred while flushing a previous logfile ([#1604]) Thanks to new contributors @dzvon and @zvkemp for contributing to this release! [RUSTSEC-2020-0159]: https://rustsec.org/advisories/RUSTSEC-2020-0159.html [#1677]: https://github.com/tokio-rs/tracing/pull/1677 [#1675]: https://github.com/tokio-rs/tracing/pull/1675 [#1652]: https://github.com/tokio-rs/tracing/pull/1675 [#1604]: https://github.com/tokio-rs/tracing/pull/1604 # 0.1.2 (December 28, 2020) ### Changed - **non_blocking**: Updated `crossbeam-channel` dependency to 0.5 (#1031) ### Fixed - **non_blocking**: Fixed a race condition when logging on shutdown (#1125) - Several documentation improvements (#1109, #1110, #941, #953) # 0.1.1 (July 20, 2020) ### Added - **rolling**: `minutely` rotation schedule to rotate the log file once every minute (#748) ### Fixed - Fixed broken links in docs (#718) - `tracing-appender` now only enables the necessary `tracing-subscriber`'s feature flags, rather than all of them (#779) Thanks to new contributors @ericjheinz and @sourcefrog for contributing to this release! # 0.1.0 (May 5, 2020) - Initial releasetracing-appender-0.2.2/Cargo.toml0000644000000027120000000000100122600ustar # 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 = "2018" rust-version = "1.53.0" name = "tracing-appender" version = "0.2.2" authors = ["Zeki Sherif ", "Tokio Contributors "] description = "Provides utilities for file appenders and making non-blocking writers.\n" homepage = "https://tokio.rs" readme = "README.md" keywords = ["logging", "tracing", "file-appender", "non-blocking-writer"] categories = ["development-tools::debugging", "asynchronous"] license = "MIT" repository = "https://github.com/tokio-rs/tracing" [dependencies.crossbeam-channel] version = "0.5.0" [dependencies.parking_lot] version = "0.12.0" optional = true [dependencies.time] version = "0.3" features = ["formatting"] default-features = false [dependencies.tracing-subscriber] version = "0.3" features = ["fmt", "std"] default-features = false [dev-dependencies.tempfile] version = "3" [dev-dependencies.time] version = "0.3" features = ["formatting", "parsing"] default-features = false [dev-dependencies.tracing] version = "0.1" tracing-appender-0.2.2/Cargo.toml.orig000064400000000000000000000020130072674642500157630ustar 00000000000000[package] name = "tracing-appender" version = "0.2.2" authors = [ "Zeki Sherif ", "Tokio Contributors " ] license = "MIT" readme = "README.md" repository = "https://github.com/tokio-rs/tracing" homepage = "https://tokio.rs" description = """ Provides utilities for file appenders and making non-blocking writers. """ categories = [ "development-tools::debugging", "asynchronous", ] keywords = ["logging", "tracing", "file-appender", "non-blocking-writer"] edition = "2018" rust-version = "1.53.0" [dependencies] crossbeam-channel = "0.5.0" time = { version = "0.3", default-features = false, features = ["formatting"] } parking_lot = { optional = true, version = "0.12.0" } [dependencies.tracing-subscriber] path = "../tracing-subscriber" version = "0.3" default-features = false features = ["fmt", "std"] [dev-dependencies] tracing = { path = "../tracing", version = "0.1" } time = { version = "0.3", default-features = false, features = ["formatting", "parsing"] } tempfile = "3" tracing-appender-0.2.2/LICENSE000064400000000000000000000020460072674642500141070ustar 00000000000000Copyright (c) 2019 Tokio Contributors 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. tracing-appender-0.2.2/README.md000064400000000000000000000142360072674642500143650ustar 00000000000000![Tracing — Structured, application-level diagnostics][splash] [splash]: https://raw.githubusercontent.com/tokio-rs/tracing/master/assets/splash.svg # tracing-appender Writers for logging events and spans [![Crates.io][crates-badge]][crates-url] [![Documentation][docs-badge]][docs-url] [![Documentation (master)][docs-master-badge]][docs-master-url] [![MIT licensed][mit-badge]][mit-url] [![Build Status][actions-badge]][actions-url] [![Discord chat][discord-badge]][discord-url] [Documentation][docs-url] | [Chat][discord-url] [crates-badge]: https://img.shields.io/crates/v/tracing-appender.svg [crates-url]: https://crates.io/crates/tracing-appender/0.2.2 [docs-badge]: https://docs.rs/tracing-appender/badge.svg [docs-url]: https://docs.rs/tracing-appender/0.2.2 [docs-master-badge]: https://img.shields.io/badge/docs-master-blue [docs-master-url]: https://tracing.rs/tracing-appender [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg [mit-url]: ../LICENSE [actions-badge]: https://github.com/tokio-rs/tracing/workflows/CI/badge.svg [actions-url]:https://github.com/tokio-rs/tracing/actions?query=workflow%3ACI [discord-badge]: https://img.shields.io/discord/500028886025895936?logo=discord&label=discord&logoColor=white [discord-url]: https://discord.gg/EeF3cQw ## Overview [`tracing`][tracing] is a framework for instrumenting Rust programs to collect structured, event-based diagnostic information. `tracing-appender` allows events and spans to be recorded in a non-blocking manner through a dedicated logging thread. It also provides a [`RollingFileAppender`][file_appender] that can be used with _or_ without the non-blocking writer. *Compiler support: [requires `rustc` 1.53+][msrv]* [msrv]: #supported-rust-versions ## Usage Add the following to your `Cargo.toml`: ```toml tracing-appender = "0.2" ``` This crate can be used in a few ways to record spans/events: - Using a [`RollingFileAppender`][file_appender] to write to a log file. This is a blocking operation. - Using *any* type implementing [`std::io::Write`][write] in a non-blocking fashion. - Using [`NonBlocking`][non_blocking] and [`RollingFileAppender`][file_appender] together to write to log files in a non-blocking fashion. ## Rolling File Appender ```rust fn main(){ let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log"); } ``` This creates an hourly rotating file appender that writes to `/some/directory/prefix.log.YYYY-MM-DD-HH`. [`Rotation::DAILY`] and [`Rotation::NEVER`] are the other available options. The file appender implements [`std::io::Write`][write]. To be used with [`tracing_subscriber::FmtSubscriber`][fmt_subscriber], it must be combined with a [`MakeWriter`][make_writer] implementation to be able to record tracing spans/event. The [rolling] module's documentation provides more detail on how to use this file appender. ## Non-Blocking Writer The example below demonstrates the construction of a `non_blocking` writer with an implementation of [`std::io::Writer`][write]. ```rust use std::io::Error; struct TestWriter; impl std::io::Write for TestWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { let buf_len = buf.len(); println!("{:?}", buf); Ok(buf_len) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } fn main() { let (non_blocking, _guard) = tracing_appender::non_blocking(TestWriter); tracing_subscriber::fmt().with_writer(non_blocking).init(); } ``` **Note:** `_guard` is a [`WorkerGuard`][guard] which is returned by `tracing_appender::non_blocking` to ensure buffered logs are flushed to their output in the case of abrupt terminations of a process. See [`WorkerGuard`][guard] module for more details. The example below demonstrates the construction of a [`tracing_appender::non_blocking`][non_blocking] writer constructed with a [`std::io::Write`][write]: ```rust fn main() { let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); tracing_subscriber::fmt() .with_writer(non_blocking) .init(); } ``` The [non_blocking] module's documentation provides more detail on how to use `non_blocking`. ## Non-Blocking Rolling File Appender ```rust fn main() { let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log"); let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); tracing_subscriber::fmt() .with_writer(non_blocking) .init(); } ``` [tracing]: https://docs.rs/tracing/latest/tracing/ [make_writer]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/trait.MakeWriter.html [write]: https://doc.rust-lang.org/std/io/trait.Write.html [non_blocking]: https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/index.html [rolling]: https://docs.rs/tracing-appender/latest/tracing_appender/rolling/index.html [guard]: https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.WorkerGuard.html [file_appender]: https://docs.rs/tracing-appender/latest/tracing_appender/rolling/struct.RollingFileAppender.html [fmt_subscriber]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/struct.Subscriber.html ## Supported Rust Versions `tracing-appender` is built against the latest stable release. The minimum supported version is 1.53. The current `tracing-appender` version is not guaranteed to build on Rust versions earlier than the minimum supported version. Tracing follows the same compiler support policies as the rest of the Tokio project. The current stable Rust compiler and the three most recent minor versions before it will always be supported. For example, if the current stable compiler version is 1.45, the minimum supported version will not be increased past 1.42, three minor versions prior. Increasing the minimum supported compiler version is not considered a semver breaking change as long as doing so complies with this policy. ## License This project is licensed under the [MIT license](../LICENSE). ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Tokio by you, shall be licensed as MIT, without any additional terms or conditions. tracing-appender-0.2.2/src/lib.rs000064400000000000000000000152170072674642500150110ustar 00000000000000//! Writers for logging events and spans //! //! # Overview //! //! [`tracing`][tracing] is a framework for structured, event-based diagnostic information. //! `tracing-appender` allows events and spans to be recorded in a non-blocking manner through //! a dedicated logging thread. It also provides a [`RollingFileAppender`][file_appender] that can //! be used with _or_ without the non-blocking writer. //! //! *Compiler support: [requires `rustc` 1.53+][msrv]* //! //! [msrv]: #supported-rust-versions //! [file_appender]: ./rolling/struct.RollingFileAppender.html //! [tracing]: https://docs.rs/tracing/ //! //! # Usage //! //! Add the following to your `Cargo.toml`: //! ```toml //! tracing-appender = "0.2" //! ``` //! //! This crate can be used in a few ways to record spans/events: //! - Using a [`RollingFileAppender`][rolling_struct] to perform writes to a log file. This will block on writes. //! - Using *any* type implementing [`std::io::Write`][write] in a non-blocking fashion. //! - Using a combination of [`NonBlocking`][non_blocking] and [`RollingFileAppender`][rolling_struct] to allow writes to a log file //! without blocking. //! //! ## Rolling File Appender //! //! ```rust //! # fn docs() { //! let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log"); //! # } //! ``` //! This creates an hourly rotating file appender that writes to `/some/directory/prefix.log.YYYY-MM-DD-HH`. //! [`Rotation::DAILY`](rolling::Rotation::DAILY) and [`Rotation::NEVER`](rolling::Rotation::NEVER) are the other available options. //! //! The file appender implements [`std::io::Write`][write]. To be used with [`tracing_subscriber::FmtSubscriber`][fmt_subscriber], //! it must be combined with a [`MakeWriter`][make_writer] implementation to be able to record tracing spans/event. //! //! The [`rolling` module][rolling]'s documentation provides more detail on how to use this file appender. //! //! ## Non-Blocking Writer //! //! The example below demonstrates the construction of a `non_blocking` writer with `std::io::stdout()`, //! which implements [`MakeWriter`][make_writer]. //! //! ```rust //! # fn doc() { //! let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); //! tracing_subscriber::fmt() //! .with_writer(non_blocking) //! .init(); //! # } //! ``` //! **Note:** `_guard` is a [`WorkerGuard`][guard] which is returned by [`tracing_appender::non_blocking`][non_blocking] //! to ensure buffered logs are flushed to their output in the case of abrupt terminations of a process. //! See [`WorkerGuard` module][guard] for more details. //! //! The example below demonstrates the construction of a [`tracing_appender::non_blocking`][non_blocking] //! writer constructed with a [`std::io::Write`][write]: //! //! ```rust //! use std::io::Error; //! //! struct TestWriter; //! //! impl std::io::Write for TestWriter { //! fn write(&mut self, buf: &[u8]) -> std::io::Result { //! let buf_len = buf.len(); //! println!("{:?}", buf); //! Ok(buf_len) //! } //! //! fn flush(&mut self) -> std::io::Result<()> { //! Ok(()) //! } //! } //! //! # fn doc() { //! let (non_blocking, _guard) = tracing_appender::non_blocking(TestWriter); //! tracing_subscriber::fmt() //! .with_writer(non_blocking) //! .init(); //! # } //! ``` //! //! The [`non_blocking` module][non_blocking]'s documentation provides more detail on how to use `non_blocking`. //! //! [non_blocking]: ./non_blocking/index.html //! [write]: https://doc.rust-lang.org/std/io/trait.Write.html //! [guard]: ./non_blocking/struct.WorkerGuard.html //! [rolling]: ./rolling/index.html //! [make_writer]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/trait.MakeWriter.html //! [rolling_struct]: ./rolling/struct.RollingFileAppender.html //! [fmt_subscriber]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/struct.Subscriber.html //! //! ## Non-Blocking Rolling File Appender //! //! ```rust //! # fn docs() { //! let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log"); //! let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); //! tracing_subscriber::fmt() //! .with_writer(non_blocking) //! .init(); //! # } //! ``` //! //! ## Supported Rust Versions //! //! `tracing-appender` is built against the latest stable release. The minimum supported //! version is 1.53. The current `tracing-appender` version is not guaranteed to build on //! Rust versions earlier than the minimum supported version. //! //! Tracing follows the same compiler support policies as the rest of the Tokio //! project. The current stable Rust compiler and the three most recent minor //! versions before it will always be supported. For example, if the current //! stable compiler version is 1.45, the minimum supported version will not be //! increased past 1.42, three minor versions prior. Increasing the minimum //! supported compiler version is not considered a semver breaking change as //! long as doing so complies with this policy. //! #![doc(html_root_url = "https://docs.rs/tracing-appender/0.2.2")] #![doc( html_logo_url = "https://raw.githubusercontent.com/tokio-rs/tracing/master/assets/logo-type.png", issue_tracker_base_url = "https://github.com/tokio-rs/tracing/issues/" )] #![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))] #![warn( missing_debug_implementations, missing_docs, rust_2018_idioms, unreachable_pub, bad_style, const_err, dead_code, improper_ctypes, non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements, patterns_in_fns_without_body, private_in_public, unconditional_recursion, unused, unused_allocation, unused_comparisons, unused_parens, while_true )] use crate::non_blocking::{NonBlocking, WorkerGuard}; use std::io::Write; pub mod non_blocking; pub mod rolling; mod worker; pub(crate) mod sync; /// Convenience function for creating a non-blocking, off-thread writer. /// /// See the [`non_blocking` module's docs][non_blocking]'s for more details. /// /// [non_blocking]: ./non_blocking/index.html /// /// # Examples /// /// ``` rust /// # fn docs() { /// let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking); /// tracing::subscriber::with_default(subscriber.finish(), || { /// tracing::event!(tracing::Level::INFO, "Hello"); /// }); /// # } /// ``` pub fn non_blocking(writer: T) -> (NonBlocking, WorkerGuard) { NonBlocking::new(writer) } #[derive(Debug)] pub(crate) enum Msg { Line(Vec), Shutdown, } tracing-appender-0.2.2/src/non_blocking.rs000064400000000000000000000416620072674642500167100ustar 00000000000000//! A non-blocking, off-thread writer. //! //! This spawns a dedicated worker thread which is responsible for writing log //! lines to the provided writer. When a line is written using the returned //! `NonBlocking` struct's `make_writer` method, it will be enqueued to be //! written by the worker thread. //! //! The queue has a fixed capacity, and if it becomes full, any logs written //! to it will be dropped until capacity is once again available. This may //! occur if logs are consistently produced faster than the worker thread can //! output them. The queue capacity and behavior when full (i.e., whether to //! drop logs or to exert backpressure to slow down senders) can be configured //! using [`NonBlockingBuilder::default()`][builder]. //! This function returns the default configuration. It is equivalent to: //! //! ```rust //! # use tracing_appender::non_blocking::{NonBlocking, WorkerGuard}; //! # fn doc() -> (NonBlocking, WorkerGuard) { //! tracing_appender::non_blocking(std::io::stdout()) //! # } //! ``` //! [builder]: ./struct.NonBlockingBuilder.html#method.default //! //!
This function returns a tuple of `NonBlocking` and `WorkerGuard`. //! `NonBlocking` implements [`MakeWriter`] which integrates with `tracing_subscriber`. //! `WorkerGuard` is a drop guard that is responsible for flushing any remaining logs when //! the program terminates. //! //! Note that the `WorkerGuard` returned by `non_blocking` _must_ be assigned to a binding that //! is not `_`, as `_` will result in the `WorkerGuard` being dropped immediately. //! Unintentional drops of `WorkerGuard` remove the guarantee that logs will be flushed //! during a program's termination, in a panic or otherwise. //! //! See [`WorkerGuard`][worker_guard] for examples of using the guard. //! //! [worker_guard]: ./struct.WorkerGuard.html //! //! # Examples //! //! ``` rust //! # fn docs() { //! let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); //! let subscriber = tracing_subscriber::fmt().with_writer(non_blocking); //! tracing::subscriber::with_default(subscriber.finish(), || { //! tracing::event!(tracing::Level::INFO, "Hello"); //! }); //! # } //! ``` use crate::worker::Worker; use crate::Msg; use crossbeam_channel::{bounded, SendTimeoutError, Sender}; use std::io; use std::io::Write; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::sync::Arc; use std::thread::JoinHandle; use std::time::Duration; use tracing_subscriber::fmt::MakeWriter; /// The default maximum number of buffered log lines. /// /// If [`NonBlocking`][non-blocking] is lossy, it will drop spans/events at capacity. /// If [`NonBlocking`][non-blocking] is _not_ lossy, /// backpressure will be exerted on senders, causing them to block their /// respective threads until there is available capacity. /// /// [non-blocking]: ./struct.NonBlocking.html /// Recommended to be a power of 2. pub const DEFAULT_BUFFERED_LINES_LIMIT: usize = 128_000; /// A guard that flushes spans/events associated to a [`NonBlocking`] on a drop /// /// Writing to a [`NonBlocking`] writer will **not** immediately write a span or event to the underlying /// output. Instead, the span or event will be written by a dedicated logging thread at some later point. /// To increase throughput, the non-blocking writer will flush to the underlying output on /// a periodic basis rather than every time a span or event is written. This means that if the program /// terminates abruptly (such as through an uncaught `panic` or a `std::process::exit`), some spans /// or events may not be written. /// /// [`NonBlocking`]: ./struct.NonBlocking.html /// Since spans/events and events recorded near a crash are often necessary for diagnosing the failure, /// `WorkerGuard` provides a mechanism to ensure that _all_ buffered logs are flushed to their output. /// `WorkerGuard` should be assigned in the `main` function or whatever the entrypoint of the program is. /// This will ensure that the guard will be dropped during an unwinding or when `main` exits /// successfully. /// /// # Examples /// /// ``` rust /// # #[clippy::allow(needless_doctest_main)] /// fn main () { /// # fn doc() { /// let (non_blocking, _guard) = tracing_appender::non_blocking(std::io::stdout()); /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking); /// tracing::subscriber::with_default(subscriber.finish(), || { /// // Emit some tracing events within context of the non_blocking `_guard` and tracing subscriber /// tracing::event!(tracing::Level::INFO, "Hello"); /// }); /// // Exiting the context of `main` will drop the `_guard` and any remaining logs should get flushed /// # } /// } /// ``` #[must_use] #[derive(Debug)] pub struct WorkerGuard { _guard: Option>, sender: Sender, shutdown: Sender<()>, } /// A non-blocking writer. /// /// While the line between "blocking" and "non-blocking" IO is fuzzy, writing to a file is typically /// considered to be a _blocking_ operation. For an application whose `Subscriber` writes spans and events /// as they are emitted, an application might find the latency profile to be unacceptable. /// `NonBlocking` moves the writing out of an application's data path by sending spans and events /// to a dedicated logging thread. /// /// This struct implements [`MakeWriter`][make_writer] from the `tracing-subscriber` /// crate. Therefore, it can be used with the [`tracing_subscriber::fmt`][fmt] module /// or with any other subscriber/layer implementation that uses the `MakeWriter` trait. /// /// [make_writer]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/trait.MakeWriter.html /// [fmt]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/index.html #[derive(Clone, Debug)] pub struct NonBlocking { error_counter: ErrorCounter, channel: Sender, is_lossy: bool, } /// Tracks the number of times a log line was dropped by the background thread. /// /// If the non-blocking writer is not configured in [lossy mode], the error /// count should always be 0. /// /// [lossy mode]: NonBlockingBuilder::lossy #[derive(Clone, Debug)] pub struct ErrorCounter(Arc); impl NonBlocking { /// Returns a new `NonBlocking` writer wrapping the provided `writer`. /// /// The returned `NonBlocking` writer will have the [default configuration][default] values. /// Other configurations can be specified using the [builder] interface. /// /// [default]: ./struct.NonBlockingBuilder.html#method.default /// [builder]: ./struct.NonBlockingBuilder.html pub fn new(writer: T) -> (NonBlocking, WorkerGuard) { NonBlockingBuilder::default().finish(writer) } fn create( writer: T, buffered_lines_limit: usize, is_lossy: bool, ) -> (NonBlocking, WorkerGuard) { let (sender, receiver) = bounded(buffered_lines_limit); let (shutdown_sender, shutdown_receiver) = bounded(0); let worker = Worker::new(receiver, writer, shutdown_receiver); let worker_guard = WorkerGuard::new(worker.worker_thread(), sender.clone(), shutdown_sender); ( Self { channel: sender, error_counter: ErrorCounter(Arc::new(AtomicUsize::new(0))), is_lossy, }, worker_guard, ) } /// Returns a counter for the number of times logs where dropped. This will always return zero if /// `NonBlocking` is not lossy. pub fn error_counter(&self) -> ErrorCounter { self.error_counter.clone() } } /// A builder for [`NonBlocking`][non-blocking]. /// /// [non-blocking]: ./struct.NonBlocking.html #[derive(Debug)] pub struct NonBlockingBuilder { buffered_lines_limit: usize, is_lossy: bool, } impl NonBlockingBuilder { /// Sets the number of lines to buffer before dropping logs or exerting backpressure on senders pub fn buffered_lines_limit(mut self, buffered_lines_limit: usize) -> NonBlockingBuilder { self.buffered_lines_limit = buffered_lines_limit; self } /// Sets whether `NonBlocking` should be lossy or not. /// /// If set to `true`, logs will be dropped when the buffered limit is reached. If `false`, backpressure /// will be exerted on senders, blocking them until the buffer has capacity again. /// /// By default, the built `NonBlocking` will be lossy. pub fn lossy(mut self, is_lossy: bool) -> NonBlockingBuilder { self.is_lossy = is_lossy; self } /// Completes the builder, returning the configured `NonBlocking`. pub fn finish(self, writer: T) -> (NonBlocking, WorkerGuard) { NonBlocking::create(writer, self.buffered_lines_limit, self.is_lossy) } } impl Default for NonBlockingBuilder { fn default() -> Self { NonBlockingBuilder { buffered_lines_limit: DEFAULT_BUFFERED_LINES_LIMIT, is_lossy: true, } } } impl std::io::Write for NonBlocking { fn write(&mut self, buf: &[u8]) -> io::Result { let buf_size = buf.len(); if self.is_lossy { if self.channel.try_send(Msg::Line(buf.to_vec())).is_err() { self.error_counter.incr_saturating(); } } else { return match self.channel.send(Msg::Line(buf.to_vec())) { Ok(_) => Ok(buf_size), Err(_) => Err(io::Error::from(io::ErrorKind::Other)), }; } Ok(buf_size) } fn flush(&mut self) -> io::Result<()> { Ok(()) } #[inline] fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { self.write(buf).map(|_| ()) } } impl<'a> MakeWriter<'a> for NonBlocking { type Writer = NonBlocking; fn make_writer(&'a self) -> Self::Writer { self.clone() } } impl WorkerGuard { fn new(handle: JoinHandle<()>, sender: Sender, shutdown: Sender<()>) -> Self { WorkerGuard { _guard: Some(handle), sender, shutdown, } } } impl Drop for WorkerGuard { fn drop(&mut self) { match self .sender .send_timeout(Msg::Shutdown, Duration::from_millis(100)) { Ok(_) => { // Attempt to wait for `Worker` to flush all messages before dropping. This happens // when the `Worker` calls `recv()` on a zero-capacity channel. Use `send_timeout` // so that drop is not blocked indefinitely. // TODO: Make timeout configurable. let _ = self.shutdown.send_timeout((), Duration::from_millis(1000)); } Err(SendTimeoutError::Disconnected(_)) => (), Err(SendTimeoutError::Timeout(e)) => println!( "Failed to send shutdown signal to logging worker. Error: {:?}", e ), } } } // === impl ErrorCounter === impl ErrorCounter { /// Returns the number of log lines that have been dropped. /// /// If the non-blocking writer is not configured in [lossy mode], the error /// count should always be 0. /// /// [lossy mode]: NonBlockingBuilder::lossy pub fn dropped_lines(&self) -> usize { self.0.load(Ordering::Acquire) } fn incr_saturating(&self) { let mut curr = self.0.load(Ordering::Acquire); // We don't need to enter the CAS loop if the current value is already // `usize::MAX`. if curr == usize::MAX { return; } // This is implemented as a CAS loop rather than as a simple // `fetch_add`, because we don't want to wrap on overflow. Instead, we // need to ensure that saturating addition is performed. loop { let val = curr.saturating_add(1); match self .0 .compare_exchange(curr, val, Ordering::AcqRel, Ordering::Acquire) { Ok(_) => return, Err(actual) => curr = actual, } } } } #[cfg(test)] mod test { use super::*; use std::sync::mpsc; use std::thread; use std::time::Duration; struct MockWriter { tx: mpsc::SyncSender, } impl MockWriter { fn new(capacity: usize) -> (Self, mpsc::Receiver) { let (tx, rx) = mpsc::sync_channel(capacity); (Self { tx }, rx) } } impl std::io::Write for MockWriter { fn write(&mut self, buf: &[u8]) -> std::io::Result { let buf_len = buf.len(); let _ = self.tx.send(String::from_utf8_lossy(buf).to_string()); Ok(buf_len) } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } #[test] fn backpressure_exerted() { let (mock_writer, rx) = MockWriter::new(1); let (mut non_blocking, _guard) = self::NonBlockingBuilder::default() .lossy(false) .buffered_lines_limit(1) .finish(mock_writer); let error_count = non_blocking.error_counter(); non_blocking.write_all(b"Hello").expect("Failed to write"); assert_eq!(0, error_count.dropped_lines()); let handle = thread::spawn(move || { non_blocking.write_all(b", World").expect("Failed to write"); }); // Sleep a little to ensure previously spawned thread gets blocked on write. thread::sleep(Duration::from_millis(100)); // We should not drop logs when blocked. assert_eq!(0, error_count.dropped_lines()); // Read the first message to unblock sender. let mut line = rx.recv().unwrap(); assert_eq!(line, "Hello"); // Wait for thread to finish. handle.join().expect("thread should not panic"); // Thread has joined, we should be able to read the message it sent. line = rx.recv().unwrap(); assert_eq!(line, ", World"); } fn write_non_blocking(non_blocking: &mut NonBlocking, msg: &[u8]) { non_blocking.write_all(msg).expect("Failed to write"); // Sleep a bit to prevent races. thread::sleep(Duration::from_millis(200)); } #[test] #[ignore] // flaky, see https://github.com/tokio-rs/tracing/issues/751 fn logs_dropped_if_lossy() { let (mock_writer, rx) = MockWriter::new(1); let (mut non_blocking, _guard) = self::NonBlockingBuilder::default() .lossy(true) .buffered_lines_limit(1) .finish(mock_writer); let error_count = non_blocking.error_counter(); // First write will not block write_non_blocking(&mut non_blocking, b"Hello"); assert_eq!(0, error_count.dropped_lines()); // Second write will not block as Worker will have called `recv` on channel. // "Hello" is not yet consumed. MockWriter call to write_all will block until // "Hello" is consumed. write_non_blocking(&mut non_blocking, b", World"); assert_eq!(0, error_count.dropped_lines()); // Will sit in NonBlocking channel's buffer. write_non_blocking(&mut non_blocking, b"Test"); assert_eq!(0, error_count.dropped_lines()); // Allow a line to be written. "Hello" message will be consumed. // ", World" will be able to write to MockWriter. // "Test" will block on call to MockWriter's `write_all` let line = rx.recv().unwrap(); assert_eq!(line, "Hello"); // This will block as NonBlocking channel is full. write_non_blocking(&mut non_blocking, b"Universe"); assert_eq!(1, error_count.dropped_lines()); // Finally the second message sent will be consumed. let line = rx.recv().unwrap(); assert_eq!(line, ", World"); assert_eq!(1, error_count.dropped_lines()); } #[test] fn multi_threaded_writes() { let (mock_writer, rx) = MockWriter::new(DEFAULT_BUFFERED_LINES_LIMIT); let (non_blocking, _guard) = self::NonBlockingBuilder::default() .lossy(true) .finish(mock_writer); let error_count = non_blocking.error_counter(); let mut join_handles: Vec> = Vec::with_capacity(10); for _ in 0..10 { let cloned_non_blocking = non_blocking.clone(); join_handles.push(thread::spawn(move || { let subscriber = tracing_subscriber::fmt().with_writer(cloned_non_blocking); tracing::subscriber::with_default(subscriber.finish(), || { tracing::event!(tracing::Level::INFO, "Hello"); }); })); } for handle in join_handles { handle.join().expect("Failed to join thread"); } let mut hello_count: u8 = 0; while let Ok(event_str) = rx.recv_timeout(Duration::from_secs(5)) { assert!(event_str.contains("Hello")); hello_count += 1; } assert_eq!(10, hello_count); assert_eq!(0, error_count.dropped_lines()); } } tracing-appender-0.2.2/src/rolling.rs000064400000000000000000000636160072674642500157170ustar 00000000000000//! A rolling file appender. //! //! Creates a new log file at a fixed frequency as defined by [`Rotation`](struct.Rotation.html). //! Logs will be written to this file for the duration of the period and will automatically roll over //! to the newly created log file once the time period has elapsed. //! //! The log file is created at the specified directory and file name prefix which *may* be appended with //! the date and time. //! //! The following helpers are available for creating a rolling file appender. //! //! - [`Rotation::minutely()`][minutely]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd-HH-mm` //! will be created minutely (once per minute) //! - [`Rotation::hourly()`][hourly]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd-HH` //! will be created hourly //! - [`Rotation::daily()`][daily]: A new log file in the format of `some_directory/log_file_name_prefix.yyyy-MM-dd` //! will be created daily //! - [`Rotation::never()`][never()]: This will result in log file located at `some_directory/log_file_name` //! //! [minutely]: fn.minutely.html //! [hourly]: fn.hourly.html //! [daily]: fn.daily.html //! [never]: fn.never.html //! //! # Examples //! //! ```rust //! # fn docs() { //! use tracing_appender::rolling::{RollingFileAppender, Rotation}; //! let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log"); //! # } //! ``` use crate::sync::{RwLock, RwLockReadGuard}; use std::{ fmt::{self, Debug}, fs::{self, File, OpenOptions}, io::{self, Write}, path::Path, sync::atomic::{AtomicUsize, Ordering}, }; use time::{format_description, Duration, OffsetDateTime, Time}; /// A file appender with the ability to rotate log files at a fixed schedule. /// /// `RollingFileAppender` implements the [`std:io::Write` trait][write] and will /// block on write operations. It may be used with [`NonBlocking`] to perform /// writes without blocking the current thread. /// /// Additionally, `RollingFileAppender` also implements the [`MakeWriter`] /// trait from `tracing-appender`, so it may also be used /// directly, without [`NonBlocking`]. /// /// [write]: std::io::Write /// [`NonBlocking`]: super::non_blocking::NonBlocking /// /// # Examples /// /// Rolling a log file once every hour: /// /// ```rust /// # fn docs() { /// let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix"); /// # } /// ``` /// /// Combining a `RollingFileAppender` with another [`MakeWriter`] implementation: /// /// ```rust /// # fn docs() { /// use tracing_subscriber::fmt::writer::MakeWriterExt; /// /// // Log all events to a rolling log file. /// let logfile = tracing_appender::rolling::hourly("/logs", "myapp-logs"); /// // Log `INFO` and above to stdout. /// let stdout = std::io::stdout.with_max_level(tracing::Level::INFO); /// /// tracing_subscriber::fmt() /// // Combine the stdout and log file `MakeWriter`s into one /// // `MakeWriter` that writes to both /// .with_writer(stdout.and(logfile)) /// .init(); /// # } /// ``` /// /// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter pub struct RollingFileAppender { state: Inner, writer: RwLock, #[cfg(test)] now: Box OffsetDateTime + Send + Sync>, } /// A [writer] that writes to a rolling log file. /// /// This is returned by the [`MakeWriter`] implementation for [`RollingFileAppender`]. /// /// [writer]: std::io::Write /// [`MakeWriter`]: tracing_subscriber::fmt::writer::MakeWriter #[derive(Debug)] pub struct RollingWriter<'a>(RwLockReadGuard<'a, File>); #[derive(Debug)] struct Inner { log_directory: String, log_filename_prefix: String, rotation: Rotation, next_date: AtomicUsize, } // === impl RollingFileAppender === impl RollingFileAppender { /// Creates a new `RollingFileAppender`. /// /// A `RollingFileAppender` will have a fixed rotation whose frequency is /// defined by [`Rotation`](struct.Rotation.html). The `directory` and /// `file_name_prefix` arguments determine the location and file name's _prefix_ /// of the log file. `RollingFileAppender` will automatically append the current date /// and hour (UTC format) to the file name. /// /// Alternatively, a `RollingFileAppender` can be constructed using one of the following helpers: /// /// - [`Rotation::minutely()`][minutely], /// - [`Rotation::hourly()`][hourly], /// - [`Rotation::daily()`][daily], /// - [`Rotation::never()`][never()] /// /// [minutely]: fn.minutely.html /// [hourly]: fn.hourly.html /// [daily]: fn.daily.html /// [never]: fn.never.html /// /// # Examples /// ```rust /// # fn docs() { /// use tracing_appender::rolling::{RollingFileAppender, Rotation}; /// let file_appender = RollingFileAppender::new(Rotation::HOURLY, "/some/directory", "prefix.log"); /// # } /// ``` pub fn new( rotation: Rotation, directory: impl AsRef, file_name_prefix: impl AsRef, ) -> RollingFileAppender { let now = OffsetDateTime::now_utc(); let (state, writer) = Inner::new(now, rotation, directory, file_name_prefix); Self { state, writer, #[cfg(test)] now: Box::new(OffsetDateTime::now_utc), } } #[inline] fn now(&self) -> OffsetDateTime { #[cfg(test)] return (self.now)(); #[cfg(not(test))] OffsetDateTime::now_utc() } } impl io::Write for RollingFileAppender { fn write(&mut self, buf: &[u8]) -> io::Result { let now = self.now(); let writer = self.writer.get_mut(); if let Some(current_time) = self.state.should_rollover(now) { let _did_cas = self.state.advance_date(now, current_time); debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp..."); self.state.refresh_writer(now, writer); } writer.write(buf) } fn flush(&mut self) -> io::Result<()> { self.writer.get_mut().flush() } } impl<'a> tracing_subscriber::fmt::writer::MakeWriter<'a> for RollingFileAppender { type Writer = RollingWriter<'a>; fn make_writer(&'a self) -> Self::Writer { let now = self.now(); // Should we try to roll over the log file? if let Some(current_time) = self.state.should_rollover(now) { // Did we get the right to lock the file? If not, another thread // did it and we can just make a writer. if self.state.advance_date(now, current_time) { self.state.refresh_writer(now, &mut *self.writer.write()); } } RollingWriter(self.writer.read()) } } impl fmt::Debug for RollingFileAppender { // This manual impl is required because of the `now` field (only present // with `cfg(test)`), which is not `Debug`... fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("RollingFileAppender") .field("state", &self.state) .field("writer", &self.writer) .finish() } } /// Creates a minutely, rolling file appender. This will rotate the log file once per minute. /// /// The appender returned by `rolling::minutely` can be used with `non_blocking` to create /// a non-blocking, minutely file appender. /// /// The directory of the log file is specified with the `directory` argument. /// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender` /// adds the current date, hour, and minute to the log file in UTC. /// /// # Examples /// /// ``` rust /// # #[clippy::allow(needless_doctest_main)] /// fn main () { /// # fn doc() { /// let appender = tracing_appender::rolling::minutely("/some/path", "rolling.log"); /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); /// /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); /// /// tracing::subscriber::with_default(subscriber.finish(), || { /// tracing::event!(tracing::Level::INFO, "Hello"); /// }); /// # } /// } /// ``` /// /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH-mm`. pub fn minutely( directory: impl AsRef, file_name_prefix: impl AsRef, ) -> RollingFileAppender { RollingFileAppender::new(Rotation::MINUTELY, directory, file_name_prefix) } /// Creates an hourly, rolling file appender. /// /// The appender returned by `rolling::hourly` can be used with `non_blocking` to create /// a non-blocking, hourly file appender. /// /// The directory of the log file is specified with the `directory` argument. /// `file_name_prefix` specifies the _prefix_ of the log file. `RollingFileAppender` /// adds the current date and hour to the log file in UTC. /// /// # Examples /// /// ``` rust /// # #[clippy::allow(needless_doctest_main)] /// fn main () { /// # fn doc() { /// let appender = tracing_appender::rolling::hourly("/some/path", "rolling.log"); /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); /// /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); /// /// tracing::subscriber::with_default(subscriber.finish(), || { /// tracing::event!(tracing::Level::INFO, "Hello"); /// }); /// # } /// } /// ``` /// /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`. pub fn hourly( directory: impl AsRef, file_name_prefix: impl AsRef, ) -> RollingFileAppender { RollingFileAppender::new(Rotation::HOURLY, directory, file_name_prefix) } /// Creates a file appender that rotates daily. /// /// The appender returned by `rolling::daily` can be used with `non_blocking` to create /// a non-blocking, daily file appender. /// /// A `RollingFileAppender` has a fixed rotation whose frequency is /// defined by [`Rotation`](struct.Rotation.html). The `directory` and /// `file_name_prefix` arguments determine the location and file name's _prefix_ /// of the log file. `RollingFileAppender` automatically appends the current date in UTC. /// /// # Examples /// /// ``` rust /// # #[clippy::allow(needless_doctest_main)] /// fn main () { /// # fn doc() { /// let appender = tracing_appender::rolling::daily("/some/path", "rolling.log"); /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); /// /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); /// /// tracing::subscriber::with_default(subscriber.finish(), || { /// tracing::event!(tracing::Level::INFO, "Hello"); /// }); /// # } /// } /// ``` /// /// This will result in a log file located at `/some/path/rolling.log.yyyy-MM-dd-HH`. pub fn daily( directory: impl AsRef, file_name_prefix: impl AsRef, ) -> RollingFileAppender { RollingFileAppender::new(Rotation::DAILY, directory, file_name_prefix) } /// Creates a non-rolling, file appender /// /// The appender returned by `rolling::never` can be used with `non_blocking` to create /// a non-blocking, non-rotating appender. /// /// The location of the log file will be specified the `directory` passed in. /// `file_name` specifies the prefix of the log file. No date or time is appended. /// /// # Examples /// /// ``` rust /// # #[clippy::allow(needless_doctest_main)] /// fn main () { /// # fn doc() { /// let appender = tracing_appender::rolling::never("/some/path", "non-rolling.log"); /// let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); /// /// let subscriber = tracing_subscriber::fmt().with_writer(non_blocking_appender); /// /// tracing::subscriber::with_default(subscriber.finish(), || { /// tracing::event!(tracing::Level::INFO, "Hello"); /// }); /// # } /// } /// ``` /// /// This will result in a log file located at `/some/path/non-rolling.log`. pub fn never(directory: impl AsRef, file_name: impl AsRef) -> RollingFileAppender { RollingFileAppender::new(Rotation::NEVER, directory, file_name) } /// Defines a fixed period for rolling of a log file. /// /// To use a `Rotation`, pick one of the following options: /// /// ### Minutely Rotation /// ```rust /// # fn docs() { /// use tracing_appender::rolling::Rotation; /// let rotation = tracing_appender::rolling::Rotation::MINUTELY; /// # } /// ``` /// /// ### Hourly Rotation /// ```rust /// # fn docs() { /// use tracing_appender::rolling::Rotation; /// let rotation = tracing_appender::rolling::Rotation::HOURLY; /// # } /// ``` /// /// ### Daily Rotation /// ```rust /// # fn docs() { /// use tracing_appender::rolling::Rotation; /// let rotation = tracing_appender::rolling::Rotation::DAILY; /// # } /// ``` /// /// ### No Rotation /// ```rust /// # fn docs() { /// use tracing_appender::rolling::Rotation; /// let rotation = tracing_appender::rolling::Rotation::NEVER; /// # } /// ``` #[derive(Clone, Eq, PartialEq, Debug)] pub struct Rotation(RotationKind); #[derive(Clone, Eq, PartialEq, Debug)] enum RotationKind { Minutely, Hourly, Daily, Never, } impl Rotation { /// Provides an minutely rotation pub const MINUTELY: Self = Self(RotationKind::Minutely); /// Provides an hourly rotation pub const HOURLY: Self = Self(RotationKind::Hourly); /// Provides a daily rotation pub const DAILY: Self = Self(RotationKind::Daily); /// Provides a rotation that never rotates. pub const NEVER: Self = Self(RotationKind::Never); pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option { let unrounded_next_date = match *self { Rotation::MINUTELY => *current_date + Duration::minutes(1), Rotation::HOURLY => *current_date + Duration::hours(1), Rotation::DAILY => *current_date + Duration::days(1), Rotation::NEVER => return None, }; Some(self.round_date(&unrounded_next_date)) } // note that this method will panic if passed a `Rotation::NEVER`. pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime { match *self { Rotation::MINUTELY => { let time = Time::from_hms(date.hour(), date.minute(), 0) .expect("Invalid time; this is a bug in tracing-appender"); date.replace_time(time) } Rotation::HOURLY => { let time = Time::from_hms(date.hour(), 0, 0) .expect("Invalid time; this is a bug in tracing-appender"); date.replace_time(time) } Rotation::DAILY => { let time = Time::from_hms(0, 0, 0) .expect("Invalid time; this is a bug in tracing-appender"); date.replace_time(time) } // Rotation::NEVER is impossible to round. Rotation::NEVER => { unreachable!("Rotation::NEVER is impossible to round.") } } } pub(crate) fn join_date(&self, filename: &str, date: &OffsetDateTime) -> String { match *self { Rotation::MINUTELY => { let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]") .expect("Unable to create a formatter; this is a bug in tracing-appender"); let date = date .format(&format) .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); format!("{}.{}", filename, date) } Rotation::HOURLY => { let format = format_description::parse("[year]-[month]-[day]-[hour]") .expect("Unable to create a formatter; this is a bug in tracing-appender"); let date = date .format(&format) .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); format!("{}.{}", filename, date) } Rotation::DAILY => { let format = format_description::parse("[year]-[month]-[day]") .expect("Unable to create a formatter; this is a bug in tracing-appender"); let date = date .format(&format) .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender"); format!("{}.{}", filename, date) } Rotation::NEVER => filename.to_string(), } } } // === impl RollingWriter === impl io::Write for RollingWriter<'_> { fn write(&mut self, buf: &[u8]) -> io::Result { (&*self.0).write(buf) } fn flush(&mut self) -> io::Result<()> { (&*self.0).flush() } } // === impl Inner === impl Inner { fn new( now: OffsetDateTime, rotation: Rotation, directory: impl AsRef, file_name_prefix: impl AsRef, ) -> (Self, RwLock) { let log_directory = directory.as_ref().to_str().unwrap(); let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap(); let filename = rotation.join_date(log_filename_prefix, &now); let next_date = rotation.next_date(&now); let writer = RwLock::new( create_writer(log_directory, &filename).expect("failed to create appender"), ); let inner = Inner { log_directory: log_directory.to_string(), log_filename_prefix: log_filename_prefix.to_string(), next_date: AtomicUsize::new( next_date .map(|date| date.unix_timestamp() as usize) .unwrap_or(0), ), rotation, }; (inner, writer) } fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) { let filename = self.rotation.join_date(&self.log_filename_prefix, &now); match create_writer(&self.log_directory, &filename) { Ok(new_file) => { if let Err(err) = file.flush() { eprintln!("Couldn't flush previous writer: {}", err); } *file = new_file; } Err(err) => eprintln!("Couldn't create writer for logs: {}", err), } } /// Checks whether or not it's time to roll over the log file. /// /// Rather than returning a `bool`, this returns the current value of /// `next_date` so that we can perform a `compare_exchange` operation with /// that value when setting the next rollover time. /// /// If this method returns `Some`, we should roll to a new log file. /// Otherwise, if this returns we should not rotate the log file. fn should_rollover(&self, date: OffsetDateTime) -> Option { let next_date = self.next_date.load(Ordering::Acquire); // if the next date is 0, this appender *never* rotates log files. if next_date == 0 { return None; } if date.unix_timestamp() as usize >= next_date { return Some(next_date); } None } fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool { let next_date = self .rotation .next_date(&now) .map(|date| date.unix_timestamp() as usize) .unwrap_or(0); self.next_date .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire) .is_ok() } } fn create_writer(directory: &str, filename: &str) -> io::Result { let path = Path::new(directory).join(filename); let mut open_options = OpenOptions::new(); open_options.append(true).create(true); let new_file = open_options.open(path.as_path()); if new_file.is_err() { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; return open_options.open(path); } } new_file } #[cfg(test)] mod test { use super::*; use std::fs; use std::io::Write; fn find_str_in_log(dir_path: &Path, expected_value: &str) -> bool { let dir_contents = fs::read_dir(dir_path).expect("Failed to read directory"); for entry in dir_contents { let path = entry.expect("Expected dir entry").path(); let file = fs::read_to_string(&path).expect("Failed to read file"); println!("path={}\nfile={:?}", path.display(), file); if file.as_str() == expected_value { return true; } } false } fn write_to_log(appender: &mut RollingFileAppender, msg: &str) { appender .write_all(msg.as_bytes()) .expect("Failed to write to appender"); appender.flush().expect("Failed to flush!"); } fn test_appender(rotation: Rotation, file_prefix: &str) { let directory = tempfile::tempdir().expect("failed to create tempdir"); let mut appender = RollingFileAppender::new(rotation, directory.path(), file_prefix); let expected_value = "Hello"; write_to_log(&mut appender, expected_value); assert!(find_str_in_log(directory.path(), expected_value)); directory .close() .expect("Failed to explicitly close TempDir. TempDir should delete once out of scope.") } #[test] fn write_minutely_log() { test_appender(Rotation::HOURLY, "minutely.log"); } #[test] fn write_hourly_log() { test_appender(Rotation::HOURLY, "hourly.log"); } #[test] fn write_daily_log() { test_appender(Rotation::DAILY, "daily.log"); } #[test] fn write_never_log() { test_appender(Rotation::NEVER, "never.log"); } #[test] fn test_rotations() { // per-minute basis let now = OffsetDateTime::now_utc(); let next = Rotation::MINUTELY.next_date(&now).unwrap(); assert_eq!((now + Duration::MINUTE).minute(), next.minute()); // per-hour basis let now = OffsetDateTime::now_utc(); let next = Rotation::HOURLY.next_date(&now).unwrap(); assert_eq!((now + Duration::HOUR).hour(), next.hour()); // daily-basis let now = OffsetDateTime::now_utc(); let next = Rotation::DAILY.next_date(&now).unwrap(); assert_eq!((now + Duration::DAY).day(), next.day()); // never let now = OffsetDateTime::now_utc(); let next = Rotation::NEVER.next_date(&now); assert!(next.is_none()); } #[test] #[should_panic( expected = "internal error: entered unreachable code: Rotation::NEVER is impossible to round." )] fn test_never_date_rounding() { let now = OffsetDateTime::now_utc(); let _ = Rotation::NEVER.round_date(&now); } #[test] fn test_path_concatination() { let format = format_description::parse( "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ sign:mandatory]:[offset_minute]:[offset_second]", ) .unwrap(); let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); // per-minute let path = Rotation::MINUTELY.join_date("app.log", &now); assert_eq!("app.log.2020-02-01-10-01", path); // per-hour let path = Rotation::HOURLY.join_date("app.log", &now); assert_eq!("app.log.2020-02-01-10", path); // per-day let path = Rotation::DAILY.join_date("app.log", &now); assert_eq!("app.log.2020-02-01", path); // never let path = Rotation::NEVER.join_date("app.log", &now); assert_eq!("app.log", path); } #[test] fn test_make_writer() { use std::sync::{Arc, Mutex}; use tracing_subscriber::prelude::*; let format = format_description::parse( "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour \ sign:mandatory]:[offset_minute]:[offset_second]", ) .unwrap(); let now = OffsetDateTime::parse("2020-02-01 10:01:00 +00:00:00", &format).unwrap(); let directory = tempfile::tempdir().expect("failed to create tempdir"); let (state, writer) = Inner::new(now, Rotation::HOURLY, directory.path(), "test_make_writer"); let clock = Arc::new(Mutex::new(now)); let now = { let clock = clock.clone(); Box::new(move || *clock.lock().unwrap()) }; let appender = RollingFileAppender { state, writer, now }; let default = tracing_subscriber::fmt() .without_time() .with_level(false) .with_target(false) .with_max_level(tracing_subscriber::filter::LevelFilter::TRACE) .with_writer(appender) .finish() .set_default(); tracing::info!("file 1"); // advance time by one second (*clock.lock().unwrap()) += Duration::seconds(1); tracing::info!("file 1"); // advance time by one hour (*clock.lock().unwrap()) += Duration::hours(1); tracing::info!("file 2"); // advance time by one second (*clock.lock().unwrap()) += Duration::seconds(1); tracing::info!("file 2"); drop(default); let dir_contents = fs::read_dir(directory.path()).expect("Failed to read directory"); println!("dir={:?}", dir_contents); for entry in dir_contents { println!("entry={:?}", entry); let path = entry.expect("Expected dir entry").path(); let file = fs::read_to_string(&path).expect("Failed to read file"); println!("path={}\nfile={:?}", path.display(), file); match path .extension() .expect("found a file without a date!") .to_str() .expect("extension should be UTF8") { "2020-02-01-10" => { assert_eq!("file 1\nfile 1\n", file); } "2020-02-01-11" => { assert_eq!("file 2\nfile 2\n", file); } x => panic!("unexpected date {}", x), } } } } tracing-appender-0.2.2/src/sync.rs000064400000000000000000000045340072674642500152170ustar 00000000000000//! Abstracts over sync primitive implementations. //! //! Optionally, we allow the Rust standard library's `RwLock` to be replaced //! with the `parking_lot` crate's implementation. This may provide improved //! performance in some cases. However, the `parking_lot` dependency is an //! opt-in feature flag. Because `parking_lot::RwLock` has a slightly different //! API than `std::sync::RwLock` (it does not support poisoning on panics), we //! wrap the `std::sync` version to ignore poisoning. #[allow(unused_imports)] // may be used later; #[cfg(feature = "parking_lot")] pub(crate) use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; #[cfg(not(feature = "parking_lot"))] pub(crate) use self::std_impl::*; #[cfg(not(feature = "parking_lot"))] mod std_impl { use std::sync::{self, PoisonError, TryLockError}; pub(crate) use std::sync::{RwLockReadGuard, RwLockWriteGuard}; #[derive(Debug)] pub(crate) struct RwLock { inner: sync::RwLock, } impl RwLock { pub(crate) fn new(val: T) -> Self { Self { inner: sync::RwLock::new(val), } } #[inline] pub(crate) fn get_mut(&mut self) -> &mut T { self.inner.get_mut().unwrap_or_else(PoisonError::into_inner) } #[inline] pub(crate) fn read(&self) -> RwLockReadGuard<'_, T> { self.inner.read().unwrap_or_else(PoisonError::into_inner) } #[inline] #[allow(dead_code)] // may be used later; pub(crate) fn try_read(&self) -> Option> { match self.inner.try_read() { Ok(guard) => Some(guard), Err(TryLockError::Poisoned(e)) => Some(e.into_inner()), Err(TryLockError::WouldBlock) => None, } } #[inline] pub(crate) fn write(&self) -> RwLockWriteGuard<'_, T> { self.inner.write().unwrap_or_else(PoisonError::into_inner) } #[inline] #[allow(dead_code)] // may be used later; pub(crate) fn try_write(&self) -> Option> { match self.inner.try_write() { Ok(guard) => Some(guard), Err(TryLockError::Poisoned(e)) => Some(e.into_inner()), Err(TryLockError::WouldBlock) => None, } } } } tracing-appender-0.2.2/src/worker.rs000064400000000000000000000057410072674642500155550ustar 00000000000000use crate::Msg; use crossbeam_channel::{Receiver, RecvError, TryRecvError}; use std::fmt::Debug; use std::io::Write; use std::{io, thread}; pub(crate) struct Worker { writer: T, receiver: Receiver, shutdown: Receiver<()>, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub(crate) enum WorkerState { Empty, Disconnected, Continue, Shutdown, } impl Worker { pub(crate) fn new(receiver: Receiver, writer: T, shutdown: Receiver<()>) -> Worker { Self { writer, receiver, shutdown, } } fn handle_recv(&mut self, result: &Result) -> io::Result { match result { Ok(Msg::Line(msg)) => { self.writer.write_all(msg)?; Ok(WorkerState::Continue) } Ok(Msg::Shutdown) => Ok(WorkerState::Shutdown), Err(_) => Ok(WorkerState::Disconnected), } } fn handle_try_recv(&mut self, result: &Result) -> io::Result { match result { Ok(Msg::Line(msg)) => { self.writer.write_all(msg)?; Ok(WorkerState::Continue) } Ok(Msg::Shutdown) => Ok(WorkerState::Shutdown), Err(TryRecvError::Empty) => Ok(WorkerState::Empty), Err(TryRecvError::Disconnected) => Ok(WorkerState::Disconnected), } } /// Blocks on the first recv of each batch of logs, unless the /// channel is disconnected. Afterwards, grabs as many logs as /// it can off the channel, buffers them and attempts a flush. pub(crate) fn work(&mut self) -> io::Result { // Worker thread yields here if receive buffer is empty let mut worker_state = self.handle_recv(&self.receiver.recv())?; while worker_state == WorkerState::Continue { let try_recv_result = self.receiver.try_recv(); let handle_result = self.handle_try_recv(&try_recv_result); worker_state = handle_result?; } self.writer.flush()?; Ok(worker_state) } /// Creates a worker thread that processes a channel until it's disconnected pub(crate) fn worker_thread(mut self) -> std::thread::JoinHandle<()> { thread::spawn(move || { loop { match self.work() { Ok(WorkerState::Continue) | Ok(WorkerState::Empty) => {} Ok(WorkerState::Shutdown) | Ok(WorkerState::Disconnected) => { let _ = self.shutdown.recv(); break; } Err(_) => { // TODO: Expose a metric for IO Errors, or print to stderr } } } if let Err(e) = self.writer.flush() { eprintln!("Failed to flush. Error: {}", e); } }) } }