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!




Previous Post
Enhancing your Docker Workflow with Local PostgreSQL Integration
Next Post
Automatically update your Elixir dependencies