CH-J Tools通过 SSH 管理服务器
菜单
已发布的源代码

CH-J Tools

浏览特定应用版本的目录和文件。

下载源代码 ZIP
CH-J / Server Manager 1704 个文件
0.1.2-beta 添加时间: · 构建时间:
logging.rs 添加时间: 未记录日期 5.2 KB · 162 行
代码 下载文件
1//! JSON-lines application log (port of `logging/logger.js` + `logService.js`).
2//! Field names that look like secrets are redacted recursively; callers must still
3//! never pass secrets inside free-text messages.
5use crate::core::error::CoreResult;
6use once_cell::sync::Lazy;
7use parking_lot::Mutex;
8use regex::Regex;
9use serde::Serialize;
10use serde_json::{json, Map, Value};
11use std::fs::{self, OpenOptions};
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::path::{Path, PathBuf};
15static SENSITIVE_KEYS: Lazy<Regex> =
16 Lazy::new(|| Regex::new(r"(?i)pass(word|phrase)?|private.?key|secret|token|credential").unwrap());
18pub const DEFAULT_TAIL: u64 = 256 * 1024;
19pub const MAX_TAIL: u64 = 1024 * 1024;
21pub fn sanitize(value: &Value, depth: usize) -> Value {
22 if depth > 5 {
23 return Value::String("[depth-limit]".into());
24 }
25 match value {
26 Value::Array(items) => Value::Array(items.iter().take(50).map(|v| sanitize(v, depth + 1)).collect()),
27 Value::Object(map) => {
28 let mut out = Map::new();
29 for (key, entry) in map {
30 if SENSITIVE_KEYS.is_match(key) {
31 out.insert(key.clone(), Value::String("[redacted]".into()));
32 } else {
33 out.insert(key.clone(), sanitize(entry, depth + 1));
34 }
35 }
36 Value::Object(out)
37 }
38 other => other.clone(),
39 }
42pub struct Logger {
43 path: PathBuf,
44 lock: Mutex<()>,
47impl Logger {
48 pub fn new(root: &Path) -> Self {
49 let dir = root.join("logs");
50 let _ = fs::create_dir_all(&dir);
51 Self { path: dir.join("core.log"), lock: Mutex::new(()) }
52 }
54 pub fn path(&self) -> &Path {
55 &self.path
56 }
58 pub fn write(&self, level: &str, message: &str, data: Value) {
59 let record = json!({
60 "timestamp": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
61 "level": level,
62 "message": message,
63 "data": sanitize(&if data.is_null() { json!({}) } else { data }, 0),
64 });
65 let line = format!("{record}\n");
66 let _guard = self.lock.lock();
67 let mut options = OpenOptions::new();
68 options.create(true).append(true);
69 #[cfg(unix)]
70 {
71 use std::os::unix::fs::OpenOptionsExt;
72 options.mode(0o600);
73 }
74 if let Ok(mut file) = options.open(&self.path) {
75 let _ = file.write_all(line.as_bytes());
76 }
77 if cfg!(debug_assertions) {
78 eprintln!("[{}] {} {}", level.to_uppercase(), message, record["data"]);
79 }
80 }
82 pub fn info(&self, message: &str, data: Value) {
83 self.write("info", message, data)
84 }
85 pub fn warn(&self, message: &str, data: Value) {
86 self.write("warn", message, data)
87 }
88 pub fn error(&self, message: &str, data: Value) {
89 self.write("error", message, data)
90 }
93#[derive(Serialize)]
94#[serde(rename_all = "camelCase")]
95pub struct LogTail {
96 pub text: String,
97 pub truncated: bool,
98 pub size: u64,
99 pub read_at: String,
102pub fn normalize_limit(value: Option<&Value>) -> u64 {
103 match value.and_then(Value::as_f64) {
104 Some(n) if n.fract() == 0.0 && n > 0.0 && n <= 9_007_199_254_740_991.0 => (n as u64).min(MAX_TAIL),
105 _ => DEFAULT_TAIL,
106 }
109pub fn read_tail(path: &Path, max_bytes: u64) -> CoreResult<LogTail> {
110 let now = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
111 let meta = match fs::metadata(path) {
112 Ok(meta) => meta,
113 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
114 return Ok(LogTail { text: String::new(), truncated: false, size: 0, read_at: now })
115 }
116 Err(e) => return Err(e.into()),
117 };
118 if !meta.is_file() {
119 return Err(crate::core::error::CoreError::plain("Application log is not a file."));
120 }
121 let size = meta.len();
122 let length = size.min(max_bytes);
123 let offset = size - length;
124 let mut file = fs::File::open(path)?;
125 file.seek(SeekFrom::Start(offset))?;
126 let mut buffer = Vec::with_capacity(length as usize);
127 file.take(length).read_to_end(&mut buffer)?;
128 let mut text = String::from_utf8_lossy(&buffer).to_string();
129 if offset > 0 {
130 text = match text.find('\n') {
131 Some(index) => text[index + 1..].to_string(),
132 None => String::new(),
133 };
134 }
135 Ok(LogTail { text, truncated: offset > 0, size, read_at: now })
138#[cfg(test)]
139mod tests {
140 use super::*;
142 #[test]
143 fn redacts_sensitive_fields() {
144 let value = sanitize(&json!({"password": "x", "nested": {"privateKey": "k", "ok": 1}, "sudoPassword": "y"}), 0);
145 assert_eq!(value["password"], "[redacted]");
146 assert_eq!(value["sudoPassword"], "[redacted]");
147 assert_eq!(value["nested"]["privateKey"], "[redacted]");
148 assert_eq!(value["nested"]["ok"], 1);
149 }
151 #[test]
152 fn tail_drops_partial_first_line() {
153 let dir = tempfile::tempdir().unwrap();
154 let logger = Logger::new(dir.path());
155 for i in 0..100 {
156 logger.info(&format!("line {i}"), json!({}));
157 }
158 let tail = read_tail(logger.path(), 300).unwrap();
159 assert!(tail.truncated);
160 assert!(tail.text.starts_with('{'));
161 }
SHA-256a845698d6600eae886f9fb7a7d90794430309988c72600e8fe17d7cc744466d6

CH-J Proprietary Software License 1.14 · 源代码依据 CH-J Proprietary Software License 1.14 提供。公开源代码不会改变许可条款,也不会授予额外权利。

压缩包 SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
文件