use crate::memory_logger::config::{
DEFAULT_LOG_ENTRIES_LIMIT, DEFAULT_MESSAGE_LENGTH_LIMIT, LogEntry, RhaiContext,
};
use crate::tracing_logger::level::log_level;
use crate::{RUNNER_AND_SYSLOG_TARGET, RUNNER_TARGET, RUNNER_TARGET_FOR_SCRIPT_LOGS};
use bounded_vec_deque::BoundedVecDeque;
use chrono::Utc;
use std::fmt::Debug;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use tracing::field::{Field, Visit};
use tracing::{Event, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
#[derive(Debug, Clone)]
pub struct ScriptLogHandle {
logs: Arc<Mutex<BoundedVecDeque<LogEntry>>>,
rhai_context_stack: Arc<Mutex<Vec<RhaiContext>>>,
max_script_log_message_length: usize,
max_limit_applied: Arc<AtomicBool>,
}
#[derive(Debug)]
pub struct RhaiContextGuard;
impl Drop for RhaiContextGuard {
fn drop(&mut self) {
pop_current_rhai_context();
}
}
pub static SCRIPT_HANDLE: OnceLock<ScriptLogHandle> = OnceLock::new();
pub fn get_script_handle() -> Option<&'static ScriptLogHandle> {
SCRIPT_HANDLE.get()
}
pub fn push_rhai_context_with_guard(
rhai_api_name: Option<&str>,
line_number: u32,
) -> RhaiContextGuard {
if let Some(handle) = SCRIPT_HANDLE.get() {
handle.push_rhai_context(RhaiContext {
rhai_api_name: rhai_api_name.map(str::to_string),
line_number,
});
}
RhaiContextGuard
}
pub fn get_current_rhai_context() -> Option<RhaiContext> {
SCRIPT_HANDLE
.get()
.and_then(ScriptLogHandle::get_current_rhai_context)
}
pub fn pop_current_rhai_context() -> Option<RhaiContext> {
SCRIPT_HANDLE
.get()
.and_then(ScriptLogHandle::pop_rhai_context)
}
impl Default for ScriptLogHandle {
fn default() -> Self {
Self::new(DEFAULT_MESSAGE_LENGTH_LIMIT, DEFAULT_LOG_ENTRIES_LIMIT)
}
}
impl ScriptLogHandle {
pub fn new(max_script_log_message_length: usize, max_log_entries: usize) -> Self {
Self {
logs: Arc::new(Mutex::new(BoundedVecDeque::new(max_log_entries))),
rhai_context_stack: Arc::new(Mutex::new(Vec::new())),
max_script_log_message_length,
max_limit_applied: Arc::new(AtomicBool::new(false)),
}
}
fn truncate_message(&self, message: String) -> String {
if message.len() > self.max_script_log_message_length {
let truncate_suffix = "...[truncate]";
let available_chars = self
.max_script_log_message_length
.saturating_sub(truncate_suffix.len());
format!("{}{}", &message[..available_chars], truncate_suffix)
} else {
message
}
}
#[allow(clippy::unused_self)]
fn convert_event_to_log_entry(&self, event: &Event<'_>) -> LogEntry {
let mut message = String::new();
let mut visitor = MessageVisitor(&mut message);
event.record(&mut visitor);
LogEntry {
timestamp: Utc::now(),
line_number: get_current_rhai_context().map_or_else(|| 0, |ctx| ctx.line_number),
level: event.metadata().level().to_string(),
message: self.truncate_message(message),
rhai_api_name: get_current_rhai_context().and_then(|ctx| ctx.rhai_api_name),
}
}
pub fn get_logs(&self) -> Vec<LogEntry> {
self.logs
.lock()
.map(|guard| guard.iter().cloned().collect())
.unwrap_or_default()
}
pub fn was_max_limit_applied(&self) -> bool {
self.max_limit_applied.load(Ordering::Relaxed)
}
fn add_log_entry(&self, log_entry: LogEntry) {
if let Ok(mut logs) = self.logs.lock() {
let displaced_entry = logs.push_back(log_entry);
if displaced_entry.is_some() {
self.max_limit_applied.store(true, Ordering::Relaxed);
}
}
}
fn push_rhai_context(&self, context: RhaiContext) {
if let Ok(mut stack) = self.rhai_context_stack.lock() {
stack.push(context);
}
}
fn pop_rhai_context(&self) -> Option<RhaiContext> {
self.rhai_context_stack
.lock()
.ok()
.and_then(|mut stack| stack.pop())
}
fn get_current_rhai_context(&self) -> Option<RhaiContext> {
self.rhai_context_stack
.lock()
.ok()
.and_then(|stack| stack.last().cloned())
}
}
impl<S> Layer<S> for ScriptLogHandle
where
S: Subscriber + for<'lookup> LookupSpan<'lookup>,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
if event.metadata().target() == RUNNER_AND_SYSLOG_TARGET
|| event.metadata().target() == RUNNER_TARGET
|| event.metadata().target() == RUNNER_TARGET_FOR_SCRIPT_LOGS
{
if event.metadata().target() == RUNNER_TARGET && get_current_rhai_context().is_none() {
return;
}
let current_level_filter = log_level();
if current_level_filter >= *event.metadata().level() {
let log_entry = self.convert_event_to_log_entry(event);
self.add_log_entry(log_entry);
}
}
}
}
struct MessageVisitor<'a>(&'a mut String);
impl Visit for MessageVisitor<'_> {
fn record_debug(&mut self, field: &Field, value: &dyn Debug) {
if field.name() == "message" {
*self.0 = format!("{value:?}");
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
*self.0 = value.to_string();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_default_implementation() {
let handle = ScriptLogHandle::default();
let logs = handle.get_logs();
assert!(logs.is_empty());
assert_eq!(logs.len(), 0);
}
#[rstest]
#[case(
50,
"This is a very long message that definitely exceeds the limit",
true
)]
#[case(30, "Short message that fits", false)]
#[case(100, "Medium length message for testing purposes", false)]
#[case(20, "This message is way too long for the limit", true)]
fn test_truncate_message_dynamic(
#[case] max_length: usize,
#[case] input: &str,
#[case] should_truncate: bool,
) {
let handler = ScriptLogHandle::new(max_length, DEFAULT_LOG_ENTRIES_LIMIT);
let result = handler.truncate_message(input.to_string());
if should_truncate {
assert_eq!(result.len(), max_length);
assert!(result.ends_with("...[truncate]"));
let expected_prefix_len = max_length - "...[truncate]".len();
assert_eq!(
&result[..expected_prefix_len],
&input[..expected_prefix_len]
);
} else {
assert_eq!(result, input);
}
}
#[test]
fn test_bounded_log_entries() {
let handle = ScriptLogHandle::new(1000, 10);
let mut all_messages = Vec::new();
assert!(!handle.was_max_limit_applied());
for i in 0..10 {
let log_entry = LogEntry {
timestamp: Utc::now(),
line_number: i + 1,
level: "INFO".to_string(),
message: format!("Test message {}", i + 1),
rhai_api_name: None,
};
all_messages.push(log_entry.clone());
handle.add_log_entry(log_entry);
}
assert!(!handle.was_max_limit_applied());
let logs = handle.get_logs();
assert_eq!(logs.len(), 10);
for (i, log) in logs.iter().enumerate() {
assert_eq!(log.message, all_messages[i].message);
}
let log_entry_11 = LogEntry {
timestamp: Utc::now(),
line_number: 11,
level: "INFO".to_string(),
message: "Test message 11".to_string(),
rhai_api_name: None,
};
all_messages.push(log_entry_11.clone());
handle.add_log_entry(log_entry_11);
assert!(handle.was_max_limit_applied());
let logs = handle.get_logs();
assert_eq!(logs.len(), 10);
for (i, log) in logs.iter().enumerate() {
let expected_index = i + 1; assert_eq!(log.message, all_messages[expected_index].message);
assert_eq!(log.line_number, all_messages[expected_index].line_number);
}
}
}