How would you do logging from a library?

Let’s say I have a library with a single interface. It would be a class, which provides a method. That’s it. But I would like to log while the method execute:

module SomeNameSpace
  class Library
    def the_method
      compute_something
      logger.info('Work Done')
    end
  end
end

Then, the client code would use this API as:

SomeNameSpace::Library.new.the_method

And expect to see the message Work Done somewhere.

But, when is the proper time to define that logger object?

I was thinking on defining the initialize method of Library with a default logger for the library developer, but allowing the client code to pass it’s own logger.

module SomeNameSpace
  class Library
    def initialize(logger: Logger.new(STDERR))
      @logger = logger
    end

    def the_method
      compute_something
      logger.info('Work Done')
    end
  end

  private

  def logger
    @logger
  end
end

So the client could do:

logger = Logger.new('a-file.log')
SomeNameSpace::Library.new(logger: logger).the_method

And then it will see the message Work Done in the file a-file.log.

Now, I’m wondering about what other ways to do this you use.

I suppose there must be a dependency injection solution, but I’m not sure if that is a good idea for libraries authors (prove me wrong).

What I’m sure, is that this must have been discussed in the Software Industry Thousands of times, and still I don’t know exactly what is the proper way to do this.

Thanks!

1 Like

What you shared is good enough.

Often, libraries will initialize their own default Logger instance internally and expose it via a public method, while also allowing the injection of a custom Logger externally if needed through a configuration attribute.

For example, Glimmer DSL for SWT initializes its Logger in its Glimmer::Config module while exposing it through the ::logger method to enable customization of the Logger, like the log level:

Glimmer::Config.logger.level = :debug

It also allows replacing the logger completely with your own custom logger:

# use the logging gem Logger instead, which supports async logging
require 'logging'
Glimmer::Config.logger = Logging.logger['custom_logger'] 

One thing to keep in mind is that the main Logger is usually a singleton in the application, so in general it is better to configure as a singleton class attribute instead of an instance attribute so that it can be accessed globally instead of passing around everywhere (e.g. Glimmer::Config.logger.info('Some Info')). That said, if you need to support multiple loggers in the same app instance for whatever reason, then you can certainly configure as an instance attribute instead.

I hope I answered your question.

Andy Maleh

1 Like

As an extra note, I forgot to mention that Glimmer DSL for SWT does not initialize its logger directly. It relies on the Glimmer DSL framework logger, and then it extends it with its own extra logic.

So for a logger reference, I would recommend looking at the code of Glimmer (not Glimmer DSL for SWT):

1 Like

Thank you very much. I think I’m getting this idea better. Decades not feeling well about how I’m logging.

I was always mixing ideas.

For one side, I wanted a way to keep functions clean, but to be able to trace them somehow without cluttering their bodies. That’s something I could achieve by AOP or DI, but it is not required.

The other idea is to delegate the Logger implementation to the users of my libraries. That is an independent concept than the first one, and it can be easily achievable as Glimmer does. Finally I GET IT.

Just a singleton service you can use anywhere in the library code to log (e.g. Glimmer::Config.logger.debug {e.message}). Now, how to hide that from functions bodies is another matter.

Thank you SO MUCH! I’m telling you, years thinking about this in the background.

Thanks for this interesting information!