Skip to content

Integrate Slack into the Elixir's logging system

Posted on:2024-02-23 | 4 min read

With the release of Elixir 1.15, it included a new integration with Erlang/OTP: “Logger Handlers”. The Logger Handlers provide the ability to plug yourself into Erlang’s logging system, the same as the previous elixir logger backends, but in a standard way defined by Erlang.

Before. Sending a Slack message manually (prone to errors).

message = "New customer signup"
Slack.Client.send_message(message)
Logger.info(message)

After. Slack is integrated into our logging system!

Logger.info("New customer signup", slack: true)

Let’s build a simple log handler that sends a notification to both STDOUT and Slack.

Table of contents

Open Table of contents

Slack Log handler module

To define a log handler, we must implement the following callback:

log(LogEvent, Config) -> void()

You can see the Erlang callback @spec here. The handler specification has additional callback functions to define if you need said functionality.

Now, onto the handler:

defmodule MyApp.Slack.LogHandler do
  @moduledoc """
  Read from logs and send a message to Slack when the log metadata contains `[slack: true]`.
  """

  ## :logger handlers callbacks

  @spec log(:logger.log_event(), :logger.handler_config()) :: :ok
  def log(%{level: log_level, meta: %{slack: true}, msg: {:string, log_msg}}, _handler_config) do
    %{level: configured_log_level} = :logger.get_primary_config()

    if Logger.compare_levels(log_level, configured_log_level) == :lt do
      :ok
    else
      Task.start({MyApp.Slack.HTTPClient, :send_message, [log_msg]})
    end
  end

  def log(_log_event, _handler_config) do
    :ignore
  end
end

As you can read, we first pattern match on the LogEvent: meta: %{slack: true} to check if this log is configured to be sent to Slack. Then, we compare the configured_log_level to know if the log level is greater than the configured application log level, see log levels here. After those checks, we fire a Task to send a message using the MyApp.Slack.Client (defined here).

Caveats

Configuring the log handler

In config/config.exs:

# Configures Logger Handlers
config :my_app, :logger, [
  {:handler, :slack_handler, MyApp.Slack.LogHandler, %{}}
]

Attaching the log handler to the logging system

In your Application.start/2 callback, found in lib/my_app/application.ex:

@impl true
def start(_type, _args) do
  Logger.add_handlers(:my_app)

  children = [
  ...
end

Logging messages to STDOUT and to Slack!

Logger.info("useful log", slack: true)

Done, the logging system will log to both STDOUT and send the same log to the Slack channel you configured.

Slack HTTP client module

You need to set up a Slack application with the required scopes to call this endpoint.

  1. Set up Slack application with the required scopes for the bot (chat:write).
  2. Install the application you created in your workspace.
  3. Set the Bot User OAuth Token (found in the left menu OAuth & Permissions) as the SLACK_APP_TOKEN env var.
  4. Set the @notifications_channel in the Slack.HTTPCLient module (right-click the desired channel, and scroll to the bottom).
  5. Call Logger.info("Test", slack: true) to test the integration.
defmodule MyApp.Slack.HTTPClient do
  @moduledoc """
  Slack HTTP Client.
  """

  @notifications_channel_id "XXXXXXXXX"

  @spec send_message(String.t()) :: Req.Response.t() | :ignore
  def send_message(message) do
    case slack_app_token() do
      {:ok, token} ->
        Req.post!(req(),
          url: "chat.postMessage",
          auth: {:bearer, token},
          json: %{
            channel: @notifications_channel_id,
            text: message
          }
        )

      :error ->
        :ignore
    end
  end

  @spec req() :: Req.Request.t()
  defp req do
    Req.new(
      base_url: "https://slack.com/api/",
      headers: %{
        content_type: "application/json"
      }
    )
  end

  @spec slack_app_token() :: {:ok, String.t()} | :error
  defp slack_app_token do
    System.fetch_env("SLACK_APP_TOKEN")
  end
end

That’s it! You are now free to implement any logger handler and use the power of Erlang’s logging system. If you want to learn more about log handlers, I recommend reading Sentry’s Elixir library. Here is the link to their logger handler.

Happy logging!