LogOut
LogOut is a pluggable Elixir Logger backend for routing exceptions and application logs directly to team chat platforms.
It uses an Adapter pattern to seamlessly format Elixir logs and send them asynchronously to services like Slack, Discord, Telegram, and Zulip.
Because LogOut hooks into Elixir's native :logger (via :gen_event), it integrates perfectly with all normal Logger.info, Logger.error, and unexpected exception traces across your app. Also, it uses Task.start/1 to dispatch HTTP requests asynchronously, meaning your Phoenix controllers or background jobs are never blocked by logging.
Installation
Add log_out to your list of dependencies in mix.exs:
def deps do
[
{:log_out, "~> 0.1.0"}
]
endConfiguration Basics
LogOut runs as an extra backend for Elixir's built-in :logger. You configure it in your environment config (e.g., config/prod.exs).
# 1. Add LogOut to your active backends
config :logger, backends: [:console, LogOut]
# 2. Configure LogOut
config :logger, LogOut,
# We recommend only forwarding :warning or :error to chat
level: :warning,
project_name: "My App Production", # Prefixes the chat messages
adapters: [
# You can configure one or multiple adapters to fire simultaneously!
{LogOut.Adapters.Slack, url: System.get_env("SLACK_WEBHOOK_URL")}
]Supported Adapters & Usage
LogOut provides four built-in adapters out of the box.
Slack
Simple webhook-based integration. Create separate channels per project or use one channel with project name prefixes.
config :logger, LogOut,
level: :warning,
project_name: "My App Production",
adapters: [
{LogOut.Adapters.Slack, url: System.get_env("SLACK_WEBHOOK_URL")}
]Multi-Project Setup:
-
Option 1: One channel (e.g.,
#prod-alerts) with differentproject_nameper app - Option 2: Separate channels per project with different webhook URLs
Discord
Similar to Slack, uses incoming webhook URLs.
config :logger, LogOut,
adapters: [
{LogOut.Adapters.Discord, url: System.get_env("DISCORD_WEBHOOK_URL")}
]Telegram
Great for instant mobile push notifications.
config :logger, LogOut,
adapters: [
{LogOut.Adapters.Telegram,
bot_token: System.get_env("TELEGRAM_BOT_TOKEN"),
chat_id: "-10012345678",
# message_thread_id: 123 (Optional: if using Telegram Topics in groups)
}
]Zulip
Unique Stream/Topic threading model that keeps logs organized across multiple projects and log types.
Prerequisites:
- Create a bot in Zulip: Settings → Personal settings → Bots → Add a new bot
- Create stream(s) in Zulip: Gear icon → Manage streams → Create stream
- Subscribe the bot to the stream(s)
config :logger, LogOut,
project_name: "MyApp Production",
adapters: [
{LogOut.Adapters.Zulip,
url: "https://zulip.example.com",
bot_email: "bot@example.com",
bot_api_key: System.get_env("ZULIP_API_KEY"),
stream: "alerts", # Must exist in Zulip
# topic defaults to project_name if not specified
topic: "my-app-production"
}
]Note: Streams must be created manually in Zulip before LogOut can post to them. If a stream doesn't exist, LogOut will log a warning and silently skip posting (your app won't crash).
Dynamic Topics:
You can override the topic on a per-log basis using Logger metadata:
# Goes to configured default topic
Logger.info("User logged out: user@example.com")
# Goes to "errors" topic in the same stream
Logger.error("Database connection failed", zulip_topic: "errors")
# Goes to "security" topic in the same stream
Logger.warning("Failed login attempt", zulip_topic: "security")Multi-Project Setup:
Recommended approach: Use stream names for projects, topics for log types:
# Project A config
config :logger, LogOut,
project_name: "ProjectA",
adapters: [{LogOut.Adapters.Zulip, stream: "ProjectA", topic: "alerts", ...}]
# Project B config
config :logger, LogOut,
project_name: "ProjectB",
adapters: [{LogOut.Adapters.Zulip, stream: "ProjectB", topic: "alerts", ...}]This keeps each project's logs in separate streams while allowing dynamic topics within each stream.
Filtering Noise
If a specific library or background worker is generating Logger.error entries that you want to ignore, you can use Elixir's built-in Logger filtering system:
# Filter out noisy events before they ever reach LogOut
config :logger, LogOut,
level: :warning,
project_name: "App",
adapters: [...],
metadata_filter: [application: :my_app] # Only send logs from :my_appWriting Your Own Adapter
If you need to send logs to Mattermost, Teams, or an internal HTTP endpoint, writing a custom adapter is trivial.
defmodule MyMattermostAdapter do
@behaviour LogOut.Adapter
@impl true
def send_message(log_event, config) do
# log_event = %{level: :error, msg: {:string, "Bad connection"}, meta: %{...}}
# Optional helpers bundled with LogOut
formatted_msg = LogOut.format_message(log_event)
emoji = LogOut.get_emoji(log_event.level)
# Use any HTTP client to fire off the web request
Req.post!("https://mattermost...", json: %{text: formatted_msg})
end
endThen just add your module to the adapters list:
config :logger, LogOut,
adapters: [
{MyMattermostAdapter, some_config_key: "value"}
]Documentation
Full documentation can be found at https://hexdocs.pm/log_out.