Monitorex

CIHex.pmHex DocsDownloadsLicense

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

Screenshots

Outbound OverviewHost DetailTimeline Inspector
OverviewHost DetailTimeline

Installation

The fastest way to add Monitorex is with Igniter, the standard installer framework for Elixir. It inspects your application and configures Monitorex automatically, showing a diff before writing anything:

# install igniter if you don't have it yet
mix archive.install hex igniter_new
# install and configure monitorex
mix igniter.install monitorex

The installer detects from your application's AST:

DecisionDetection
Which sources to enablewhich of :tesla / :finch / :req are in your dep tree (:phoenix is always enabled)
Whether dedup is neededconfig :tesla, adapter: {Tesla.Adapter.Finch, _} present → sets clients: [:tesla, :finch]
REST API handlingthe built-in API is disabled (http_dashboard api_path: false) so it never collides with an existing scope "/api"
Where to mount--path option, default /monitoring (warns if it collides with an existing scope)
req_telemetry requirement:req present but :req_telemetry absent → offered as a dependency

It writes the config :monitorex, :sources config, adds import Monitorex.Router plus a scope + pipe_through :monitoring mount in your router, and adds req_telemetry when needed. The mount uses a dedicated :monitoring pipeline withoutprotect_from_forgery so the dashboard assets aren't rejected with a 403 (see the installation guide). The manual steps below remain fully supported — the installer automates them, it does not replace them.

Manual installation

Add monitorex to your mix.exs:

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

Then run:

mix deps.get

Quick Start

For a real Phoenix app, read the Installation Guide first. The steps below include the essentials that the happy path leaves out (REST API security, pipeline requirements, Tesla-on-Finch dedup).

1. Configure sources and deduplication

In config/config.exs:

# Only attach the sources you use. :phoenix monitors inbound; the rest are outbound.
config :monitorex, :sources, [:tesla, :finch, :req, :phoenix]
# REQUIRED if Tesla runs on the Finch adapter (very common): both libraries
# emit telemetry for the same request, so without this every outbound request
# is counted twice.
config :monitorex, :clients, [:tesla, :finch]

2. Mount the dashboard in your router

The dashboard scope needs a pipeline withoutprotect_from_forgery (it rejects the cross-origin script GET with a 403 on app.js; CSS is unaffected) and without your app's CSP plug (Monitorex scripts carry no nonce). Mount on a dedicated host or a path prefix:

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
import Monitorex.Router
# Dashboard pipeline — mirrors :browser minus protect_from_forgery / CSP.
pipeline :monitoring do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
end
scope "/", host: "monitoring." do
pipe_through :monitoring
# api_path: false — the REST API is unauthenticated and defaults to /api.
http_dashboard api_path: false
end
end

Point a monitoring. subdomain at your app, or use a path prefix (scope "/monitoring").

3. Start your server

mix phx.server

Visit your dashboard (e.g. http://monitoring.localhost:4000 or /monitoring) to see it.

Mounting under a path prefix

The dashboard is fully mountable under a path prefix. Asset links and navigation links are derived from the mount point, so a scope "/monitoring" (or mounting the endpoint behind a reverse proxy) works out of the box.

The LiveView client connects to your app's LiveView socket endpoint. By default it uses /live; pass :socket_path if your app mounts Phoenix.LiveView.Socket elsewhere:

scope "/monitoring" do
pipe_through :browser
http_dashboard socket_path: "/live"
end

Make sure the pipeline includes :protect_from_forgery (or Plug.CSRFProtection) so the dashboard can read the CSRF token it needs to establish the LiveView socket connection.

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. Alternatively, Req runs on Finch, so the :finch source captures Req traffic (with full request/response details) without req_telemetry.

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

⚠️ Required if Tesla runs on the Finch adapter. Tesla's Finch adapter emits both :tesla and :finch telemetry events for the same request. Without dedup, every outbound request is counted twice.

Enable dedup by listing both clients:

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. Without it, the timeline detail pane shows headers but empty bodies — enable it to see request/response bodies:

# 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

Slow requests (past :slow_request_threshold_ms) capture bodies even when this is disabled.

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)

Data lifetime: storage is ETS (in-memory) by default — everything is lost on restart. Enable the SQLite backend for persistence (below). The recent buffers hold :max_recent events per direction and silently evict older events, which breaks ?selected= deep links (e.g. /timeline?selected=1779232233155167) once an event is evicted.

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_backend, Monitorex.Storage.ETS
# Use SQLite — requires :exqlite in your deps
config :monitorex, :storage_backend, 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

⚠️ Security warning. Monitorex ships a built-in JSON REST API that mounts outsidelive_session with no authentication — anyone who can reach your endpoint can read hosts, events, and metrics. It defaults to api_path: "/api", which collides with the API scope of most real apps. Disable it unless you need it: pass api_path: false to http_dashboard/1 (see below). The API also sends Access-Control-Allow-Origin: *, so it is effectively public even on an authenticated dashboard scope.

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:

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

Pass api_path: false to http_dashboard/1 — recommended unless you need programmatic access:

http_dashboard api_path: false

The API is mounted inside the scope pipeline. If you keep it, mount it under your own pipeline and/or a dedicated path (see the Installation Guide).

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