if Code.ensure_loaded?(Plug) and Code.ensure_loaded?(:telemetry) do defmodule GSMLG.Logger.Plug do @moduledoc """ A telemetry handler that logs request information in JSON format. """ require Logger @doc """ Attaches the telemetry handler to the given event. ### Available options * `:level` - log level which is used to log requests. Defaults to `:info`. ### Dynamic log level In some cases you may wish to set the log level dynamically on a per-query basis. To do so, set the `:level` option to a tuple, `{Mod, Fun, Args}`. The query and map of time measures will be prepended to the provided list of arguments. When invoked, your function must return a [`Logger.level()`](`t:Logger.level()/0`) or `false` to disable logging for the request. ### Examples Attaching the telemetry handler to the `MyApp.Plug` events with the `:info` log level: # in the endpoint plug Plug.Telemetry, event_prefix: [:myapp, :plug] # in your application.ex GSMLG.Logger.Plug.attach("gsmlg-logger-requests", [:myapp, :plug, :stop], :info) To make plug broadcast those events see [`Plug.Telemetry`](https://hexdocs.pm/plug/Plug.Telemetry.html) documentation. You can also attach to the `[:phoenix, :endpoint, :stop]` event to log request latency from Phoenix endpoints: GSMLG.Logger.Plug.attach("gsmlg-logger-phoenix-requests", [:phoenix, :endpoint, :stop], :info) """ def attach(name, event, level) do :telemetry.attach(name, event, &__MODULE__.telemetry_logging_handler/4, level) end @doc """ A telemetry handler that logs requests in a structured format. """ @spec telemetry_logging_handler( event_name :: [atom()], query_time :: %{duration: non_neg_integer()}, metadata :: %{conn: Plug.Conn.t()}, level :: Logger.level() | {module :: module(), function :: atom(), arguments :: [term()]} | false ) :: :ok def telemetry_logging_handler(_event_name, %{duration: duration}, %{conn: conn}, level) do duration = System.convert_time_unit(duration, :native, :microsecond) if level = level(level, conn) do Logger.log( level, fn -> %{ method: method, request_path: request_path, state: state, status: status } = conn [ method, ?\s, request_path, ?\s, "[", connection_type(state), ?\s, status(status), "in ", duration(duration), "]" ] end, conn: conn, duration_us: duration ) else :ok end end defp connection_type(:set_chunked), do: "Chunked" defp connection_type(_), do: "Sent" defp status(nil), do: "" defp status(status), do: [status |> Plug.Conn.Status.code() |> Integer.to_string(), ?\s] defp duration(duration) do if duration > 1000 do [duration |> div(1000) |> Integer.to_string(), "ms"] else [Integer.to_string(duration), "us"] end end defp level({m, f, a}, conn), do: apply(m, f, [conn | a]) defp level(level, _conn) when is_atom(level), do: level end end