-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Provide a clean way to add custom LogLevels #33418
Comments
Two questions:
|
I don't think much of a change is needed, actually 🤔 Changing the struct LogLevel
level::Int32
end to struct LogLevel
level::Int32
name::String
end would allow the As for the macro, The real question is if it should be incorporated into a general logging rework.. I think there are already a bunch of things planned in that direction? |
Would that be a braking change or could it be added to Julia 1.4? |
I'm not a core developer, but I think changing the printing as I proposed with a fallback constructor to |
Now we just need a pull request... |
Good feature request! I agree with @Seelengrab's assessment: there's some relatively minor fairly backward compatible changes which could make this work better, and having a macro frontend is orthogonal to the internal changes required to I'm not entirely sure that changing
Yes, I do have some thoughts about this. In particular it would be nice to resolve the following design problems:
What is a log level really?This is the central design question; here is my current answer: I think log levels conflate two different concepts:
To really solve the design issues above we need to tease apart these concepts in a coherent way. Proposed DesignA possible solution to this is to model log levels as user-defined types with a minimal interface, possibly a new generic function module Logging # stdlib (possibly imported from CoreLogging if required)
"""
An abstract type for log levels
"""
abstract type AbstractLogLevel; end
"""
importance(log_level)
Return an `Int` defining the default importance level of `log_level`, as it will be seen by
the current logger. Logging backends may choose to upgrade or downgrade the importance
separately from the log level itself.
User defined importance levels should be relative to the standard log levels which are defined to have importance levels of `importance.([Debug, Info, Warn, Error]) == [-1000, 0, 1000, 2000]`.
"""
function importance
end
end Now, to use this, we'd do something like: module A
using Logging
struct Trace <: AbstractLogLevel ; end
Logging.importance(t::Trace) = 500
Base.show(io::IO, t::Trace) = print(io, "Trace")
end Note that with this design, a user's module To solve design point 2, I think we should modify This allows a user-defined logging backend to decide "Oh, the author of @oxinabox I'm interested in your thoughts on this too. User-defined macro frontendsFor this I think we could supply some convenience functions in stdlib |
Sound promising! Having the opportunity to filter log messages independent of the severity level is sometimes important to me, for example if I am debugging different sections of a service. |
I would like to make the argument for: Log levels being only importance. The categorical part is covered by the cross-ref: JuliaLogging/LoggingExtras.jl#13 If we stick to that, then we don't need to worry about changing the importance while also tracking the original label. Most of the time you only want to filter by importance, that is why we have loglevel filters. |
oxinabox wrote:
I do not agree. To give an example: You can group messages by function or by module. At some point in time you are ONLY interested in Trace messages in the function XYZ. You could use a filter from LoggingExtras to filter the Trace message in the group XYZ. The default level of a Trace message could be between Info and Warn, but it could be moved to between Info and Debug if required. But the level and the identity are distinct. Grouping by "Category" is an additional, useful feature. |
Thanks Frames! Yes, there's a tension and potential overlap with Just thinking out loud here: One possible, speculative and rather breaking way to take your idea seriously would be to make the default value of Having said that, I feel like there might be multiple types of category simmering below the surface here and if we could only identify what they are we'd have a better idea of what to do.
|
Just to be clear: I am not saying you shouldn't be able to group by log level. So the real interesting case is if you do not control the full logging pipeline, So we imagine that
(important aside: I'm not sure that universally dropping all levels, like that is a realistic use case. More reasonable might be just to drop Info down to debug and leave warn and error as is. This is a question of would decreasing important across the board be done?) So now as the developer of I think one goes to a very complicated place, if one starts from the assumption that the libraries one is calling have bugs. If we were going to go deeper we would be Note that it is a feature of the logging system that any package (e.g. GitHub.jl) can do what ever it likes with the logs for any function it calls (e.g. the functions from HTTP.jl). Lets write some more user stories like this, to try and get more clarity on what we are trying to solve. |
@oxinabox I love user stories! But they're only good when they come from the real user (I'm not a real user for this stuff right now), and are (at least initially) available in gory detail in the context of the real user's workflow. When you have a bunch of those real stories there's at least a chance of removing the problems entirely by design. Unfortunately it's actually really hard to gather real stories with enough detail. User's time is precious and there is a tendency to simplify which often removes the most important parts. Anyway, philosophy blah blah. I agree that composibility of transitive logging ( But this is straying somewhat from the original feature request (albeit related in design space). @ufechner7 perhaps you could describe what you're trying to achieve in a bit more "gory detail"? What do you think "trace" would imply for your application? In what circumstances would you use trace rather than |
I have real user stories of running Memento in production, with on-call paging, not quiet the same system but some of the same problems. That is where the story about using groups, rather than severity for alerts comes from
Yes, we can take this to another thread after. I think the core notion that one should be able to add new log-levels at different numbers is a very solid one. |
That's excellent (and I'm happy to be distracted by hearing about those. In gory detail if possible :-) ) Especially about debugging live distributed systems. I've dealt with a little of this in the past, but not enough that I think I have much insight into the problems faced by users of logging backends. I guess it would help if we had a
Yes! I think the fact that you want to treat My current feeling is still that log level should be seen as a category which
In contrast, we could then consider |
A fair point. |
Good question. All of those do seem rather dissatisfying TBH. Going with your nonlinear transformation idea, perhaps it could be argued that it's the current linear spacing of default importance levels which is at fault, and we should have |
@ufechner7 I've prototyped my proposed solution on a branch (cjf/logging-custom-levels), but I'd really like to hear more about your use case before making a PR. Knowing more about your use case is really important! In fact, the reason that the system doesn't neatly support this stuff "out of the box" in julia-1.0 is really due to a lack of motivating use cases.
|
My current use case for trace messages is a database backend. In the end the code shall run on a cluster and there will be a central logging server for all services. Does this answer your question? |
Thanks, I've been thinking about this and doing a bit more prototyping. It strikes me that |
Hi! I'm coming from the Oceananigans.jl repo where I'm working on helping out with the project's logging. I started work on a custom logger that is focusing on formatting the way the logging macros print their output. There is a desire to define custom There is also a desire to report specifically on when the project is performing setup of the environment (which I think would where a |
This can be done basically as well with groups (or some other othoganal marker). If it were a level then to disable by default would have to be somewhere below info. And one aslso might want to copy all depwarns to a file as a CI artifact so one can generate a report of how a set of packages are doing. I guess one could do this all by treating a depwarn level purely as a categorical concept. I feel like we have a nice ordinal loglevel property, To use the
I missed this one before;
Solution is to make group a tuple, and make sure group handling code can deal with it.
or compose two of them:
Look at all the logging things that have been happy with orginal levels only representing severity,
Sorry I think I wandered off topic again. People should be able to (IMO) abuse it for categories if they want, |
@oxinabox I get that you would like log level and group to be orthogonal concepts which are ordinal and categorical respectively. But people using other logging libraries just don't describe log level this way! Every link you posted talks about both the categorical and the ordinal components of log levels. The most clearly written of those links is https://reflectoring.io/logging-levels/; they call
To quote https://stackify.com/logging-levels-101:
|
Of course, just because other logging libraries conflate ordinal and categorical concepts doesn't mean we need to! But we need a concrete way forward from the current state of affairs. Currently we have:
|
Coming from JuliaLogging/ProgressLogging.jl#9, I'm interested in what kind of "category" is best for choosing a special kind of handlers like ProgressLogging.jl and TensorBoardLogger.jl. For those application, desirable properties are:
(Those points may indicate that using logging system for this is not the right direction. For example, you probably can build a similar system with a global For point 1, using custom For point 2, I don't think the Achieving point 2 and 3 together is somewhat tricky. We do point 2 typically by dispatching which requires a type to be imported, contracting to point 3. One solution may be to add a group namespaced by the package identity: struct NamespacedLogGroup
pkgname::Symbol
pkguuid::UUID
key::Symbol
end This way, you can send an event to a package without importing it. Similar namespacing is possible with log level by bringing the package identity to type domain. struct NamespacedLogLevel{pkgname, pkguuid}
level::Int32
end |
Yes, The question of level as ordinal or categorical really boils down to: |
That would be consistent and probably a helpful clarification. I've always been uncomfortable about how
This is a good question. So your point of view is: "in a logging backend, one should transform category, not importance"? That does make sense (I'm not completely sold, but I think I see some merits to this). Practically, how should we model custom levels in a way which they can interoperate with arbitrary backends? One possible way would be to declare that custom levels correspond in some way to an BTW, having a type per level visible to the logging frontend has interesting benefits; at least including the ability to define a new level-based |
I can imagine defining
Can this be done out-of-the-box? As One possibility may be to use the trick const logstatevar = gensym("logstatevar")
# `@logmsg` etc. use `@current_logstate()` instead of `current_logstate()`:
macro current_logstate()
var = esc(logstatevar)
quote
$(Expr(:isdefined, var)) ? $var : current_logstate()
end
end
macro with_logstate(logstate, ex)
quote
let $logstatevar = $logstate
$ex
end
end |> esc
end then users can put function f()
g(@current_logstate)
end
function g(logstate)
@with_logstate logstate begin
@debug "hello" # may be compiled away if debug information is in `typeof(logstate)`.
end
end As this is rather annoying to write, I think it can be automated via a macro @taskspecialize function f()
@debug "hello"
end which is expanded to something like above where the name of inner function |
THe idea of including the log level in the group had not occured to me. |
I was thinking at the extreme position where all categorical things go into |
Yes, this was exactly the kind of thing I had in mind; if we claim that More concretely, how should we display messages which have had their level deconstructed prior to the backend seeing them? If we just preserve the level that the user supplies, we can simply prefix the message with |
This is exactly the problem for the current What I'm proposing with Line 307 in 570b508
This allows for levels which are truly off at compile time. |
Ah, this is the difficulty I wasn't aware of. It makes me think This makes me realize that
If the output of Alternatively, I can imagine adding a module-local callable |
If someone needs that they can just do:
Which is the status quo. |
Right. I meant that, if we can agree that the current design has been in the right track all along, we can be confident when start exposing/documenting public API to make new levels easier. I also didn't realize that implementing new level could be done already https://discourse.julialang.org/t/creating-a-custom-log-message-categorie/29295/17
This sounds like an important property to have. |
Can we summarize what remains to be done in this issue, |
I think we need at least some documentation to mention that you need to implement at least Base.isless(a::MyLogLevel, b::LogLevel) = isless(a.level, b.level)
Base.isless(a::LogLevel, b::MyLogLevel) = isless(a.level, b.level)
Base.convert(::Type{LogLevel}, level::MyLogLevel) = LogLevel(level.level) for a custom log level. But they are rather redundant. It would be nice if it can be done by something like Base.Integer(x::MyLogLevel) = x.level We can also recommend implementing |
But |
I think this issue as a whole could be renamed "Taking log categories seriously" :-) To move forward here, I think we need to
Regarding compatibility: any changes clearly cannot modify the syntax of |
Looking back at my original proposal (#33418 (comment)), I find my thoughts on a concrete way forward for log levels are largely unchanged. The conversation here about For log level API, we could debate a better name for One point which was mentioned in my original proposal (and may have derailed the conversation) is the idea of passing along the "current severity" through the backends in a way which is decoupled from the level. I still think this might work out but it could probably be explored separately. |
How about specifying that the minimal and mandatory API is that it has a mapping to an integer (= As an optional API, it can overload
I find
Can't this be already done with struct LabelTransform <: AbstractLogLevel
label::String
original::AbstractLogLevel
end
severity(level::LabelTransform) = severity(level.original)
Base.print(io::IO, level::LabelTransform) =
print(io, level.label) We can also change severity while keeping the log label ( struct SeverityTransform <: AbstractLogLevel
level::Int32
original::AbstractLogLevel
end
changeseverity(f, level::AbstractLogLevel) =
SeverityTransform(f(severity(level)), level)
Base.print(io::IO, level::SeverityTransform) =
print(io, level.original) Then, you can use it like this using LoggingExtras, Setfield
downgrade_logger(sink) = TransformLogger(logger) do log
return @set log.level = changeseverity(x -> x - 1000, log.level)
end |
I am in agreement with @tkf on basically everything just said. How is this for deciding on a semantic for log levels.
|
Excellent. Yes, I agree that @oxinabox I like that discussion of level vs group, and I think your description of the standard levels is sharper and clearer than what we have in the current Logging docs (see https://docs.julialang.org/en/v1/stdlib/Logging/#Log-event-structure-1)
I would describe
Yes to the first part, but I do disagree with the second part of this: this advice will generally cause errors to be logged twice (once before the |
Ah, I had been thinking of |
Exactly. The technical term seems to be preorder but this refers to a whole grab-bag of ordering relations which are "almost" a total order but not quite due to missing some subset of the total ordering axioms. In this case our ordering relation is reflexive, transitive and total, but not antisymmetric; this last point means that it fails to be a total order. An (exact?) analogy is ordering the complex numbers by magnitude: this is also reflexive, transitive, and total, but not antisymmetric. (A partial order is a particular type of preorder which is reflexive, transitive and also antisymmetric but not total; this is not what we have here!) |
I've got a PR up for this which is mostly complete in implementation: #33960. I have ditched any attempt at passing the Please do review, I value all your feedback! |
Continuing some thoughts brought on by the mention of I think it's instructive to consider how we would like log level and group for depwarns to relate to It seems there's at least three ways to do this:
In principle, I feel that a "deprecation warning" does not cease to be a "warning", even though it may have a severity which is lowered by setting Option (2) is quite easy to implement efficiently if #33960 is merged in the current form, though it's still a global setting which clashes with the idea of configurable logger backends. Option (3) is quite conceptually clean: the level is always |
A thought about efficient handling of |
I think another way to propagate categorical information to from the logger frontend to the handler is to use a custom type of |
This is a good point. So.. I think the problem with log record categories is that we simply have too many candidates for what might be considered categorical information. We have, at least:
This is somewhat embarrassing complexity! If not for lazy record construction, I'd suggest that Some other connections to this suggestion:
Putting aside compatibility for just a moment in the pursuit of conceptual clarity, what if we had the following correspondences: @info "msg"
@logmsg InfoSeverity Info("msg")
@warn "msg"
@logmsg WarnSeverity Warning("msg")
@show a b c
@logmsg InfoSeverity ShownVars() a b c
@logprogress "msg" val
@logmsg ProgressLevel ProgressString("msg", #= ... =# progress=val) Does this seem conceptually clean? |
I would find this idea really useful if we could have e.g. Symbols as ids.
which would show up as
and we would have a logger which filters based on the labels. Use case: Time stepping, nested nonlinear and linear iterations... |
On discourse a workaround for this feature was provided, but it is not future proof, uses type piracy and cannot be used well in any module. See: https://discourse.julialang.org/t/creating-a-custom-log-message-categorie/29295/11
My feature request: Add a function to the module Logging:
Example usage:
This function should do two things:
a. make a macro available with the name of the new LogLevel
b. extend the printing of log messages to use the new, registered name
The text was updated successfully, but these errors were encountered: