BrunoBonacci / mulog

μ/log is a micro-logging library that logs events and data, not words!
https://cljdoc.org/d/com.brunobonacci/mulog/
Apache License 2.0
485 stars 47 forks source link

µ/log and µ/trace should accept a logger #121

Open cch1 opened 5 months ago

cch1 commented 5 months ago

I occasionally have two independent "systems" operating concurrently in one JVM. I have found that mulog events from system A will randomly appear in the output of the publisher of system B and vice-versa. This is the inevitable problem with the stateful log buffer being held in a global (dynamic) var.

With careful use of binding and bound-fn it might be possible to overcome this problem but the generally accepted standard for Clojure libs and SPIs is to at least allow for explicit configuration and to only fall back to global mutable state when explicit stateful parameters are not supplied. This appears to have been taken into consideration in the start-publisher! function which has an arity to explicitly supply the buffer to be published.

Would it be possible for mulog to expose an explicit interface to log that does not rely on global mutable state?

BrunoBonacci commented 5 months ago

Hi,

It is possible to create separate loggers and use them independently if you use the log function µ/log* instead of the convenience macro µ/log and µ/trace.

In practice, this is not convenient at all, as you have to propagate down the current logger in every single function that might want to use it. Like you suggested, one alternative is to use dynamic binding which is also supported. However it becomes less effective when tasks spanning multiple threads.

The recommended approach is to use just one logger and then have two publishers that will publish only the events on the subsystem you want.

;; add the system as a key-value pair or local-context
(μ/log :order-completed, :system :myapp/system-A, :order-id order-id ) 

;; start independent publishers
(u/start-publisher!
  {:type :simple-file
    :filename "/tmp/mulog/system-A-events.log"

   :transform
   (fn [events]
     (filter #(= (:system %) :myapp/system-A)) events))})

(u/start-publisher!
  {:type :simple-file
    :filename "/tmp/mulog/system-B-events.log"

   :transform
   (fn [events]
     (filter #(= (:system %) :myapp/system-B)) events))})

The value :myapp/system-A might be injected via a dynamic var in your code, or passed down as parameter, or injected as TheadLocal via the µ/with-context. This approach is so much simpler to handle.

Anyway, I will keep this ticket open, and evaluate if it is worthy to extend the logging macros to accept the logger explicitly.

cch1 commented 5 months ago

Thanks for the assessment -I agree with it without exception.

I will add that in my practice, it's common to have development and test systems running concurrently -and even occasionally a production system (particularly if one is not diligently shutting them down). Having all the log statements include a differentiating key/value combo and the configured publishers filter on that would be annoying and further diverge production from test/development. I am soundly opposed to having the running subsystems (everything that logs and the publishers) be aware of what system they are running in.

My current solution is to use a wrapper around µ/log* that requires the logger (ring buffer) as a parameter but otherwise mimics the signature of µ/log Each system gets a dedicated buffer and the only prod/dev/test differentiator is where the publishers send the log.