Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 56 additions & 28 deletions lib/prom_ex/plugins/phoenix.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ if Code.ensure_loaded?(Phoenix) do
- `duration_unit`: This is an OPTIONAL option and is a `Telemetry.Metrics.time_unit()`. It can be one of:
`:second | :millisecond | :microsecond | :nanosecond`. It is `:millisecond` by default.

- `http_metrics_tags`: This is an OPTIONAL option and controls which tags are emitted on the Phoenix HTTP
metric families. The default is `[:status, :method, :path, :controller, :action, :host]`.
Supported values are `:status`, `:status_class`, `:method`, `:path`, `:controller`, `:action`,
and `:host`.

### Single Endpoint/Router
- `router`: This option is REQUIRED and is the full module name of your Phoenix Router (e.g MyAppWeb.Router).

Expand Down Expand Up @@ -162,6 +167,8 @@ if Code.ensure_loaded?(Phoenix) do

require Logger

@supported_http_metric_tags [:status, :status_class, :method, :path, :controller, :action, :host]

alias Phoenix.Socket
alias Plug.Conn
alias PromEx.Utils
Expand Down Expand Up @@ -261,7 +268,7 @@ if Code.ensure_loaded?(Phoenix) do
defp http_events(metric_prefix, opts) do
routers = fetch_routers!(opts)
additional_routes = fetch_additional_routes!(opts)
http_metrics_tags = [:status, :method, :path, :controller, :action, :host]
http_metrics_tags = fetch_http_metrics_tags!(opts)
duration_unit = Keyword.get(opts, :duration_unit, :millisecond)
duration_unit_plural = Utils.make_plural_atom(duration_unit)

Expand All @@ -277,7 +284,7 @@ if Code.ensure_loaded?(Phoenix) do
reporter_options: [
buckets: [10, 100, 500, 1_000, 5_000, 10_000, 30_000]
],
tag_values: get_conn_tags(routers, additional_routes),
tag_values: get_conn_tags(routers, additional_routes, http_metrics_tags),
tags: http_metrics_tags,
unit: {:native, duration_unit}
),
Expand All @@ -296,7 +303,7 @@ if Code.ensure_loaded?(Phoenix) do
_ -> :erlang.iolist_size(metadata.conn.resp_body)
end
end,
tag_values: get_conn_tags(routers, additional_routes),
tag_values: get_conn_tags(routers, additional_routes, http_metrics_tags),
tags: http_metrics_tags,
unit: :byte
),
Expand All @@ -306,7 +313,7 @@ if Code.ensure_loaded?(Phoenix) do
metric_prefix ++ [:http, :requests, :total],
event_name: @stop_event,
description: "The number of requests have been serviced.",
tag_values: get_conn_tags(routers, additional_routes),
tag_values: get_conn_tags(routers, additional_routes, http_metrics_tags),
tags: http_metrics_tags
)
]
Expand Down Expand Up @@ -386,46 +393,52 @@ if Code.ensure_loaded?(Phoenix) do
)
end

defp get_conn_tags(routers, []) do
defp get_conn_tags(routers, additional_routes, http_metrics_tags) do
fn
%{conn: %Conn{} = conn} ->
default_route_tags = %{
path: "Unknown",
controller: "Unknown",
action: "Unknown"
}
default_route_tags = default_route_tags(conn, additional_routes)

conn
|> do_get_router_info(routers, default_route_tags)
|> Map.merge(%{
status: conn.status,
method: conn.method,
host: conn.host
})
|> Map.merge(http_status_tags(conn.status, http_metrics_tags))
|> Map.put(:method, conn.method)
|> maybe_put_host(conn.host, http_metrics_tags)
|> Map.take(http_metrics_tags)

_ ->
Logger.warning("Could not resolve path for request")
end
end

defp get_conn_tags(routers, additional_routes) do
fn
%{conn: %Conn{} = conn} ->
default_route_tags = handle_additional_routes_check(conn, additional_routes)
defp default_route_tags(_conn, []),
do: %{path: "Unknown", controller: "Unknown", action: "Unknown"}

conn
|> do_get_router_info(routers, default_route_tags)
|> Map.merge(%{
status: conn.status,
method: conn.method,
host: conn.host
})
defp default_route_tags(conn, additional_routes),
do: handle_additional_routes_check(conn, additional_routes)

_ ->
Logger.warning("Could not resolve path for request")
defp maybe_put_host(tags, host, http_metrics_tags) do
if :host in http_metrics_tags, do: Map.put(tags, :host, host), else: tags
end

defp http_status_tags(status, http_metrics_tags) do
%{}
|> maybe_put_status(:status, status, http_metrics_tags)
|> maybe_put_status(:status_class, status_class(status), http_metrics_tags)
end

defp maybe_put_status(tags, tag_name, value, http_metrics_tags) do
if tag_name in http_metrics_tags, do: Map.put(tags, tag_name, value), else: tags
end

defp status_class(status) when is_integer(status) do
case div(status, 100) do
class when class in 1..5 -> "#{class}xx"
_ -> "Unknown"
end
end

defp status_class(_status), do: "Unknown"

defp do_get_router_info(conn, routers, default_route_tags) do
routers
|> Enum.find_value(default_route_tags, fn router ->
Expand Down Expand Up @@ -505,6 +518,21 @@ if Code.ensure_loaded?(Phoenix) do
defp normalize_action(action) when is_atom(action), do: action
defp normalize_action(_action), do: "Unknown"

defp fetch_http_metrics_tags!(opts) do
http_metrics_tags =
Keyword.get(opts, :http_metrics_tags, [:status, :method, :path, :controller, :action, :host])

invalid_tags = http_metrics_tags -- @supported_http_metric_tags

if invalid_tags != [] do
raise ArgumentError,
"unsupported http_metrics_tags: #{inspect(invalid_tags)}. " <>
"Supported tags are: #{inspect(@supported_http_metric_tags)}"
end

http_metrics_tags
end

defp fetch_additional_routes!(opts) do
opts
|> fetch_either!(:router, :endpoints)
Expand Down
58 changes: 58 additions & 0 deletions test/prom_ex/plugins/phoenix_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule PromEx.Plugins.PhoenixTest do
use ExUnit.Case, async: false

alias PromEx.MetricTypes.Event
alias PromEx.Plugins.Phoenix
alias PromEx.Test.Support.{Events, Metrics}

Expand Down Expand Up @@ -86,11 +87,68 @@ defmodule PromEx.Plugins.PhoenixTest do
test "should return the correct number of metrics" do
assert length(Phoenix.event_metrics(otp_app: :prom_ex, router: Some.Module)) == 4
end

test "can configure a reduced set of HTTP metric tags" do
metric =
assert_http_metric(
[:prom_ex, :plugin, :phoenix, :stop],
otp_app: :prom_ex,
router: TestApp.Router,
http_metrics_tags: [:status, :method, :path]
)

assert metric.tags == [:status, :method, :path]

conn =
Plug.Test.conn(:get, "/users/123")
|> Map.put(:host, "preview-123.example.com")
|> Map.put(:status, 200)

assert %{status: 200, method: "GET", path: "/users/:id"} = metric.tag_values.(%{conn: conn})
end

test "can emit status_class instead of exact status" do
metric =
assert_http_metric(
[:prom_ex, :plugin, :phoenix, :stop],
otp_app: :prom_ex,
router: TestApp.Router,
http_metrics_tags: [:status_class, :method, :path]
)

assert metric.tags == [:status_class, :method, :path]

conn =
Plug.Test.conn(:get, "/users/123")
|> Map.put(:status, 404)

assert %{status_class: "4xx", method: "GET", path: "/users/:id"} =
metric.tag_values.(%{conn: conn})
end

test "raises when unsupported HTTP metric tags are configured" do
assert_raise ArgumentError, ~r/unsupported http_metrics_tags/, fn ->
Phoenix.event_metrics(
otp_app: :prom_ex,
router: TestApp.Router,
http_metrics_tags: [:status, :unsupported]
)
end
end
end

describe "polling_metrics/1" do
test "should return the correct number of metrics" do
assert Phoenix.polling_metrics([]) == []
end
end

defp assert_http_metric(event_name, opts) do
assert event_metrics = Phoenix.event_metrics(opts)

assert %Event{metrics: http_metrics} =
Enum.find(event_metrics, fn event -> event.group_name == :phoenix_http_event_metrics end)

Enum.find(http_metrics, fn metric -> metric.event_name == event_name end)
end
end