<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/logo-dark.svg">
<source media="(prefers-color-scheme: light)" srcset="docs/logo-light.svg">
<img src="docs/logo-light.svg" width="300" alt="Timeless">
</picture>
</p>
<h3 align="center">Unified Observability for Phoenix</h3>
<p align="center">
<a href="https://hex.pm/packages/timeless_phoenix"><img src="https://img.shields.io/hexpm/v/timeless_phoenix.svg" alt="Hex.pm"></a>
<a href="https://hexdocs.pm/timeless_phoenix"><img src="https://img.shields.io/badge/docs-hexdocs-blue.svg" alt="Docs"></a>
<a href="LICENSE"><img src="https://img.shields.io/hexpm/l/timeless_phoenix.svg" alt="License"></a>
</p>
---
> "I found it ironic that the first thing you do to time series data is squash the timestamp. That's how the name Timeless was born." --Mark Cotner
Unified observability for Phoenix: persistent metrics, logs, and traces in LiveDashboard.
One dep, one child_spec, one router macro — you get:
- **Metrics** — TimelessMetrics stores telemetry metrics that survive restarts
- **Logs** — TimelessLogs captures and indexes Elixir Logger output
- **Traces** — TimelessTraces stores OpenTelemetry spans
- **Dashboard** — All three as LiveDashboard pages, plus built-in charts with history
## Documentation
- [Getting Started](docs/getting_started.md)
- [Configuration Reference](docs/configuration.md)
- [Architecture](docs/architecture.md)
- [Dashboard](docs/dashboard.md)
- [Metrics](docs/metrics.md)
- [Demo Traffic Generator](docs/demo_traffic.md)
- [Production Deployment](docs/production.md)
- [Interactive Demo Livebook](livebook/demo.livemd)
## Installation
### With Igniter (recommended)
Add the dependency to `mix.exs`:
```elixir
{:timeless_phoenix, path: "../timeless_phoenix"},
{:igniter, "~> 0.6"}
```
Then run:
```bash
mix deps.get
mix timeless_phoenix.install
```
This automatically:
1. Adds `{TimelessPhoenix, ...}` to your supervision tree
2. Configures OpenTelemetry to export spans to TimelessTraces
3. Adds `import TimelessPhoenix.Router` to your Phoenix router
4. Adds `timeless_phoenix_dashboard "/dashboard"` to your browser scope
5. Removes the default `live_dashboard` route (avoids live_session conflict)
6. Updates `.formatter.exs`
By default, metrics, logs, and traces are all persisted to disk under
`priv/observability`. If you want logs and traces to stay in memory only for
CI or ephemeral demo environments:
```bash
mix timeless_phoenix.install --storage memory
```
### HTTP Endpoints
To expose HTTP ingest/query endpoints for external tooling (Grafana, curl, etc.),
use the `--http` flag to enable all three:
```bash
mix timeless_phoenix.install --http
```
Or enable them individually:
```bash
mix timeless_phoenix.install --http-metrics --http-logs
```
Default ports are 8428 (metrics), 9428 (logs), and 10428 (traces). Override with:
```bash
mix timeless_phoenix.install --http --metrics-port 9090 --logs-port 3100 --traces-port 4318
```
| Flag | Description |
|------|-------------|
| `--http` | Enable all HTTP endpoints |
| `--http-metrics` | Enable metrics HTTP endpoint |
| `--http-logs` | Enable logs HTTP endpoint |
| `--http-traces` | Enable traces HTTP endpoint |
| `--metrics-port` | Metrics port (default 8428) |
| `--logs-port` | Logs port (default 9428) |
| `--traces-port` | Traces port (default 10428) |
### Manual
Add the dependency to `mix.exs`:
```elixir
{:timeless_phoenix, "~> 1.5"}
```
Add to your application's supervision tree (`lib/my_app/application.ex`):
```elixir
children = [
# ... existing children ...
{TimelessPhoenix, data_dir: "priv/observability"}
]
```
Add to your router (`lib/my_app_web/router.ex`):
```elixir
import TimelessPhoenix.Router
scope "/" do
pipe_through :browser
timeless_phoenix_dashboard "/dashboard"
end
```
Configure OpenTelemetry to export spans (`config/config.exs`):
```elixir
config :opentelemetry, traces_exporter: {TimelessTraces.Exporter, []}
```
Remove the default `live_dashboard` route from your router — it's
typically inside an `if Application.compile_env(:my_app, :dev_routes)`
block. TimelessPhoenix provides its own dashboard at the same path, and
having both causes a live_session conflict.
Add `:timeless_phoenix` to your `.formatter.exs` import_deps:
```elixir
[import_deps: [:timeless_phoenix, ...]]
```
## Configuration
### Child spec options
| Option | Default | Description |
|--------|---------|-------------|
| `:data_dir` | **required** | Base directory; creates `metrics/`, `logs/`, `spans/` subdirs |
| `:name` | `:default` | Instance name for process naming |
| `:metrics` | `DefaultMetrics.all()` | `Telemetry.Metrics` list for the reporter |
| `:timeless` | `[]` | Extra opts forwarded to TimelessMetrics |
| `:timeless_logs` | `[]` | Application env overrides for TimelessLogs |
| `:timeless_traces` | `[]` | Application env overrides for TimelessTraces |
| `:reporter` | `[]` | Extra opts for Reporter (`:flush_interval`, `:prefix`) |
### Router macro options
```elixir
timeless_phoenix_dashboard "/dashboard",
name: :default, # TimelessPhoenix instance name
metrics: MyApp.Telemetry, # custom metrics module
download_path: "/timeless/downloads", # backup download path
live_dashboard: [csp_nonce_assign_key: :csp] # extra LiveDashboard opts
```
### Manual LiveDashboard setup
If you need full control over the LiveDashboard configuration instead of
using the macro:
```elixir
import Phoenix.LiveDashboard.Router
forward "/timeless/downloads", TimelessMetricsDashboard.DownloadPlug,
store: :tp_default_timeless
live_dashboard "/dashboard",
metrics: MyApp.Telemetry,
metrics_history: {TimelessPhoenix, :metrics_history, []},
additional_pages: TimelessPhoenix.dashboard_pages()
```
## Running in Production
The Igniter installer places `timeless_phoenix_dashboard` in a top-level
browser scope so it's available in all environments. To restrict access in
production, add authentication.
### Authentication
#### Pipeline-based auth (recommended)
Create an admin pipeline with your existing auth plugs:
```elixir
pipeline :admin do
plug :fetch_current_user
plug :require_admin_user
end
scope "/" do
pipe_through [:browser, :admin]
timeless_phoenix_dashboard "/dashboard"
end
```
#### Basic HTTP auth
For a quick setup using environment variables:
```elixir
pipeline :dashboard_auth do
plug :admin_basic_auth
end
scope "/" do
pipe_through [:browser, :dashboard_auth]
timeless_phoenix_dashboard "/dashboard"
end
# In your router or a plug module:
defp admin_basic_auth(conn, _opts) do
username = System.fetch_env!("DASHBOARD_USER")
password = System.fetch_env!("DASHBOARD_PASS")
Plug.BasicAuth.basic_auth(conn, username: username, password: password)
end
```
#### LiveView on_mount hook
For LiveView-level auth, pass `on_mount` through to LiveDashboard:
```elixir
timeless_phoenix_dashboard "/dashboard",
live_dashboard: [on_mount: [{MyAppWeb.AdminAuth, :ensure_admin, []}]]
```
### WebSocket proxies
If your app is behind nginx or a reverse proxy, ensure WebSocket upgrades
are allowed. LiveDashboard uses LiveView, which requires a WebSocket
connection.
Nginx example:
```nginx
location /dashboard {
proxy_pass http://localhost:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
```
### Production data directory
In production, use a persistent path outside the release:
```elixir
{TimelessPhoenix, data_dir: "/var/lib/my_app/observability"}
```
Or configure at runtime:
```elixir
{TimelessPhoenix, data_dir: System.get_env("OBS_DATA_DIR", "/var/lib/my_app/observability")}
```
## Data Retention
TimelessPhoenix ships with sensible defaults for embedded use. All three
engines retain 7 days of data by default.
| Engine | Default Retention | Size Limit |
|--------|------------------|------------|
| Metrics (raw) | 7 days | none |
| Metrics (daily rollup) | 90 days | none |
| Logs | 7 days | none |
| Traces | 7 days | none |
### Customizing retention
Override via the `:timeless_logs` and `:timeless_traces` child spec options:
```elixir
{TimelessPhoenix,
data_dir: "priv/observability",
timeless_logs: [
retention_max_age: 30 * 86_400, # 30 days
retention_max_size: 1_073_741_824, # 1 GB cap (nil = unlimited)
retention_check_interval: 120_000 # check every 2 minutes
],
timeless_traces: [
retention_max_age: 14 * 86_400, # 14 days
retention_max_size: 512 * 1_048_576 # 512 MB cap
]}
```
For metrics, use the `:timeless` key:
```elixir
{TimelessPhoenix,
data_dir: "priv/observability",
timeless: [
raw_retention_seconds: 14 * 86_400, # 14 days raw
daily_retention_seconds: 180 * 86_400 # 180 days rolled up
]}
```
Setting `retention_max_age` to `nil` disables time-based retention.
Setting `retention_max_size` to `nil` disables size-based retention (default).
## Custom Metrics
The default metrics include VM, Phoenix, LiveView, TimelessMetrics,
TimelessLogs, and TimelessTraces telemetry. To add your own:
```elixir
defmodule MyApp.Telemetry do
import Telemetry.Metrics
def metrics do
TimelessPhoenix.DefaultMetrics.all() ++ [
counter("my_app.orders.created"),
summary("my_app.checkout.duration", unit: {:native, :millisecond}),
last_value("my_app.queue.depth")
]
end
end
```
Then pass it to both the child spec and router:
```elixir
# application.ex
{TimelessPhoenix, data_dir: "priv/observability", metrics: MyApp.Telemetry.metrics()}
# router.ex
timeless_phoenix_dashboard "/dashboard", metrics: MyApp.Telemetry
```