Log: When to use emit vs. context

Hello,

when do you use one over the other? I can’t really find docs about this. The following just prints both data structures, making me wonder why there are two different ways of doing it. The only difference I can see is that you can set context on a per-Fiber basis while emit sends volatile data.

require "log"
Log.with_context(x: { y: 1 }) do
  Log.error do |emitter|
    emitter.emit("...", a: { b: 2 })
  end
end

While Crystal’s docs are usually fantastic, I have found the logging module hard to understand. Personally I would have loved more elaborate infos on the various building blocks. They are scattered among the different submodules and it’s rarely clear to me when to use what.

As a use case, I’m trying to build a class where the logging calls of all instance methods will be prefixed with the value of a certain instance property. It doesn’t seem like the Log module supports that, so I guess I have to build a custom log method for my class which I then call. But I wonder which of the two syntaxes I should use.

As an aside, I also wonder: If I do

Log.trace { my_large_var.to_json }

this appears to create a heap var, as the log backend abstraction is indifferent to the consumer, aka you cannot access the IO here. Logging in particular is a use case of perhaps wanting to write to a file or in memory WAL thousands of times per second. This could be ugly wrt performance, it seems to me. Even the official performance guide recommends to work on IO instead of string operations. Am I missing something? Or should we perhaps just always instead do

Log.trace &.emit(my_large_var.to_json)

?

Pretty much yea. The way the docs name these are context and metadata. The main difference being context applies to all emitted logs within that block (or fiber depending the API used), while metadata is scoped to a single log. The main use case being is you can use context for common high level info like user_id, whereas metadata can be used to capture log/message specific info.

I’m pretty sure this’ll create a closure with you my_large_var, BUT #to_json will only be called if the log severity level is >= trace. E.g. the block of a log method doesn’t get executed if not used, thus saving you the actual allocation of your var’s JSON representation.

It doesn’t really matter. Log.trace &.emit(my_large_var.to_json) is the same as Log.trace { my_large_var.to_json }.

There is def going to be an overhead in logging, but ultimately depends on how you implement it. Like it would be a bit unexpected if you’re logging a bunch of trace level logs in production. But as you mentioned log entries have to store the message a string instead just writing to an IO directly because there isn’t a guarantee that the backend it’ll write to IS an IO at all. You could have a backend that writes things to Elasticsearch for example.