1import Kernel, except: [inspect: 2]
2
3defmodule Logger.Formatter do
4  @moduledoc ~S"""
5  Conveniences for formatting data for logs.
6
7  This module allows developers to specify a string that
8  serves as template for log messages, for example:
9
10      $time $metadata[$level] $message\n
11
12  Will print error messages as:
13
14      18:43:12.439 user_id=13 [error] Hello\n
15
16  The valid parameters you can use are:
17
18    * `$time`     - the time the log message was sent
19    * `$date`     - the date the log message was sent
20    * `$message`  - the log message
21    * `$level`    - the log level
22    * `$node`     - the node that prints the message
23    * `$metadata` - user controlled data presented in `"key=val key2=val2 "` format
24    * `$levelpad` - sets to a single space if level is 4 characters long,
25      otherwise set to the empty space. Used to align the message after level.
26
27  Backends typically allow developers to supply such control
28  strings via configuration files. This module provides `compile/1`,
29  which compiles the string into a format for fast operations at
30  runtime and `format/5` to format the compiled pattern into an
31  actual IO data.
32
33  ## Metadata
34
35  Metadata to be sent to the logger can be read and written with
36  the `Logger.metadata/0` and `Logger.metadata/1` functions. For example,
37  you can set `Logger.metadata([user_id: 13])` to add user_id metadata
38  to the current process. The user can configure the backend to choose
39  which metadata it wants to print and it will replace the `$metadata`
40  value.
41  """
42
43  @type time :: {{1970..10000, 1..12, 1..31}, {0..23, 0..59, 0..59, 0..999}}
44  @type pattern :: :date | :level | :levelpad | :message | :metadata | :node | :time
45  @valid_patterns [:time, :date, :message, :level, :node, :metadata, :levelpad]
46  @default_pattern "\n$time $metadata[$level] $levelpad$message\n"
47  @replacement "�"
48
49  @doc """
50  Prunes invalid Unicode code points from lists and invalid UTF-8 bytes.
51
52  Typically called after formatting when the data cannot be printed.
53  """
54  @spec prune(IO.chardata()) :: IO.chardata()
55  def prune(binary) when is_binary(binary), do: prune_binary(binary, "")
56  def prune([h | t]) when h in 0..1_114_111, do: [h | prune(t)]
57  def prune([h | t]), do: [prune(h) | prune(t)]
58  def prune([]), do: []
59  def prune(_), do: @replacement
60
61  defp prune_binary(<<h::utf8, t::binary>>, acc), do: prune_binary(t, <<acc::binary, h::utf8>>)
62  defp prune_binary(<<_, t::binary>>, acc), do: prune_binary(t, <<acc::binary, @replacement>>)
63  defp prune_binary(<<>>, acc), do: acc
64
65  @doc """
66  Compiles a format string into a data structure that `format/5` can handle.
67
68  Check the module doc for documentation on the valid parameters that
69  will be interpolated in the pattern. If you pass `nil` as the pattern,
70  the pattern defaults to:
71
72      #{inspect(@default_pattern)}
73
74  If you want to customize formatting through a custom formatter, you can
75  pass a `{module, function}` tuple as the `pattern`.
76
77      iex> Logger.Formatter.compile("$time $metadata [$level] $message\\n")
78      [:time, " ", :metadata, " [", :level, "] ", :message, "\\n"]
79
80      iex> Logger.Formatter.compile({MyLoggerFormatter, :format})
81      {MyLoggerFormatter, :format}
82
83  """
84  @spec compile(binary | nil) :: [pattern | binary]
85  @spec compile(pattern) :: pattern when pattern: {module, function :: atom}
86  def compile(pattern)
87
88  def compile(nil), do: compile(@default_pattern)
89  def compile({mod, fun}) when is_atom(mod) and is_atom(fun), do: {mod, fun}
90
91  def compile(str) when is_binary(str) do
92    regex = ~r/(?<head>)\$[a-z]+(?<tail>)/
93
94    for part <- Regex.split(regex, str, on: [:head, :tail], trim: true) do
95      case part do
96        "$" <> code -> compile_code(String.to_atom(code))
97        _ -> part
98      end
99    end
100  end
101
102  defp compile_code(key) when key in @valid_patterns, do: key
103
104  defp compile_code(key) when is_atom(key) do
105    raise ArgumentError, "$#{key} is an invalid format pattern"
106  end
107
108  @doc """
109  Formats time as chardata.
110  """
111  @spec format_time({0..23, 0..59, 0..59, 0..999}) :: IO.chardata()
112  def format_time({hh, mi, ss, ms}) do
113    [pad2(hh), ?:, pad2(mi), ?:, pad2(ss), ?., pad3(ms)]
114  end
115
116  @doc """
117  Formats date as chardata.
118  """
119  @spec format_date({1970..10000, 1..12, 1..31}) :: IO.chardata()
120  def format_date({yy, mm, dd}) do
121    [Integer.to_string(yy), ?-, pad2(mm), ?-, pad2(dd)]
122  end
123
124  defp pad3(int) when int < 10, do: [?0, ?0, Integer.to_string(int)]
125  defp pad3(int) when int < 100, do: [?0, Integer.to_string(int)]
126  defp pad3(int), do: Integer.to_string(int)
127
128  defp pad2(int) when int < 10, do: [?0, Integer.to_string(int)]
129  defp pad2(int), do: Integer.to_string(int)
130
131  @doc """
132  Takes a compiled format and injects the level, timestamp, message, and
133  metadata keyword list and returns a properly formatted string.
134
135  ## Examples
136
137      iex> pattern = Logger.Formatter.compile("[$level] $message")
138      iex> timestamp = {{1977, 01, 28}, {13, 29, 00, 000}}
139      iex> formatted = Logger.Formatter.format(pattern, :info, "hello", timestamp, [])
140      iex> IO.chardata_to_string(formatted)
141      "[info] hello"
142
143  """
144  @spec format({atom, atom} | [pattern | binary], Logger.level(), Logger.message(), time, keyword) ::
145          IO.chardata()
146  def format({mod, fun}, level, msg, timestamp, metadata) do
147    apply(mod, fun, [level, msg, timestamp, metadata])
148  end
149
150  def format(config, level, msg, timestamp, metadata) do
151    for config_option <- config do
152      output(config_option, level, msg, timestamp, metadata)
153    end
154  end
155
156  defp output(:message, _, msg, _, _), do: msg
157  defp output(:date, _, _, {date, _time}, _), do: format_date(date)
158  defp output(:time, _, _, {_date, time}, _), do: format_time(time)
159  defp output(:level, level, _, _, _), do: Atom.to_string(level)
160  defp output(:node, _, _, _, _), do: Atom.to_string(node())
161  defp output(:metadata, _, _, _, []), do: ""
162  defp output(:metadata, _, _, _, meta), do: metadata(meta)
163  defp output(:levelpad, level, _, _, _), do: levelpad(level)
164  defp output(other, _, _, _, _), do: other
165
166  defp levelpad(:debug), do: ""
167  defp levelpad(:info), do: " "
168  defp levelpad(:warn), do: " "
169  defp levelpad(:error), do: ""
170
171  defp metadata([{key, value} | metadata]) do
172    if formatted = metadata(key, value) do
173      [to_string(key), ?=, formatted, ?\s | metadata(metadata)]
174    else
175      metadata(metadata)
176    end
177  end
178
179  defp metadata([]) do
180    []
181  end
182
183  defp metadata(:time, _), do: nil
184  defp metadata(:gl, _), do: nil
185  defp metadata(:report_cb, _), do: nil
186
187  defp metadata(_, nil), do: nil
188  defp metadata(_, string) when is_binary(string), do: string
189  defp metadata(_, integer) when is_integer(integer), do: Integer.to_string(integer)
190  defp metadata(_, float) when is_float(float), do: Float.to_string(float)
191  defp metadata(_, pid) when is_pid(pid), do: :erlang.pid_to_list(pid)
192
193  defp metadata(_, atom) when is_atom(atom) do
194    case Atom.to_string(atom) do
195      "Elixir." <> rest -> rest
196      "nil" -> ""
197      binary -> binary
198    end
199  end
200
201  defp metadata(_, ref) when is_reference(ref) do
202    '#Ref' ++ rest = :erlang.ref_to_list(ref)
203    rest
204  end
205
206  defp metadata(:file, file) when is_list(file), do: file
207
208  defp metadata(:domain, [head | tail]) when is_atom(head) do
209    Enum.map_intersperse([head | tail], ?., &Atom.to_string/1)
210  end
211
212  defp metadata(:mfa, {mod, fun, arity})
213       when is_atom(mod) and is_atom(fun) and is_integer(arity) do
214    Exception.format_mfa(mod, fun, arity)
215  end
216
217  defp metadata(:initial_call, {mod, fun, arity})
218       when is_atom(mod) and is_atom(fun) and is_integer(arity) do
219    Exception.format_mfa(mod, fun, arity)
220  end
221
222  defp metadata(_, list) when is_list(list), do: nil
223
224  defp metadata(_, other) do
225    case String.Chars.impl_for(other) do
226      nil -> nil
227      impl -> impl.to_string(other)
228    end
229  end
230end
231