timber_rust/service/vector.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Dante Doménech Martinez dante19031999@gmail.com
3
4use crate::service::{ServiceError, StandardWriteMessageFormatter, WriteMessageFormatter};
5use crate::{Fallback, LoggerStatus, Message, Service};
6use std::any::Any;
7use std::sync::Mutex;
8
9/// A simple structured representation of a log message stored within a [`Vector`] service.
10///
11/// Unlike the dynamic [`Message`], this struct stores the level and content
12/// as owned [`String`]s, making it easy to inspect after the logger has finished.
13pub struct VectorMessage {
14 /// The string representation of the log level (e.g., "INFO").
15 pub level: String,
16 /// The formatted content of the log message.
17 pub message: String,
18}
19
20/// A logging [`Service`] that collects log entries into an in-memory [`Vec`].
21///
22/// This service is primarily used for **integration testing** or **UI feedback loops**,
23/// allowing you to capture every log generated by a specific operation and
24/// inspect them programmatically afterward.
25///
26/// ### Thread Safety
27/// The internal vector is protected by a [`Mutex`]. Multiple worker threads can
28/// push logs concurrently without data races or interleaving.
29pub struct Vector {
30 /// The thread-safe storage for captured log messages.
31 logs: Mutex<Vec<VectorMessage>>,
32}
33
34impl Vector {
35 /// Creates a new [`Vector`] service on the heap with a pre-allocated capacity.
36 ///
37 /// # Parameters
38 /// - `capacity`: The initial number of messages the vector can hold without reallocating.
39 pub fn new(capacity: usize) -> Box<Self> {
40 Box::new(Self {
41 logs: Mutex::new(Vec::with_capacity(capacity)),
42 })
43 }
44
45 /// Allows safe, read-only access to the captured logs without consuming the service.
46 ///
47 /// This is useful for "heartbeat" checks or assertions in tests while the logger
48 /// is still active.
49 ///
50 /// ### Returns
51 /// - [`Some(R)`][`Some`]: The result of the closure `f`.
52 /// - [`None`]: If the internal lock was poisoned.
53 pub fn inspect_vector<R>(&self, f: impl FnOnce(&Vec<VectorMessage>) -> R) -> Option<R> {
54 self.logs.lock().ok().map(|r| f(&*r))
55 }
56
57 /// Consumes the service and returns all captured log messages.
58 ///
59 /// This is the most efficient way to retrieve logs for final assertions
60 /// or post-processing, as it extracts the data from the mutex.
61 ///
62 /// # Errors
63 /// Returns [`ServiceError::LockPoisoned`] if a thread panicked while holding the lock.
64 pub fn recover_vector(self) -> Result<Vec<VectorMessage>, ServiceError> {
65 self.logs
66 .into_inner()
67 .map_err(|_| ServiceError::LockPoisoned)
68 }
69}
70
71impl Service for Vector {
72 /// Returns [`LoggerStatus::Running`].
73 fn status(&self) -> LoggerStatus {
74 LoggerStatus::Running
75 }
76
77 /// Transforms the [`Message`] into a [`VectorMessage`] and pushes it onto the internal stack.
78 ///
79 /// # Errors
80 /// Returns [`ServiceError::LockPoisoned`] if the internal mutex is unreachable.
81 fn work(&self, msg: &Message) -> Result<(), ServiceError> {
82 let mut logs = self.logs.lock().map_err(|_| ServiceError::LockPoisoned)?;
83 logs.push(VectorMessage {
84 level: msg.level().to_string(),
85 message: msg.content().to_string(),
86 });
87 Ok(())
88 }
89
90 fn as_any(&self) -> &dyn Any {
91 self
92 }
93}
94
95impl Fallback for Vector {
96 /// Attempts to log an error to `stdout` if the primary [`work`](Self::work) call fails.
97 ///
98 /// This method performs a best-effort write. If the mutex is locked by a hanging
99 /// thread, the fallback will be skipped to avoid a deadlock.
100 fn fallback(&self, error: &ServiceError, msg: &Message) {
101 let mut formatter = StandardWriteMessageFormatter::default();
102 let mut out = std::io::stdout();
103 let _ = formatter.format_io(msg, &mut out);
104 let _ = eprintln!("FmtWriteService Error: {}", error);
105 }
106}