feat: 封装日志基础设施

This commit is contained in:
2026-06-26 18:43:29 +08:00
parent b95824fc9c
commit 1dae260646
9 changed files with 404 additions and 2 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "oxide_log"
version.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "OxideJS - Structured logging subsystem (tracing-based)"
[dependencies]
tracing = { workspace = true, features = ["max_level_trace", "release_max_level_info"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "registry"] }
tracing-appender = "0.2.5"
+99
View File
@@ -0,0 +1,99 @@
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use tracing::Subscriber;
use tracing_subscriber::layer::Filter;
use crate::level::Level;
use crate::subsystem::{SubsystemId, SUBSYSTEM_COUNT};
#[derive(Clone)]
pub struct SubsystemFilter {
levels: Arc<[AtomicU8; SUBSYSTEM_COUNT]>,
}
impl SubsystemFilter {
pub fn new() -> Self {
Self {
levels: Arc::new([
AtomicU8::new(0),
AtomicU8::new(0),
AtomicU8::new(0),
AtomicU8::new(0),
]),
}
}
pub fn set_level(&self, id: SubsystemId, level: Level) {
self.levels[id as usize].store(level as u8, Ordering::Relaxed);
}
pub fn get_level(&self, id: SubsystemId) -> Level {
let raw = self.levels[id as usize].load(Ordering::Relaxed);
match raw {
1 => Level::Error,
2 => Level::Warn,
3 => Level::Info,
4 => Level::Debug,
5 => Level::Trace,
_ => Level::Off,
}
}
}
impl SubsystemFilter {
fn is_enabled_for(&self, target: &str, tracing_level: &tracing::Level) -> bool {
let sid = subsystem_for_target(target);
let max = self.get_level(sid);
match max {
Level::Off => false,
Level::Error => *tracing_level <= tracing::Level::ERROR,
Level::Warn => *tracing_level <= tracing::Level::WARN,
Level::Info => *tracing_level <= tracing::Level::INFO,
Level::Debug => *tracing_level <= tracing::Level::DEBUG,
Level::Trace => true,
}
}
}
impl Default for SubsystemFilter {
fn default() -> Self {
Self::new()
}
}
impl<S: Subscriber> Filter<S> for SubsystemFilter {
fn enabled(
&self,
meta: &tracing::Metadata<'_>,
_cx: &tracing_subscriber::layer::Context<'_, S>,
) -> bool {
self.is_enabled_for(meta.target(), meta.level())
}
}
fn subsystem_for_target(target: &str) -> SubsystemId {
if let Some(rest) = target.strip_prefix("oxide::") {
match rest {
"vm" => SubsystemId::Vm,
"ic" => SubsystemId::Ic,
"kernel" => SubsystemId::Kernel,
"builtins" => SubsystemId::Builtins,
_ => {
if rest.starts_with("vm::") {
SubsystemId::Vm
} else if rest.starts_with("ic::") {
SubsystemId::Ic
} else if rest.starts_with("kernel::") {
SubsystemId::Kernel
} else if rest.starts_with("builtins::") {
SubsystemId::Builtins
} else {
SubsystemId::Kernel
}
}
}
} else {
SubsystemId::Kernel
}
}
+109
View File
@@ -0,0 +1,109 @@
use std::io;
use std::path::PathBuf;
use std::sync::{Once, OnceLock};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
use tracing_subscriber::Layer;
use crate::filter::SubsystemFilter;
use crate::level::Level;
use crate::subsystem::{self, SubsystemId, SUBSYSTEM_COUNT};
#[derive(Debug, Clone)]
pub enum Output {
Stderr,
Stdout,
File(PathBuf),
}
pub struct LogConfig {
pub output: Output,
pub levels: [Level; SUBSYSTEM_COUNT],
}
impl Default for LogConfig {
fn default() -> Self {
Self {
output: Output::Stderr,
levels: [Level::Off; SUBSYSTEM_COUNT],
}
}
}
static INIT: Once = Once::new();
static SUBSYSTEM: OnceLock<SubsystemFilter> = OnceLock::new();
static FILE_GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new();
pub fn get_subsystem() -> &'static SubsystemFilter {
SUBSYSTEM.get().expect("oxide_log::init() must be called before logging")
}
pub fn set_level(id: SubsystemId, level: Level) {
get_subsystem().set_level(id, level);
}
pub fn init(config: &LogConfig) {
INIT.call_once(|| {
let filter = SubsystemFilter::new();
for (i, level) in config.levels.iter().enumerate() {
let id = match i {
0 => SubsystemId::Vm,
1 => SubsystemId::Ic,
2 => SubsystemId::Kernel,
3 => SubsystemId::Builtins,
_ => continue,
};
filter.set_level(id, *level);
}
SUBSYSTEM.set(filter.clone()).ok();
let env_filter = EnvFilter::try_from_env("OXIDE_LOG").unwrap_or_else(|_| EnvFilter::new("oxide=off"));
subsystem::apply_env_levels();
let stderr_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(io::stderr)
.with_ansi(false)
.without_time()
.compact();
match &config.output {
Output::Stderr => {
let subscriber = tracing_subscriber::Registry::default()
.with(env_filter)
.with(stderr_layer.with_filter(filter));
subscriber.init();
}
Output::Stdout => {
let stdout_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(io::stdout)
.with_ansi(false)
.without_time()
.compact();
let subscriber = tracing_subscriber::Registry::default()
.with(env_filter)
.with(stdout_layer.with_filter(filter));
subscriber.init();
}
Output::File(dir) => {
let file_appender = tracing_appender::rolling::daily(dir, "oxide.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
FILE_GUARD.set(guard).ok();
let file_layer = tracing_subscriber::fmt::Layer::default()
.with_writer(non_blocking)
.with_ansi(false)
.without_time()
.compact();
let subscriber = tracing_subscriber::Registry::default()
.with(env_filter)
.with(stderr_layer.with_filter(filter.clone()))
.with(file_layer.with_filter(filter));
subscriber.init();
}
}
});
}
+22
View File
@@ -0,0 +1,22 @@
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Off = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
impl Level {
pub fn as_tracing_level(self) -> tracing::Level {
match self {
Level::Off | Level::Error => tracing::Level::ERROR,
Level::Warn => tracing::Level::WARN,
Level::Info => tracing::Level::INFO,
Level::Debug => tracing::Level::DEBUG,
Level::Trace => tracing::Level::TRACE,
}
}
}
+10
View File
@@ -0,0 +1,10 @@
pub mod filter;
pub mod init;
pub mod level;
pub mod macros;
pub mod subsystem;
pub use filter::SubsystemFilter;
pub use init::{init, set_level, LogConfig, Output};
pub use level::Level;
pub use subsystem::{SubsystemId, SUBSYSTEM_COUNT};
+6
View File
@@ -0,0 +1,6 @@
#[macro_export]
macro_rules! __log_event {
($target:expr, $level:expr, $($arg:tt)*) => {
tracing::event!(target: $target, $level, $($arg)*)
};
}
+57
View File
@@ -0,0 +1,57 @@
use crate::level::Level;
#[repr(usize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubsystemId {
Vm = 0,
Ic = 1,
Kernel = 2,
Builtins = 3,
}
pub const SUBSYSTEM_COUNT: usize = 4;
pub fn parse_level(s: &str) -> Option<Level> {
if s.eq_ignore_ascii_case("off") {
Some(Level::Off)
} else if s.eq_ignore_ascii_case("error") {
Some(Level::Error)
} else if s.eq_ignore_ascii_case("warn") {
Some(Level::Warn)
} else if s.eq_ignore_ascii_case("info") {
Some(Level::Info)
} else if s.eq_ignore_ascii_case("debug") {
Some(Level::Debug)
} else if s.eq_ignore_ascii_case("trace") {
Some(Level::Trace)
} else {
None
}
}
pub fn apply_env_levels() {
let Ok(spec) = std::env::var("OXIDE_LOG") else {
return;
};
for directive in spec.split(',') {
let Some((target, level_str)) = directive.split_once('=') else {
continue;
};
let Some(level) = parse_level(level_str.trim()) else {
continue;
};
match target.trim() {
"oxide" => {
crate::set_level(SubsystemId::Vm, level);
crate::set_level(SubsystemId::Ic, level);
crate::set_level(SubsystemId::Kernel, level);
crate::set_level(SubsystemId::Builtins, level);
}
"oxide::vm" => crate::set_level(SubsystemId::Vm, level),
"oxide::ic" => crate::set_level(SubsystemId::Ic, level),
"oxide::kernel" => crate::set_level(SubsystemId::Kernel, level),
"oxide::builtins" => crate::set_level(SubsystemId::Builtins, level),
_ => {}
}
}
}