Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I require Logger in all modules automatically?

Tags:

elixir

In most of my modules, I

require Logger

and then do some logging. In modules where I don't, I often make a change that includes adding some logging, and then I have to mark that I'm using the logger.

What's the cost of requireing the Logger in every single module? Is there a clean way to just always require the Logger without mentioning it in every module?

like image 227
Nathaniel Waisbrot Avatar asked Sep 12 '25 02:09

Nathaniel Waisbrot


1 Answers

The main purpose of require is to provide an access to macros declared in the required module:

Public functions in modules are globally available, but in order to use macros, you need to opt-in by requiring the module they are defined in.
— from docs for Kernel.SpecialForms.require/2

Also, from Elixir guide:

Macros are lexical: it is impossible to inject code or macros globally. In order to use a macro, you need to explicitly require or import the module that defines the macro.

Emphasis is mine. That is done on purpose of your own safety: unlike functions, macros might drastically modify your existing AST. Logger is a perfect example of this behaviour: it does not leave any trail in the AST when the level is less than the respective compile_time_purge_level specified in config. No single noop. None. Nihil.

That is basically why Logger is implemented as a macro.


There are two approaches to overcome this behaviour.

1. Simple. Not recommended.

Make your own wrapper of Logger as a function:

defmodule Log do 
  require Logger
  def log(whatever), do: Logger.log(whatever)
end

Log.log is now available everywhere (because it’s a function.)

Drawbacks:

  • in production you’ll have noop calls to Log.log left in the code on debug level. That might impact the performance.
  • metadata (the other reason Logger has to be a macro) will be rendered useless — you can't fetch the line number, module, etc of the log-line because they'll always give the site of the macro invocation (credits @NathanielWaisbrot)

2. Correct. Recommended.

Make your own MyApp.Utils module, which nevertheless exists in every single project slightly more complicated than todo-or-tictactoe-exercise. In this module, save for all your utility functions etc, implement the macro callback __using__:

defmodule MyApp.Utils do
  defmacro __using__(opts \\ []) do
    quote do
      require Logger
      require MyApp.Utils # if this module has macros itself

      def helper, do: Logger.log("Hey, I am always included")
      ...
    end

    ...
  end
end

Yes, now in all your modules using Logger you have to use MyApp.Utils explicitly, but since it nevertheless contains handy helpers that are used application-wide, the Logger itself is required for free.

like image 126
Aleksei Matiushkin Avatar answered Sep 13 '25 16:09

Aleksei Matiushkin