Skip to content

Logging interface #91

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 1 commit into from
Aug 17, 2018
Merged

Conversation

ariard
Copy link

@ariard ariard commented Jul 26, 2018

Implement error, warn, info, debug and trace macros, internally calling
an instance of Logger, and passing it to every main structures
Issue #54

@ariard
Copy link
Author

ariard commented Jul 26, 2018

Still early work, need to add more comments and a test part, but want your thinking about it. Mainly, it s a lightweight copy of crate log, without a global logger, and less features. Maybe there is still to much stuff, and I'll prune more if needed.
Need also to test the conditional compilation part, to check that format_args! doesn t pop if it isn't necessary.

@ariard
Copy link
Author

ariard commented Jul 26, 2018

travis failures because I need to add Logger to test, working on that

@yuntai
Copy link
Contributor

yuntai commented Jul 26, 2018

Could it be worth exploring to use static ref Logger instead of passing around Arc<Logger>?

@TheBlueMatt
Copy link
Collaborator

TheBlueMatt commented Jul 26, 2018 via email

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Awesome, looks cool!

}

/// A trait encapsulating the operations required of a logger
pub trait Logger: Sync + Send {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This needs to be full-pub somehow, so I presume the macros and this need to be in a separate module?

@ariard ariard force-pushed the logging_interface branch from 6736566 to e5c6c54 Compare July 28, 2018 18:57
@ariard
Copy link
Author

ariard commented Jul 28, 2018

Updated, currently you need to pass self to each macro call because of Rust hygienic macro. This isn't awesome but that's the Rust way for now rust-lang/rust#39922.
In this commit, you have a one-time setting of a max debug level at compilation and a per-logger setting. Per-logger has to be inferior to global.
Still need to comment more and clean warnings and some scopes.

@ariard ariard force-pushed the logging_interface branch from e5c6c54 to d2ab686 Compare July 28, 2018 19:06
@ariard
Copy link
Author

ariard commented Jul 28, 2018

d2ab686 rebased

@ariard ariard force-pushed the logging_interface branch from d2ab686 to 3356d6e Compare July 29, 2018 00:44
@ariard
Copy link
Author

ariard commented Jul 29, 2018

Should build + rebase

@ariard ariard force-pushed the logging_interface branch from 3356d6e to 36273c9 Compare July 29, 2018 21:28
Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

This is really awesome work, thanks! It does seem to me to be a bit overengineered, though. It looks like there's about three ways to filter log messages (client-side, build-time, and via the filter stuff). I originally suggested supporting build-time to avoid performance issues with formatting strings and passing them to the user for every debug message, but if you can encapsulate that in the object passed to the client, it seems like there should be pretty low overhead for something like that, so maybe we can just always call into the client and let them filter as they see fit? If it becomes an issue maybe we can template our types based on the logger type so LLVM can inline the call?

)
}

macro_rules! warn {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you make these names a little less generic if we're gonna use them all over the crate? Like log_debug instead of debug, at least?


/// An enum representing the available verbosity level filters of the logger.
#[derive(Copy, Eq, Debug, Hash)]
pub enum LevelFilter {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems way overdesigned to have a filter API like this where our log logic does the filtering. Seems much simpler to me to just call into the logger for every log message and let the client filter it how they see fit? Would cut out a ton of code here, no?

#[derive(Clone,Debug)]
pub struct Record<'a> {
metadata: Metadata<'a>,
args: fmt::Arguments<'a>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh this is cool! If we have this do we need features to disable logging of low-level messages? Most of the reason I suggested having a feature was to avoid calling format!() for every debug message even when its just going to be discarded as that can be a performance issue in some applications, but it looks like maybe that wouldn't be an issue with this?

@ariard ariard force-pushed the logging_interface branch from 36273c9 to 2f9b8a7 Compare July 31, 2018 23:24
@ariard
Copy link
Author

ariard commented Jul 31, 2018

Instead of relaying on static variable for global filtering, I added conditional compilation directly in macro, same effect with less code. Based on playing with rustc linking flags of dead code, it prunes useless call to logger in macro body. It doesn't speed up compile-time as macro expanding is handled earlier, but it's a little decrease in binary size.
Digging into format_args, in fact most of the overhead is at run-time, at compile-time it's only parses the templates strings, places the Arguments values and check argument types. Formatting is only handled by writer at runtime.
So with this design, you have 2 filtering, one a build time and one per-logger. Per-logger can't be superior at build time, code isn't built. Client-side filtering let you manage the performance hit as you want, you have metadata to decide on it.

@ariard
Copy link
Author

ariard commented Jul 31, 2018

I cut the filter API, at first I was dubious to let it for already existents logger implementations being backward compatible with us, but well I've already cut too much for this being right.
Tell me for comments and also I can disable warning with macros.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

I still think this is a bit overengineered, but I'm getting increasingly excited to start using it...I'm tired of maintaining a patch that adds a ton of println!()s everywhere :p.

}
}

impl FromStr for Level {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we just not implement FromStr for Level? I'm not sure who would ever really need that, and it adds a whole Err type and associated impls.

Cargo.toml Outdated
@@ -15,6 +15,11 @@ build = "build.rs"
# Supports tracking channels with a non-bitcoin chain hashes. Currently enables all kinds of fun DoS attacks.
non_bitcoin_chain_hash_routing = []
fuzztarget = ["secp256k1/fuzztarget", "bitcoin/fuzztarget"]
max_level_error = []
Copy link
Collaborator

Choose a reason for hiding this comment

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

My point about overhead was more a question of "would someone care about the peroformance hit of this" - the reason to have the conditional compilation is that the act of doing the format!() itself can often be nontrivial, especially when you're running at something like trace, but because rust lets us do format_args!() without actually doing the full format!() unless the client actually wants to write out the line to their log, the overhead of doing a log call that gets dropped should be pretty low (mostly the indirect call through the Arc, but given we're doing a ton of crypto anyway so what's some indirect calls, which theoretically we could get dropped by templating if we needed to in the future). All that said, having the options isn't many lines of code, so I don't feel so strongly anymore.

Still, if we do want to keep the features, I'd think its more intuitive to have them go the other way - default to call the user's Logger for everything TRACE and up, and let the user turn things off at compile-time if they want to opt into it.


use bitcoin::blockdata::transaction::Transaction;

use secp256k1::PublicKey;

use std::sync::{Arc,Mutex};
use std::{mem};
use std::fmt::Write;
use std::cell::RefCell;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unused.


use bitcoin::blockdata::transaction::Transaction;

use secp256k1::PublicKey;

use std::sync::{Arc,Mutex};
use std::{mem};
use std::fmt::Write;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is unused.

use std::str::FromStr;
use std::sync::Arc;

use std::ascii::AsciiExt;
Copy link
Collaborator

Choose a reason for hiding this comment

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

This appears unused now?

}
}

#[allow(dead_code)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you just add a few sample logs in obvious places and remove the allow(dead_code)s? That way we can see it works and no need for bending over backwards here.


#[allow(dead_code)]
impl Level {
fn from_usize(u: usize) -> Option<Level> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here - if you remove from_str can probably remove from_usize as well.

/// A trait encapsulating the operations required of a logger
pub trait Logger: Sync + Send {
/// Enable the logger with a maximum log level
fn enable(&mut self, level: Level);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this (and flush, and level) need to be present? I think that's all client-side stuff that a library doesn't really need to concern itself with?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm still unsure why we'd need a third way to limit the log level (which this appears to be?).

@ariard ariard force-pushed the logging_interface branch from 2f9b8a7 to 51482c5 Compare August 1, 2018 03:20
@ariard
Copy link
Author

ariard commented Aug 1, 2018

Updated, reverse the logic for filtering, but I think it's fine to keep the 2 ways as you can have multiples loggers, each with its level policy and you want some global setting, without having to update each one. Also cut down the useless stuff and throw sample logs in some places (trying to find others ones)

@ariard ariard force-pushed the logging_interface branch from 51482c5 to f97ef28 Compare August 12, 2018 00:56
@ariard
Copy link
Author

ariard commented Aug 12, 2018

Right, no need of enable in the API, let it be implemented client-side. Rebased

Cargo.toml Outdated
@@ -15,6 +15,11 @@ build = "build.rs"
# Supports tracking channels with a non-bitcoin chain hashes. Currently enables all kinds of fun DoS attacks.
non_bitcoin_chain_hash_routing = []
fuzztarget = ["secp256k1/fuzztarget", "bitcoin/fuzztarget"]
max_level_off = []
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make these a bit more descriptive (eg "disable_log_level_X")? Also, these need to be documented somewhere (I guess in the docs for Logger?).

@@ -1176,6 +1184,7 @@ impl Channel {
let (remote_initial_commitment_tx, our_signature) = match self.funding_created_signature(&msg.signature) {
Ok(res) => res,
Err(e) => {
log_error!(self, "Got error handling message {}", e.err);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oops, log_error probably shouldn't be used for things remote peers can trivially trigger (eg a failed HTLC, we could use it for things where a channel had a confirmed transaction but something resulted in it being closed due to error), but, in general, we should try to keep log_error "DoS-resistant" and have some docs indicating that its safe to use and a remote peer cannot trivially fill up a client's disk using it.


static LOG_LEVEL_NAMES: [&'static str; 6] = ["OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"];

static LEVEL_PARSE_ERROR: &'static str =
Copy link
Collaborator

Choose a reason for hiding this comment

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

This appears unused now?

Trace,
}

impl Clone for Level {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't this just be derived?

pub struct Record<'a> {
metadata: Metadata<'a>,
args: fmt::Arguments<'a>,
module_path: Option<&'a str>,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this (and the below two additional vars) an Option? It seems like its always available (and should always be available), no?

}

macro_rules! log_internal {
($self: ident, target: $target:expr, $lvl:expr, $($arg:tt)+) => (
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we accept a $target here when we always are called without it until we call outselves? Can't we just drop the indirection?

Copy link
Author

Choose a reason for hiding this comment

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

Yes my mistake. Legacy design let you opt out using different pattern-matching of the macro. But in fact it's up to the implementation if it doesn't want to bother with additional infos


#[test]
fn test_logging_macros() {
let mut logger = TestLogger::new();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, would be nice if this actually checked that logs were generated, but not a big deal wither way.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah at first, I wawed to pipe the output or implement Write trait on a wrapper giving it the output of format_args! but not sure if it worth the hurdle. For me, it's testing that format_args! and stdout works well.


impl Logger for TestLogger {
fn log(&self, record: &Record) {
if self.level >= record.metadata().level() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, maybe just always log the record since output isnt printed unless the test fails anyway? Could skip the enable() function then, too.

src/util/mod.rs Outdated

#[cfg(not(feature = "fuzztarget"))]
#[macro_use]
pub(crate) mod logger;
Copy link
Collaborator

Choose a reason for hiding this comment

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

The Logger trait needs to be always-pub, but the macros for logging should probably be pub(crate). I guess they need to be separated out into different mods?

src/util/mod.rs Outdated
@@ -12,3 +12,12 @@ pub use self::rng::reset_rng_state;

#[cfg(test)]
pub(crate) mod test_utils;
#[cfg(feature = "fuzztarget")]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hmm, the Logger thing is simple enough I figure fuzz can impl its own test logger (which it can tune to enable logging info only when running in test so that it doesn't slow down the fuzzer too much).

@ariard ariard force-pushed the logging_interface branch 2 times, most recently from 932c81c to b1a1d3d Compare August 16, 2018 22:06
@ariard
Copy link
Author

ariard commented Aug 16, 2018

Updated and rebased. I cleaned the useless stuff as Metadata and others which were legacy design due to copy from log crate.


impl Logger for TestLogger {
fn log(&self, record: &Record) {
if self.level >= record.level() {
Copy link
Author

Choose a reason for hiding this comment

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

Maybe worth it to let it as an example for future logger implementers ?

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

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

Just gonna merge, will fix the two comments here later as logs are added.


/// The message body.
#[inline]
pub fn args(&self) -> &fmt::Arguments<'a> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not just make the vars in Record pub (with docs) and not worry about accessors?

($self: ident, $lvl:expr, $($arg:tt)+) => (
&$self.logger.log(&Record::new($lvl, format_args!($($arg)+), module_path!(), file!(), line!()));
);
($self: ident, $lvl:expr, $($arg:tt)+) => (log_internal!($self, $lvl, $($arg)+))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like this comment wasn't addressed: #91 (comment)

@ariard ariard force-pushed the logging_interface branch 2 times, most recently from 951e7d5 to 19c84c5 Compare August 17, 2018 00:01
Implement error, warn, info, debug and trace macros, internally calling
an instance of Logger, and passing it to every main structures
Build-time or client-side filtering.
Issue lightningdevkit#54
@ariard ariard force-pushed the logging_interface branch from 19c84c5 to 0029f04 Compare August 17, 2018 00:01
@TheBlueMatt TheBlueMatt merged commit 1a9ef40 into lightningdevkit:master Aug 17, 2018
@TheBlueMatt TheBlueMatt mentioned this pull request Aug 20, 2018
@ariard ariard deleted the logging_interface branch August 27, 2018 22:29
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.

3 participants