feat(project): init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/target
|
||||||
|
.env
|
||||||
|
Cargo.lock
|
||||||
24
Cargo.toml
Normal file
24
Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[package]
|
||||||
|
name = "rust_logger"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# 异步运行时
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
# 数据库 ORM (PostgreSQL)
|
||||||
|
sqlx = { version = "0.8", features = [
|
||||||
|
"runtime-tokio-native-tls",
|
||||||
|
"postgres",
|
||||||
|
"chrono",
|
||||||
|
] }
|
||||||
|
# 时间处理
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
# 异步 Trait 支持
|
||||||
|
async-trait = "0.1"
|
||||||
|
# 全局单例支持
|
||||||
|
once_cell = "1.19"
|
||||||
|
# 错误处理
|
||||||
|
anyhow = "1.0"
|
||||||
|
# 读取 .env 文件
|
||||||
|
dotenv = "0.15"
|
||||||
136
README.md
Normal file
136
README.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class LogLevel {
|
||||||
|
<<enumeration>>
|
||||||
|
DEBUG
|
||||||
|
INFO
|
||||||
|
WARNING
|
||||||
|
ERROR
|
||||||
|
FATAL
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogRecord {
|
||||||
|
+DateTime timestamp
|
||||||
|
+LogLevel level
|
||||||
|
+String message
|
||||||
|
+String target
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogOutput {
|
||||||
|
<<interface>>
|
||||||
|
+write(record: LogRecord) Future
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleOutput {
|
||||||
|
+write(record: LogRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostgresOutput {
|
||||||
|
-PgPool pool
|
||||||
|
+new(pool: PgPool)
|
||||||
|
+write(record: LogRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileOutput {
|
||||||
|
-File file
|
||||||
|
+write(record: LogRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoggerConfig {
|
||||||
|
+LogLevel min_level
|
||||||
|
+Vec~Box~LogOutput~~ outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
-Sender~LogRecord~ tx
|
||||||
|
-LogLevel min_level
|
||||||
|
+init(config: LoggerConfig) Arc~Logger~
|
||||||
|
+log(level, message, target)
|
||||||
|
-run_background_task(rx, outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
%% Relationships
|
||||||
|
LogOutput <|.. ConsoleOutput : Implements
|
||||||
|
LogOutput <|.. PostgresOutput : Implements
|
||||||
|
LogOutput <|.. FileOutput : Implements
|
||||||
|
|
||||||
|
Logger ..> LogRecord : Creates
|
||||||
|
Logger o-- LoggerConfig : Uses
|
||||||
|
LoggerConfig o-- LogOutput : Aggregates (0..*)
|
||||||
|
LogRecord -- LogLevel : Has
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
%% 接口定义:强调 Send + Sync 约束
|
||||||
|
class LogOutput {
|
||||||
|
<<interface>>
|
||||||
|
<<Send + Sync>>
|
||||||
|
+write(record: LogRecord) Future
|
||||||
|
}
|
||||||
|
|
||||||
|
%% 具体实现
|
||||||
|
class PostgresOutput {
|
||||||
|
-PgPool pool
|
||||||
|
+write(record)
|
||||||
|
}
|
||||||
|
class ConsoleOutput {
|
||||||
|
+write(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
%% 业务线程持有的 Logger (Producer)
|
||||||
|
class Logger {
|
||||||
|
<<Thread-Safe>>
|
||||||
|
<<Shared via Arc>>
|
||||||
|
-mpsc::Sender~LogRecord~ tx
|
||||||
|
-LogLevel min_level
|
||||||
|
+log(level, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
%% 后台异步任务 (Consumer)
|
||||||
|
class BackgroundWorker {
|
||||||
|
<<Active Object>>
|
||||||
|
<<Running in tokio::spawn>>
|
||||||
|
-mpsc::Receiver~LogRecord~ rx
|
||||||
|
-Vec~Box~LogOutput~~ outputs
|
||||||
|
+run()
|
||||||
|
}
|
||||||
|
|
||||||
|
%% 数据包
|
||||||
|
class LogRecord {
|
||||||
|
<<Immutable>>
|
||||||
|
+timestamp
|
||||||
|
+level
|
||||||
|
+message
|
||||||
|
}
|
||||||
|
|
||||||
|
%% 关系描述
|
||||||
|
LogOutput <|.. PostgresOutput
|
||||||
|
LogOutput <|.. ConsoleOutput
|
||||||
|
|
||||||
|
%% 关键的线程安全机制:MPSC Channel
|
||||||
|
Logger "1" o-- "1" `mpsc::Sender` : Owns
|
||||||
|
BackgroundWorker "1" o-- "1" `mpsc::Receiver` : Owns
|
||||||
|
|
||||||
|
%% 逻辑流
|
||||||
|
ClientThread ..> Logger : 1. Calls log() (Non-blocking)
|
||||||
|
Logger ..> `mpsc::Sender` : 2. Sends Record
|
||||||
|
`mpsc::Sender` ..> `mpsc::Receiver` : 3. Channel Transfer (Thread-Safe)
|
||||||
|
`mpsc::Receiver` ..> BackgroundWorker : 4. Receives Record
|
||||||
|
BackgroundWorker --> LogOutput : 5. Serialized Writes
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
my-logger/
|
||||||
|
├── Cargo.toml
|
||||||
|
├── .env # 数据库配置
|
||||||
|
├── src/
|
||||||
|
│ ├── lib.rs # 库入口,定义宏
|
||||||
|
│ ├── model.rs # 定义 LogLevel, LogRecord
|
||||||
|
│ ├── outputs/ # 输出模块
|
||||||
|
│ │ ├── mod.rs # Trait 定义
|
||||||
|
│ │ ├── console.rs # 控制台输出实现
|
||||||
|
│ │ └── postgres.rs # 数据库输出实现
|
||||||
|
│ ├── core.rs # Logger 核心逻辑 (Channel, Spawn)
|
||||||
|
│ └── main.rs # 模拟业务服务使用
|
||||||
|
```
|
||||||
26
init.sql
Normal file
26
init.sql
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
-- 1. 创建 rust_logger 专用用户
|
||||||
|
CREATE USER rust_logger_user WITH PASSWORD 'rust_logger_password';
|
||||||
|
|
||||||
|
-- 2. 创建 rust_logger 专用数据库
|
||||||
|
CREATE DATABASE rust_logger_db OWNER rust_logger_user;
|
||||||
|
|
||||||
|
-- 3. 赋予权限(确保它能在 rust_logger_db 库里创建 Schema)
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE rust_logger_db TO rust_logger_user;
|
||||||
|
|
||||||
|
CREATE TABLE app_logs (
|
||||||
|
id BIGSERIAL, -- 注意:后面会讲分区,分区表通常不直接用主键
|
||||||
|
service_name VARCHAR(50) NOT NULL, -- 新增:用于区分是哪个服务
|
||||||
|
log_level VARCHAR(10) NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
module VARCHAR(100),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL
|
||||||
|
) PARTITION BY RANGE (created_at); -- 核心优化:按时间分区
|
||||||
|
|
||||||
|
|
||||||
|
-- 建立索引(包含 service_name)
|
||||||
|
CREATE INDEX idx_logs_service_time ON app_logs(service_name, created_at);
|
||||||
|
|
||||||
|
-- create_next_month.sql
|
||||||
|
-- 这里的日期逻辑需要通过外部脚本(Python/Shell)动态生成 SQL 语句
|
||||||
|
CREATE TABLE IF NOT EXISTS app_logs_2026_01 PARTITION OF app_logs
|
||||||
|
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
|
||||||
73
src/core.rs
Normal file
73
src/core.rs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// src/core.rs
|
||||||
|
use crate::model::{LogLevel, LogRecord};
|
||||||
|
use crate::outputs::LogOutput;
|
||||||
|
use chrono::Utc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
// Logger 配置
|
||||||
|
pub struct LoggerConfig {
|
||||||
|
pub min_level: LogLevel,
|
||||||
|
// Box<dyn LogOutput> 允许我们在 Vec 中存放不同类型的 Output
|
||||||
|
pub outputs: Vec<Box<dyn LogOutput>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger 结构体
|
||||||
|
pub struct Logger {
|
||||||
|
tx: mpsc::Sender<LogRecord>,
|
||||||
|
min_level: LogLevel,
|
||||||
|
service_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
// 初始化函数:设置 channel 并启动后台任务
|
||||||
|
pub fn init(service_name: &str, config: LoggerConfig) -> Arc<Self> {
|
||||||
|
// 1. 创建异步通道 (缓冲区大小 1000)
|
||||||
|
let (tx, mut rx) = mpsc::channel::<LogRecord>(1000);
|
||||||
|
|
||||||
|
let min_level = config.min_level;
|
||||||
|
let outputs = Arc::new(config.outputs);
|
||||||
|
|
||||||
|
// 2. 启动后台 Worker (Consumer)
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// 不断从 channel 接收消息
|
||||||
|
while let Some(record) = rx.recv().await {
|
||||||
|
// 遍历所有输出目标并写入
|
||||||
|
// 这里可以用 join_all 并发写,也可以串行写,视 DB 压力而定
|
||||||
|
for output in outputs.iter() {
|
||||||
|
output.write(&record).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// channel 关闭后,任务自动结束
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 返回 Logger 实例 (Producer)
|
||||||
|
Arc::new(Self {
|
||||||
|
tx,
|
||||||
|
min_level,
|
||||||
|
service_name: service_name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送日志的方法
|
||||||
|
pub fn log(&self, level: LogLevel, message: String, module: String) {
|
||||||
|
// 1. 级别过滤
|
||||||
|
if level < self.min_level {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构造记录
|
||||||
|
let record = LogRecord {
|
||||||
|
service_name: self.service_name.clone(),
|
||||||
|
timestamp: Utc::now(),
|
||||||
|
level,
|
||||||
|
message,
|
||||||
|
module,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. 发送 (非阻塞,如果队列满则丢弃,防止拖死业务)
|
||||||
|
if self.tx.try_send(record).is_err() {
|
||||||
|
eprintln!("Logger warning: Channel full, dropping log message.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/lib.rs
Normal file
45
src/lib.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// src/lib.rs
|
||||||
|
pub mod core;
|
||||||
|
pub mod model;
|
||||||
|
pub mod outputs;
|
||||||
|
|
||||||
|
use crate::core::Logger;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
// 全局静态 Logger 实例
|
||||||
|
pub static GLOBAL_LOGGER: OnceCell<Arc<Logger>> = OnceCell::new();
|
||||||
|
|
||||||
|
// 初始化入口
|
||||||
|
pub fn set_global_logger(logger: Arc<Logger>) {
|
||||||
|
if GLOBAL_LOGGER.set(logger).is_err() {
|
||||||
|
eprintln!("Logger already initialized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义宏
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_info {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
if let Some(logger) = $crate::GLOBAL_LOGGER.get() {
|
||||||
|
logger.log(
|
||||||
|
$crate::model::LogLevel::INFO,
|
||||||
|
format!($($arg)*),
|
||||||
|
module_path!().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! log_error {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
if let Some(logger) = $crate::GLOBAL_LOGGER.get() {
|
||||||
|
logger.log(
|
||||||
|
$crate::model::LogLevel::ERROR,
|
||||||
|
format!($($arg)*),
|
||||||
|
module_path!().to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
37
src/model.rs
Normal file
37
src/model.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// src/model.rs
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
// 1. 日志级别,支持比较大小 (PartialOrd)
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
DEBUG = 0,
|
||||||
|
INFO = 1,
|
||||||
|
WARNING = 2,
|
||||||
|
ERROR = 3,
|
||||||
|
FATAL = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为了方便转换成字符串存库
|
||||||
|
impl fmt::Display for LogLevel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let level_str = match self {
|
||||||
|
LogLevel::DEBUG => "DEBUG",
|
||||||
|
LogLevel::INFO => "INFO",
|
||||||
|
LogLevel::WARNING => "WARNING",
|
||||||
|
LogLevel::ERROR => "ERROR",
|
||||||
|
LogLevel::FATAL => "FATAL",
|
||||||
|
};
|
||||||
|
write!(f, "{}", level_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 日志记录实体
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LogRecord {
|
||||||
|
pub service_name: String, // 新增
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
pub level: LogLevel,
|
||||||
|
pub message: String,
|
||||||
|
pub module: String,
|
||||||
|
}
|
||||||
20
src/outputs/console.rs
Normal file
20
src/outputs/console.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
// src/outputs/console.rs
|
||||||
|
use super::LogOutput;
|
||||||
|
use crate::model::LogRecord;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
pub struct ConsoleOutput;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LogOutput for ConsoleOutput {
|
||||||
|
async fn write(&self, record: &LogRecord) {
|
||||||
|
// 简单的控制台打印,生产环境可以加上颜色代码
|
||||||
|
println!(
|
||||||
|
"[{}] [{}] ({}) - {}",
|
||||||
|
record.timestamp.format("%Y-%m-%d %H:%M:%S"),
|
||||||
|
record.level,
|
||||||
|
record.module,
|
||||||
|
record.message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/outputs/mod.rs
Normal file
12
src/outputs/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// src/outputs/mod.rs
|
||||||
|
pub mod console;
|
||||||
|
pub mod postgres;
|
||||||
|
|
||||||
|
use crate::model::LogRecord;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
// 定义 Trait,要求实现者必须是线程安全的 (Send + Sync)
|
||||||
|
#[async_trait]
|
||||||
|
pub trait LogOutput: Send + Sync {
|
||||||
|
async fn write(&self, record: &LogRecord);
|
||||||
|
}
|
||||||
39
src/outputs/postgres.rs
Normal file
39
src/outputs/postgres.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// src/outputs/postgres.rs
|
||||||
|
use super::LogOutput;
|
||||||
|
use crate::model::LogRecord;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
pub struct PostgresOutput {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostgresOutput {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl LogOutput for PostgresOutput {
|
||||||
|
async fn write(&self, record: &LogRecord) {
|
||||||
|
let query = r#"
|
||||||
|
INSERT INTO app_logs (log_level, message, module, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
// 注意:这里的 write 是在后台任务中执行的,就算慢也不会阻塞主业务
|
||||||
|
// 我们忽略错误,因为如果日志系统挂了,不能让它导致业务逻辑崩溃 (Panic)
|
||||||
|
// 生产环境可以考虑加一个 fallback 机制(比如降级写文件)
|
||||||
|
if let Err(e) = sqlx::query(query)
|
||||||
|
.bind(record.level.to_string())
|
||||||
|
.bind(&record.message)
|
||||||
|
.bind(&record.module)
|
||||||
|
.bind(record.timestamp)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!("Failed to write log to PostgreSQL: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user