Current section

Files

Jump to
plug_telemetry_server_timing lib plug_telemetry_server_timing.ex
Raw

lib/plug_telemetry_server_timing.ex

defmodule Plug.Telemetry.ServerTiming do
@behaviour Plug
@moduledoc """
This plug provide support for [`Server-Timing`][st] header that allows to
display server-side measurements in browser developer tools and access it
programatically via Performance API in JavaScript.
## Usage
Just add it as a plug into your pipeline:
```
plug Plug.Telemetry.ServerTiming
```
And call `install/1` with list of `{event_name, measurement}` in your
application startup, for example for Phoenix and Ecto application:
```
Plug.Telemetry.ServerTiming.install([
{[:phoenix, :endpoint, :stop], :duration},
{[:my_app, :repo, :query], :queue_time},
{[:my_app, :repo, :query], :query_time},
{[:my_app, :repo, :query], :decode_time}
])
```
And then it will be visible in your DevTools.
### Important
You need to place this plug **BEFORE** `Plug.Telemetry` call as otherwise it
will not see it's events (`before_send` callbacks are called in reverse order
of declaration, so this one need to be added before `Plug.Telemetry` one.
## Caveats
This will not respond with events that happened in separate processes, only
events that happened in the Plug process will be recorded.
[st]: https://w3c.github.io/server-timing/#the-server-timing-header-field
"""
import Plug.Conn
@impl true
@doc false
def init(opts), do: opts
@impl true
@doc false
def call(conn, _opts) do
Process.put(__MODULE__, {true, %{}})
register_before_send(conn, &timings/1)
end
@doc """
Define which events should be available within response headers.
"""
@spec install(events) :: :ok when events: map() | [{:telemetry.event_name(), atom()}]
def install(events) do
for {name, metric} <- events do
:ok = :telemetry.attach({__MODULE__, name, metric}, name, &__MODULE__.__handle__/4, metric)
end
:ok
end
@doc false
def __handle__(metric_name, measurements, _metadata, metric) do
with %{^metric => duration} <- measurements,
{true, data} <- Process.get(__MODULE__) do
Process.put(
__MODULE__,
{true, Map.update(data, {metric_name, metric}, duration, &(&1 + duration))}
)
:ok
else
_ -> :ok
end
end
defp timings(conn) do
{_, measurements} = Process.get(__MODULE__, {false, %{}})
if measurements == %{} do
conn
else
put_resp_header(conn, "server-timing", render_measurements(measurements))
end
end
defp render_measurements(measurements) do
millis = System.convert_time_unit(1, :millisecond, :native)
measurements
|> Enum.map(fn {{metric_name, metric}, measurement} ->
name = "#{Enum.join(metric_name, ".")}.#{metric}"
duration = measurement / millis
"#{name};dur=#{duration}"
end)
|> Enum.join(",")
end
end