-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
[WIP] Better Logger #6901
[WIP] Better Logger #6901
Conversation
I like the interface but the implementation is a little on the complex side. I'd rather have a singleton root logger instead of The This is a really good starting point though! |
Perhaps (not sure) also just have the So perhaps: class Logger
getter component : String
property backend : Logger | Emitter
end
class Emitter
property filter : Filter
end This design allows having a "root logger" which isn't I'd prefer to have filtering in a single place for understandability purposes, and this is the best way I can think to do it. |
src/logger/entry.cr
Outdated
|
||
def message : String | ||
if (msg = @message).is_a? Proc(String) | ||
return @message = msg.call |
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.
redundant return
, ditto below.
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.
Please let's focus on the general design before going into such details.
If we don't assume a separator then
The issue with this is that it makes the filter difficult to access from the outside - changing the default log level becomes
Whoops, I had meant to move |
@ezrast if you add a |
@ezrast Note you haven't set the a mail linked with your github's account in your |
@j8r Good catch! That explains why Github thinks I've never committed before. @RX14 That still seems like fighting the type system. If we don't want components to have their own filters we should give them a type that doesn't have filters. Does this change do what you want in that regard? https://github.com/ezrast/crystal/pull/3/files |
@ezrast if you go that route then i'd rather just have But I'll wait for other people on the core team to chime in since they'll have their own opinions. |
This However, this implies a very centralized architecture with every log message routing through one main logger instance. It can get pretty complex to apply filters to specific component paths. So, I'm not sure that's the best solution. Looking at Java's logging API, every |
I tend to think that copying what java does is the best. There have been about 5-10 java logging libraries over the years, and they're very well optimized by now for production usecases. |
I've checked through way too many levels of abstraction to confirm that in log4j2, each logger has a filter, a set of appenders (handlers) and then forwards to it's parent. I'd probably just copy that for now, although it does require a registry of logger instances to look up parents, but that's required anyway to reconfigure loggers at runtime. |
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 like the interface. The implementation is a bit hard to follow in some cases.
There are some specific comments. As long as we can be clear on the smallest public interface the user should expect I'm positive to move forward in this overall direction.
I definitely would like to see a module the user can be referenced to emit events and a factory method. And decouple the story of filtering in another module/interface. That should help the description to the user.
src/logger/entry.cr
Outdated
require "./severity" | ||
|
||
class Logger | ||
class Entry |
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.
Entry should be a struct
like Message
was before. So we avoid allocations for each entry.
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.
Entry
mutates itself so that message strings are evaluated at most once in the case of calls like logger.debug{ do_expensive_thing.to_s }
. IMO this is a good semantic to preserve.
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 not sure that's worth it. Log messages are usually not very expensive and then it's much better to avoid allocations.
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 agree with @straight-shoota . It is tempting the idea of lazy building the logger message, but not at the cost of stressing the GC.
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 lazy evaluation will save allocations in real-world scenarios, but putting them back in should be a non-breaking change, so I've removed them and will re-raise the issue in a later ticket if I can generate some meaningful benchmarks.
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.
Yeah, lazy evaluation itself really isn't the issue. It's memoizing the result...
src/logger.cr
Outdated
when Severity | ||
return unless entry.severity >= ff | ||
end | ||
emitter.try &.call(entry) |
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.
It was hard to follow how the Logger.class
that is a Logger::Base
ends up chained as the default emitter
.
I think we still have architecture issues to discuss before nitpicking this PR. |
@RX14, as a very general response to your Java comment: in my experience Java things tend to be a pain to deal with operationally, and one of the many reasons is that their components write things to lots of different log files, including duplicating entries between log files, all loosely controlled by a configuration file according to a scheme that is largely inscrutable to non-Java programmers. So I'm in favor of pushing configuration into the root logger instance. I don't want to encourage components to write to their own log files "just in case" and then also push logs downstream to be written again somewhere else. That said, changing The main thing missing is a registry, which I don't understand the use case for. Global configuration should go in the root logger; why make every component's logger be available globally? |
@bcardiff, Can you clarify what you mean by this? |
@ezrast okay, yes, root logger with an array of Also the naming is iffy. I'd propose:
Thoughts? |
@RX14 I'm not sure if we have agreed on using thin wrappers. Honestly, I'm not even sure myself if this is a great idea. |
@straight-shoota because if loggers have state, they need to be stored in a registry so that they can be configured using a central API. That complicates the whole design. |
Hm, yeah. But now |
The current PR has no registry, distinguishes between components hierarchically in the root logger if you want it to (for which the entire filter class is 23 lines of code and not complex at all), and still has fat loggers at the leaves just in case you want to do something wacky. I'm skeptical that it could get much less complicated. |
@straight-shoota except in this design the "registry" (really just parsing the component) is handled by |
src/logger.cr
Outdated
when Filter, Proc(Entry, Bool) | ||
return unless ff.call(entry) | ||
when Severity | ||
return unless entry.severity >= ff |
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.
You mean return if entry.severity < ff
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.
Why? The expressions are semantically equal.
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 X is inferior than Y" is simpler to understand than "unless X is superior or equal than Y", my opinion.
I made some of the less-controversial (I think; feel free to prove me wrong) requested changes and updated https://gist.github.com/ezrast/37c3377c172cd225efe0ed40f4da1e59 accordingly. Outstanding discussion topics include:
|
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.
It's getting into really good shape, I think!
src/logger.cr
Outdated
emitters.each &.call(entry) | ||
property component : String | ||
property filter : FilterType? | ||
property emitters : Array(EmitterType) |
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.
Why did the ivar accessors became properties? I don't think they need to be mutable. getter
should be enough.
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 made filter
a property since Logger::ROOT.filter = ...
needs to be possible, and then brought the rest along for consistency. But I agree immutability would be better in other cases. I think that's an argument in favor of making Logger.get
return a class that is thinner than Logger
.
component
can be made immutable, in any case. emitters
doesn't really matter since the array will always be mutable.
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.
Ah yes, I see the last commit. Yeah, when we're not using thin wrappers, filter
should probably be mutable. But component
and emitters
do not.
|
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.
Logger
needs to be stateless and immutable, because otherwise you need to keep a registry of loggers to make filters and appenders configurable by the application.
I much prefer the simple and understandable way of forwarding all logs to a central manager, which distributes those logs between several consumers which can optionally filter logs. The design I described before has the same flexibility as this design, while massively reducing the amount of mutable state.
src/logger.cr
Outdated
require "logger/entry" | ||
require "logger/filter" | ||
require "logger/emitter" | ||
require "logger/severity" |
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.
logger/*
?
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 you prefer, sure.
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.
IMHO individual includes are preferable. They show exactly what gets included and where. And you can apply an order which can be of significance.
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.
@straight-shoota I agree, but for now I'll be consistent with the rest of the codebase. Removing wildcard requires from the language is a fight for tomorrow. ;)
Ultimately, what i'm trying to say is that the data flow graph for my proposal is this:
whereas, the same graph for this PR is spaghetti |
@RX14, a registry isn't in the question at this point. It's "thin loggers; central configuration" vs "fat loggers; central configuration". In practice, the current PR's data flow is like this --
-- which has the potential to turn into spaghetti if people go adding filters and emitters to the leaf loggers, but the docs should make it clear that that's not the intended use case. Otherwise, the only effective difference between your graph and mine is where the filters go, which is a separate question. I am not actually against the idea of giving leaf loggers their own immutable class, I just think you're overstating the difference between the two setups. |
One important difference between both data flow diagrams isn't shown. In RX14's design, |
Yeah, that just occurred to me as well and is definitely pushing me in favor of immutable loggers. |
@ezrast that's what i meant by "Logger needs to be stateless and immutable, because otherwise you need to keep a registry of loggers". If Loggers hold configuration data, applications then need to be able to access the loggers of their libraries through Also, if you give people the ability to add filters and emitters to leaf loggers, they will add filters and emitters to leaf loggers. If you add filters and emitters to leaf loggers, the data flow diagram does turn into spaghetti. |
^ This splits out the guts of I think @straight-shoota has a good point about putting filters on the Emitter. Emitters may do globally-stateful things like rate limiting or database locking so it makes sense to want to use the same Emitter in two different places that may not have the same log level. Dispatcher includes Emitter so you can chain Dispatchers together if you want some Emitters to use a separate log level: # This MyEmitter instance will respond only to warnings and above
Logger.default_dispatcher.emitters.push(
Logger::Dispatcher.new(Logger::WARN, MyEmitter.new)
) |
Looks good. I'll wait for @bcardiff to comment on the design before doing a full review. |
Could this get a review :)? 🏓 This is at least blocking It would be great if the design here could be approved so that this PR can continue, or the work can be picked up and completed in another. |
Thanks for the bump, @z64. If it would help this get reviewed I can find time to add in documentation soon. Otherwise I'm holding off on docs/tests until the design gets approval. |
Prob can be closed now due to #8847. |
See #5874
Still needs specs and documentation, but see https://gist.github.com/ezrast/37c3377c172cd225efe0ed40f4da1e59 for example usage.