Strategies for conditional logging in library apps

In some of my library apps I want to add logging - but I also want consumers of these library apps to be able to enable/disable the logging dynamically.

Whats an appropriate strategy to do so?

Its a runtime concern so needs to be configurable dynamically (i.e. not a Mix.exs config - at least not alone). Perhaps its also appropriate to receive metadata config options at runtime too?

3 Likes

The :sys module in OTP actually provides a really good example of a good way to do this.

If you look at the :gen_statem loop it has the Debug parameter. That’s the metadata for the sys_debug function.
If Debug is [] returns [], in other words it’s a noop, otherwise it invokes :sys.handle_debug with the Debug metadata.
Since, the :sys module has a ton of different methods to log/debug I won’t go into what the meta data contains.But, if you used :sys.trace/2 it basically invokes the print_event function with the IO of stdout being the Dev argument.

It’s been a couple of months so I might be a bit rusty but, a working example may be:

defmodule Stuff do
  use GenServer

  def start_link(args, opts \\ []) do
    {trace, opts} = Keyword.pop(opts, :trace, [])
    {log, opts} = Keyword.pop(opts, :log, [])

    GenServer.start_link(__MODULE__, {args, trace ++ log}, opts)
  end

  def trace(pid, flag) do
    GenServer.call(pid, {:trace, flag})
  end
  def log(pid, flag) do
    GenServer.call(pid, {:log, flag})
  end

  def init({state, log_meta}) do
    log_fun(log_meta, state, :init)
    {:ok, {state, log_meta}}
  end

  def handle_call({:trace, true}, {pid, _}, {state, log_meta}) do
    log_fun(log_meta, state, {:trace_on, pid})
    {:reply, :ok, {state, [{:trace, true} | log_meta]}}
  end
  def handle_call({:trace, false}, {pid, _}, {state, log_meta}) do
    log_fun(log_meta, state, {:trace_off, pid})
    {:reply, :ok, {state, Keyword.delete(log_meta, :trace)}
  end
  def handle_call({:log, true}, {pid, _}, {state, log_meta}) do
    log_fun(log_meta, state, {:log_on, pid})
    {:reply, :ok, {state, [{:log, true} | log_meta]}}
  end
  def handle_call({:log, false}, {pid, _}, {state, log_meta}) do
    log_fun(log_meta, state, {:log_off, pid})
    {:reply, :ok, {state, Keyword.delete(log_meta, :log)}
  end

  defp log_fun([], _, _), do: :noop
  defp log_fun([{:trace, true} | log_meta], state, event) do
    IO.puts format_event(state, event)
    log_fun(log_meta, state, event)
  end
  defp log_fun([{:log, true} | log_meta], state, event) do
    Logger.info format_event(state, event)
    log_fun(log_meta, state, event)
  end

  defp format_event(state, :init) do
    "Initializing with #{inspect state}"
  end
  defp format_event(_, {:trace_on, pid}) do
    "Tracing turned on by #{inspect pid}"
  end
  defp format_event(_, {:trace_off, pid}) do
    "Tracing turned off by #{inspect pid}"
  end
  defp format_event(_, {:log_on, pid}) do
    "Logging turned on by #{inspect pid}"
  end
  defp format_event(_, {:log_off, pid}) do
    "Logging turned off by #{inspect pid}"
  end
end

It’s a lot of work for a pretty basic example, but once you have this much adding new events or even new logging methods is super simple.

Plus, if you wanted you could forego the entire format_event function. I just think that it cleans it up a bit and defers the binary creation until it actually needs to be written.

I personally just use Logger in Elixir and enable/disable loggers based on metadata (like what module or function did the logging).

This sounds the basic strategy I’m after, given that the logging macros include the callers context and therefore the module, function metadata.

But I’m not clear on how to approach “enable/disable loggers based upon metadata” - can you point me in the right direction?

Thank much - but seems more complex than needed for the simple idea of filtering (or enabling) based upon calling module. I think OvermindDL1’s description is what I’m looking for.

You’ve a lot of ideas I can reason about though for some of there parts of what I’m working on.

When doing your own back-end see how logger_file_backend does things, like it exposes config options for metadata_filter, and if specified then this backend will only log messages that include such metadata. You can easily make one that filters based on if it does not exist too, or filter based on values or even a function or whatever you need. You can do it all in your back-end pretty easily.

I do wish Logger had a ‘filter’ set that filtered messages before they hit a back-end though, but it is easy enough to add such functionality to a back-end itself (though it does seem effort-duplicating).

But you can see how logger_file_backend does it by following the calls starting at here (it is quite simple):

2 Likes