Current section
Files
Jump to
Current section
Files
lib/plug/stats.ex
defmodule Glia.StatsPlug do
@moduledoc """
This Plug emits metrics after every processed HTTP request.
The metric emitted is called "web.request" (configurable) and
contains the following tags:
* `action` - Controller and action name, underscored (e.g. "queue_controller.show")
* `http_method` - HTTP method in lower case
* `http_status` - Response status code (200, 404 etc)
* `http_status_class` - Response status class (2xx, 4xx etc)
Emitted value is processing time in milliseconds.
## Configuration
Initialize the plug like this: `plug PlugStats, backend: ExStatsD`
"""
@behaviour Plug
@default_config [
backend: WebRequestStats.BackendProtocol,
metric_name: "web.request"
]
@impl Plug
def init(opts \\ []) do
Keyword.merge(@default_config, opts)
end
@impl Plug
def call(conn, opts) do
start_time = System.monotonic_time()
Plug.Conn.register_before_send(
conn,
fn conn ->
stop_time = System.monotonic_time()
elapsed_ms = native_to_milliseconds(stop_time - start_time)
if processed_by_phoenix?(conn) do
send_metric(conn, elapsed_ms, opts)
end
conn
end
)
end
defp send_metric(conn, elapsed_ms, config) do
backend = config[:backend]
metric_name = config[:metric_name]
tags = calculate_tags(conn)
backend.histogram(metric_name, elapsed_ms, tags: tags)
end
defp calculate_tags(conn) do
[
"action:#{action_name(conn)}",
"http_method:#{String.downcase(conn.method)}",
"http_status:#{conn.status}",
"http_status_class:#{status_class(conn)}xx"
]
end
defp status_class(conn) do
div(conn.status, 100)
end
defp action_name(conn) do
# If the request has not reached the controller, the following values will be nil
if conn.private[:phoenix_controller] == nil || conn.private[:phoenix_action] == nil do
"unknown"
else
controller_name = Macro.underscore(conn.private[:phoenix_controller])
action_name = conn.private[:phoenix_action]
# Any initial string looking like "my_app_web/my_controller/index"
# would result in "my_controller.index"
"#{controller_name}/#{action_name}"
|> String.split("/")
|> Enum.drop(1)
|> Enum.join("/")
|> String.replace("/", ".")
end
end
defp processed_by_phoenix?(conn) do
conn.private[:phoenix_controller] && conn.private[:phoenix_action]
end
defp native_to_milliseconds(duration) do
# convert_time_unit rounds the number using the floor function. To get
# milliseconds with 3 decimal points we convert the duration to
# microseconds and then divide it by a thousand.
System.convert_time_unit(duration, :native, :microsecond) / 1000
end
end