forked from boos/BoosAPI
fix: show runtime logs in program logs
This commit is contained in:
@@ -8,6 +8,7 @@ pub mod oauth;
|
||||
pub mod register_captcha;
|
||||
pub mod registration_limiter;
|
||||
pub mod route_rebuild;
|
||||
pub mod runtime_logs;
|
||||
pub mod shared;
|
||||
pub mod update_center;
|
||||
pub mod workspace;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use serde::Serialize;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use tracing::{
|
||||
field::{Field, Visit},
|
||||
Level, Subscriber,
|
||||
};
|
||||
use tracing_subscriber::{layer::Context, Layer};
|
||||
|
||||
const MAX_RUNTIME_LOGS: usize = 500;
|
||||
|
||||
static NEXT_ID: AtomicI64 = AtomicI64::new(1);
|
||||
static RUNTIME_LOGS: Lazy<RwLock<VecDeque<RuntimeLogEntry>>> =
|
||||
Lazy::new(|| RwLock::new(VecDeque::with_capacity(MAX_RUNTIME_LOGS)));
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RuntimeLogEntry {
|
||||
pub id: i64,
|
||||
#[serde(rename = "type")]
|
||||
pub event_type: String,
|
||||
pub title: String,
|
||||
pub message: Option<String>,
|
||||
pub level: String,
|
||||
#[serde(rename = "read")]
|
||||
pub is_read: bool,
|
||||
pub related_id: Option<i64>,
|
||||
pub related_type: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RuntimeLogLayer;
|
||||
|
||||
impl<S> Layer<S> for RuntimeLogLayer
|
||||
where
|
||||
S: Subscriber,
|
||||
{
|
||||
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
|
||||
let metadata = event.metadata();
|
||||
let level = metadata.level();
|
||||
if !matches!(*level, Level::ERROR | Level::WARN | Level::INFO) {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut visitor = LogFieldVisitor::default();
|
||||
event.record(&mut visitor);
|
||||
|
||||
let title = visitor
|
||||
.message
|
||||
.unwrap_or_else(|| metadata.name().to_string());
|
||||
let mut details = Vec::with_capacity(visitor.fields.len() + 1);
|
||||
details.push(metadata.target().to_string());
|
||||
details.extend(visitor.fields);
|
||||
|
||||
push(RuntimeLogEntry {
|
||||
id: -NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
||||
event_type: "runtime".to_string(),
|
||||
title,
|
||||
message: Some(details.join(" · ")).filter(|value| !value.trim().is_empty()),
|
||||
level: level_name(level).to_string(),
|
||||
is_read: true,
|
||||
related_id: None,
|
||||
related_type: None,
|
||||
created_at: Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list() -> Vec<RuntimeLogEntry> {
|
||||
RUNTIME_LOGS.read().iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn clear() {
|
||||
RUNTIME_LOGS.write().clear();
|
||||
}
|
||||
|
||||
fn push(entry: RuntimeLogEntry) {
|
||||
let mut logs = RUNTIME_LOGS.write();
|
||||
if logs.len() >= MAX_RUNTIME_LOGS {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(entry);
|
||||
}
|
||||
|
||||
fn level_name(level: &Level) -> &'static str {
|
||||
match *level {
|
||||
Level::ERROR => "error",
|
||||
Level::WARN => "warning",
|
||||
_ => "info",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct LogFieldVisitor {
|
||||
message: Option<String>,
|
||||
fields: Vec<String>,
|
||||
}
|
||||
|
||||
impl LogFieldVisitor {
|
||||
fn record_value(&mut self, field: &Field, value: String) {
|
||||
if field.name() == "message" {
|
||||
self.message = Some(value);
|
||||
} else {
|
||||
self.fields.push(format!("{}={}", field.name(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for LogFieldVisitor {
|
||||
fn record_str(&mut self, field: &Field, value: &str) {
|
||||
self.record_value(field, value.to_string());
|
||||
}
|
||||
|
||||
fn record_bool(&mut self, field: &Field, value: bool) {
|
||||
self.record_value(field, value.to_string());
|
||||
}
|
||||
|
||||
fn record_i64(&mut self, field: &Field, value: i64) {
|
||||
self.record_value(field, value.to_string());
|
||||
}
|
||||
|
||||
fn record_u64(&mut self, field: &Field, value: u64) {
|
||||
self.record_value(field, value.to_string());
|
||||
}
|
||||
|
||||
fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
|
||||
self.record_value(field, value.to_string());
|
||||
}
|
||||
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
|
||||
self.record_value(field, normalize_debug_value(format!("{value:?}")));
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_debug_value(value: String) -> String {
|
||||
if value.len() >= 2 && value.starts_with('"') && value.ends_with('"') {
|
||||
value[1..value.len() - 1].replace("\\\"", "\"")
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn runtime_log_layer_captures_info_events() {
|
||||
clear();
|
||||
let subscriber = tracing_subscriber::registry().with(RuntimeLogLayer);
|
||||
|
||||
tracing::subscriber::with_default(subscriber, || {
|
||||
tracing::info!(key_id = 42, "Built-in key ready");
|
||||
});
|
||||
|
||||
let logs = list();
|
||||
let entry = logs.last().expect("runtime log entry");
|
||||
assert_eq!(entry.event_type, "runtime");
|
||||
assert_eq!(entry.title, "Built-in key ready");
|
||||
assert_eq!(entry.level, "info");
|
||||
assert_eq!(entry.is_read, true);
|
||||
assert!(entry
|
||||
.message
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("key_id=42"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user