Current section
Files
Jump to
Current section
Files
lib/plug/logger_json.ex
defmodule Plug.LoggerJSON do
@moduledoc """
A plug for logging basic request information in the format:
```json
{
"api_version": "N/A"
"client_ip": "23.235.46.37"
"client_version": "ios/1.6.7",
"date_time": "2016-05-31T18:00:13Z",
"duration": 4.670,
"fastly_duration": 2.670,
"handler": "fronts#index"
"log_type": "http",
"method": "POST",
"params": {
"user":"jkelly",
"password":"[FILTERED]"
},
"path": "/",
"request_id": "d90jcl66vp09r8tke3utjsd1pjrg4ln8",
"status": "200"
}
```
To use it, just plug it into the desired module.
plug Plug.LoggerJSON, log: :debug
## Options
* `:log` - The log level at which this plug should log its request info.
Default is `:info`.
"""
require Logger
alias Plug.Conn
@behaviour Plug
def init(opts) do
Keyword.get(opts, :log, :info)
end
@spec call(Plug.Conn.t, atom) :: Plug.Conn.t
def call(conn, level) do
start = :os.timestamp()
Conn.register_before_send(conn, fn conn ->
:ok = log(conn, level, start)
conn
end)
end
@spec log(Plug.Conn.t, atom, {non_neg_integer, non_neg_integer, non_neg_integer}) :: atom
def log(conn, level, start) do
Logger.log level, fn ->
stop = :os.timestamp()
duration = :timer.now_diff(stop, start)
req_id = Logger.metadata[:request_id]
req_headers = format_map_list(conn.req_headers)
req_params = format_map_list(conn.params)
%{
"api_version" => Map.get(req_headers, "accept", "N/A"),
"client_ip" => format_ip(Map.get(req_headers, "x-forwarded-for", "N/A")),
"client_version" => client_version(req_headers),
"date_time" => iso8601(:calendar.now_to_datetime(:os.timestamp)),
"duration" => Float.round(duration / 1000, 3),
"fastly_duration" => fastly_duration(req_headers),
"log_type" => "http",
"method" => conn.method,
"params" => req_params,
"path" => conn.request_path,
"request_id" => req_id,
"status" => Integer.to_string(conn.status)
}
|> Map.merge(phoenix_attributes(conn))
|> Poison.encode!
end
end
@spec client_version(%{String.t => String.t}) :: String.t
defp client_version(headers) do
headers
|> Map.get("x-client-version", "N/A")
|> case do
"N/A" ->
Map.get(headers, "user-agent", "N/A")
accept_value ->
accept_value
end
end
@spec fastly_duration(%{String.t => String.t}) :: integer
defp fastly_duration(headers) do
x_timer = Map.get(headers, "x-timer", "")
case String.split(x_timer, ",") do
[_, "VS" <> start, "VE" <> stop] ->
String.to_integer(stop) - String.to_integer(start)
[_, "VS" <> _, "VS" <> _] ->
0
_ ->
-1
end
end
@spec filter_values({String.t, String.t}) :: map
defp filter_values({k,v}) do
filtered_keys = Application.get_env(:plug_logger_json, :filtered_keys, [])
if Enum.member?(filtered_keys, k) do
%{k => "[FILTERED]"}
else
%{k => format_value(v)}
end
end
@spec format_ip(String.t) :: String.t
defp format_ip("N/A") do
"N/A"
end
defp format_ip(x_forwarded_for) do
hd(String.split(x_forwarded_for, ", "))
end
@spec format_map_list([%{String.t => String.t}]) :: map
defp format_map_list(list) do
list
|> Enum.take(20)
|> Enum.map(&filter_values/1)
|> Enum.reduce(%{}, &(Map.merge(&2, &1)))
end
defp format_value(value) when is_binary(value) do
String.slice(value, 0..500)
end
defp format_value(value) do
value
end
defp iso8601({{year, month, day}, {hour, minute, second}}) do
zero_pad(year, 4) <> "-" <> zero_pad(month, 2) <> "-" <> zero_pad(day, 2) <> "T" <>
zero_pad(hour, 2) <> ":" <> zero_pad(minute, 2) <> ":" <> zero_pad(second, 2) <> "Z"
end
@spec phoenix_attributes(Plug.Conn.t) :: map
defp phoenix_attributes(%{private: %{phoenix_controller: controller, phoenix_action: action}}) do
%{"handler" => "#{controller}##{action}"}
end
defp phoenix_attributes(_) do
%{"handler" => "N/A"}
end
@spec zero_pad(1..3_000, non_neg_integer) :: String.t
defp zero_pad(val, count) do
num = Integer.to_string(val)
:binary.copy("0", count - byte_size(num)) <> num
end
end