Current section
Files
Jump to
Current section
Files
lib/plug_bunyan.ex
defmodule Plug.Bunyan do
@moduledoc """
A plug for logging JSON messages.
This [Plug](https://github.com/elixir-lang/plug) wraps the standard
[Elixir Logger](http://elixir-lang.org/docs/stable/logger/Logger.html)
and automatically generates JSON log messages.
All requests that are processed by your plug pipeline will log the following
fields, when avaiable:
* `level`
* `timestamp` (UTC)
* `request_id` (when used in conjuction with `Plug.RequestId`)
* `method`
* `host`
* `path`
* `status`
* `logger_name`
* `params`
* `duration`
* `controller` (when used in a Phoenix application)
* `action` (when used in a Phoenix application)
* `format` (when used in a Phoenix application)
To avoid logging sensitive information passed in via HTTP headers or
params, configure headers/params to be filtered within config.exs using
the `filter_paramaters` key, e.g.:
```
config :bunyan,
filter_parameters: ["password", "ssn"]
```
Parameter filtering is case insensitive and will replace filtered values with
the string `"[FILTERED]"`.
If you wish to log any environment variables with `Plug.Bunyan`, provide
Bunyan config with a list of environment variables to log along with your
desired output names.
For example:
```
config :bunyan,
env_vars: [{"CUSTOM_ENV_VAR", "our_env_var"}]
```
...will output the value of `CUSTOM_ENV_VAR` under a JSON key of `"our_env_var"`
"""
alias Plug.Conn
alias Bunyan.{Params, Timestamp}
@behaviour Plug
require Logger
@env_vars Application.get_env(:bunyan, :env_vars, [])
def init(_), do: false
@spec call(Plug.Conn.t, any) :: Plug.Conn.t
def call(conn, _opts) do
start = :os.timestamp
Conn.register_before_send(conn, fn connection ->
:ok = log(connection, start)
connection
end)
end
@spec log(Plug.Conn.t, {non_neg_integer, non_neg_integer, non_neg_integer}) :: atom
defp log(conn, start) do
Logger.info fn ->
stop = :os.timestamp
duration = :timer.now_diff(stop, start)
%{
"level" => :info,
"method" => conn.method,
"timestamp" => Timestamp.format_string(stop),
"host" => conn.host,
"path" => conn.request_path,
"status" => conn.status |> Integer.to_string,
"duration" => duration |> format_duration |> List.to_string,
"logger_name" => "Plug.Bunyan"
}
|> merge_request_id(Logger.metadata[:request_id])
|> merge_params(conn)
|> merge_phoenix_attributes(conn)
|> merge_headers(conn)
|> merge_env_vars
|> Poison.encode!
end
end
@spec merge_params(map, Plug.Conn.t) :: map
defp merge_params(log, %{params: params}) when params == %{}, do: log
defp merge_params(log, %{params: params}) do
Map.put(log, :params, Params.filter(params))
end
@spec merge_headers(map, Plug.Conn.t) :: map
defp merge_headers(log, %{req_headers: headers}) when headers == %{}, do: log
defp merge_headers(log, %{req_headers: headers}) do
request_headers = headers
|> Enum.into(%{})
|> Params.filter
Map.put(log, :headers, request_headers)
end
@spec merge_phoenix_attributes(map, Plug.Conn.t) :: map
defp merge_phoenix_attributes(log, %{
private: %{
phoenix_controller: controller,
phoenix_action: action,
phoenix_format: format
}
}) do
Map.merge(log, %{"controller" => controller, "action" => action, "format" => format})
end
defp merge_phoenix_attributes(log, _), do: log
@spec merge_env_vars(map) :: map
defp merge_env_vars(log) do
vars = Enum.reduce(@env_vars, %{}, fn({env_var, output_name}, m) ->
Map.put(m, output_name, System.get_env(env_var))
end)
Map.put(log, :env_vars, vars)
end
@spec format_duration(non_neg_integer) :: IO.chardata
defp format_duration(duration) when duration > 1000 do
[duration |> div(1000) |> Integer.to_string, "ms"]
end
defp format_duration(duration) do
[duration |> Integer.to_string, "µs"]
end
defp merge_request_id(log, nil), do: log
defp merge_request_id(log, request_id) do
Map.put(log, :request_id, request_id)
end
end