feat: 后端支持输出日志

This commit is contained in:
lanyeeee
2025-07-11 05:38:53 +08:00
parent 13e07fd9b3
commit 3ca3a48c38
11 changed files with 521 additions and 2 deletions
+196
View File
@@ -268,6 +268,7 @@ name = "bilibili-video-downloader"
version = "0.1.0"
dependencies = [
"anyhow",
"notify",
"parking_lot",
"serde",
"serde_json",
@@ -277,6 +278,10 @@ dependencies = [
"tauri-build",
"tauri-plugin-opener",
"tauri-specta",
"tokio",
"tracing",
"tracing-appender",
"tracing-subscriber",
]
[[package]]
@@ -1011,6 +1016,15 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]]
name = "futf"
version = "0.1.5"
@@ -1721,6 +1735,26 @@ dependencies = [
"cfb",
]
[[package]]
name = "inotify"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.1",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
"libc",
]
[[package]]
name = "io-uring"
version = "0.7.8"
@@ -1861,6 +1895,26 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "kqueue"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
dependencies = [
"kqueue-sys",
"libc",
]
[[package]]
name = "kqueue-sys"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
dependencies = [
"bitflags 1.3.2",
"libc",
]
[[package]]
name = "kuchikiki"
version = "0.8.8-speedreader"
@@ -2032,6 +2086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -2112,6 +2167,40 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "notify"
version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
dependencies = [
"bitflags 2.9.1",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"walkdir",
"windows-sys 0.60.2",
]
[[package]]
name = "notify-types"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -2149,6 +2238,15 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "objc-sys"
version = "0.3.5"
@@ -2406,6 +2504,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "pango"
version = "0.18.3"
@@ -3288,6 +3392,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
@@ -3944,6 +4057,15 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]]
name = "time"
version = "0.3.41"
@@ -3952,7 +4074,9 @@ checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
@@ -3996,12 +4120,26 @@ dependencies = [
"io-uring",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"tokio-macros",
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "tokio-util"
version = "0.7.15"
@@ -4134,6 +4272,18 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf"
dependencies = [
"crossbeam-channel",
"thiserror 1.0.69",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.30"
@@ -4152,6 +4302,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-serde"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
dependencies = [
"serde",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
dependencies = [
"nu-ansi-term",
"serde",
"serde_json",
"sharded-slab",
"smallvec",
"thread_local",
"time",
"tracing-core",
"tracing-log",
"tracing-serde",
]
[[package]]
@@ -4306,6 +4496,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]]
name = "version-compare"
version = "0.2.0"
+5
View File
@@ -29,6 +29,11 @@ specta-typescript = { version = "=0.0.7" }
anyhow = { version = "1.0.98" }
parking_lot = { version = "0.12.4", features = ["send_guard"] }
tracing = { version = "0.1.41" }
tracing-subscriber = { version = "0.3.19", features = ["json", "time", "local-time"] }
tracing-appender = { version = "0.2.3" }
notify = { version = "8.0.0" }
tokio = { version = "1.46.0", features = ["full"] }
[profile.release]
strip = true
+2
View File
@@ -8,6 +8,7 @@ use tauri::{AppHandle, Manager};
#[serde(rename_all = "camelCase")]
pub struct Config {
pub download_dir: PathBuf,
pub enable_file_logger: bool,
}
impl Config {
@@ -64,6 +65,7 @@ impl Config {
fn default(app_data_dir: &Path) -> Config {
Config {
download_dir: app_data_dir.join("视频下载"),
enable_file_logger: true,
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use specta::Type;
use crate::extensions::AnyhowErrorToStringChain;
pub type CommandResult<T> = Result<T, CommandError>;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub struct CommandError {
pub err_title: String,
pub err_message: String,
}
impl CommandError {
pub fn from<E>(err_title: &str, err: E) -> Self
where
E: Into<anyhow::Error>,
{
let string_chain = err.into().to_string_chain();
tracing::error!(err_title, message = string_chain);
Self {
err_title: err_title.to_string(),
err_message: string_chain,
}
}
}
+19
View File
@@ -0,0 +1,19 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri_specta::Event;
use crate::types::LogLevel;
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
#[serde(rename_all = "camelCase")]
pub struct LogEvent {
pub timestamp: String,
pub level: LogLevel,
pub fields: HashMap<String, serde_json::Value>,
pub target: String,
pub filename: String,
#[serde(rename = "line_number")]
pub line_number: i64,
}
+15
View File
@@ -1,3 +1,8 @@
use parking_lot::RwLock;
use tauri::{Manager, State};
use crate::config::Config;
pub trait AnyhowErrorToStringChain {
/// 将 `anyhow::Error` 转换为chain格式
/// # Example
@@ -18,3 +23,13 @@ impl AnyhowErrorToStringChain for anyhow::Error {
})
}
}
pub trait AppHandleExt {
fn get_config(&self) -> State<RwLock<Config>>;
}
impl AppHandleExt for tauri::AppHandle {
fn get_config(&self) -> State<RwLock<Config>> {
self.state::<RwLock<Config>>()
}
}
+9 -1
View File
@@ -1,6 +1,10 @@
mod commands;
mod config;
mod errors;
mod events;
mod extensions;
mod logger;
mod types;
mod utils;
use anyhow::Context;
@@ -9,6 +13,8 @@ use config::Config;
use parking_lot::RwLock;
use tauri::{Manager, Wry};
use crate::events::LogEvent;
fn generate_context() -> tauri::Context<Wry> {
tauri::generate_context!()
}
@@ -17,7 +23,7 @@ fn generate_context() -> tauri::Context<Wry> {
pub fn run() {
let builder = tauri_specta::Builder::<Wry>::new()
.commands(tauri_specta::collect_commands![greet, get_config])
.events(tauri_specta::collect_events![]);
.events(tauri_specta::collect_events![LogEvent]);
#[cfg(debug_assertions)]
builder
@@ -47,6 +53,8 @@ pub fn run() {
let config = RwLock::new(Config::new(app.handle())?);
app.manage(config);
logger::init(app.handle())?;
Ok(())
})
.run(generate_context())
+221
View File
@@ -0,0 +1,221 @@
use std::{io::Write, sync::OnceLock};
use anyhow::Context;
use notify::{RecommendedWatcher, Watcher};
use tauri::{AppHandle, Manager};
use tauri_specta::Event;
use tracing::{Level, Subscriber};
use tracing_appender::{
non_blocking::WorkerGuard,
rolling::{RollingFileAppender, Rotation},
};
use tracing_subscriber::{
filter::{filter_fn, FilterExt, Targets},
fmt::{layer, time::LocalTime},
layer::SubscriberExt,
registry::LookupSpan,
util::SubscriberInitExt,
Layer, Registry,
};
use crate::{
events::LogEvent,
extensions::{AnyhowErrorToStringChain, AppHandleExt},
};
struct LogEventWriter {
app: AppHandle,
}
impl Write for LogEventWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let log_string = String::from_utf8_lossy(buf);
match serde_json::from_str::<LogEvent>(&log_string) {
Ok(log_event) => {
let _ = log_event.emit(&self.app);
}
Err(err) => {
let log_string = log_string.to_string();
let err_msg = err.to_string();
tracing::error!(log_string, err_msg, "将日志字符串解析为LogEvent失败");
}
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
static RELOAD_FN: OnceLock<Box<dyn Fn() -> anyhow::Result<()> + Send + Sync>> = OnceLock::new();
static GUARD: OnceLock<parking_lot::Mutex<Option<WorkerGuard>>> = OnceLock::new();
pub fn init(app: &AppHandle) -> anyhow::Result<()> {
let lib_module_path = module_path!();
let lib_target = lib_module_path.split("::").next().context(format!(
"解析lib_target失败: lib_module_path={lib_module_path}"
))?;
// 过滤掉来自其他库的日志
let target_filter = Targets::new().with_target(lib_target, Level::TRACE);
// 输出到文件
let (file_layer, guard) = create_file_layer(app)?;
let (reloadable_file_layer, reload_handle) = tracing_subscriber::reload::Layer::new(file_layer);
// 输出到控制台
let console_layer = layer()
.with_writer(std::io::stdout)
.with_timer(LocalTime::rfc_3339())
.with_file(true)
.with_line_number(true);
// 发送到前端
let log_event_writer = std::sync::Mutex::new(LogEventWriter { app: app.clone() });
let log_event_layer = layer()
.with_writer(log_event_writer)
.with_timer(LocalTime::rfc_3339())
.with_file(true)
.with_line_number(true)
.json()
// 过滤掉来自这个文件的日志(LogEvent解析失败的日志),避免无限递归
.with_filter(target_filter.clone().and(filter_fn(|metadata| {
metadata.module_path() != Some(lib_module_path)
})));
Registry::default()
.with(target_filter)
.with(reloadable_file_layer)
.with(console_layer)
.with(log_event_layer)
.init();
GUARD.get_or_init(|| parking_lot::Mutex::new(guard));
RELOAD_FN.get_or_init(move || {
let app = app.clone();
Box::new(move || {
let (file_layer, guard) = create_file_layer(&app)?;
reload_handle.reload(file_layer).context("reload失败")?;
*GUARD.get().context("GUARD未初始化")?.lock() = guard;
Ok(())
})
});
tauri::async_runtime::spawn(file_log_watcher(app.clone()));
Ok(())
}
pub fn reload_file_logger() -> anyhow::Result<()> {
RELOAD_FN.get().context("RELOAD_FN未初始化")?()
}
pub fn disable_file_logger() -> anyhow::Result<()> {
if let Some(guard) = GUARD.get().context("GUARD未初始化")?.lock().take() {
drop(guard);
}
Ok(())
}
fn create_file_layer<S>(
app: &AppHandle,
) -> anyhow::Result<(Box<dyn Layer<S> + Send + Sync>, Option<WorkerGuard>)>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
let enable_file_logger = app.get_config().read().enable_file_logger;
// 如果不启用文件日志,则返回一个占位用的sink layer,不创建也不输出日志文件
if !enable_file_logger {
let sink_layer = layer()
.with_writer(std::io::sink)
.with_timer(LocalTime::rfc_3339())
.with_ansi(false)
.with_file(true)
.with_line_number(true);
return Ok((Box::new(sink_layer), None));
}
let logs_dir = logs_dir(app).context("获取日志目录失败")?;
let file_appender = RollingFileAppender::builder()
.filename_prefix("bilibili-video-downloader")
.filename_suffix("log")
.rotation(Rotation::DAILY)
.build(&logs_dir)
.context("创建RollingFileAppender失败")?;
let (non_blocking_appender, guard) = tracing_appender::non_blocking(file_appender);
let file_layer = layer()
.with_writer(non_blocking_appender)
.with_timer(LocalTime::rfc_3339())
.with_ansi(false)
.with_file(true)
.with_line_number(true);
Ok((Box::new(file_layer), Some(guard)))
}
async fn file_log_watcher(app: AppHandle) {
let (sender, mut receiver) = tokio::sync::mpsc::channel(1);
let event_handler = move |res| {
tauri::async_runtime::block_on(async {
if let Err(err) = sender.send(res).await.map_err(anyhow::Error::from) {
let err_title = "发送日志文件watcher事件失败";
let string_chain = err.to_string_chain();
tracing::error!(err_title, message = string_chain);
}
});
};
let mut watcher = match RecommendedWatcher::new(event_handler, notify::Config::default())
.map_err(anyhow::Error::from)
{
Ok(watcher) => watcher,
Err(err) => {
let err_title = "创建日志文件watcher失败";
let string_chain = err.to_string_chain();
tracing::error!(err_title, message = string_chain);
return;
}
};
let logs_dir = match logs_dir(&app) {
Ok(logs_dir) => logs_dir,
Err(err) => {
let err_title = "日志文件watcher获取日志目录失败";
let string_chain = err.to_string_chain();
tracing::error!(err_title, message = string_chain);
return;
}
};
if let Err(err) = watcher
.watch(&logs_dir, notify::RecursiveMode::NonRecursive)
.map_err(anyhow::Error::from)
{
let err_title = "日志文件watcher监听日志目录失败";
let string_chain = err.to_string_chain();
tracing::error!(err_title, message = string_chain);
return;
}
while let Some(res) = receiver.recv().await {
match res.map_err(anyhow::Error::from) {
Ok(event) => {
if let notify::EventKind::Remove(_) = event.kind {
if let Err(err) = reload_file_logger() {
let err_title = "重置日志文件失败";
let string_chain = err.to_string_chain();
tracing::error!(err_title, message = string_chain);
}
}
}
Err(err) => {
let err_title = "接收日志文件watcher事件失败";
let string_chain = err.to_string_chain();
tracing::error!(err_title, message = string_chain);
}
}
}
}
pub fn logs_dir(app: &AppHandle) -> anyhow::Result<std::path::PathBuf> {
let app_data_dir = app
.path()
.app_data_dir()
.context("获取app_data_dir目录失败")?;
Ok(app_data_dir.join("日志"))
}
+16
View File
@@ -0,0 +1,16 @@
use serde::{Deserialize, Serialize};
use specta::Type;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type)]
pub enum LogLevel {
#[serde(rename = "TRACE")]
Trace,
#[serde(rename = "DEBUG")]
Debug,
#[serde(rename = "INFO")]
Info,
#[serde(rename = "WARN")]
Warn,
#[serde(rename = "ERROR")]
Error,
}
+3
View File
@@ -0,0 +1,3 @@
mod log_level;
pub use log_level::*;
+9 -1
View File
@@ -16,6 +16,11 @@ async getConfig() : Promise<Config> {
/** user-defined events **/
export const events = __makeEvents__<{
logEvent: LogEvent
}>({
logEvent: "log-event"
})
/** user-defined constants **/
@@ -23,7 +28,10 @@ async getConfig() : Promise<Config> {
/** user-defined types **/
export type Config = { downloadDir: string }
export type Config = { downloadDir: string; enableFileLogger: boolean }
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key in string]: JsonValue }
export type LogEvent = { timestamp: string; level: LogLevel; fields: { [key in string]: JsonValue }; target: string; filename: string; line_number: number }
export type LogLevel = "TRACE" | "DEBUG" | "INFO" | "WARN" | "ERROR"
/** tauri-specta globals **/