LogOut

Hex.pm VERSIONHex.pm LICENSE

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"}
  ]
end

Configuration 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:

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:

  1. Create a bot in Zulip: Settings → Personal settings → Bots → Add a new bot
  2. Create stream(s) in Zulip: Gear icon → Manage streams → Create stream
  3. 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_app

Writing 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
end

Then 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.