nydus_app/
lib.rs

1// Copyright 2020 Ant Group. All rights reserved.
2// Copyright (C) 2021 Alibaba Cloud. All rights reserved.
3//
4// SPDX-License-Identifier: Apache-2.0
5
6//! Application framework and utilities for Nydus.
7//!
8//! The `nydus-app` crates provides common helpers and utilities to support Nydus application:
9//! - Application Building Information: [`struct BuildTimeInfo`](struct.BuildTimeInfo.html) and
10//!   [`fn dump_program_info()`](fn.dump_program_info.html).
11//! - Logging helpers: [`fn setup_logging()`](fn.setup_logging.html) and
12//!   [`fn log_level_to_verbosity()`](fn.log_level_to_verbosity.html).
13//! - Signal handling: [`fn register_signal_handler()`](signal/fn.register_signal_handler.html).
14//!
15//! ```rust,ignore
16//! #[macro_use(crate_authors, crate_version)]
17//! extern crate clap;
18//!
19//! use clap::App;
20//! use nydus_app::{BuildTimeInfo, setup_logging};
21//! # use std::io::Result;
22//!
23//! fn main() -> Result<()> {
24//!     let level = cmd.value_of("log-level").unwrap().parse().unwrap();
25//!     let (bti_string, build_info) = BuildTimeInfo::dump();
26//!     let _cmd = App::new("")
27//!                 .version(bti_string.as_str())
28//!                 .author(crate_authors!())
29//!                 .get_matches();
30//!
31//!     setup_logging(None, level, 0)?;
32//!     print!("{}", build_info);
33//!
34//!     Ok(())
35//! }
36//! ```
37
38#[macro_use]
39extern crate log;
40#[macro_use]
41extern crate nydus_error;
42#[macro_use]
43extern crate serde;
44
45use std::env::current_dir;
46use std::io::Result;
47use std::path::PathBuf;
48
49use flexi_logger::{
50    self, style, Cleanup, Criterion, DeferredNow, FileSpec, Logger, Naming,
51    TS_DASHES_BLANK_COLONS_DOT_BLANK,
52};
53use log::{Level, LevelFilter, Record};
54
55pub mod signal;
56
57pub fn log_level_to_verbosity(level: log::LevelFilter) -> usize {
58    if level == log::LevelFilter::Off {
59        0
60    } else {
61        level as usize - 1
62    }
63}
64
65pub mod built_info {
66    pub const PROFILE: &str = env!("PROFILE");
67    pub const RUSTC_VERSION: &str = env!("RUSTC_VERSION");
68    pub const BUILT_TIME_UTC: &str = env!("BUILT_TIME_UTC");
69    pub const GIT_COMMIT_VERSION: &str = env!("GIT_COMMIT_VERSION");
70    pub const GIT_COMMIT_HASH: &str = env!("GIT_COMMIT_HASH");
71}
72
73/// Dump program build and version information.
74pub fn dump_program_info() {
75    info!(
76        "Program Version: {}, Git Commit: {:?}, Build Time: {:?}, Profile: {:?}, Rustc Version: {:?}",
77        built_info::GIT_COMMIT_VERSION,
78        built_info::GIT_COMMIT_HASH,
79        built_info::BUILT_TIME_UTC,
80        built_info::PROFILE,
81        built_info::RUSTC_VERSION,
82    );
83}
84
85/// Application build and version information.
86#[derive(Serialize, Clone)]
87pub struct BuildTimeInfo {
88    pub package_ver: String,
89    pub git_commit: String,
90    build_time: String,
91    profile: String,
92    rustc: String,
93}
94
95impl BuildTimeInfo {
96    pub fn dump() -> (String, Self) {
97        let info_string = format!(
98            "\rVersion: \t{}\nGit Commit: \t{}\nBuild Time: \t{}\nProfile: \t{}\nRustc: \t\t{}\n",
99            built_info::GIT_COMMIT_VERSION,
100            built_info::GIT_COMMIT_HASH,
101            built_info::BUILT_TIME_UTC,
102            built_info::PROFILE,
103            built_info::RUSTC_VERSION,
104        );
105
106        let info = Self {
107            package_ver: built_info::GIT_COMMIT_VERSION.to_string(),
108            git_commit: built_info::GIT_COMMIT_HASH.to_string(),
109            build_time: built_info::BUILT_TIME_UTC.to_string(),
110            profile: built_info::PROFILE.to_string(),
111            rustc: built_info::RUSTC_VERSION.to_string(),
112        };
113
114        (info_string, info)
115    }
116}
117
118fn get_file_name<'a>(record: &'a Record) -> Option<&'a str> {
119    record.file().map(|v| match v.rfind("/src/") {
120        None => v,
121        Some(pos) => match v[..pos].rfind('/') {
122            None => &v[pos..],
123            Some(p) => &v[p..],
124        },
125    })
126}
127
128fn opt_format(
129    w: &mut dyn std::io::Write,
130    now: &mut DeferredNow,
131    record: &Record,
132) -> std::result::Result<(), std::io::Error> {
133    let level = record.level();
134    if level == Level::Info {
135        write!(
136            w,
137            "[{}] {} {}",
138            now.format(TS_DASHES_BLANK_COLONS_DOT_BLANK),
139            record.level(),
140            &record.args()
141        )
142    } else {
143        write!(
144            w,
145            "[{}] {} [{}:{}] {}",
146            now.format(TS_DASHES_BLANK_COLONS_DOT_BLANK),
147            record.level(),
148            get_file_name(record).unwrap_or("<unnamed>"),
149            record.line().unwrap_or(0),
150            &record.args()
151        )
152    }
153}
154
155fn colored_opt_format(
156    w: &mut dyn std::io::Write,
157    now: &mut DeferredNow,
158    record: &Record,
159) -> std::result::Result<(), std::io::Error> {
160    let level = record.level();
161    if level == Level::Info {
162        write!(
163            w,
164            "[{}] {} {}",
165            style(level).paint(now.format(TS_DASHES_BLANK_COLONS_DOT_BLANK)),
166            style(level).paint(level.to_string()),
167            style(level).paint(&record.args().to_string())
168        )
169    } else {
170        write!(
171            w,
172            "[{}] {} [{}:{}] {}",
173            style(level).paint(now.format(TS_DASHES_BLANK_COLONS_DOT_BLANK)),
174            style(level).paint(level.to_string()),
175            get_file_name(record).unwrap_or("<unnamed>"),
176            record.line().unwrap_or(0),
177            style(level).paint(&record.args().to_string())
178        )
179    }
180}
181
182/// Setup logging infrastructure for application.
183///
184/// `log_file_path` is an absolute path to logging files or relative path from current working
185/// directory to logging file.
186/// Flexi logger always appends a suffix to file name whose default value is ".log"
187/// unless we set it intentionally. I don't like this passion. When the basename of `log_file_path`
188/// is "bar", the newly created log file will be "bar.log"
189pub fn setup_logging(
190    log_file_path: Option<PathBuf>,
191    level: LevelFilter,
192    rotation_size: u64,
193) -> Result<()> {
194    if let Some(ref path) = log_file_path {
195        // Do not try to canonicalize the path since the file may not exist yet.
196        let mut spec = FileSpec::default().suppress_timestamp();
197
198        // Parse log file to get the `basename` and `suffix`(extension) because `flexi_logger`
199        // will automatically add `.log` suffix if we don't set explicitly, see:
200        // https://github.com/emabee/flexi_logger/issues/74
201        let basename = path
202            .file_stem()
203            .ok_or_else(|| {
204                eprintln!("invalid file name input {:?}", path);
205                einval!()
206            })?
207            .to_str()
208            .ok_or_else(|| {
209                eprintln!("invalid file name input {:?}", path);
210                einval!()
211            })?;
212        spec = spec.basename(basename);
213
214        // `flexi_logger` automatically add `.log` suffix if the file name has not extension.
215        if let Some(suffix) = path.extension() {
216            let suffix = suffix.to_str().ok_or_else(|| {
217                eprintln!("invalid file extension {:?}", suffix);
218                einval!()
219            })?;
220            spec = spec.suffix(suffix);
221        }
222
223        // Set log directory
224        let parent_dir = path.parent();
225        if let Some(p) = parent_dir {
226            let cwd = current_dir()?;
227            let dir = if !p.has_root() {
228                cwd.join(p)
229            } else {
230                p.to_path_buf()
231            };
232            spec = spec.directory(dir);
233        }
234
235        // We rely on rust `log` macro to limit current log level rather than `flexi_logger`
236        // So we set `flexi_logger` log level to "trace" which is High enough. Otherwise, we
237        // can't change log level to a higher level than what is passed to `flexi_logger`.
238        let mut logger = Logger::try_with_env_or_str("trace")
239            .map_err(|_e| enosys!())?
240            .log_to_file(spec)
241            .append()
242            .format(opt_format);
243
244        // Set log rotation
245        if rotation_size > 0 {
246            let log_rotation_size_byte: u64 = rotation_size * 1024 * 1024;
247            logger = logger.rotate(
248                Criterion::Size(log_rotation_size_byte),
249                Naming::Timestamps,
250                Cleanup::KeepCompressedFiles(10),
251            );
252        }
253
254        logger.start().map_err(|e| {
255            eprintln!("{:?}", e);
256            eother!(e)
257        })?;
258    } else {
259        // We rely on rust `log` macro to limit current log level rather than `flexi_logger`
260        // So we set `flexi_logger` log level to "trace" which is High enough. Otherwise, we
261        // can't change log level to a higher level than what is passed to `flexi_logger`.
262        Logger::try_with_env_or_str("trace")
263            .map_err(|_e| enosys!())?
264            .format(colored_opt_format)
265            .start()
266            .map_err(|e| eother!(e))?;
267    }
268
269    log::set_max_level(level);
270
271    // Dump panic info and backtrace to logger.
272    log_panics::Config::new()
273        .backtrace_mode(log_panics::BacktraceMode::Resolved)
274        .install_panic_hook();
275
276    Ok(())
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_log_level_to_verbosity() {
285        assert_eq!(log_level_to_verbosity(log::LevelFilter::Off), 0);
286        assert_eq!(log_level_to_verbosity(log::LevelFilter::Error), 0);
287        assert_eq!(log_level_to_verbosity(log::LevelFilter::Warn), 1);
288    }
289
290    #[test]
291    fn test_log_rotation() {
292        let log_file = Some(PathBuf::from("test_log_rotation"));
293        let level = LevelFilter::Info;
294        let rotation_size = 1; // 1MB
295
296        assert!(setup_logging(log_file, level, rotation_size).is_ok());
297    }
298}