use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use crate::affinity;
use crate::drain::{Drain, LogMetadata};
use crate::error::TicklogError;
use crate::guard::Guard;
use crate::level::Level;
use crate::sink::{ConsoleSink, LogSink};
use crate::thread_buf::REGISTRY;
use crate::timestamp;
const MIN_TZ_OFFSET: i32 = -43_200;
const MAX_TZ_OFFSET: i32 = 50_400;
static MAX_LEVEL: AtomicU8 = AtomicU8::new(0);
static BACKPRESSURE: AtomicU8 = AtomicU8::new(0);
fn set_max_level(level: Level) {
MAX_LEVEL.store(level.to_u8(), Ordering::Relaxed);
}
pub(crate) fn disable_logging() {
MAX_LEVEL.store(0, Ordering::Relaxed);
}
fn set_backpressure(policy: Backpressure) {
BACKPRESSURE.store(policy as u8, Ordering::Relaxed);
}
pub(crate) fn level_enabled(level: Level) -> bool {
level.to_u8() <= MAX_LEVEL.load(Ordering::Relaxed)
}
pub(crate) fn backpressure() -> Backpressure {
if BACKPRESSURE.load(Ordering::Relaxed) == Backpressure::Block as u8 {
Backpressure::Block
} else {
Backpressure::Drop
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Backpressure {
Drop,
Block,
}
pub struct Builder {
sink: Box<dyn LogSink>,
max_level: Level,
backpressure: Backpressure,
timezone_offset: i32,
drain_affinity: Option<Vec<usize>>,
}
pub fn builder() -> Builder {
Builder {
sink: Box::new(ConsoleSink::stderr()),
max_level: Level::Info,
backpressure: Backpressure::Drop,
timezone_offset: 0,
drain_affinity: None,
}
}
impl Default for Builder {
fn default() -> Self {
builder()
}
}
impl Builder {
pub fn sink<S: LogSink>(mut self, sink: S) -> Self {
self.sink = Box::new(sink);
self
}
pub fn max_level(mut self, level: Level) -> Self {
self.max_level = level;
self
}
pub fn backpressure(mut self, policy: Backpressure) -> Self {
self.backpressure = policy;
self
}
pub fn timezone_offset(mut self, seconds: i32) -> Self {
self.timezone_offset = seconds;
self
}
pub fn drain_affinity(mut self, cores: &[usize]) -> Self {
if cores.is_empty() {
self.drain_affinity = None;
} else {
self.drain_affinity = Some(cores.to_vec());
}
self
}
pub fn build(self) -> Result<Guard, TicklogError> {
if !(MIN_TZ_OFFSET..=MAX_TZ_OFFSET).contains(&self.timezone_offset) {
return Err(TicklogError::InvalidTimezoneOffset(self.timezone_offset));
}
REGISTRY
.set(Mutex::new(Vec::new()))
.map_err(|_| TicklogError::AlreadyInitialized)?;
let calibration = timestamp::calibrate();
let shutdown = Arc::new(AtomicBool::new(false));
let drain = Drain::new(
self.sink,
self.timezone_offset,
LogMetadata::default(),
Arc::clone(&shutdown),
calibration,
);
let drain_affinity = self.drain_affinity.clone();
let handle = thread::Builder::new()
.name("ticklog-drain".to_string())
.spawn(move || {
if let Some(ref cores) = drain_affinity {
affinity::pin_thread(cores);
}
let mut drain = drain;
drain.run();
})
.map_err(TicklogError::DrainSpawnFailed)?;
set_max_level(self.max_level);
set_backpressure(self.backpressure);
Ok(Guard::new(handle, shutdown))
}
}
pub fn init<S: LogSink>(sink: S) -> Result<Guard, TicklogError> {
builder().sink(sink).build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builder_has_expected_defaults() {
let b = builder();
assert!(b.max_level == Level::Info);
assert_eq!(b.backpressure, Backpressure::Drop);
assert_eq!(b.timezone_offset, 0);
}
#[test]
fn fluent_setters_update_fields() {
let b = builder()
.max_level(Level::Warn)
.backpressure(Backpressure::Block)
.timezone_offset(3600)
.drain_affinity(&[2]);
assert!(b.max_level == Level::Warn);
assert_eq!(b.backpressure, Backpressure::Block);
assert_eq!(b.timezone_offset, 3600);
assert_eq!(b.drain_affinity.as_deref(), Some(&[2usize][..]));
}
#[test]
fn drain_affinity_empty_is_none() {
let b = builder().drain_affinity(&[2]).drain_affinity(&[]);
assert!(b.drain_affinity.is_none());
}
#[test]
fn backpressure_discriminants() {
assert_eq!(Backpressure::Drop as u8, 0);
assert_eq!(Backpressure::Block as u8, 1);
}
#[test]
fn set_max_level_stores_level_byte() {
set_max_level(Level::Warn);
assert_eq!(MAX_LEVEL.load(Ordering::Relaxed), Level::Warn.to_u8());
MAX_LEVEL.store(0, Ordering::Relaxed);
}
#[test]
fn drop_guard_disables_logging_ceiling() {
set_max_level(Level::Info);
assert!(level_enabled(Level::Info));
let shutdown = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&shutdown);
let handle = thread::spawn(move || {
while !flag.load(Ordering::Acquire) {
std::hint::spin_loop();
}
});
let guard = Guard::new(handle, shutdown);
drop(guard);
assert!(!level_enabled(Level::Info));
assert!(!level_enabled(Level::Error));
MAX_LEVEL.store(0, Ordering::Relaxed);
}
#[test]
fn set_backpressure_stores_discriminant() {
set_backpressure(Backpressure::Block);
assert_eq!(
BACKPRESSURE.load(Ordering::Relaxed),
Backpressure::Block as u8
);
set_backpressure(Backpressure::Drop);
assert_eq!(
BACKPRESSURE.load(Ordering::Relaxed),
Backpressure::Drop as u8
);
}
#[test]
fn build_after_registry_claimed_is_already_initialized() {
let _ = REGISTRY.set(Mutex::new(Vec::new()));
let result = builder().build();
assert!(matches!(result, Err(TicklogError::AlreadyInitialized)));
}
#[test]
fn build_rejects_out_of_range_timezone_offset() {
assert!(matches!(
builder().timezone_offset(50_401).build(),
Err(TicklogError::InvalidTimezoneOffset(50_401))
));
assert!(matches!(
builder().timezone_offset(-43_201).build(),
Err(TicklogError::InvalidTimezoneOffset(-43_201))
));
}
}