timber_rust 2.0.1

A high-performance, asynchronous logging library with support for Grafana Loki and AWS CloudWatch.
// SPDX-License-Identifier: Apache-2.0
// Copyright 2026 Dante Doménech Martinez dante19031999@gmail.com

use crate::service::{ServiceError, StandardWriteMessageFormatter, WriteMessageFormatter};
use crate::{Fallback, LoggerStatus, Message, Service};
use std::any::Any;
use std::sync::Mutex;

/// A simple structured representation of a log message stored within a [`Vector`] service.
///
/// Unlike the dynamic [`Message`], this struct stores the level and content
/// as owned [`String`]s, making it easy to inspect after the logger has finished.
pub struct VectorMessage {
    /// The string representation of the log level (e.g., "INFO").
    pub level: String,
    /// The formatted content of the log message.
    pub message: String,
}

/// A logging [`Service`] that collects log entries into an in-memory [`Vec`].
///
/// This service is primarily used for **integration testing** or **UI feedback loops**,
/// allowing you to capture every log generated by a specific operation and
/// inspect them programmatically afterward.
///
/// ### Thread Safety
/// The internal vector is protected by a [`Mutex`]. Multiple worker threads can
/// push logs concurrently without data races or interleaving.
pub struct Vector {
    /// The thread-safe storage for captured log messages.
    logs: Mutex<Vec<VectorMessage>>,
}

impl Vector {
    /// Creates a new [`Vector`] service on the heap with a pre-allocated capacity.
    ///
    /// # Parameters
    /// - `capacity`: The initial number of messages the vector can hold without reallocating.
    pub fn new(capacity: usize) -> Box<Self> {
        Box::new(Self {
            logs: Mutex::new(Vec::with_capacity(capacity)),
        })
    }

    /// Allows safe, read-only access to the captured logs without consuming the service.
    ///
    /// This is useful for "heartbeat" checks or assertions in tests while the logger
    /// is still active.
    ///
    /// ### Returns
    /// - [`Some(R)`][`Some`]: The result of the closure `f`.
    /// - [`None`]: If the internal lock was poisoned.
    pub fn inspect_vector<R>(&self, f: impl FnOnce(&Vec<VectorMessage>) -> R) -> Option<R> {
        self.logs.lock().ok().map(|r| f(&*r))
    }

    /// Consumes the service and returns all captured log messages.
    ///
    /// This is the most efficient way to retrieve logs for final assertions
    /// or post-processing, as it extracts the data from the mutex.
    ///
    /// # Errors
    /// Returns [`ServiceError::LockPoisoned`] if a thread panicked while holding the lock.
    pub fn recover_vector(self) -> Result<Vec<VectorMessage>, ServiceError> {
        self.logs
            .into_inner()
            .map_err(|_| ServiceError::LockPoisoned)
    }
}

impl Service for Vector {
    /// Returns [`LoggerStatus::Running`].
    fn status(&self) -> LoggerStatus {
        LoggerStatus::Running
    }

    /// Transforms the [`Message`] into a [`VectorMessage`] and pushes it onto the internal stack.
    ///
    /// # Errors
    /// Returns [`ServiceError::LockPoisoned`] if the internal mutex is unreachable.
    fn work(&self, msg: &Message) -> Result<(), ServiceError> {
        let mut logs = self.logs.lock().map_err(|_| ServiceError::LockPoisoned)?;
        logs.push(VectorMessage {
            level: msg.level().to_string(),
            message: msg.content().to_string(),
        });
        Ok(())
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl Fallback for Vector {
    /// Attempts to log an error to `stdout` if the primary [`work`](Self::work) call fails.
    ///
    /// This method performs a best-effort write. If the mutex is locked by a hanging
    /// thread, the fallback will be skipped to avoid a deadlock.
    fn fallback(&self, error: &ServiceError, msg: &Message) {
        let mut formatter = StandardWriteMessageFormatter::default();
        let mut out = std::io::stdout();
        let _ = formatter.format_io(msg, &mut out);
        let _ = eprintln!("FmtWriteService Error: {}", error);
    }
}