Monitorex

Copy Markdown

CI Hex.pm Hex Docs Downloads License

Real-time HTTP telemetry dashboard for Elixir/Phoenix applications.

Monitorex monitors outbound (Tesla, Finch/Req) and inbound (Phoenix) HTTP traffic, aggregates it into ETS-backed metrics, and renders a live-updating dark-theme dashboard — no database required.

Outbound Overview

Features

  • Outbound monitoring — track HTTP requests from Tesla, Finch, or Req
  • Inbound monitoring — track Phoenix router dispatch with per-consumer breakdowns
  • Live dashboard — 8 pages: Overview, Outbound/Inbound, host/route detail, timeline, consumer analytics
  • Timeline inspector — split-pane page with event list + request/response detail viewer
  • Auto-refresh — LiveView updates every 2 seconds
  • Sort, filter, paginate — interactive data tables on every page
  • Responsive — works on desktop and mobile (collapsible sidebar, card-layout tables)
  • Dark theme — polished design system with SVG icons and custom properties
  • Cluster support — aggregate data across multiple BEAM nodes
  • Health checkGET /monitorex/health with Collector status, queue depths, ETS sizes
  • Prometheus metricsGET /monitorex/metrics for requests, errors, latency, ETS sizes
  • Alert webhooks — configurable thresholds (error_rate, host_down, high_latency) with debounced dispatch
  • CSV/JSON export — download any dashboard view as .csv or .json
  • REST API — programmatic access to hosts, routes, events, and metrics via JSON endpoints
  • Slow request tracing — automatic capture of request/response bodies for requests exceeding a latency threshold
  • Alert Center — live alerts page with firing status, history, acknowledge, and snooze controls
  • Alert History — GenServer-backed ETS storage for alert records with lifecycle management
  • Native notifications — Slack, Discord, and Email notifiers with debounced dispatch
  • Persistent storage — optional SQLite backend via swappable Storage.Backend behaviour (ETS remains default)
  • No database required — all data lives in ETS tables (in-memory) by default

Screenshots

Outbound OverviewHost DetailTimeline Inspector
OverviewHost DetailTimeline

Installation

Add monitorex to your mix.exs:

def deps do
  [
    {:monitorex, "~> 0.6.0"}
  ]
end

Then run:

mix deps.get

Quick Start

1. Configure sources

In config/config.exs:

config :monitorex, :sources, [:tesla, :finch, :req, :phoenix]

2. Mount the dashboard in your router

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use Phoenix.Router
  import Monitorex.Router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
  end

  scope "/monitoring" do
    pipe_through :browser
    http_dashboard []
  end
end

3. Start your server

mix phx.server

Visit /monitoring to see your dashboard.

Configuration

Sources

config :monitorex, :sources, [:tesla, :finch, :phoenix]

Available sources: :tesla, :finch, :req, :phoenix. Only attach the sources you use.

⚠️ Req source requires req_telemetry — Req 0.5.x removed built-in telemetry. Add {:req_telemetry, "~> 0.1"} to your mix.exs deps for Req events to fire.

Inbound path filtering

Only track requests under specific path prefixes:

config :monitorex, :inbound_path_prefixes, ["/api", "/graphql"]

When not configured, all paths are tracked.

Authentication & Access Control

Implement the Monitorex.Resolver behaviour to control dashboard access:

defmodule MyApp.MonitorexResolver do
  @behaviour Monitorex.Resolver

  @impl true
  def resolve_user(conn) do
    # Return a map with user info from your session/auth system
    case get_session(conn, :current_user) do
      nil -> %{id: nil, name: "guest"}
      user -> %{id: user.id, name: user.name}
    end
  end

  @impl true
  def resolve_access(%{id: nil}) do
    # Redirect unauthenticated users to login
    {:forbidden, "/login"}
  end

  def resolve_access(_user) do
    :all
  end
end

Configure it:

config :monitorex, :resolver, MyApp.MonitorexResolver

If no resolver is configured, a default resolver grants full access (:all).

Consumer Identification

Monitorex identifies inbound consumers by priority:

  1. Custom function — your own consumer_fn:
    config :monitorex, :consumer_fn, &MyApp.extract_consumer/1
  2. Basic-auth username — decoded from Authorization: Basic ...
  3. API key header — value of X-Api-Key (first 8 characters)

Deduplication

When both Tesla and Finch are used in the same application, the same HTTP request may fire events from both libraries. Enable dedup to prevent double-counting:

config :monitorex, :clients, [:tesla, :finch]

Request/Response Detail Capture

Monitorex can capture HTTP headers and bodies for detailed inspection.

Header redaction

Sensitive header values are automatically redacted before storage:

config :monitorex, :redacted_headers, [
  "authorization",
  "cookie",
  "set-cookie",
  "x-api-key",
  "x-auth-token"
]

Body storage

Body capture is disabled by default to limit memory usage:

# Store request and/or response bodies on the Event struct
config :monitorex, :store_request_body, true
config :monitorex, :store_response_body, true

# Truncate bodies larger than N bytes (default: 10_000)
config :monitorex, :max_body_bytes, 10_000

Memory Management

To prevent unbounded ETS growth in production, Monitorex caps aggregate tables and prunes stale entries:

# Maximum entries per aggregate table (hosts, endpoints, routes, consumers)
# When exceeded, oldest entries are dropped during cleanup.
config :monitorex, :max_endpoints, 2_000

# Recent event ring buffers (per direction)
config :monitorex, :max_recent, 500       # outbound
config :monitorex, :max_recent_inbound, 500  # inbound

# Stale entry TTL (aggregate tables)
config :monitorex, :endpoint_ttl, :timer.hours(1)

Slow Request Tracing

Monitorex can automatically flag and retain detailed traces for slow requests. When a request exceeds the configured threshold, full request/response bodies are captured even if body storage is globally disabled — providing debugging data without the memory overhead of storing all bodies.

# Latency threshold in milliseconds (default: 2_000)
config :monitorex, :slow_request_threshold_ms, 2_000

# Maximum slow requests retained per direction (default: 200)
config :monitorex, :max_slow, 200

Set :slow_request_threshold_ms to nil or 0 to disable slow request tracing entirely.

Slow events are stored in separate ETS tables (:monitorex_slow_outbound and :monitorex_slow_inbound) and exposed via Monitorex.Storage.list_slow_outbound/1 and list_slow_inbound/1 for custom dashboards or alerting integrations.

Monitor runtime memory usage:

Monitorex.memory_usage()
# => %{tables: %{monitorex_outbound_hosts: %{size: 42, memory_words: 1234}, ...},
#     total_words: 46089, total_kb: 18.53}

The health endpoint (GET /monitorex/health) also exposes current ETS table sizes and total memory under ets_table_sizes and total_ets_memory_words.

Storage Backend

By default, Monitorex stores all data in ETS tables (in-memory). You can optionally enable SQLite persistence so metrics survive BEAM restarts:

# Use ETS (default)
config :monitorex, :storage, Monitorex.Storage.ETS

# Use SQLite — requires :exqlite in your deps
config :monitorex, :storage, Monitorex.Storage.SQLite
config :monitorex, :sqlite_path, "/var/lib/monitorex/data.db"

SQLite is compiled conditionally — if exqlite is not present, Monitorex falls back to ETS automatically. Add {:exqlite, "~> 0.29"} to your mix.exs to use it.

Alerts & Notifications

Configure alert rules and notification channels:

# Alert thresholds evaluated every cleanup cycle
config :monitorex, :alerts, [
  %{name: :high_error_rate, condition: :error_rate, threshold: 0.05},
  %{name: :host_down, condition: :host_down, threshold: 3},
  %{name: :high_latency, condition: :high_latency, threshold: 1_000}
]

# Slack webhook
config :monitorex, :slack_webhook_url, "https://hooks.slack.com/services/..."

# Discord webhook
config :monitorex, :discord_webhook_url, "https://discord.com/api/webhooks/..."

# SMTP (requires :gen_smtp)
config :monitorex, :smtp,
  relay: "smtp.example.com",
  username: "alerts@example.com",
  password: "secret",
  from: "monitorex@example.com",
  to: ["oncall@example.com"]

Rules can also be added/removed at runtime via Monitorex.Alerts.add_rule/1 and remove_rule/1.

Pages

PageURLDescription
Outbound Overview/Summary cards + host table
Outbound Recent/outbound_recentLive feed with status filter
Host Detail/host/:hostPer-endpoint breakdown + recent requests
Inbound Overview/inboundRoute table + summary
Inbound Consumers/inbound_consumersPer-consumer stats
Inbound Recent/inbound_recentLive feed with filters
Timeline/timelineSplit-pane event inspector with request/response detail
Route Detail/route/:keyConsumer breakdown + recent requests
Alerts/alertsAlert summary, firing alerts, history table

REST API

Monitorex ships a built-in JSON REST API for programmatic access to telemetry data. The API is auto-mounted at /api (configurable via the :api_path option in http_dashboard/1).

Endpoints

EndpointDescription
GET /api/healthHealth status (same as /monitorex/health)
GET /api/hostsList all hosts with aggregate stats
GET /api/hosts/:hostPer-host detail with endpoint breakdown
GET /api/routesInbound route aggregates
GET /api/consumersConsumer stats
GET /api/eventsRecent events with filters (see below)
GET /api/events/:timestampSingle event detail
GET /api/metricsComputed metrics (RPS, error rate, latency quantiles)

All endpoints return a consistent JSON envelope:

{"ok": true, "data": ...}

Errors return:

{"ok": false, "error": "message"}

Query parameters

Events (GET /api/events):

ParamTypeDefaultDescription
directionstring"outbound""outbound" or "inbound"
limitinteger50Max results (max: 500)
offsetinteger0Pagination offset
hoststringFilter by host (outbound only)
methodstringFilter by HTTP method (GET, POST, etc.)
statusintegerFilter by HTTP status code
consumerstringFilter by consumer (inbound only)
routestringFilter by route key (inbound only)
sinceISO 8601Events after this timestamp

Metrics (GET /api/metrics):

ParamTypeDefaultDescription
hoststringallFilter to a specific host
windowinteger300Time window in seconds for RPS/error rate

Pagination

Paginated endpoints (/api/events) return these response headers:

  • X-Total-Count — total matching events
  • X-Page-Size — the limit parameter used
  • X-Page-Offset — the offset parameter used
  • X-Returned-Count — actual returned count

CORS

All endpoints include Access-Control-Allow-Origin: * and respond to OPTIONS preflight requests.

Examples

# List all hosts
curl http://localhost:4000/api/hosts

# Outbound events filtered by host and status
curl "http://localhost:4000/api/events?direction=outbound&host=api.example.com&status=500"

# Metrics with 5-minute window
curl "http://localhost:4000/api/metrics?window=300"

# Single event by timestamp
curl "http://localhost:4000/api/events/1779232233155167"

Disabling the API

Set :api_path to nil or false:

http_dashboard api_path: false

Note: The API is mounted inside the scope pipeline. For production use, consider mounting it in a separate scope without :browser pipeline to avoid CSRF protection on non-GET methods:

scope "/monitoring" do
  pipe_through :browser
  http_dashboard api_path: false
end

scope "/monitoring/api" do
  forward "/", Monitorex.ApiPlug
end

Asset Pipeline

Monitorex ships pre-built CSS and JS assets. To rebuild them from source:

mix assets.build

Source files are in assets/css/app.css and assets/js/app.js. The build uses Tailwind CSS v4 and esbuild.

Development

git clone https://github.com/GustavoZiaugra/monitorex.git
cd monitorex
mix deps.get
mix compile --warnings-as-errors

# Run tests
mix test

# Run demo server
mix run scripts/demo.exs

# Validate as Phoenix dependency
cd /tmp
mix phx.new demo_monitorex --no-ecto --no-mailer --no-dashboard --no-gettext
cd demo_monitorex
# add {:monitorex, path: "/path/to/monitorex"} to mix.exs
mix deps.get && mix compile

Docs

mix docs

Then open doc/index.html.

License

MIT