Skip to main content

nitro_cli/common/
logger.rs

1// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3#![deny(missing_docs)]
4#![deny(warnings)]
5
6use chrono::offset::{Local, Utc};
7use chrono::DateTime;
8use flexi_logger::writers::LogWriter;
9use flexi_logger::{DeferredNow, Record};
10use nix::unistd::Uid;
11use std::env;
12use std::fs::{File, OpenOptions, Permissions};
13use std::io::{Error, Result, Write};
14use std::ops::{Deref, DerefMut};
15use std::os::unix::fs::{MetadataExt, PermissionsExt};
16use std::path::{Path, PathBuf};
17use std::sync::{Arc, Mutex};
18
19use crate::common::{NitroCliErrorEnum, NitroCliFailure, NitroCliResult};
20use crate::new_nitro_cli_failure;
21
22/// The default logging level used by the logger.
23const DEFAULT_LOG_LEVEL: &str = "info";
24
25/// The environment variable which holds the path to the logging directory.
26const LOGS_DIR_PATH_ENV_VAR: &str = "NITRO_CLI_LOGS_PATH";
27
28/// The default path to the logging directory.
29const LOGS_DIR_PATH: &str = "/var/log/nitro_enclaves";
30
31/// The name of the output log file.
32const LOG_FILE_NAME: &str = "nitro_enclaves.log";
33
34/// A log writer which outputs its messages to a custom file. It also
35/// allows the updating of its ID, in order to indicate which process
36/// is actually logging a message. This implementation will also enable
37/// synchronized logging to a centralized file for multiple enclaves.
38#[derive(Clone)]
39pub struct EnclaveProcLogWriter {
40    out_file: Arc<Mutex<File>>,
41    logger_id: Arc<Mutex<String>>,
42}
43
44impl EnclaveProcLogWriter {
45    /// Create a new log writer.
46    pub fn new() -> NitroCliResult<Self> {
47        // All logging shall be directed to a centralized file.
48        Ok(EnclaveProcLogWriter {
49            out_file: Arc::new(Mutex::new(
50                open_log_file(&get_log_file_path())
51                    .map_err(|e| e.add_subaction("Failed to open log file".to_string()))?,
52            )),
53            logger_id: Arc::new(Mutex::new(String::new())),
54        })
55    }
56
57    /// Check if the log file is present and if it is not, (re)open it.
58    fn safe_open_log_file(&self) -> NitroCliResult<()> {
59        let log_path = &get_log_file_path();
60        if !log_path.exists() {
61            let new_file = open_log_file(log_path)
62                .map_err(|e| e.add_subaction(String::from("Failed to open log file")))?;
63            let mut file_ref = self.out_file.lock().map_err(|e| {
64                new_nitro_cli_failure!(
65                    &format!("Failed to acquire lock: {e:?}"),
66                    NitroCliErrorEnum::LockAcquireFailure
67                )
68            })?;
69            *file_ref.deref_mut() = new_file;
70        }
71
72        Ok(())
73    }
74
75    /// Update the logger ID (correlated with the process which is doing logging).
76    pub fn update_logger_id(&self, new_id: &str) -> NitroCliResult<()> {
77        let mut old_id = self.logger_id.lock().map_err(|e| {
78            new_nitro_cli_failure!(
79                &format!("Failed to acquire logger ID lock: {e:?}"),
80                NitroCliErrorEnum::LockAcquireFailure
81            )
82        })?;
83        old_id.deref_mut().clear();
84        old_id.deref_mut().push_str(new_id);
85
86        Ok(())
87    }
88
89    /// Generate a single message string.
90    fn create_msg(&self, now: &DateTime<Local>, record: &Record) -> NitroCliResult<String> {
91        // UTC timestamp according to RFC 2822
92        let timestamp = DateTime::<Utc>::from_naive_utc_and_offset(now.naive_utc(), Utc)
93            .to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
94        let logger_id = self.logger_id.lock().map_err(|e| {
95            new_nitro_cli_failure!(
96                &format!("Failed to acquire logger ID lock: {e:?}"),
97                NitroCliErrorEnum::LockAcquireFailure
98            )
99        })?;
100        Ok(format!(
101            "[{}][{}][{}][{}:{}] {}\n",
102            logger_id.deref(),
103            record.level(),
104            timestamp,
105            record.file().unwrap_or("?"),
106            record.line().unwrap_or(0),
107            record.args()
108        ))
109    }
110}
111
112impl LogWriter for EnclaveProcLogWriter {
113    fn write(&self, now: &mut DeferredNow, record: &Record) -> Result<()> {
114        if self.safe_open_log_file().is_err() {
115            return Err(Error::other("Failed to safely open log file for writing"));
116        }
117
118        if let Ok(record_str) = self.create_msg(now.now(), record) {
119            if let Ok(mut out_file) = self.out_file.lock() {
120                out_file.deref_mut().write_all(record_str.as_bytes())?;
121
122                return Ok(());
123            }
124
125            return Err(Error::other("Failed to lock log file"));
126        }
127
128        Err(Error::other("Failed to create logger message"))
129    }
130
131    fn flush(&self) -> Result<()> {
132        Ok(())
133    }
134
135    fn max_log_level(&self) -> log::LevelFilter {
136        // The log level is either given in RUST_LOG or defaults to a specified value.
137        let level = std::env::var("RUST_LOG").unwrap_or_else(|_| DEFAULT_LOG_LEVEL.to_string());
138
139        match level.to_lowercase().as_ref() {
140            "info" => log::LevelFilter::Info,
141            "debug" => log::LevelFilter::Debug,
142            "warn" => log::LevelFilter::Warn,
143            "error" => log::LevelFilter::Error,
144            "trace" => log::LevelFilter::Trace,
145            _ => log::LevelFilter::Info,
146        }
147    }
148}
149
150/// Get the directory containing Nitro CLI related log files.
151pub fn get_log_file_base_path() -> String {
152    match env::var(LOGS_DIR_PATH_ENV_VAR) {
153        Ok(env_path) => env_path,
154        Err(_) => LOGS_DIR_PATH.to_string(),
155    }
156}
157
158/// Get the path to the log file.
159fn get_log_file_path() -> PathBuf {
160    Path::new(&get_log_file_base_path()).join(LOG_FILE_NAME)
161}
162
163/// Open a file at a given location for writing and appending.
164fn open_log_file(file_path: &Path) -> NitroCliResult<File> {
165    let file = OpenOptions::new()
166        .create(true)
167        .append(true)
168        .read(false)
169        .open(file_path)
170        .map_err(|e| {
171            new_nitro_cli_failure!(
172                &format!("Failed to open log file: {e:?}"),
173                NitroCliErrorEnum::FileOperationFailure
174            )
175            .add_info(vec![
176                file_path
177                    .to_str()
178                    .unwrap_or("Invalid unicode log file name"),
179                "Open",
180            ])
181        })?;
182
183    let log_file_uid = Uid::from_raw(
184        file.metadata()
185            .map_err(|e| {
186                new_nitro_cli_failure!(
187                    &format!("Failed to get log file metadata: {e:?}"),
188                    NitroCliErrorEnum::FileOperationFailure
189                )
190                .add_info(vec![
191                    file_path
192                        .to_str()
193                        .unwrap_or("Invalid unicode log file name"),
194                    "Get metadata",
195                ])
196            })?
197            .uid(),
198    );
199
200    // The log file should be write-accessible to any user, since
201    // any user may launch a CLI instance. Only the file's owner
202    // may change its permissions.
203    if log_file_uid == Uid::current() {
204        let perms = Permissions::from_mode(0o766);
205        file.set_permissions(perms).map_err(|e| {
206            new_nitro_cli_failure!(
207                &format!("Failed to change log file permissions: {e:?}"),
208                NitroCliErrorEnum::FilePermissionsError
209            )
210        })?;
211    }
212
213    Ok(file)
214}
215
216/// Initialize logging.
217pub fn init_logger() -> NitroCliResult<EnclaveProcLogWriter> {
218    // The log file is "nitro-cli.log" and is stored in the NPE resources directory.
219    let log_writer = EnclaveProcLogWriter::new()?;
220
221    // Initialize logging with the new log writer.
222    flexi_logger::Logger::try_with_env_or_str(DEFAULT_LOG_LEVEL)
223        .map_err(|e| {
224            new_nitro_cli_failure!(
225                &format!("Failed to initialize enclave process logger: {e:?}"),
226                NitroCliErrorEnum::LoggerError
227            )
228        })?
229        .log_to_writer(Box::new(log_writer.clone()))
230        .start()
231        .map_err(|e| {
232            new_nitro_cli_failure!(
233                &format!("Failed to initialize enclave process logger: {e:?}"),
234                NitroCliErrorEnum::LoggerError
235            )
236        })?;
237
238    // The log writer is provided for sharing between CLI-related processes.
239    Ok(log_writer)
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    use std::fs;
247    use std::os::unix::fs::PermissionsExt;
248
249    use tempfile::NamedTempFile;
250
251    /// Tests that `open_log_file()` creates a file
252    /// with the expected permissions.
253    #[test]
254    fn test_open_log_file() {
255        let file0 = NamedTempFile::new();
256
257        if let Ok(file0) = file0 {
258            let test_file_path = file0.path();
259
260            let f = open_log_file(test_file_path).unwrap();
261            let metadata = f.metadata();
262            assert!(metadata.is_ok());
263
264            if let Ok(metadata) = metadata {
265                assert!(metadata.is_file());
266                let permissions = metadata.permissions();
267                let mode = permissions.mode();
268
269                assert_eq!(mode & 0o777, 0o766);
270            }
271        }
272    }
273
274    /// Tests that the logger id is initially empty ("").
275    #[test]
276    fn test_init_logger() {
277        let tmp_log_dir: &str = "./.tmp_logs_init_logger";
278
279        // Get old environment variable value
280        let old_log_path = env::var(LOGS_DIR_PATH_ENV_VAR);
281        let path_existed = Path::new(tmp_log_dir).exists();
282
283        // Update environment variable value
284        env::set_var(LOGS_DIR_PATH_ENV_VAR, tmp_log_dir);
285        let _ = fs::create_dir(tmp_log_dir);
286
287        let log_writer = EnclaveProcLogWriter::new().unwrap();
288        let lock_result = log_writer.logger_id.lock();
289
290        assert!(lock_result.unwrap().is_empty());
291
292        if !path_existed {
293            // Remove whole `tmp_log_dir` if necessary
294            let _ = fs::remove_dir_all(tmp_log_dir);
295        } else {
296            // Only remove the log file
297            let _ = fs::remove_file(format!("{}/{}", tmp_log_dir, &LOG_FILE_NAME));
298        }
299
300        // Reset old environment variable value if necessary
301        if let Ok(old_log_path) = old_log_path {
302            env::set_var(LOGS_DIR_PATH_ENV_VAR, old_log_path);
303        }
304    }
305
306    /// Tests that the logger id is altered after issuing a
307    /// call to `update_logger_id()`.
308    #[test]
309    fn test_update_logger_id() {
310        let tmp_log_dir: &str = "./.tmp_logs_update_logger_id";
311
312        // Get old environment variable value
313        let old_log_path = env::var(LOGS_DIR_PATH_ENV_VAR);
314        let path_existed = Path::new(tmp_log_dir).exists();
315
316        // Update environment variable value
317        env::set_var(LOGS_DIR_PATH_ENV_VAR, tmp_log_dir);
318        let _ = fs::create_dir(tmp_log_dir);
319
320        let log_writer = EnclaveProcLogWriter::new().unwrap();
321        log_writer.update_logger_id("new-logger-id").unwrap();
322        let lock_result = log_writer.logger_id.lock();
323
324        assert!(lock_result.unwrap().eq("new-logger-id"));
325
326        log_writer.update_logger_id("").unwrap();
327
328        if !path_existed {
329            // Remove whole `tmp_log_dir` if necessary
330            let _ = fs::remove_dir_all(tmp_log_dir);
331        } else {
332            // Only remove the log file
333            let _ = fs::remove_file(format!("{}/{}", tmp_log_dir, &LOG_FILE_NAME));
334        }
335
336        // Reset old environment variable value if necessary
337        if let Ok(old_log_path) = old_log_path {
338            env::set_var(LOGS_DIR_PATH_ENV_VAR, old_log_path);
339        }
340    }
341}