Daniel Perez

CTO@ClaudeTech

danhper

https://daniel.perez.sh/talks/2016/elixir-logger/#/

Today's topic

Elixir logger


Target audience

  • Getting started with Elixir

About Elixir logger

  • Part of Elixir core
  • Runs as an OTP application
  • Macro based
  • Features
    • Log level
    • Multiple backends support (using GenEvent)
    • Message formatting
    • Sync/async mode

Macro based

require Logger

Logger.info("This is a log message")

Logger.debug(fn -> expensive_computation() end)

Logger.error("An error occurred")

We need to require before use

Macro based

Can purge macros at compile time

iex(1)> quote(do: Logger.debug("hello")) |> Macro.expand(__ENV__)
        |> Macro.to_string() |> IO.puts()
Logger.bare_log(:debug, "hello", [module: nil, function: nil, file: "iex", line: 0] ++ [])

iex(2)> Logger.configure(compile_time_purge_level: :info)

iex(3)> quote(do: Logger.debug("hello")) |> Macro.expand(__ENV__)
        |> Macro.to_string() |> IO.puts()
:ok

Multiple backends

config :logger,
  backends: [:console, {LoggerFileBackend, :file_log}]

config :logger, :console,
  level: :debug

config :logger, :file_log,
  path: "/var/log/my_app/error.log",
  level: :info

Multiple backends

Logger.add_backend({LoggerFileBackend, :debug})
Logger.configure_backend({LoggerFileBackend, :debug},
  path: "/path/to/debug.log",
  format: ...,
  metadata: ...,
  metadata_filter: ...
)

Custom backend

Creating a (very) simple Slack backend

GenEvent implementation

defmodule SlackLogger do
  use GenEvent

  def handle_event({level, _gl, {Logger, message, _ts, _md}}, state) do
    IO.inspect(message)
    {:ok, state}
  end
end

GenEvent implementation

defmodule SlackLogger do
  use GenEvent

  def init(__MODULE__) do
    {:ok, configure([], %{})}
  end

  def handle_call({:configure, opts}, state) do
    {:ok, :ok, configure(opts, state)}
  end

  defp configure(opts, state) do
    config = Application.get_env(:logger, __MODULE__, [])
    |> Keyword.merge(opts)
    Application.put_env(:logger, __MODULE__, config)
    Map.merge(state, Enum.into(%{}, config))
  end
end

Sending to Slack

defmodule SlackLogger do
  def handle_event({level, _gl, {Logger, msg, _, _}}, %{level: log_level} = state) do
    if meet_level?(level, log_level) do
      post_to_slack(level, msg, state)
    end
    {:ok, state}
  end

  defp meet_level?(_lvl, nil), do: true
  defp meet_level?(lvl, min) do
    Logger.compare_levels(lvl, min) != :lt
  end

  defp post_to_slack(level, message, %{hook_url: hook_url} = state) do
    headers = [{"Content-Type", "application/json"}]
    payload = ~s({"text": "#{message}", "icon_emoji": ":warning:"})
    :hackney.post(hook_url, headers, payload)
  end
end

Source code

http://bit.ly/ex-slack-logger

Wrapping up

  • Elixir logger is powerful
  • No runtime penalty with macros
  • Custom backends are easy to develop
  • Logging is async by default
  • Check the docs for more