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
- The LogHandler runs in the client process, meaning that it will block when logging information. All the
log/2
processing must be asynchronous. Here’s a test you can do without async processing.- Add sleep in the
MyApp.Slack.LogHandler.log/2
callback:Process.sleep(5000)
. - Call
Logger.info("test")
. Immediately logs and returns. - Call
Logger.info("test", slack: true)
. You’ll see that the Slack log blocks the calling process.
- Add sleep in the
- I’m using a fire-and-forget process (Task.start/3) as I don’t care about the result. If you want to monitor the created process (the Slack request), you can use Task.Supervisor.start_child/2.
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.
- Set up Slack application with the required scopes for the bot (
chat:write
). - Install the application you created in your workspace.
- Set the
Bot User OAuth Token
(found in the left menu OAuth & Permissions) as theSLACK_APP_TOKEN
env var. - Set the
@notifications_channel
in theSlack.HTTPCLient
module (right-click the desired channel, and scroll to the bottom). - 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!