defmodule Bunyan.Formatter do
@moduledoc """
* `$date` - date the log message was sent (yy-mm-dd)
* `$time` - time the log message was sent (hh:mm:ss.mmm)
* `$datetime` - "$date $time"
* `$message` - whole log message
* `$msg_first_line` - just the first line of the message
* `$msg_rest` - lines 2... of the message
* `$level` - the log level
* `$node` - the node that prints the message
* `$pid` - the pid that generated the message
* `$remote_info` - if the node of the message is not this node, then a
newline and the pid and node name
* `$extra` - any term. maps and keyword lists are formatted nicely
"""
@ansi_reset IO.ANSI.reset
# stolen in part from Logger.Formatter
@regex Regex.recompile!(~r/(?
)\$[a-z_]+(?:\(\d+\))?(?)/)
# this is all compile time, so we try to do as much work here as
# we can.
def compile_format(main_format, extra_format, options) do
is_color? = !!options.use_ansi_color?
main_format_fields = fields_in(main_format)
main_message = Enum.map(main_format_fields, &field_builder(&1, is_color?, options))
prefix_size = prefix_size(main_format_fields)
padding = String.duplicate(" ", prefix_size)
extra_message =
fields_in(extra_format)
|> Enum.map(&field_builder(&1, is_color?, options))
preload = if main_format <> extra_format =~ ~r/\$(date|time)/ do
quote do
{ utc_date, utc_time } = :calendar.now_to_universal_time(timestamp)
end
else
nil
end
quote do
fn (%Bunyan.Shared.LogMsg{
level: level,
msg: msg,
extra: extra,
timestamp: timestamp,
pid: pid,
node: node
}
) ->
{ msg_first_line, msg_rest } = case String.split(msg, ~r/\n/, trim: true, parts: 2) do
[] ->
{ "", [] }
[ first | rest ] ->
{ first, rest }
end
unquote(preload)
[
unquote(__MODULE__).indent_all_but_first(unquote(main_message), unquote(padding)),
unquote(__MODULE__).indent(unquote(extra_message), unquote(padding))
]
end
end
|> Code.eval_quoted()
|> elem(0)
end
##################################################################################
#---------+
# $date |
#---------+
defp field_builder("$date", _ansi = true, options) do
quote do
[
unquote(options.timestamp_color),
unquote(__MODULE__).format_date(utc_date),
unquote(@ansi_reset)
]
end
end
defp field_builder("$date", _ansi = false, _) do
quote do
unquote(__MODULE__).format_date(utc_date)
end
end
#---------+
# $time |
#---------+
defp field_builder("$time", _ansi = true, options) do
quote do
[
unquote(options.timestamp_color),
unquote(__MODULE__).format_time(utc_time, timestamp),
unquote(@ansi_reset)
]
end
end
defp field_builder("$time", _ansi = false, _) do
quote do
unquote(__MODULE__).format_time(utc_time, timestamp)
end
end
#-------------+
# $datetime |
#-------------+
defp field_builder("$datetime", _ansi = true, options) do
quote do
[
unquote(options.timestamp_color),
"#{unquote(__MODULE__).format_date(utc_date)} #{unquote(__MODULE__).format_time(utc_time, timestamp)}",
unquote(@ansi_reset)
]
end
end
defp field_builder("$datetime", _ansi = false, _) do
quote do
"#{unquote(__MODULE__).format_date(utc_date)} #{unquote(__MODULE__).format_time(utc_time, timestamp)}"
end
end
#-----------------------+
# $message_first_line |
#-----------------------+
defp field_builder("$message_first_line", _ansi = true, options) do
quote do
[
unquote(Macro.escape(options.message_colors))[level],
msg_first_line,
unquote(@ansi_reset)
]
end
end
defp field_builder("$message_first_line", _ansi = false, _) do
quote do
msg_first_line
end
end
#-----------------+
# $message_rest |
#-----------------+
defp field_builder("$message_rest", _ansi = true, options) do
quote do
case msg_rest do
[] ->
""
msg ->
[
unquote(Macro.escape(options.message_colors))[level],
msg,
unquote(@ansi_reset)
]
end
end
end
defp field_builder("$message_rest", _ansi = false, _) do
quote do
case msg_rest do
[] ->
""
msg ->
msg
end
end
end
#------------+
# $message |
#------------+
defp field_builder("$message", _ansi = true, options) do
quote do
[
unquote(Macro.escape(options.message_colors))[level],
msg,
unquote(@ansi_reset)
]
end
end
defp field_builder("$message", _ansi = false, _) do
quote do
msg
end
end
#----------+
# $level |
#----------+
defp field_builder("$level", _ansi = true, options) do
quote do
[
unquote(Macro.escape(options.level_colors))[level],
Bunyan.Shared.Level.to_s(level),
unquote(@ansi_reset)
]
end
end
defp field_builder("$level", _ansi = false, _) do
quote do
Bunyan.Shared.Level.to_s(level)
end
end
#---------+
# $node |
#---------+
defp field_builder("$node", _, _) do
quote do
unquote(__MODULE__).format_node(node)
end
end
#--------+
# $pid |
#--------+
defp field_builder("$pid", _, _) do
quote do
unquote(__MODULE__).format_pid(pid)
end
end
#----------------+
# $remote_info |
#----------------+
defp field_builder("$remote_info", _, _) do
quote do
if node == node() do
""
else
[
"(from ",
unquote(__MODULE__).format_pid(pid),
" on ",
unquote(__MODULE__).format_node(node),
")\n"
]
end
end
end
#----------+
# $extra |
#----------+
defp field_builder("$extra", _ansi = true, options) do
quote do
case extra do
[] ->
""
nil ->
""
data ->
[
unquote(options.extra_color),
unquote(__MODULE__).format_extra(data),
unquote(@ansi_reset)
]
end
end
end
defp field_builder("$extra", _ansi = false, _) do
quote do
case extra do
[] ->
""
nil ->
""
data ->
unquote(__MODULE__).format_extra(data)
end
end
end
#-----------------+
# unknown field |
#-----------------+
defp field_builder("$" <> rest, _, _) do
raise """
Unknown Logger format field: $#{rest}
Valid formats are:
$date - date the log message was sent (yy-mm-dd)
$time - time the log message was sent (hh:mm:ss.mmm)
$datetime - "$date $time"
$message - the log message
$level - the log level
$node - the node that prints the message
$pid - the pid that generated the message
$remote_info - the pid and node, if the message is remote
$extra - any term. maps and keyword lists are formatted nicely
"""
end
defp field_builder(text, _, _) do
quote do
unquote(text)
end
end
##################################################################################
defp field_size("$message_first_line"), do: :stop
defp field_size("$message_rest"), do: :stop
defp field_size("$message"), do: :stop
defp field_size("$date"), do: 10
defp field_size("$time"), do: 12
defp field_size("$datetime"), do: 23
defp field_size("$level"), do: 1
defp field_size("$node"), do: 15
defp field_size("$pid"), do: 12
defp field_size("$remote_info"), do: 0
defp field_size("$extra"), do: 0
defp field_size(text) when is_binary(text), do: String.length(text)
#####################################################################################
defp fields_in(format) do
Regex.split(@regex, format <> "\n", on: [:head, :tail], trim: true)
end
defp prefix_size(fields) do
fields
|> Enum.reduce_while(0, fn field, size ->
case field_size(field) do
:stop ->
{ :halt, size }
n ->
{ :cont, size + n }
end
end)
end
def format_extra(nil), do: []
def format_extra(data) when is_map(data) do
max_key_len= Map.keys(data) |> Enum.map(&inspect/1) |> Enum.map(&String.length/1) |> Enum.max
key_len = min(max_key_len, 20)
Enum.map(data, fn { key, value } ->
"#{String.pad_trailing(inspect(key), key_len)} => #{inspect value}"
end)
|> Enum.join("\n")
end
def format_extra(data) do
inspect(data, pretty: true, width: 65)
end
def format_date({y, m, d}) do
"#{y}-#{pad2(m)}-#{pad2(d)}"
end
def format_time({ h, m, s }, { _, _, micros }) do
"#{pad2(h)}:#{pad2(m)}:#{pad2(s)}.#{pad_millis(div(micros, 1000))}"
end
def format_pid(pid) do
pid
|> inspect()
|> String.replace("PID", "")
end
def format_node(node) do
node
|> to_string()
end
defp pad2(0), do: "00"
defp pad2(1), do: "01"
defp pad2(2), do: "02"
defp pad2(3), do: "03"
defp pad2(4), do: "04"
defp pad2(5), do: "05"
defp pad2(6), do: "06"
defp pad2(7), do: "07"
defp pad2(8), do: "08"
defp pad2(9), do: "09"
defp pad2(n), do: n
defp pad_millis(n) when n < 10, do: "00#{n}"
defp pad_millis(n) when n < 100, do: "0#{n}"
defp pad_millis(n) when n < 1000, do: "#{n}"
defp pad_millis(_), do: "***"
def indent(nil, _), do: ""
def indent([], _), do: ""
def indent(extra, padding) do
extra
|> List.flatten
|> Enum.join("")
|> String.split(~r/\n/, trim: true)
|> Enum.map(fn line -> [ padding, line, "\n" ] end)
end
def indent_all_but_first(msg, padding) do
[ first | rest ] =
msg
|> List.flatten
|> Enum.join("")
|> String.split(~r/\n/, trim: true)
[ first, "\n" | Enum.map(rest, fn line -> [ padding, line, "\n" ] end) ]
end
end