I started working with the new Log
module to get some logging setup in Athena. So far everything is going quite well. However, I have some ideas/suggestions that could be used to make it even better (IMO). Can update later if I/others have any other suggestions. From here if approved these can turn into issues or PRs.
Add context to log methods
To start, I really love the context concept. It makes it super easy to add context common to all logged messages. Sometimes tho, I only want to add context to a specific entry, such as that a route was found for the given request path. I found myself doing this quite often:
LOGGER.with_context do
LOGGER.context.set uri: request.path, ...
LOGGER.info ..
end
Thoughts on allowing entry specific context to be passed via the log methods? I.e. something like LOGGER.info uri: request.path { ... }
Essentially if any context was given this would just pass the args to Log::Context.new
and set that on the created entry. This would probably result in a few overloads similar to Log::Context.new
.
Related to this, maybe also define an overload that accepts Log::Severity
. Would be useful when you want to log the same thing but conditionally determine the severity.
Log::Severity::Verbose
Would it make more sense if this was below Debug
?
Log::Context#to_json
Iām using a custom IO formatter and JSON serializing the context is the easiest/most readable way to display the context within a line on STDOUT. This really would just be:
def to_json(builder : JSON::Builder) : Nil
@raw.to_json builder
end
Log::Context#empty?
Related to the previous suggestion; it would be nice to know if there is any data that needs to be written in the first place. Main use case would be to prevent empty {}
objects in the formatted entry. Something like:
def empty? : Bool
case raw = @raw
when Hash, Array
raw.empty?
else
false
end
end
Message Placeholders
Just a thought I had, donāt really care either way. The concept would be doing like:
Log.context.set user: "Jim"
Log.info { "{user} logged in" }
When logged the message would have {user}
replaced with the value in the context.
Namespacing
Possibly move the default backends into a Log::Backends
namespace? Downside would be longer type names. This could also be something for a feature of the docs command to organize things better without changing the actual FQN of the type.
Formatting
Currently only the IOBackend
is formatable. One suggestion would be to extract this logic into a module. This module could be like:
module Log::Formatable
property formatter : Log::Formatter
def default_formatter : Log::Formatter
# Some common format
# Children could override this if they wish to change it
end
end
Another related suggestion would be to support type based formatters. Could just be structs that implement #call
to keep API the same as the proc formatters.
Log::Builder#append_backend
Log::Builder#append_backend
I can see internally there is a private method that adds another backend to the given Log
instance. It would be nice if this was exposed, or at least a public version of it. The use case would be for creating a stack of backends that should be executed for each entry. This is already currently possible via Log::BroadcastBackend
, but the docs on that say it shouldnāt be used directly, hence this idea.
Another idea would be allow Log::Builder#bind
to also accepts an Array(Log::Backend)
.
NVM: This is already doable by binding to the same source multiple times with different backends.
Bubble
If a method is exposed to allow a stack of backends, it might be helpful to include a concept from the logger library we use at work called bubble. It essentially controls if an entry should be sent to the next backend. Essentially it is a boolean that defines whether a backend blocks the entry or not if they handled it. I.e. if you had two backends, A
and B
; if A
had bubble
set to false
, and was able to process a given entry (i.e. its configured severity allowed it), then B
would not execute. Otherwise if A
was not able to handle an entry, it would be passed onto B
.
I suppose this would only work well if the severity was on the backend side of things? It could be an optional thing that uses the severity defined on the backend or default to the severity provided when bound if the backend did not explicitly set one.
Processors
Most of the time, manually adding context is going to be enough, such as for context related to a given user action. E.x. user_id
of currently logged in user. However, there are some use cases where you want some piece of data to be included in every entryās context, but that doesnāt originate from singular source. E.x. say including git
information. It wouldnāt really make sense to manually add this context as it would have to handle entries from multiple origins.
A processor would just be a type/proc similar to a handler, but ran before the handlers in order to add global context.