-
Notifications
You must be signed in to change notification settings - Fork 51
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
Code refactoring + cleanup + maybe some new features #84
base: main
Are you sure you want to change the base?
Conversation
Well, that turned weird quickly 😅. It usually never takes me long to wreck the commit history, and I also tend to run into the limits of Rust's type system fairly quickly … Needless to say, I'm unsatisfied with my new approach. However, I see multiple alternatives going forward from here:
|
f85d471
to
491800b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good so far!
I know this is still a draft, but I've left some comments just since I had some time to look over it.
I'm definitely liking the changes here - opening everything up is great, and does seem to simplify things.
src/builders.rs
Outdated
@@ -1184,103 +1029,17 @@ impl fmt::Debug for Dispatch { | |||
"format", | |||
&self.format.as_ref().map(|_| "<formatter closure>"), | |||
) | |||
.field("children", &self.children) | |||
.field("children_count", &self.children.len()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. Wondering if there's a simple way we could keep this functionality? It probably isn't too useful for production code, but I imagine printing the full logger could be useful when debugging complicated fern setups.
I don't think it should be too much of a burden to require Debug
, but then again it would definitely mean requiring double-indirection if we ever want to accept Box<dyn Log>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requiring Debug
indeed isn't a hard constraint. The problem is that Rust is really difficult when working with trait objects, especially when multiple traits are involved. There simply is no such thing as dyn Log + Debug
. I could build a custom trait, but that would lead me back to dyn LogExt
which cannot be cast to dyn Log
.
I'll see if I can come up with an external solution that isn't too expensive, but no guarantees
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I can propose you to try the following:
- Attach a type information string to
Output
that gets filled in when constructed - When using general
Log
implementations instead of the provided methods, no information can be provided - The
Dispatch
struct contains compressed information of all its children - The
log_impl::Dispatch
does not. I do not want to keep this information after initialization - Especially, since a nested
Dispatch
gets converted into alog_impl::Dispatch
, this means that you'll only get the full filter information for the outermost level, and only coarse structure information for inner dispatchers.
src/lib.rs
Outdated
@@ -220,6 +238,11 @@ mod builders; | |||
mod errors; | |||
mod log_impl; | |||
|
|||
/// Logger implementations for different output targets | |||
pub mod logger { | |||
pub use crate::log_impl::{Panic, Reopen, Sender, Stderr, Stdout, Writer}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we expose these directly, I think we should at least make all the fields of these structs private - they're public to the crate right now, but exposing them to the world would lock us in on these particular structures, and that's a pain when updating code.
If we get rid of the configuration altogether, or rather, merge config
and log_impl
, the loggers
name you have here would make a good new name for the module file as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agree. I haven't decided on what API these will have, but probably simple a few constructors and From
implementations. My goal is indeed to move them to an logger
(or loggers
module), but I'm using the export trick right now to keep the diff more manageable.
src/log_impl.rs
Outdated
@@ -428,6 +446,12 @@ impl<'a> FormatCallback<'a> { | |||
// No need to write this twice (used for Stdout and Stderr structs) | |||
macro_rules! std_log_impl { | |||
($ident:ident) => { | |||
impl $ident { | |||
pub fn new(stream: io::$ident) -> Self { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think Stdout
and Stderr
constructors, if public, should take no arguments, as the streams they wrap are singletons.
src/builders.rs
Outdated
|
||
/* Sadly we cannot store and compose std::fmt::Arguments due to lifetime issues. | ||
* In order to avoid unnecessary write calls nevertheless, we must enumerate all options | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tried composing them using nested matches, with no temporary variables? I think something like this should work:
format_args!("{}{}", match time { ... => format_args!(...) }, match level { ... => format_args!(...) })
At least in the past, I've never been able to store the result of format_args!
in a variable, but it seems to always work to let it live temporarily while being passed to another function. That's the origin of out.finish
's design, too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sadly, no. As soon as I wrap the inner format_args!
in an expression, the error message appears. To be honest, this is really frustrating and I'll open an issue to change this in Rust because according to a quick search we're not the only ones running into this.
src/builders.rs
Outdated
chrono: bool, | ||
level: bool, | ||
target: bool, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm all for this, and we can definitely keep it here for now, but I'd rather the final "refactor" PR not include any new features. Ideally I'd be able to merge one PR which refactors everything but keeps the exact same feature set, and then review and merge future PRs which depend on that refactoring separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gotcha. Do you prefer to have the formatter feature PR first or the other way around? At the moment, they are completely independent from each other.
Right - I'm down for option one. I believe I originally added |
I definitely like the internal refactoring here, but I'm wondering about the external API. I originally designed That does make it a bit messy, but I'd like to think the experience of searching through docs on I don't think there's anything else which actually keeps all the functions still up and available in the same way, besides just keeping a single struct - and that doesn't really match the new paradigm. One possibility I was thinking of for a refactor like this which would retain some of the flatness would be having all constructor functions exported as public functions of the The one thing I'm mostly against here is having the API be 5+ different structs with 2-3 constructors each. It's nice from an internal organization perspective, but in my opinion would not be that great to look through. It requires users to care about what internal fern struct they're making, and I don't want to require that. Ideally, I want someone looking at fern to be able to choose from a flat list of things to log to. They should not care whether "a logging file with a static name", "a logging file whose name changes based on time" and "a logging file whose name rotates based on size" are the same struct or different structs - we represent them differently right now, but that's an implementation detail. I'm OK exposing that, but understanding that choice shouldn't be part of the main API (and if we didn't expose it at all, that would almost be better?) That's a lot of rambling from me about API design. Thoughts on this? I'm open to almost anything here, but I do hold some opinions on what's user-friendly, enumerated above. I'd like contradicting opinions, but I also wanted to write all that to give background on why I've designed it the way I have so far. |
src/builders.rs
Outdated
#[cfg(feature = "chrono")] | ||
{ | ||
self.chrono | ||
.then(|| chrono::Local::now().format("%Y-%m-%d %H:%M:%S,%3f")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this could easily become a standard-ish formatter, what would you think of matching the env_logger format, or at least, matching it as closely as we can given the enable features?
For apps which don't need custom configuration, env_logger
seems to be fairly standard, and so at least from my perspective, that format (something like [2017-11-09T02:12:24Z ERROR main]
) is "normal" for rust apps. It didn't used to be my personal favorite, and env_logger was not as popular when fern
started, so this was not the format I included in the original docs. However, given that it's already fairly normalized now, I think it would make for a pretty safe choice?
Hope too many comments aren't bad! Well, if they are, that's what I've written anyways :p |
Thanks for having a look at this!
I tried removing
Personally, I kind of like the style of having a list of structs for each thing I want to do and then a list of constructors for each configuration how I want to do it. But I can see why you're against this. I suggest adding one convenience method for each to |
I've thought some more about the public output API. Maybe making all those structs public isn't that a great idea. However, I can now point down my problem with the current way of doing things: it's inconsistent. See, it's really cool to be able to call |
I know this isn't part of this PR (I'll factor it out later, but at the moment it makes dogfooding easier), but why do we have a copy of the |
Just wanted to acknowledge that I've seen this, and haven't had time to review it in depth yet! Looking forward to it, just been busy. A few comments just from reading this thread, without rereading the code:
Right, I'd forgotten about that. If we can keep the transformation on My one worry is that this could turn into a situation where chaining another dispatch is not as intuitive as chaining a "regular" logger, and that breaks some of the niceness of things. There's also a potential of adding more non-Dispatch loggers in the future which chain things. I think the main one I've considered is a "dispatch-to-thread" logger which takes in one child logger, and handles records by formatting them to a
Thanks for pointing that out - that's something I think I completely missed in designing the current interface. I'm definitely interested in fixing this inconsistency. However, I'm not yet convinced that individual constructor methods are the way to do it - I think we could still have a solution which adds all possible outputs as methods in one place, such as on I've got two concerns with a solution which has individual constructors and First, if we only add a single convenience method to Second, if we mirror everything, or even if we only mirror some of the individual methods in I'm coming around somewhat to the idea of having just individual struct constructors. My main worry is still just exposing too many implementation details by how we split up the structures, and locking ourselves into a specific split even if another one comes up which makes more sense later. I'd be down for a different compromise though - what if we had everything as individual constructors, but they all returned That could let us change internal details in the future, if we want to, while keep existing constructors around. As an example, say we wanted to merge Thanks so much for continuing to push this forward, and I hope my delayed responses are still useful! Like I said I haven't had a lot of time, but I'm happy to continue forward with this, and I'll see if I can carve out some time to look at code again soon. |
I think that having all structs opaque should give us enough room to work with: in your example, we could simply change the inner of Btw I've just tried to unify the structs using generics, because they seemed so similar. Sadly, the stdout/stderr ones stick out because they don't use a I've been thinking a lot about possible API designs and how to keep them backwards compatible. One thing that always sticks out as odd is the Some other random findings I should write down before I forget them again:
Suppose I made |
Picking this up again. I noticed I had kind of painted myself into a corner and things ground to a halt. I thus lifted my requirement of keeping backwards compatibility and then progress came back. This is obviously a draft, but please generate the docs and have a look.
|
Awesome, thanks for being on top of this! Wanted to say I saw this, and I'm looking forward to reading your comments and code, and I'll reply in depth once I have. It may be more than a few days due to my current schedule and mental health, but I'll get to this ASAP. |
Sure, no need to rush. A quick note on the completeness state of the changes:
|
Hi! Sorry for the 6 month delay here. I'm hoping to get a full review soon. If you want to, we can then go back to back-and-forth & get it merged, or otherwise I'll probably make additional changes myself & merge it. I want to get these changes in, but especially given how unresponsive I've been here, I don't want to require any more of your time than necessary. |
Thanks. Do it however suits you best. I am still interested in this and ready to invest some more time. I'm using my fork here and there as dependency for a while now, and I'm really happy with the changes, although it still is not finished. The logging and output handling is okay IMO, but the formatters and coloring will need some more work. I think the best way forward would be to take these into a separate PR. |
See #83.
At the moment, I'm trying to get rid of all large enums and replace them with traits or something else suitable.
I'm not sure where I'm actually heading with this, but I have a vague idea of exposing a lot of
log_impl
as a public API. The goal is to assimilateOutput
andLog
, since at the moment they are 99% the same. I'd like to have outputs be individual structs instead of methods on theOutput
struct.