Skip to content

Switch from syslog to some hostfile-backed mechanism #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 30, 2018
Merged

Switch from syslog to some hostfile-backed mechanism #214

merged 5 commits into from
Apr 30, 2018

Conversation

dianpopa
Copy link
Contributor

Changes

  • added new logger crate which is based on external rust crate 'log'
    • user can customize log messages (include file path, showing line numbers, log level)
    • if the user does not provide a log file, the output will go to stderr (for error and warnings) and to stdout for all other log levels)
  • removed syslog

Testing

  • wrote unit tests for newly added logger crate
  • current kcov coverage for logger:
    • Overall -> 90.5%
    • writers.rs -> 95.7%
    • lib.rs -> 94.6%
    • errors.rs -> 73.5%
  • kcov coverage (for all crates) -> 41.9
  • tested only in vmm-no-api mode (current API call not supported)
# put some logs in each vcpu thread containing the thread id and see that the tmp.log contains all of them
target/debug/firecracker vmm-no-api -k vmlinux.bin -r ami-fea26484.ext4 --log-file tmp.log --vcpu-count 2

Dependencies

  • toolchain: stable-x86_64-unknown-linux-gnu
  • rustc: rustc 1.25.0
  • kcov: cargo-kcov 0.4.2
  • rustfmt: 0.3.8-nightly

Copy link
Member

@andreeaflorescu andreeaflorescu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Part 1: will do some more tomorrow. As overall feedback, I would not return self for the configuration methods. I would go for the "classical" class and clearly specify that init will move ownership of Self.

[package]
name = "logger"
version = "0.1.0"
authors = ["The Chromium OS Authors"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be Firecracker?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it should, I changed it.

log = { version = "0.4", features = ["std"] }

[build-dependencies]
gcc = "=0.3.54"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add new line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added.

mod writers;
mod error;

use std::result;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: import stuff in alphabetical order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did it.

///
pub fn new() -> Logger {
Logger {
show_level: SHOW_LEVEL,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why don't you use directly true rather than having another constant in the file? I understand why it makes sense for the separators and the default level, but for boolean types it just adds unnecessary code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I used true and false and deleted the conts.

/// warn!("This will print '[WARN:file_path.rs]' followed by log message");
/// }
/// ```
pub fn show_file_path(mut self, option: bool) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of the method doesn't do justice to what it actually does. It also sets the line numbers configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now I only have a function called show_log_origin that receives two parameters, one for the file path and one for the line numbers. It sets the showing of the line numbers to false if the showing of the file path is also set to false.

/// warn!("This will print '[WARN:file_path.rs:179]' followed by log message");
/// }
/// ```
pub fn show_line_numbers(mut self, option: bool) -> Self {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't this function sort of do the same thing as show_file_path? We can probably have a single function with 2 parameters: line number and file path. You can then avoid invalid states by checking that you can't set the line number (=true) if the path != true.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See answer from above.

pub fn new(log_path: &String) -> Result<FileLogWriter> {
let s_file: String = log_path.to_string();
let p_file = Path::new(&s_file);
match File::create(&p_file) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This currently overrides the log file if one exists, is this the desired functionality?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per discussion with @filipposironi , if the log file already exists it means we are running in a chroot that was not cleaned up. What I should do is add a warning that advises against the file being overwritten before going ahead with execution.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the code that does the checking, throws an error and then moves on with the logging.

Copy link
Contributor

@filipposironi filipposironi Apr 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this needs to be handled differently on live-update though.
We either carry around the file descriptor of the log file and re-initialize the logger accordingly or we'll need to be able to tell the logger that the file needs to be opened in append mode.

/// Logger representing the logging subsystem.
///
#[derive(Debug)]
pub struct Logger {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some kind of timestamp would be very useful here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, but I prefer to add it in a separate commit. Created an issue for that: #228.

@@ -7,10 +7,11 @@
extern crate byteorder;
extern crate epoll;
extern crate libc;
#[macro_use]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get rid of the macro_use by publicly exporting the log macros in logger.rs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some investigation, what we could do in order to reuse the macros in the external log crate, would be to use macro_reexport. But, as per rust issue, this is currently labeled as unstable and using it in your code triggers build failure. What I did was use the workaround that some user suggested there: `Since 1.15.0, macro_rules! macro can be re-exported with:

extern crate somelib;
pub use somelib::*;

'
and that is what I did.

@@ -79,10 +78,8 @@ impl Kvm {
/// Gets the recommended maximum number of VCPUs per VM.
pub fn get_nr_vcpus(&self) -> usize {
match self.check_extension_int(Cap::NrVcpus) {
0 => 4, // according to api.txt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this line also go away?

Copy link
Contributor Author

@dianpopa dianpopa Apr 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the logic before:

  • In case of 0, return 4
  • In case of a positive number return the number
  • In case of anything else, warning and return 4

The logic now:

  • In case of positive number, return the number
  • Anything else, return 4 ( no warning is necessary in case kvm return negative, see api.txt and qemu code); this is why the line with 0 went away

log = { version = "0.4", features = ["std"] }

[build-dependencies]
gcc = "=0.3.54"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can you please add a newline at the end of this file for us OCD ppl? :(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCD trigger resolved.

/// Structure `FileLogWriter` used for writing to a file in a thread-safe way.
#[derive(Debug)]
pub struct FileLogWriter {
line_writer: Mutex<RefCell<LineWriter<File>>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need the RefCell if the Writer is already wrapped inside a Mutex?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will add a comment explaining this.
For the moment, we do not need this, you are right.
The RefCell is used for its internal mutability and the Mutex for ensuring thread safety. If at some point we may want our structure to hold things that may be changing while the logger runs (rotating logs when the size increases above a certain size, using more files to log to when the size increases above a certain limit), the contents of line_writer will change and so I added the RefCell.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Mutex already provides interior mutability via the MutexGuard you get when you call mutex.lock().map_err(..)?; The MutexGuard implements both Defer and DerefMut, so you can mutate whatever is held inside the Mutex.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, seems like Mutex already guards against internal mutability, I will remove the RefCell.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the RefCell.


impl FileLogWriter {
pub fn new(log_path: &String) -> Result<FileLogWriter> {
let s_file: String = log_path.to_string();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this String needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

Ok(t) => Ok(FileLogWriter {
line_writer: Mutex::new(RefCell::new(LineWriter::new(t))),
}),
Err(e) => return Err(LoggerError::BadDirectory(e)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who do all errors here get turned into BadDirectory? Is that the only possible error condition which may occur here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name does not fit the purpose indeed. Changed the name of it to a more intuitive one.


/// Values used by the Logger.
///
pub const SHOW_LEVEL: bool = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These constants look a bit awkward to me :-s Wouldn't it make more sense if they were parameters to the logger, and chosen appropriately when each Logger instance is created, instead of being defined at the top of this particular file? :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are default values. I prefer to do it this way instead of having a constructor with multiple parameters. These default values can be changed by calling the associated set functions.

/// warn!("This will print 'WARN' surrounded by square brackets followed by log message");
/// }
/// ```
pub fn show_level(mut self, option: bool) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function appears to consume self, and then return it again. I don't know if it's inefficient or not, but it does look a bit weird; why does the function work that way?

Copy link
Contributor Author

@dianpopa dianpopa Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So,the init function calls set_boxed_logger which needs to receive self. All the setting up of the logger needs to happen before that. That function consumes self, so that I can do the setting up from one call (see src/main.rs):

Logger::new().set_bla().set_bla2().init();

If I were to implement the function with reference to mut self I would need to do it:

Logger::new()
l.set_bla()
l.set_bla2()

If the logger could be modified during its functioning, maybe there would be a reason in doing it by reference, but given that init needs to be called last, I basically enforced the setting up of the logger to take place from one call only.

let fw = FileLogWriter::new(&file).unwrap();
let msg = String::from("some message");
assert!(&fw.write(&msg).is_ok());
remove_file(file).unwrap();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the write fails for whatever reasons, the file doesn't get cleaned up. You could keep track of the result for write, clean up and assert after that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. The new changes should remove the file whether or not the assert fails.

let file: String = "tmp_writers_flush.log".to_string();
let fw = FileLogWriter::new(&file).unwrap();
assert!(&fw.flush().is_ok());
remove_file(file).unwrap();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See answer from above.

FileLogLock(String),
}

impl fmt::Display for LoggerError {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curios, why do we implement Display for LoggerError?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you asking why Debug would not be enough?

Copy link
Contributor

@alexandruag alexandruag Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I guess that would have been the more straightforward question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug seems to be for programmer facing output while the fmt is for user facing output and since the errors the logger throws are like the mother of errors, I though it would make sense to also implement fmt.

/// Structure `FileLogWriter` used for writing to a file in a thread-safe way.
#[derive(Debug)]
pub struct FileLogWriter {
line_writer: Mutex<RefCell<LineWriter<File>>>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Mutex already provides interior mutability via the MutexGuard you get when you call mutex.lock().map_err(..)?; The MutexGuard implements both Defer and DerefMut, so you can mutate whatever is held inside the Mutex.

}

pub fn write(&self, msg: &String) -> Result<()> {
let guard = self.line_writer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would make sense to implement a helper function which provides guard, so we don't have to repeat that relatively long code sequence twice (here, and once more for flush) :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, take a look now.

/// warn!("This will print 'WARN' surrounded by square brackets followed by log message");
/// }
/// ```
pub fn show_level(mut self, option: bool) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the name of this function is a bit misleading; maybe you could rename it to something like set_show_level or something :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to set_include_level.

/// warn!("This will print '[WARN:file_path.rs:155]' followed by log message");
/// }
/// ```
pub fn show_log_origin(mut self, file_path: bool, line_numbers: bool) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename this to something more suggestive towards the fact that it actually sets something :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, renamed it to set_include_origin

/// }
/// ```
pub fn init(mut self, log_path: Option<String>) -> Result<()> {
unsafe {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the unsafe block? If it's just for accessing the mut static, maybe it would make sense to refactor the code a bit, such that the actual unsafe block can be a lot smaller than it is now. It's a bit scary how large it is right now :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed for the INIT_RES variable which requires an unsafe block which is basically used each two lines approximately and with different purposes...if you have a suggestion of how to refactor and keep functionality, I am open to it ;)

pub fn init(mut self, log_path: Option<String>) -> Result<()> {
unsafe {
if INIT_RES.is_err() {
INIT_RES = Err(LoggerError::Poisoned(format!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplication seems kinda ugly :-s Can't we create the error once, and then clone it or something? :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would mean to implement clone for Result<(), LoggerError> which is a type containing a generic inside, not sure how to do that... and since I cannot return INIT_RES, I think this is the quickest way to achieve it (even it it is kind of ugly).

self.level_info.writer = Destination::File;
}
Err(ref e) => {
INIT_RES = Err(LoggerError::NeverInitialized(format!("{}", e)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it a bit strange to use the NeverInitialized error here, since that should mean the initialization was never attempted?

Copy link
Contributor Author

@dianpopa dianpopa Apr 23, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial purpose was to use NeverInitialized for when the first attempt to initialize fails. I wanted to use Poisoned when the user tries to intiailize again a logger that had previously failed initialization. Is it ok if I change the comments from the enum error to better express my intent in using those values?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

set_max_level(self.level_info.code.to_level_filter());

if let Err(e) = set_boxed_logger(Box::new(self)) {
INIT_RES = Err(LoggerError::NeverInitialized(format!("{}", e)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, NeverInitialized seems a bit out of place :-s

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See answer from above.

}
});
if INIT_RES.is_err() {
return Err(LoggerError::NeverInitialized(format!(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto answer

Copy link
Contributor

@filipposironi filipposironi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a drive by review.
Looks good to me.

alexandruag
alexandruag previously approved these changes Apr 26, 2018
@dianpopa dianpopa dismissed stale reviews from alexandruag and andreeaflorescu via 2fc0a66 April 27, 2018 09:49
Not necessary to throw a warning when the ioctl
return error (see qemu).

Signed-off-by: Diana Popa <[email protected]>
Simple logger that uses the external log crate to
output log messages to either stdout, stderr or a file.
Writing to the file is thread safe.

Signed-off-by: Diana Popa <[email protected]>
Signed-off-by: Diana Popa <[email protected]>
For now, the functionality is not available through API commands.
User can specify a file on disk (--log-file) for logging messages.
Otherwise, all messages will be flushed to either stdout or stderr.

Signed-off-by: Diana Popa <[email protected]>
...and all related stuff

Signed-off-by: Diana Popa <[email protected]>
@dianpopa dianpopa merged commit 25755f3 into firecracker-microvm:master Apr 30, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants