LoggerHandlerKit.Arrange (Logger Handler Kit v0.1.0)

View Source

Functions that help set up logger handler tests.

Summary

Functions

Attaches a logger handler with individual translation and ownership filters.

Give a particular process access to a logger handler attached by add_handler/4.

Replace global logger translator with per-handler ones, so that each test can configure it independently.

Ownership filter drops all events that do not have access to the logger handler attached by add_handler/4. This is one of the main things that makes async tests possible.

Functions

add_handler(handler_id, handler_module, config, big_config_override \\ %{})

@spec add_handler(
  :logger_handler.id(),
  module(),
  term(),
  :logger_handler.config() | map()
) ::
  {%{handler_id: :logger_handler.id(), handler_ref: reference()}, (-> term())}

Attaches a logger handler with individual translation and ownership filters.

Arguments

  • handler_id is an id that will be used for handler. A good idea is to use a test name or a test module.

  • handler_module a handler module that we want to attach. For example, :logger_std_h or Sentry.LoggerHandler.

  • config this is a handler config. The one that handler_module defines. A small one, of :term() type.

  • big_config_override is a map that will be merged into handler config. But a different one, the one of :logger_handler.config() type. This is also the place to put handle_otp_reports and handle_sasl_reports options, they will be passed to the translator.

Return

The function returns a two element tuple. The first element is a map that contains handler_id and handler_ref. It can be merged into the test context. The second element is an anonymous function that detaches the handler. Drop it into ExUnit.Callbacks.on_exit/1 callback.

The handler_module is not attached as-is. Instead, it is wrapped in LoggerHandlerKit.HandlerWrapper. Every time a handlers' :logger_handler.log/2 callback is invoked, it sends a message to the test process which can be received with LoggerHandlerKit.Assert.assert_logged/1 function.

Example

setup %{test: test} = context do
  {context, on_exit} =
    LoggerHandlerKit.Arrange.add_handler(
      test,
      :logger_std_h,
      %{},
      %{formatter: Logger.default_formatter(), level: :debug}
    )

  on_exit(on_exit)
  context
end

allow(owner_pid, pid, handler_id)

Give a particular process access to a logger handler attached by add_handler/4.

It shouldn't be necessary when using LoggerHandlerKit.Act functions, but for custom cases you might need it.

Remember!

In a lot of cases, caller tracking is enough to automatically propagate ownership information. Use it!

ensure_per_handler_translation(context)

Replace global logger translator with per-handler ones, so that each test can configure it independently.

Normally, handle_otp_reports and handle_sasl_reports are global configuration options, and changing them in tests is sufficient to make the tests sync. However, there is a workaround! In reality, both options are read at startup and passed to the logger_translator filter, which Elixir Logger attaches as a primary filter. We can detach this primary filter and reattach it to each logger handler independently. This way, each handler can take advantage of different translator configurations.

This function is designed to be run as a test setup, and it does exactly that. It detaches the global translator filter and attaches it to each existing handler.

defmodule LoggerHandlerKit.DefaultLoggerTest do
  use ExUnit.Case, async: true

  setup_all {LoggerHandlerKit.Arrange, :ensure_per_handler_translation}
  ...
end

Exception

Not all handlers will get a personal translator. Attaching logger translator to a logger handler can sometimes lead to surprising results because of the way logger filters work. If a handler has filter_default: :stop in configuration, it effectively drops all events by default and expects filters to explicitly tell which events they want through. Logger Translator follows a different paradigm, it blocks things that it wants to be blocked and happily allows everything else. When paired, a conservative handler and a underzelous filter will result in handler receiving events it didn't expect. To mitigate this, we only attach translator to handlers with filter_default: :log

ownership_filter(log_event, map)

Ownership filter drops all events that do not have access to the logger handler attached by add_handler/4. This is one of the main things that makes async tests possible.

Ownership filter is built with NimbleOwnership mechanism, the same that powers Mox, Req, etc. For the most part, caller tracking is enough to correctly propagate ownership information across processes. However, in some cases the logging process is completely detached from the originating process. In this case, ownership filter will check pid key in metadata, and if that pid has access to the handler, it will allow it and also ask Mox to allow it, if present.