# Monitorex
[](https://github.com/GustavoZiaugra/monitorex/actions/workflows/ci.yml)
[](https://hex.pm/packages/monitorex)
[](https://hexdocs.pm/monitorex)
[](https://hex.pm/packages/monitorex)
[](https://github.com/GustavoZiaugra/monitorex/blob/main/LICENSE.md)
**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.

## 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 check** — `GET /monitorex/health` with Collector status, queue depths, ETS sizes
- **Prometheus metrics** — `GET /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 Overview | Host Detail | Timeline Inspector |
|:---:|:---:|:---:|
|  |  |  |
## Installation
Add `monitorex` to your `mix.exs`:
```elixir
def deps do
[
{:monitorex, "~> 0.6.0"}
]
end
```
Then run:
```bash
mix deps.get
```
## Quick Start
### 1. Configure sources
In `config/config.exs`:
```elixir
config :monitorex, :sources, [:tesla, :finch, :req, :phoenix]
```
### 2. Mount the dashboard in your router
```elixir
# 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
```bash
mix phx.server
```
Visit `/monitoring` to see your dashboard.
## Configuration
### Sources
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
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`:
```elixir
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:
```elixir
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:
```elixir
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:
```elixir
# 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:
```elixir
# 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.
```elixir
# 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:
```elixir
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:
```elixir
# 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:
```elixir
# 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
| Page | URL | Description |
|------|-----|-------------|
| Outbound Overview | `/` | Summary cards + host table |
| Outbound Recent | `/outbound_recent` | Live feed with status filter |
| Host Detail | `/host/:host` | Per-endpoint breakdown + recent requests |
| Inbound Overview | `/inbound` | Route table + summary |
| Inbound Consumers | `/inbound_consumers` | Per-consumer stats |
| Inbound Recent | `/inbound_recent` | Live feed with filters |
| Timeline | `/timeline` | Split-pane event inspector with request/response detail |
| Route Detail | `/route/:key` | Consumer breakdown + recent requests |
| Alerts | `/alerts` | Alert 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
| Endpoint | Description |
|---|---|
| `GET /api/health` | Health status (same as `/monitorex/health`) |
| `GET /api/hosts` | List all hosts with aggregate stats |
| `GET /api/hosts/:host` | Per-host detail with endpoint breakdown |
| `GET /api/routes` | Inbound route aggregates |
| `GET /api/consumers` | Consumer stats |
| `GET /api/events` | Recent events with filters (see below) |
| `GET /api/events/:timestamp` | Single event detail |
| `GET /api/metrics` | Computed metrics (RPS, error rate, latency quantiles) |
All endpoints return a consistent JSON envelope:
```json
{"ok": true, "data": ...}
```
Errors return:
```json
{"ok": false, "error": "message"}
```
### Query parameters
**Events** (`GET /api/events`):
| Param | Type | Default | Description |
|---|---|---|---|
| `direction` | string | `"outbound"` | `"outbound"` or `"inbound"` |
| `limit` | integer | 50 | Max results (max: 500) |
| `offset` | integer | 0 | Pagination offset |
| `host` | string | — | Filter by host (outbound only) |
| `method` | string | — | Filter by HTTP method (`GET`, `POST`, etc.) |
| `status` | integer | — | Filter by HTTP status code |
| `consumer` | string | — | Filter by consumer (inbound only) |
| `route` | string | — | Filter by route key (inbound only) |
| `since` | ISO 8601 | — | Events after this timestamp |
**Metrics** (`GET /api/metrics`):
| Param | Type | Default | Description |
|---|---|---|---|
| `host` | string | all | Filter to a specific host |
| `window` | integer | 300 | Time 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
```bash
# 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`:
```elixir
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:
> ```elixir
> 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:
```bash
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
```bash
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
```bash
mix docs
```
Then open `doc/index.html`.
## License
MIT