#![deny(missing_docs, unsafe_code)]
use log::{log, Level};
use std::{
fmt::Display,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
#[cfg(feature = "pretty_counts")]
use thousands::{
policies::{COMMA_SEPARATOR, DOT_SEPARATOR, HEX_FOUR, SPACE_SEPARATOR, UNDERSCORE_SEPARATOR},
Separable,
};
static DEFAULT_NAME: &str = "proglog";
static DEFAULT_NOUN: &str = "records";
static DEFAULT_VERB: &str = "Processed";
static DEFAULT_UNIT: u64 = 100_000;
static DEFAULT_LEVEL: Level = Level::Info;
#[cfg(feature = "pretty_counts")]
pub enum CountFormatterKind {
Comma,
Dot,
HexFour,
Space,
Underscore,
Nothing,
}
#[cfg(feature = "pretty_counts")]
impl CountFormatterKind {
fn fmt(&self, count: u64) -> String {
match self {
CountFormatterKind::Nothing => count.to_string(),
CountFormatterKind::Comma => count.separate_by_policy(COMMA_SEPARATOR),
CountFormatterKind::Dot => count.separate_by_policy(DOT_SEPARATOR),
CountFormatterKind::HexFour => count.separate_by_policy(HEX_FOUR),
CountFormatterKind::Space => count.separate_by_policy(SPACE_SEPARATOR),
CountFormatterKind::Underscore => count.separate_by_policy(UNDERSCORE_SEPARATOR),
}
}
}
pub struct ProgLog {
counter: Arc<AtomicU64>,
name: String,
noun: String,
verb: String,
unit: u64,
level: Level,
#[cfg(feature = "pretty_counts")]
count_formatter: CountFormatterKind,
}
impl Default for ProgLog {
fn default() -> Self {
Self {
counter: Default::default(),
name: String::from(DEFAULT_NAME),
noun: String::from(DEFAULT_NOUN),
verb: String::from(DEFAULT_VERB),
unit: DEFAULT_UNIT,
level: DEFAULT_LEVEL,
#[cfg(feature = "pretty_counts")]
count_formatter: CountFormatterKind::Nothing,
}
}
}
impl ProgLog {
#[allow(clippy::must_use_candidate)]
pub fn new(
name: String,
noun: String,
verb: String,
unit: u64,
level: Level,
#[cfg(feature = "pretty_counts")] count_formatter: CountFormatterKind,
) -> Self {
Self {
counter: Arc::new(AtomicU64::new(0)),
name,
noun,
verb,
unit,
level,
#[cfg(feature = "pretty_counts")]
count_formatter,
}
}
pub fn seen(&self) -> u64 {
self.counter.load(Ordering::Relaxed)
}
#[inline]
fn log_it(&self, total: u64) {
#[cfg(feature = "pretty_counts")]
{
log!(
self.level,
"[{name}] {verb} {seen} {noun}",
name = &self.name,
verb = &self.verb,
seen = self.count_formatter.fmt(total),
noun = &self.noun
);
}
#[cfg(not(feature = "pretty_counts"))]
{
log!(
self.level,
"[{name}] {verb} {seen} {noun}",
name = &self.name,
verb = &self.verb,
seen = total,
noun = &self.noun
);
}
}
#[inline]
fn log_it_with<F, T>(&self, f: F, total: u64)
where
F: Fn() -> T,
T: Display,
{
#[cfg(feature = "pretty_counts")]
{
log!(
self.level,
"[{name}] {verb} {seen} {noun}: {extra}",
name = &self.name,
verb = &self.verb,
seen = self.count_formatter.fmt(total),
noun = &self.noun,
extra = f()
);
}
#[cfg(not(feature = "pretty_counts"))]
{
log!(
self.level,
"[{name}] {verb} {seen} {noun}: {extra}",
name = &self.name,
verb = &self.verb,
seen = total,
noun = &self.noun,
extra = f()
);
}
}
pub fn record(&self) -> bool {
let prev = self.counter.fetch_add(1, Ordering::Relaxed);
let total = prev + 1;
if total % self.unit == 0 {
self.log_it(total);
true
} else {
false
}
}
pub fn record_with<T, F>(&self, f: F) -> bool
where
F: Fn() -> T,
T: Display,
{
let prev = self.counter.fetch_add(1, Ordering::Relaxed);
let total = prev + 1;
if total % self.unit == 0 {
self.log_it_with(f, total);
true
} else {
false
}
}
pub fn flush_with<T, F>(&self, f: F)
where
F: Fn() -> T,
T: Display,
{
let total = self.counter.load(Ordering::Relaxed);
if total % self.unit != 0 {
self.log_it_with(f, total);
}
}
pub fn flush(&self) {
let total = self.counter.load(Ordering::Relaxed);
if total % self.unit != 0 {
self.log_it(total);
}
}
}
impl Drop for ProgLog {
fn drop(&mut self) {
self.flush();
}
}
pub struct ProgLogBuilder {
name: String,
noun: String,
verb: String,
unit: u64,
level: Level,
#[cfg(feature = "pretty_counts")]
count_formatter: CountFormatterKind,
}
impl ProgLogBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn noun(mut self, noun: impl Into<String>) -> Self {
self.noun = noun.into();
self
}
pub fn verb(mut self, verb: impl Into<String>) -> Self {
self.verb = verb.into();
self
}
pub fn unit(mut self, unit: u64) -> Self {
self.unit = unit;
self
}
pub fn level(mut self, level: Level) -> Self {
self.level = level;
self
}
#[cfg(feature = "pretty_counts")]
pub fn count_formatter(mut self, formatter: CountFormatterKind) -> Self {
self.count_formatter = formatter;
self
}
pub fn build(self) -> ProgLog {
ProgLog::new(
self.name,
self.noun,
self.verb,
self.unit,
self.level,
#[cfg(feature = "pretty_counts")]
self.count_formatter,
)
}
}
impl Default for ProgLogBuilder {
fn default() -> Self {
Self {
name: String::from(DEFAULT_NAME),
noun: String::from(DEFAULT_NOUN),
verb: String::from(DEFAULT_VERB),
unit: DEFAULT_UNIT,
level: DEFAULT_LEVEL,
#[cfg(feature = "pretty_counts")]
count_formatter: CountFormatterKind::Nothing,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use logtest::Logger;
use rayon::prelude::*;
fn drain_logger(logger: &mut Logger) {
while let Some(_msg) = logger.pop() {}
}
#[test]
fn test_log_messages() {
let mut logger = logtest::Logger::start();
test_simple_case(&mut logger);
test_rayon(&mut logger);
test_messages_simple(&mut logger);
assert_eq!(logger.len(), 0);
test_messages_simple_verify_unit(&mut logger);
assert_eq!(logger.len(), 0);
test_messages_rayon(&mut logger);
assert_eq!(logger.len(), 0);
#[cfg(feature = "pretty_counts")]
{
test_pretty_counts(&mut logger);
assert_eq!(logger.len(), 0);
}
}
fn test_simple_case(logger: &mut Logger) {
let my_logger = ProgLogBuilder::new().build();
for _i in 0..101 {
my_logger.record();
}
assert_eq!(my_logger.seen(), 101);
drain_logger(logger);
}
fn test_rayon(logger: &mut Logger) {
let my_logger = ProgLogBuilder::new().build();
(0..1_000_000).par_bridge().for_each(|_i| {
my_logger.record();
});
assert_eq!(my_logger.seen(), 1_000_000);
drain_logger(logger);
}
fn test_messages_simple(logger: &mut Logger) {
let my_logger = ProgLogBuilder::new().unit(1).build();
my_logger.record_with(|| "This is a test");
assert_eq!(logger.len(), 1);
assert!(logger.pop().unwrap().args().ends_with("This is a test"));
drain_logger(logger);
}
fn test_messages_simple_verify_unit(logger: &mut Logger) {
let my_logger = ProgLogBuilder::new().unit(10).build();
for _ in 0..9 {
my_logger.record_with(|| "This is a test");
}
assert_eq!(logger.len(), 0);
my_logger.record_with(|| "The 10th");
assert_eq!(logger.len(), 1);
assert!(logger.pop().unwrap().args().ends_with("The 10th"));
drain_logger(logger)
}
fn test_messages_rayon(logger: &mut Logger) {
let my_logger = ProgLogBuilder::new().unit(100_000).build();
(1..=1_000_000).par_bridge().for_each(|i| {
my_logger.record_with(|| format!("Logged {}", i));
});
assert_eq!(my_logger.seen(), 1_000_000);
assert_eq!(logger.len(), 10);
for msg in (100_000..=1_000_000).step_by(100_000) {
let found = logger.pop().unwrap();
assert!(found.args().ends_with(&msg.to_string()));
}
drain_logger(logger);
}
#[cfg(feature = "pretty_counts")]
fn test_pretty_counts(logger: &mut Logger) {
let my_logger = ProgLogBuilder::new()
.unit(100_000)
.count_formatter(CountFormatterKind::Underscore)
.build();
for _ in 0..99_999 {
my_logger.record_with(|| "This is a test");
}
assert_eq!(logger.len(), 0);
my_logger.record_with(|| "The 100,000th");
assert_eq!(logger.len(), 1);
assert_eq!(
logger.pop().unwrap().args(),
"[proglog] Processed 100_000 records: The 100,000th"
);
drain_logger(logger)
}
}