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