π° πππππ πππ π΄πππππππ πππ πΎπππππππππ ππππππ.
15 August 2025
This tutorial walks you through integrating Oak with your Phoenix application step by step. Oak is a high-performance metrics collection and aggregation library written in Elixir that provides Prometheus-compatible metrics.
First, add Oak to your project dependencies. Open your mix.exs
file and add the Oak dependency:
defp deps do
[
# ... your existing dependencies
{:oak, "~> 0.2"}
]
end
After adding the dependency, install it:
mix deps.get
Oak needs to be started as part of your applicationβs supervision tree. Open lib/your_app/application.ex
and add Oak.MetricsStore
to your children list:
defmodule YourApp.Application do
use Application
def start(_type, _args) do
children = [
# ... your existing children
YourAppWeb.Telemetry,
YourApp.Repo,
# ... other services
# Add Oak metrics store
{Oak.MetricsStore, %{}},
# ... other children
YourAppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: YourApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Important: Place Oak.MetricsStore
before your endpoint to ensure itβs started before HTTP requests begin.
This plug automatically tracks HTTP request metrics. Create a new file at lib/your_app_web/plugs/route_metrics.ex
:
defmodule YourAppWeb.Plugs.RouteMetrics do
@moduledoc """
Plug that tracks route metrics and pushes them to Oak metrics store.
"""
import Plug.Conn
require Logger
def init(opts), do: opts
def call(conn, _opts) do
start_time = System.monotonic_time(:millisecond)
conn
|> register_before_send(&track_metrics(&1, start_time))
end
defp track_metrics(conn, start_time) do
end_time = System.monotonic_time(:millisecond)
duration = end_time - start_time
# Get route information
route = conn.request_path
method = conn.method
status = conn.status || 500
# Push metrics to Oak
try do
# HTTP request counter
http_requests_total = Oak.Metric.Counter.new("http_requests_total", "HTTP requests total", %{
method: method,
route: route,
status: status
})
case Oak.Prometheus.get_metric(Oak.MetricsStore, Oak.Prometheus.get_counter_id(http_requests_total)) do
nil ->
Oak.Prometheus.push_metric(Oak.MetricsStore, http_requests_total |> Oak.Metric.Counter.inc(1))
metric ->
Oak.Prometheus.push_metric(Oak.MetricsStore, metric |> Oak.Metric.Counter.inc(1))
end
# Request duration histogram
request_duration = Oak.Metric.Histogram.new("request_duration", "Request duration", [
10, 50, 100, 250, 500, 1000, 2500, 5000
], %{
method: method,
route: route
})
case Oak.Prometheus.get_metric(Oak.MetricsStore, Oak.Prometheus.get_histogram_id(request_duration)) do
nil ->
Oak.Prometheus.push_metric(Oak.MetricsStore, request_duration |> Oak.Metric.Histogram.observe(duration))
metric ->
Oak.Prometheus.push_metric(Oak.MetricsStore, metric |> Oak.Metric.Histogram.observe(duration))
end
rescue
e -> Logger.warning("Failed to push route metrics: #{inspect(e)}")
end
conn
end
end
What this plug does:
register_before_send/2
to capture the final response statusCritical: The metrics plug must be placed before your router to capture actual response statuses. Open lib/your_app_web/endpoint.ex
:
defmodule YourAppWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :your_app
# ... other plugs and configuration
plug Plug.Session, @session_options
# Route metrics tracking plug - MUST be before router
plug YourAppWeb.Plugs.RouteMetrics
plug YourAppWeb.Router
end
Create a controller to expose metrics for Prometheus scraping. Create lib/your_app_web/controllers/metrics_controller.ex
:
defmodule YourAppWeb.MetricsController do
use YourAppWeb, :controller
def metrics(conn, _params) do
# Collect runtime metrics from Erlang VM
Oak.Prometheus.collect_runtime_metrics(Oak.MetricsStore)
# Get all metrics in Prometheus format
metrics_text = Oak.Prometheus.output_metrics(Oak.MetricsStore)
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, metrics_text)
end
end
What this controller does:
/metrics
endpointAdd the metrics endpoint to your router. Open lib/your_app_web/router.ex
:
defmodule YourAppWeb.Router do
use YourAppWeb, :router
# ... your existing routes
scope "/", YourAppWeb do
pipe_through :browser
get "/", PageController, :home
get "/metrics", MetricsController, :metrics # Add this line
end
# ... other scopes
end
Note: The /metrics
path is a common convention for Prometheus scraping, but you can use any path you prefer.
Oak isnβt just for HTTP metrics - you can track any business logic. Hereβs an example of tracking user registrations:
defmodule YourApp.Accounts do
# ... existing code
def register_user(user_params) do
case create_user(user_params) do
{:ok, user} ->
# Increment user registration counter
counter = Oak.Metric.Counter.new("user_registrations_total", "Total user registrations", %{})
case Oak.Prometheus.get_metric(Oak.MetricsStore, Oak.Prometheus.get_counter_id(counter)) do
nil ->
Oak.Prometheus.push_metric(Oak.MetricsStore, counter |> Oak.Metric.Counter.inc(1))
metric ->
Oak.Prometheus.push_metric(Oak.MetricsStore, metric |> Oak.Metric.Counter.inc(1))
end
{:ok, user}
{:error, changeset} ->
{:error, changeset}
end
end
end
Key points for custom metrics:
Now letβs test that everything is working:
Start your application:
$ mix phx.server
Visit your home page to generate some HTTP metrics. Then check the metrics endpoint
$ curl http://localhost:4000/metrics
Look for your metrics** in the output. You should see:
http_requests_total
countersrequest_duration
histogramsConsider protecting your metrics endpoint in production with basic auth:
# Add basic authentication
plug :basic_auth
defp basic_auth(conn, _opts) do
case get_req_header(conn, "authorization") do
["Basic " <> credentials] ->
# Verify credentials
conn
_ ->
conn
|> put_status(401)
|> put_resp_header("www-authenticate", "Basic realm=\"Metrics\"")
|> halt()
end
end
Oak provides a solid foundation for monitoring and observability in your Phoenix applications. Start with the basics and gradually add more sophisticated metrics as your needs grow.