diff --git a/Cargo.lock b/Cargo.lock index 0b93890..8118c16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2197,6 +2197,7 @@ dependencies = [ "sys-locale", "tabled", "tachyonix", + "tempfile", "thiserror 1.0.63", "thunk-rs", "tikv-jemalloc-ctl", @@ -2213,7 +2214,6 @@ dependencies = [ "toml 0.8.19", "tonic-build", "tracing", - "tracing-appender", "tracing-subscriber", "tun-easytier", "url", @@ -2636,9 +2636,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" @@ -8843,15 +8843,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.12.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ - "cfg-if", "fastrand", + "getrandom 0.3.2", "once_cell", - "rustix 0.38.34", - "windows-sys 0.59.0", + "rustix 1.0.7", + "windows-sys 0.60.2", ] [[package]] @@ -9480,18 +9480,6 @@ 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.63", - "time", - "tracing-subscriber", -] - [[package]] name = "tracing-attributes" version = "0.1.28" diff --git a/easytier-gui/src-tauri/src/lib.rs b/easytier-gui/src-tauri/src/lib.rs index 7acf558..2cb646e 100644 --- a/easytier-gui/src-tauri/src/lib.rs +++ b/easytier-gui/src-tauri/src/lib.rs @@ -216,6 +216,8 @@ pub fn run() { dir: Some(log_dir.to_string_lossy().to_string()), level: None, file: None, + size_mb: None, + count: None, }) .build() .map_err(|e| e.to_string())?; diff --git a/easytier-web/src/main.rs b/easytier-web/src/main.rs index b594956..35a2cd7 100644 --- a/easytier-web/src/main.rs +++ b/easytier-web/src/main.rs @@ -119,6 +119,8 @@ impl LoggingConfigLoader for &Cli { dir: self.file_log_dir.clone(), level: self.file_log_level.clone(), file: None, + size_mb: None, + count: None, } } } diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index d5524fb..72e9216 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -36,7 +36,6 @@ tracing-subscriber = { version = "0.3", features = [ "local-time", "time", ] } -tracing-appender = "0.2.3" console-subscriber = { version = "0.4.1", optional = true } thiserror = "1.0" auto_impl = "1.1.0" @@ -296,6 +295,7 @@ serial_test = "3.0.0" rstest = "0.25.0" futures-util = "0.3.30" maplit = "1.0.2" +tempfile = "3.22.0" [target.'cfg(target_os = "linux")'.dev-dependencies] defguard_wireguard_rs = "0.4.2" diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 2dfa8f5..542cad1 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -217,6 +217,12 @@ core_clap: check_config: en: Check config validity without starting the network zh-CN: 检查配置文件的有效性并退出 + file_log_size_mb: + en: "per file log size in MB, default is 100MB" + zh-CN: "单个文件日志大小,单位 MB,默认值为 100MB" + file_log_count: + en: "max file log count, default is 10" + zh-CN: "最大文件日志数量,默认值为 10" core_app: panic_backtrace_save: diff --git a/easytier/src/common/config.rs b/easytier/src/common/config.rs index ef6913b..ada2fbb 100644 --- a/easytier/src/common/config.rs +++ b/easytier/src/common/config.rs @@ -312,6 +312,8 @@ pub struct FileLoggerConfig { pub level: Option, pub file: Option, pub dir: Option, + pub size_mb: Option, + pub count: Option, } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] diff --git a/easytier/src/common/mod.rs b/easytier/src/common/mod.rs index 7d2572c..8663206 100644 --- a/easytier/src/common/mod.rs +++ b/easytier/src/common/mod.rs @@ -26,6 +26,7 @@ pub mod stats_manager; pub mod stun; pub mod stun_codec_ext; pub mod token_bucket; +pub mod tracing_rolling_appender; pub fn get_logger_timer( format: F, diff --git a/easytier/src/common/tracing_rolling_appender/LICENSE b/easytier/src/common/tracing_rolling_appender/LICENSE new file mode 100644 index 0000000..3872c78 --- /dev/null +++ b/easytier/src/common/tracing_rolling_appender/LICENSE @@ -0,0 +1,242 @@ +This module is copied and modified from https://github.com/cavivie/tracing-rolling-file + +tracing-rolling-file is dual-licensed under The MIT License [1] and +Apache 2.0 License [2]. + +Copyright (c) 2021 Cavivie and contributors. + +This is same as the Rust Project's own license. + + +[1]: , which is reproduced below: + +~~~~ +The MIT License (MIT) + +Copyright (c) 2021, eFolder Inc dba Axcient. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +~~~~ + + +[2]: , which is reproduced below: + +~~~~ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +~~~~ \ No newline at end of file diff --git a/easytier/src/common/tracing_rolling_appender/base.rs b/easytier/src/common/tracing_rolling_appender/base.rs new file mode 100644 index 0000000..99af38d --- /dev/null +++ b/easytier/src/common/tracing_rolling_appender/base.rs @@ -0,0 +1,517 @@ +use super::*; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct RollingConditionBase { + last_write_opt: Option>, + frequency_opt: Option, + max_size_opt: Option, +} + +impl RollingConditionBase { + /// Constructs a new struct that does not yet have any condition set. + pub fn new() -> RollingConditionBase { + RollingConditionBase { + last_write_opt: None, + frequency_opt: None, + max_size_opt: None, + } + } + + /// Sets a condition to rollover on the given frequency + pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBase { + self.frequency_opt = Some(x); + self + } + + /// Sets a condition to rollover when the date changes + pub fn daily(mut self) -> RollingConditionBase { + self.frequency_opt = Some(RollingFrequency::EveryDay); + self + } + + /// Sets a condition to rollover when the date or hour changes + pub fn hourly(mut self) -> RollingConditionBase { + self.frequency_opt = Some(RollingFrequency::EveryHour); + self + } + + /// Sets a condition to rollover when the date or minute changes + pub fn minutely(mut self) -> RollingConditionBase { + self.frequency_opt = Some(RollingFrequency::EveryMinute); + self + } + + /// Sets a condition to rollover when a certain size is reached + pub fn max_size(mut self, x: u64) -> RollingConditionBase { + self.max_size_opt = Some(x); + self + } +} + +impl Default for RollingConditionBase { + fn default() -> Self { + RollingConditionBase::new().frequency(RollingFrequency::EveryDay) + } +} + +impl RollingCondition for RollingConditionBase { + fn should_rollover(&mut self, now: &DateTime, current_filesize: u64) -> bool { + let mut rollover = false; + if let Some(frequency) = self.frequency_opt.as_ref() { + if let Some(last_write) = self.last_write_opt.as_ref() { + if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) { + rollover = true; + } + } + } + if let Some(max_size) = self.max_size_opt.as_ref() { + if current_filesize >= *max_size { + rollover = true; + } + } + self.last_write_opt = Some(*now); + rollover + } +} + +pub struct RollingFileAppenderBaseBuilder { + condition: RollingConditionBase, + filename: String, + max_filecount: usize, + current_filesize: u64, + writer_opt: Option>, +} + +impl Default for RollingFileAppenderBaseBuilder { + fn default() -> Self { + RollingFileAppenderBaseBuilder { + condition: RollingConditionBase::default(), + filename: String::new(), + max_filecount: 10, + current_filesize: 0, + writer_opt: None, + } + } +} + +impl RollingFileAppenderBaseBuilder { + /// Sets the log filename. Uses absolute path if provided, otherwise + /// creates files in the current working directory. + pub fn filename(mut self, filename: String) -> Self { + self.filename = filename; + self + } + + /// Sets a condition for the maximum number of files to create before rolling + /// over and deleting the oldest one. + pub fn max_filecount(mut self, max_filecount: usize) -> Self { + self.max_filecount = max_filecount; + self + } + + /// Sets a condition to rollover on a daily basis + pub fn condition_daily(mut self) -> Self { + self.condition.frequency_opt = Some(RollingFrequency::EveryDay); + self + } + + /// Sets a condition to rollover when the date or hour changes + pub fn condition_hourly(mut self) -> Self { + self.condition.frequency_opt = Some(RollingFrequency::EveryHour); + self + } + + /// Sets a condition to rollover when the date or minute changes + pub fn condition_minutely(mut self) -> Self { + self.condition.frequency_opt = Some(RollingFrequency::EveryMinute); + self + } + + /// Sets a condition to rollover when a certain size is reached + pub fn condition_max_file_size(mut self, x: u64) -> Self { + self.condition.max_size_opt = Some(x); + self + } + + /// Builds a RollingFileAppenderBase instance from the current settings. + /// + /// Returns an error if the filename is empty. + pub fn build(self) -> Result { + if self.filename.is_empty() { + return Err("A filename is required to be set and can not be blank"); + } + Ok(RollingFileAppenderBase { + condition: self.condition, + filename: self.filename, + max_filecount: self.max_filecount, + current_filesize: self.current_filesize, + writer_opt: self.writer_opt, + }) + } +} + +impl RollingFileAppenderBase { + /// Creates a new rolling file appender builder instance with the default + /// settings without a filename set. + pub fn builder() -> RollingFileAppenderBaseBuilder { + RollingFileAppenderBaseBuilder::default() + } +} + +/// A rolling file appender with a rolling condition based on date/time or size. +pub type RollingFileAppenderBase = RollingFileAppender; + +// LCOV_EXCL_START +#[cfg(test)] +mod test { + use super::*; + + struct Context { + _tempdir: tempfile::TempDir, + rolling: RollingFileAppenderBase, + } + + impl Context { + fn verify_contains(&mut self, needle: &str, n: usize) { + self.rolling.flush().unwrap(); + let p = self.rolling.filename_for(n); + let haystack = fs::read_to_string(&p).unwrap(); + if !haystack.contains(needle) { + panic!("file {:?} did not contain expected contents {}", p, needle); + } + } + } + + fn build_context(condition: RollingConditionBase, max_files: usize) -> Context { + let tempdir = tempfile::tempdir().unwrap(); + let filename = tempdir.path().join("test.log"); + let rolling = RollingFileAppenderBase::new(filename, condition, max_files).unwrap(); + Context { + _tempdir: tempdir, + rolling, + } + } + + fn build_builder_context(mut builder: RollingFileAppenderBaseBuilder) -> Context { + if builder.filename.is_empty() { + builder = builder.filename(String::from("test.log")); + } + let tempdir = tempfile::tempdir().unwrap(); + let filename = tempdir.path().join(&builder.filename); + builder = builder.filename(String::from(filename.as_os_str().to_str().unwrap())); + Context { + _tempdir: tempdir, + rolling: builder.build().unwrap(), + } + } + + #[test] + fn frequency_every_day() { + let mut c = build_context(RollingConditionBase::new().daily(), 9); + c.rolling + .write_with_datetime( + b"Line 1\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 2\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 3\n", + &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 4\n", + &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 5\n", + &Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(4)).exists()); + c.verify_contains("Line 1", 3); + c.verify_contains("Line 2", 3); + c.verify_contains("Line 3", 2); + c.verify_contains("Line 4", 1); + c.verify_contains("Line 5", 0); + } + + #[test] + fn frequency_every_day_limited_files() { + let mut c = build_context(RollingConditionBase::new().daily(), 2); + c.rolling + .write_with_datetime( + b"Line 1\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 2\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 3\n", + &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 4\n", + &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 5\n", + &Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(4)).exists()); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(3)).exists()); + c.verify_contains("Line 3", 2); + c.verify_contains("Line 4", 1); + c.verify_contains("Line 5", 0); + } + + #[test] + fn frequency_every_hour() { + let mut c = build_context(RollingConditionBase::new().hourly(), 9); + c.rolling + .write_with_datetime( + b"Line 1\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 2\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 2).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 3\n", + &Local.with_ymd_and_hms(2021, 3, 30, 2, 1, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 4\n", + &Local.with_ymd_and_hms(2021, 3, 31, 2, 1, 0).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(3)).exists()); + c.verify_contains("Line 1", 2); + c.verify_contains("Line 2", 2); + c.verify_contains("Line 3", 1); + c.verify_contains("Line 4", 0); + } + + #[test] + fn frequency_every_minute() { + let mut c = build_context( + RollingConditionBase::new().frequency(RollingFrequency::EveryMinute), + 9, + ); + c.rolling + .write_with_datetime( + b"Line 1\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 2\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 3\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 4).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 4\n", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 5\n", + &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"Line 6\n", + &Local.with_ymd_and_hms(2022, 3, 30, 2, 3, 0).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(4)).exists()); + c.verify_contains("Line 1", 3); + c.verify_contains("Line 2", 3); + c.verify_contains("Line 3", 3); + c.verify_contains("Line 4", 2); + c.verify_contains("Line 5", 1); + c.verify_contains("Line 6", 0); + } + + #[test] + fn max_size() { + let mut c = build_context(RollingConditionBase::new().max_size(10), 9); + c.rolling + .write_with_datetime( + b"12345", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"6789", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap()) + .unwrap(); + c.rolling + .write_with_datetime( + b"abcdefghijkl", + &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"ZZZ", + &Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(3)).exists()); + c.verify_contains("1234567890", 2); + c.verify_contains("abcdefghijkl", 1); + c.verify_contains("ZZZ", 0); + } + + #[test] + fn max_size_existing() { + let mut c = build_context(RollingConditionBase::new().max_size(10), 9); + c.rolling + .write_with_datetime( + b"12345", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + // close the file and make sure that it can re-open it, and that it + // resets the file size properly. + c.rolling.writer_opt.take(); + c.rolling.current_filesize = 0; + c.rolling + .write_with_datetime( + b"6789", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap()) + .unwrap(); + c.rolling + .write_with_datetime( + b"abcdefghijkl", + &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"ZZZ", + &Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(3)).exists()); + c.verify_contains("1234567890", 2); + c.verify_contains("abcdefghijkl", 1); + c.verify_contains("ZZZ", 0); + } + + #[test] + fn daily_and_max_size() { + let mut c = build_context(RollingConditionBase::new().daily().max_size(10), 9); + c.rolling + .write_with_datetime( + b"12345", + &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"6789", + &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap()) + .unwrap(); + c.rolling + .write_with_datetime( + b"abcdefghijkl", + &Local.with_ymd_and_hms(2021, 3, 31, 3, 3, 3).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"ZZZ", + &Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap(), + ) + .unwrap(); + assert!(!AsRef::::as_ref(&c.rolling.filename_for(3)).exists()); + c.verify_contains("123456789", 2); + c.verify_contains("0abcdefghijkl", 1); + c.verify_contains("ZZZ", 0); + } + + #[test] + fn rolling_file_appender_builder() { + let builder = RollingFileAppender::builder(); + + let builder = builder.condition_daily().condition_max_file_size(10); + let mut c = build_builder_context(builder); + c.rolling + .write_with_datetime( + b"abcdefghijklmnop", + &Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap(), + ) + .unwrap(); + c.rolling + .write_with_datetime( + b"12345678", + &Local.with_ymd_and_hms(2021, 3, 31, 5, 4, 4).unwrap(), + ) + .unwrap(); + assert!(AsRef::::as_ref(&c.rolling.filename_for(1)).exists()); + assert!(Path::new(&c.rolling.filename_for(0)).exists()); + c.verify_contains("abcdefghijklmnop", 1); + c.verify_contains("12345678", 0); + } + + #[test] + fn rolling_file_appender_builder_no_filename() { + let builder = RollingFileAppender::builder(); + let appender = builder.condition_daily().build(); + assert!(appender.is_err()); + } +} +// LCOV_EXCL_STOP diff --git a/easytier/src/common/tracing_rolling_appender/mod.rs b/easytier/src/common/tracing_rolling_appender/mod.rs new file mode 100644 index 0000000..bc47c3a --- /dev/null +++ b/easytier/src/common/tracing_rolling_appender/mod.rs @@ -0,0 +1,224 @@ +#![deny(warnings)] + +use chrono::prelude::*; +use std::{ + convert::TryFrom, + fs::{self, File, OpenOptions}, + io::{self, BufWriter, Write}, + path::Path, +}; + +/// Determines when a file should be "rolled over". +pub trait RollingCondition { + /// Determine and return whether or not the file should be rolled over. + fn should_rollover(&mut self, now: &DateTime, current_filesize: u64) -> bool; +} + +/// Determines how often a file should be rolled over +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum RollingFrequency { + EveryDay, + EveryHour, + EveryMinute, +} + +impl RollingFrequency { + /// Calculates a datetime that will be different if data should be in + /// different files. + pub fn equivalent_datetime(&self, dt: &DateTime) -> DateTime { + let (year, month, day) = (dt.year(), dt.month(), dt.day()); + let (hour, min, sec) = match self { + RollingFrequency::EveryDay => (0, 0, 0), + RollingFrequency::EveryHour => (dt.hour(), 0, 0), + RollingFrequency::EveryMinute => (dt.hour(), dt.minute(), 0), + }; + Local + .with_ymd_and_hms(year, month, day, hour, min, sec) + .unwrap() + } +} + +/// Writes data to a file, and "rolls over" to preserve older data in +/// a separate set of files. Old files have a Debian-style naming scheme +/// where we have base_filename, base_filename.1, ..., base_filename.N +/// where N is the maximum number of rollover files to keep. +#[derive(Debug)] +pub struct RollingFileAppender +where + RC: RollingCondition, +{ + condition: RC, + filename: String, + max_filecount: usize, + current_filesize: u64, + writer_opt: Option>, +} + +impl RollingFileAppender +where + RC: RollingCondition, +{ + /// Creates a new rolling file appender with the given condition. + /// The filename parent path must already exist. + pub fn new( + filename: impl AsRef, + condition: RC, + max_filecount: usize, + ) -> io::Result> { + let filename = filename.as_ref().to_str().unwrap().to_string(); + let mut appender = RollingFileAppender { + condition, + filename, + max_filecount, + current_filesize: 0, + writer_opt: None, + }; + // Fail if we can't open the file initially... + appender.open_writer_if_needed()?; + Ok(appender) + } + + /// Determines the final filename, where n==0 indicates the current file + fn filename_for(&self, n: usize) -> String { + let f = self.filename.clone(); + if n > 0 { + format!("{}.{}", f, n) + } else { + f + } + } + + /// Rotates old files to make room for a new one. + /// This may result in the deletion of the oldest file + fn rotate_files(&mut self) -> io::Result<()> { + // ignore any failure removing the oldest file (may not exist) + let _ = fs::remove_file(self.filename_for(self.max_filecount.max(1))); + let mut r = Ok(()); + for i in (0..self.max_filecount.max(1)).rev() { + let rotate_from = self.filename_for(i); + let rotate_to = self.filename_for(i + 1); + if let Err(e) = fs::rename(&rotate_from, &rotate_to).or_else(|e| match e.kind() { + io::ErrorKind::NotFound => Ok(()), + _ => Err(e), + }) { + // capture the error, but continue the loop, + // to maximize ability to rename everything + r = Err(e); + } + } + r + } + + /// Forces a rollover to happen immediately. + pub fn rollover(&mut self) -> io::Result<()> { + // Before closing, make sure all data is flushed successfully. + self.flush()?; + // We must close the current file before rotating files + self.writer_opt.take(); + self.current_filesize = 0; + self.rotate_files()?; + self.open_writer_if_needed() + } + + /// Opens a writer for the current file. + fn open_writer_if_needed(&mut self) -> io::Result<()> { + if self.writer_opt.is_none() { + let path = self.filename_for(0); + let path = Path::new(&path); + let mut open_options = OpenOptions::new(); + open_options.append(true).create(true); + let new_file = match open_options.open(path) { + Ok(new_file) => new_file, + Err(err) => { + let Some(parent) = path.parent() else { + return Err(err); + }; + fs::create_dir_all(parent)?; + open_options.open(path)? + } + }; + self.writer_opt = Some(BufWriter::new(new_file)); + self.current_filesize = path.metadata().map_or(0, |m| m.len()); + } + Ok(()) + } + + /// Writes data using the given datetime to calculate the rolling condition + pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime) -> io::Result { + if self.condition.should_rollover(now, self.current_filesize) { + if let Err(e) = self.rollover() { + // If we can't rollover, just try to continue writing anyway + // (better than missing data). + // This will likely used to implement logging, so + // avoid using log::warn and log to stderr directly + eprintln!("WARNING: Failed to rotate logfile {}: {}", self.filename, e); + } + } + self.open_writer_if_needed()?; + if let Some(writer) = self.writer_opt.as_mut() { + let buf_len = buf.len(); + writer.write_all(buf).map(|_| { + self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX); + buf_len + }) + } else { + Err(io::Error::other("unexpected condition: writer is missing")) + } + } +} + +impl io::Write for RollingFileAppender +where + RC: RollingCondition, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + let now = Local::now(); + self.write_with_datetime(buf, &now) + } + + fn flush(&mut self) -> io::Result<()> { + if let Some(writer) = self.writer_opt.as_mut() { + writer.flush()?; + } + Ok(()) + } +} + +pub struct FileAppenderWrapper { + appender: std::sync::Arc>, +} + +impl tracing_subscriber::fmt::MakeWriter<'_> for FileAppenderWrapper { + type Writer = FileAppenderWriter; + + fn make_writer(&self) -> Self::Writer { + FileAppenderWriter { + appender: self.appender.clone(), + } + } +} + +impl FileAppenderWrapper { + pub fn new(appender: RollingFileAppenderBase) -> Self { + Self { + appender: std::sync::Arc::new(parking_lot::Mutex::new(appender)), + } + } +} + +pub struct FileAppenderWriter { + appender: std::sync::Arc>, +} + +impl std::io::Write for FileAppenderWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.appender.lock().write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.appender.lock().flush() + } +} + +pub mod base; +pub use base::*; diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index a76dc85..3fac958 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -30,16 +30,18 @@ use easytier::{ cli::{ list_peer_route_pair, AclManageRpc, AclManageRpcClientFactory, AddPortForwardRequest, ConnectorManageRpc, ConnectorManageRpcClientFactory, DumpRouteRequest, - GetAclStatsRequest, GetPrometheusStatsRequest, GetStatsRequest, + GetAclStatsRequest, GetLoggerConfigRequest, GetPrometheusStatsRequest, GetStatsRequest, GetVpnPortalInfoRequest, GetWhitelistRequest, ListConnectorRequest, ListForeignNetworkRequest, ListGlobalForeignNetworkRequest, ListMappedListenerRequest, ListPeerRequest, ListPeerResponse, ListPortForwardRequest, ListRouteRequest, - ListRouteResponse, ManageMappedListenerRequest, MappedListenerManageAction, - MappedListenerManageRpc, MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc, + ListRouteResponse, LogLevel, LoggerRpc, LoggerRpcClientFactory, + ManageMappedListenerRequest, MappedListenerManageAction, MappedListenerManageRpc, + MappedListenerManageRpcClientFactory, NodeInfo, PeerManageRpc, PeerManageRpcClientFactory, PortForwardManageRpc, PortForwardManageRpcClientFactory, - RemovePortForwardRequest, SetWhitelistRequest, ShowNodeInfoRequest, StatsRpc, - StatsRpcClientFactory, TcpProxyEntryState, TcpProxyEntryTransportType, TcpProxyRpc, - TcpProxyRpcClientFactory, VpnPortalRpc, VpnPortalRpcClientFactory, + RemovePortForwardRequest, SetLoggerConfigRequest, SetWhitelistRequest, + ShowNodeInfoRequest, StatsRpc, StatsRpcClientFactory, TcpProxyEntryState, + TcpProxyEntryTransportType, TcpProxyRpc, TcpProxyRpcClientFactory, VpnPortalRpc, + VpnPortalRpcClientFactory, }, common::{NatType, SocketType}, peer_rpc::{GetGlobalPeerMapRequest, PeerCenterRpc, PeerCenterRpcClientFactory}, @@ -105,6 +107,8 @@ enum SubCommand { Whitelist(WhitelistArgs), #[command(about = "show statistics information")] Stats(StatsArgs), + #[command(about = "manage logger configuration")] + Logger(LoggerArgs), #[command(about = t!("core_clap.generate_completions").to_string())] GenAutocomplete { shell: Shell }, } @@ -272,6 +276,23 @@ enum StatsSubCommand { Prometheus, } +#[derive(Args, Debug)] +struct LoggerArgs { + #[command(subcommand)] + sub_command: Option, +} + +#[derive(Subcommand, Debug)] +enum LoggerSubCommand { + /// Get current logger configuration + Get, + /// Set logger level + Set { + #[arg(help = "Log level (disabled, error, warning, info, debug, trace)")] + level: String, + }, +} + #[derive(Args, Debug)] struct ServiceArgs { #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] @@ -443,6 +464,18 @@ impl CommandHandler<'_> { .with_context(|| "failed to get stats client")?) } + async fn get_logger_client( + &self, + ) -> Result>, Error> { + Ok(self + .client + .lock() + .await + .scoped_client::>("".to_string()) + .await + .with_context(|| "failed to get logger client")?) + } + async fn list_peers(&self) -> Result { let client = self.get_peer_manager_client().await?; let request = ListPeerRequest::default(); @@ -1163,6 +1196,66 @@ impl CommandHandler<'_> { Ok(()) } + async fn handle_logger_get(&self) -> Result<(), Error> { + let client = self.get_logger_client().await?; + let request = GetLoggerConfigRequest {}; + let response = client + .get_logger_config(BaseController::default(), request) + .await?; + + match self.output_format { + OutputFormat::Table => { + let level_str = match response.level() { + LogLevel::Disabled => "disabled", + LogLevel::Error => "error", + LogLevel::Warning => "warning", + LogLevel::Info => "info", + LogLevel::Debug => "debug", + LogLevel::Trace => "trace", + }; + println!("Current Log Level: {}", level_str); + } + OutputFormat::Json => { + let json = serde_json::to_string_pretty(&response)?; + println!("{}", json); + } + } + + Ok(()) + } + + async fn handle_logger_set(&self, level: &str) -> Result<(), Error> { + let log_level = match level.to_lowercase().as_str() { + "disabled" => LogLevel::Disabled, + "error" => LogLevel::Error, + "warning" => LogLevel::Warning, + "info" => LogLevel::Info, + "debug" => LogLevel::Debug, + "trace" => LogLevel::Trace, + _ => return Err(anyhow::anyhow!("Invalid log level: {}. Valid levels are: disabled, error, warning, info, debug, trace", level)), + }; + + let client = self.get_logger_client().await?; + let request = SetLoggerConfigRequest { + level: log_level.into(), + }; + let response = client + .set_logger_config(BaseController::default(), request) + .await?; + + match self.output_format { + OutputFormat::Table => { + println!("Log level successfully set to: {}", level); + } + OutputFormat::Json => { + let json = serde_json::to_string_pretty(&response)?; + println!("{}", json); + } + } + + Ok(()) + } + fn parse_port_list(ports_str: &str) -> Result, Error> { let mut ports = Vec::new(); for port_spec in ports_str.split(',') { @@ -1996,6 +2089,14 @@ async fn main() -> Result<(), Error> { println!("{}", response.prometheus_text); } }, + SubCommand::Logger(logger_args) => match &logger_args.sub_command { + Some(LoggerSubCommand::Get) | None => { + handler.handle_logger_get().await?; + } + Some(LoggerSubCommand::Set { level }) => { + handler.handle_logger_set(level).await?; + } + }, SubCommand::GenAutocomplete { shell } => { let mut cmd = Cli::command(); easytier::print_completions(shell, &mut cmd, "easytier-cli"); diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 2c8fecb..ab211a1 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -608,6 +608,20 @@ struct LoggingOptions { help = t!("core_clap.file_log_dir").to_string() )] file_log_dir: Option, + + #[arg( + long, + env = "ET_FILE_LOG_SIZE", + help = t!("core_clap.file_log_size_mb").to_string() + )] + file_log_size: Option, + + #[arg( + long, + env = "ET_FILE_LOG_COUNT", + help = t!("core_clap.file_log_count").to_string() + )] + file_log_count: Option, } rust_i18n::i18n!("locales", fallback = "en"); @@ -972,6 +986,8 @@ impl LoggingConfigLoader for &LoggingOptions { level: self.file_log_level.clone(), dir: self.file_log_dir.clone(), file: None, + size_mb: self.file_log_size, + count: self.file_log_count, } } } @@ -1100,7 +1116,7 @@ fn win_service_main(arg: Vec) { } async fn run_main(cli: Cli) -> anyhow::Result<()> { - init_logger(&cli.logging_options, false)?; + init_logger(&cli.logging_options, true)?; if cli.config_server.is_some() { set_default_machine_id(cli.machine_id); diff --git a/easytier/src/instance/instance.rs b/easytier/src/instance/instance.rs index 9d49d5e..caa65fb 100644 --- a/easytier/src/instance/instance.rs +++ b/easytier/src/instance/instance.rs @@ -990,6 +990,7 @@ impl Instance { return Ok(()); }; + use crate::instance::logger_rpc_service::LoggerRpcService; use crate::proto::cli::*; let peer_mgr = self.peer_manager.clone(); @@ -999,6 +1000,7 @@ impl Instance { let mapped_listener_manager_rpc = self.get_mapped_listener_manager_rpc_service(); let port_forward_manager_rpc = self.get_port_forward_manager_rpc_service(); let stats_rpc_service = self.get_stats_rpc_service(); + let logger_rpc_service = LoggerRpcService::new(); let s = self.rpc_server.as_mut().unwrap(); let peer_mgr_rpc_service = PeerManagerRpcService::new(peer_mgr.clone()); @@ -1027,6 +1029,8 @@ impl Instance { crate::proto::cli::StatsRpcServer::new(stats_rpc_service), "", ); + s.registry() + .register(LoggerRpcServer::new(logger_rpc_service), ""); if let Some(ip_proxy) = self.ip_proxy.as_ref() { s.registry().register( diff --git a/easytier/src/instance/logger_rpc_service.rs b/easytier/src/instance/logger_rpc_service.rs new file mode 100644 index 0000000..07ee785 --- /dev/null +++ b/easytier/src/instance/logger_rpc_service.rs @@ -0,0 +1,109 @@ +use std::sync::{mpsc::Sender, Mutex, OnceLock}; + +use crate::proto::{ + cli::{ + GetLoggerConfigRequest, GetLoggerConfigResponse, LogLevel, LoggerRpc, + SetLoggerConfigRequest, SetLoggerConfigResponse, + }, + rpc_types::{self, controller::BaseController}, +}; + +pub static LOGGER_LEVEL_SENDER: std::sync::OnceLock>> = OnceLock::new(); +pub static CURRENT_LOG_LEVEL: std::sync::OnceLock> = OnceLock::new(); + +#[derive(Clone)] +pub struct LoggerRpcService; + +impl LoggerRpcService { + pub fn new() -> Self { + Self + } + + fn log_level_to_string(level: LogLevel) -> String { + match level { + LogLevel::Disabled => "off".to_string(), + LogLevel::Error => "error".to_string(), + LogLevel::Warning => "warn".to_string(), + LogLevel::Info => "info".to_string(), + LogLevel::Debug => "debug".to_string(), + LogLevel::Trace => "trace".to_string(), + } + } + + fn string_to_log_level(level_str: &str) -> LogLevel { + match level_str.to_lowercase().as_str() { + "off" | "disabled" => LogLevel::Disabled, + "error" => LogLevel::Error, + "warn" | "warning" => LogLevel::Warning, + "info" => LogLevel::Info, + "debug" => LogLevel::Debug, + "trace" => LogLevel::Trace, + _ => LogLevel::Info, // 默认为 Info 级别 + } + } +} + +#[async_trait::async_trait] +impl LoggerRpc for LoggerRpcService { + type Controller = BaseController; + + async fn set_logger_config( + &self, + _: BaseController, + request: SetLoggerConfigRequest, + ) -> Result { + let level_str = Self::log_level_to_string(request.level()); + + // 更新当前日志级别 + if let Some(current_level) = CURRENT_LOG_LEVEL.get() { + if let Ok(mut level) = current_level.lock() { + *level = level_str.clone(); + } + } + + // 发送新的日志级别到 logger 重载器 + if let Some(sender) = LOGGER_LEVEL_SENDER.get() { + if let Ok(sender) = sender.lock() { + if let Err(e) = sender.send(level_str) { + tracing::warn!("Failed to send new log level to reloader: {}", e); + return Err(rpc_types::error::Error::ExecutionError(anyhow::anyhow!( + "Failed to update log level: {}", + e + ))); + } + } else { + return Err(rpc_types::error::Error::ExecutionError(anyhow::anyhow!( + "Logger sender is not available" + ))); + } + } else { + return Err(rpc_types::error::Error::ExecutionError(anyhow::anyhow!( + "Logger reloader is not initialized" + ))); + } + + Ok(SetLoggerConfigResponse {}) + } + + async fn get_logger_config( + &self, + _: BaseController, + _request: GetLoggerConfigRequest, + ) -> Result { + let current_level_str = if let Some(current_level) = CURRENT_LOG_LEVEL.get() { + if let Ok(level) = current_level.lock() { + level.clone() + } else { + "info".to_string() // 默认级别 + } + } else { + "info".to_string() // 默认级别 + }; + + let level = Self::string_to_log_level(¤t_level_str); + + Ok(GetLoggerConfigResponse { + level: level.into(), + }) + } +} diff --git a/easytier/src/instance/mod.rs b/easytier/src/instance/mod.rs index 82918ff..8575b73 100644 --- a/easytier/src/instance/mod.rs +++ b/easytier/src/instance/mod.rs @@ -6,3 +6,5 @@ pub mod listeners; #[cfg(feature = "tun")] pub mod virtual_nic; + +pub mod logger_rpc_service; diff --git a/easytier/src/proto/cli.proto b/easytier/src/proto/cli.proto index d1ecb3f..b76b2f6 100644 --- a/easytier/src/proto/cli.proto +++ b/easytier/src/proto/cli.proto @@ -325,3 +325,31 @@ service StatsRpc { rpc GetStats(GetStatsRequest) returns (GetStatsResponse); rpc GetPrometheusStats(GetPrometheusStatsRequest) returns (GetPrometheusStatsResponse); } + +enum LogLevel { + DISABLED = 0; + ERROR = 1; + WARNING = 2; + INFO = 3; + DEBUG = 4; + TRACE = 5; +} + +message SetLoggerConfigRequest { + LogLevel level = 1; +} + +message SetLoggerConfigResponse { +} + +message GetLoggerConfigRequest { +} + +message GetLoggerConfigResponse { + LogLevel level = 1; +} + +service LoggerRpc { + rpc SetLoggerConfig(SetLoggerConfigRequest) returns (SetLoggerConfigResponse); + rpc GetLoggerConfig(GetLoggerConfigRequest) returns (GetLoggerConfigResponse); +} diff --git a/easytier/src/utils.rs b/easytier/src/utils.rs index fa2927e..58c5305 100644 --- a/easytier/src/utils.rs +++ b/easytier/src/utils.rs @@ -6,7 +6,9 @@ use tracing_subscriber::{ layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry, }; -use crate::common::{config::LoggingConfigLoader, get_logger_timer_rfc3339}; +use crate::common::{ + config::LoggingConfigLoader, get_logger_timer_rfc3339, tracing_rolling_appender::*, +}; pub type PeerRoutePair = crate::proto::cli::PeerRoutePair; @@ -28,6 +30,8 @@ pub fn init_logger( config: impl LoggingConfigLoader, need_reload: bool, ) -> Result, anyhow::Error> { + use crate::instance::logger_rpc_service::{CURRENT_LOG_LEVEL, LOGGER_LEVEL_SENDER}; + let file_config = config.get_file_logger_config(); let file_level = file_config .level @@ -50,7 +54,12 @@ pub fn init_logger( if need_reload { let (sender, recver) = std::sync::mpsc::channel(); - ret_sender = Some(sender); + ret_sender = Some(sender.clone()); + + // 初始化全局状态 + let _ = LOGGER_LEVEL_SENDER.set(std::sync::Mutex::new(sender)); + let _ = CURRENT_LOG_LEVEL.set(std::sync::Mutex::new(file_level.to_string())); + std::thread::spawn(move || { println!("Start log filter reloader"); while let Ok(lf) = recver.recv() { @@ -72,15 +81,20 @@ pub fn init_logger( }); } - let file_appender = tracing_appender::rolling::Builder::new() - .rotation(tracing_appender::rolling::Rotation::DAILY) - .max_log_files(5) - .filename_prefix(file_config.file.unwrap_or("easytier".to_string())) - .filename_suffix("log") - .build(file_config.dir.unwrap_or("./".to_string())) - .with_context(|| "failed to initialize rolling file appender")?; + let builder = RollingFileAppenderBase::builder(); + let file_appender = builder + .filename(file_config.file.unwrap_or("easytier.log".to_string())) + .condition_daily() + .max_filecount(file_config.count.unwrap_or(10)) + .condition_max_file_size(file_config.size_mb.unwrap_or(100) * 1024 * 1024) + .build() + .unwrap(); + + let wrapper = FileAppenderWrapper::new(file_appender); + + // Create a simple wrapper that implements MakeWriter file_layer = Some( - l.with_writer(file_appender) + l.with_writer(wrapper) .with_timer(get_logger_timer_rfc3339()) .with_filter(file_filter), );