1defmodule Logger.Translator do
2  @moduledoc """
3  Default translation for Erlang log messages.
4
5  Logger allows developers to rewrite log messages provided by
6  OTP applications into a format more compatible with Elixir
7  log messages by providing a translator.
8
9  A translator is simply a tuple containing a module and a function
10  that can be added and removed via the `Logger.add_translator/1` and
11  `Logger.remove_translator/1` functions and is invoked for every Erlang
12  message above the minimum log level with four arguments:
13
14    * `min_level` - the current Logger level
15    * `level` - the level of the message being translated
16    * `kind` - if the message is a `:report` or `:format`
17    * `message` - the message to format. If it is `:report`, it is a tuple
18      with `{report_type, report_data}`, if it is `:format`, it is a
19      tuple with `{format_message, format_args}`.
20
21  The function must return:
22
23    * `{:ok, chardata, metadata}` - if the message translation with its metadata
24    * `{:ok, chardata}` - the translated message
25    * `:skip` - if the message is not meant to be translated nor logged
26    * `:none` - if there is no translation, which triggers the next translator
27
28  See the function `translate/4` in this module for an example implementation
29  and the default messages translated by Logger.
30  """
31
32  @doc """
33  Built-in translation function.
34  """
35  def translate(min_level, level, kind, message)
36
37  def translate(min_level, _level, :report, {:logger, %{label: label} = report}) do
38    case label do
39      {:gen_server, :terminate} ->
40        report_gen_server_terminate(min_level, report)
41
42      {:gen_event, :terminate} ->
43        report_gen_event_terminate(min_level, report)
44
45      _ ->
46        :skip
47    end
48  end
49
50  def translate(min_level, _level, :report, {{:proc_lib, :crash}, data}) do
51    report_crash(min_level, data)
52  end
53
54  def translate(min_level, _level, :report, {{:supervisor, :progress}, data}) do
55    report_supervisor_progress(min_level, data)
56  end
57
58  def translate(min_level, _level, :report, {{:supervisor, _}, data}) do
59    report_supervisor(min_level, data)
60  end
61
62  def translate(
63        _min_level,
64        _level,
65        :report,
66        {{:application_controller, :progress}, [application: app, started_at: node]}
67      ) do
68    {:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
69  end
70
71  def translate(
72        _min_level,
73        _level,
74        :report,
75        {{:application_controller, :exit}, [application: app, exited: reason, type: _type]}
76      ) do
77    {:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
78  end
79
80  def translate(
81        _min_level,
82        :error,
83        :report,
84        {{Task.Supervisor, :terminating},
85         %{
86           name: name,
87           starter: starter,
88           function: function,
89           args: args,
90           reason: reason
91         }}
92      ) do
93    opts = Application.get_env(:logger, :translator_inspect_opts)
94
95    {formatted, reason} = format_reason(reason)
96    metadata = [crash_reason: reason] ++ registered_name(name)
97
98    msg =
99      ["Task #{inspect(name)} started from #{inspect(starter)} terminating"] ++
100        [formatted, "\nFunction: #{inspect(function, opts)}"] ++
101        ["\n    Args: #{inspect(args, opts)}"]
102
103    {:ok, msg, metadata}
104  end
105
106  def translate(min_level, :error, :format, message) do
107    case message do
108      # This is no longer emitted by Erlang/OTP but it may be
109      # manually emitted by libraries like connection.
110      {'** Generic server ' ++ _, [name, last, state, reason | client]} ->
111        opts = Application.fetch_env!(:logger, :translator_inspect_opts)
112        {formatted, reason} = format_reason(reason)
113        metadata = [crash_reason: reason] ++ registered_name(name)
114
115        msg =
116          ["GenServer #{inspect(name)} terminating", formatted] ++
117            ["\nLast message#{format_from(client)}: #{inspect(last, opts)}"]
118
119        if min_level == :debug do
120          msg = [msg, "\nState: #{inspect(state, opts)}" | format_client(client)]
121          {:ok, msg, metadata}
122        else
123          {:ok, msg, metadata}
124        end
125
126      {'Error in process ' ++ _, [pid, node, {reason, stack}]} ->
127        reason = Exception.normalize(:error, reason, stack)
128
129        msg = [
130          "Process ",
131          inspect(pid),
132          " on node ",
133          inspect(node),
134          " raised an exception" | format(:error, reason, stack)
135        ]
136
137        {:ok, msg, [crash_reason: exit_reason(:error, reason, stack)]}
138
139      {'Error in process ' ++ _, [pid, {reason, stack}]} ->
140        reason = Exception.normalize(:error, reason, stack)
141        msg = ["Process ", inspect(pid), " raised an exception" | format(:error, reason, stack)]
142        {:ok, msg, [crash_reason: exit_reason(:error, reason, stack)]}
143
144      _ ->
145        :none
146    end
147  end
148
149  def translate(_min_level, :info, :report, {
150        :std_info,
151        [application: app, exited: reason, type: _type]
152      }) do
153    {:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
154  end
155
156  def translate(min_level, :error, :report, {{:error_logger, :error_report}, data}) do
157    report_supervisor(min_level, data)
158  end
159
160  def translate(min_level, :error, :report, {:supervisor_report, data}) do
161    report_supervisor(min_level, data)
162  end
163
164  def translate(min_level, :error, :report, {:crash_report, data}) do
165    report_crash(min_level, data)
166  end
167
168  def translate(min_level, :info, :report, {:progress, [{:supervisor, _} | _] = data}) do
169    report_supervisor_progress(min_level, data)
170  end
171
172  def translate(_min_level, :info, :report, {:progress, [application: app, started_at: node]}) do
173    {:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
174  end
175
176  ## Helpers
177
178  def translate(_min_level, _level, _kind, _message) do
179    :none
180  end
181
182  defp report_gen_server_terminate(min_level, report) do
183    inspect_opts = Application.fetch_env!(:logger, :translator_inspect_opts)
184
185    %{
186      client_info: client,
187      last_message: last,
188      name: name,
189      reason: reason,
190      state: state
191    } = report
192
193    {formatted, reason} = format_reason(reason)
194    metadata = [crash_reason: reason] ++ registered_name(name)
195
196    msg =
197      ["GenServer ", inspect(name), " terminating", formatted] ++
198        ["\nLast message", format_last_message_from(client), ": ", inspect(last, inspect_opts)]
199
200    if min_level == :debug do
201      msg = [msg, "\nState: ", inspect(state, inspect_opts) | format_client_info(client)]
202      {:ok, msg, metadata}
203    else
204      {:ok, msg, metadata}
205    end
206  end
207
208  defp report_gen_event_terminate(min_level, report) do
209    inspect_opts = Application.fetch_env!(:logger, :translator_inspect_opts)
210
211    %{
212      handler: handler,
213      last_message: last,
214      name: name,
215      reason: reason,
216      state: state
217    } = report
218
219    reason =
220      case reason do
221        {:EXIT, why} -> why
222        _ -> reason
223      end
224
225    {formatted, reason} = format_reason(reason)
226    metadata = [crash_reason: reason] ++ registered_name(name)
227
228    msg =
229      [":gen_event handler ", inspect(handler), " installed in ", inspect(name), " terminating"] ++
230        [formatted, "\nLast message: ", inspect(last, inspect_opts)]
231
232    if min_level == :debug do
233      {:ok, [msg, "\nState: ", inspect(state, inspect_opts)], metadata}
234    else
235      {:ok, msg, metadata}
236    end
237  end
238
239  defp report_supervisor_progress(
240         min_level,
241         supervisor: sup,
242         started: [{:pid, pid}, {:id, id} | started]
243       ) do
244    msg =
245      ["Child ", inspect(id), " of Supervisor ", sup_name(sup), " started"] ++
246        ["\nPid: ", inspect(pid)] ++ child_info(min_level, started)
247
248    {:ok, msg}
249  end
250
251  defp report_supervisor_progress(
252         min_level,
253         supervisor: sup,
254         started: [{:pid, pid} | started]
255       ) do
256    msg =
257      ["Child of Supervisor ", sup_name(sup), " started", "\nPid: ", inspect(pid)] ++
258        child_info(min_level, started)
259
260    {:ok, msg}
261  end
262
263  defp report_supervisor_progress(_min_level, _other), do: :none
264
265  defp report_supervisor(
266         min_level,
267         supervisor: sup,
268         errorContext: context,
269         reason: reason,
270         offender: [{:pid, pid}, {:id, id} | offender]
271       ) do
272    pid_info =
273      if is_pid(pid) and context != :shutdown do
274        ["\nPid: ", inspect(pid)]
275      else
276        []
277      end
278
279    msg =
280      ["Child ", inspect(id), " of Supervisor ", sup_name(sup)] ++
281        [?\s, sup_context(context), "\n** (exit) ", offender_reason(reason, context)] ++
282        pid_info ++ child_info(min_level, offender)
283
284    {:ok, msg}
285  end
286
287  defp report_supervisor(
288         min_level,
289         supervisor: sup,
290         errorContext: context,
291         reason: reason,
292         offender: [{:nb_children, n}, {:id, id} | offender]
293       ) do
294    msg =
295      ["Children ", inspect(id), " of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
296        ["\n** (exit) ", offender_reason(reason, context), "\nNumber: ", Integer.to_string(n)] ++
297        child_info(min_level, offender)
298
299    {:ok, msg}
300  end
301
302  defp report_supervisor(
303         min_level,
304         supervisor: sup,
305         errorContext: context,
306         reason: reason,
307         offender: [{:pid, pid} | offender]
308       ) do
309    msg =
310      ["Child of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
311        ["\n** (exit) ", offender_reason(reason, context), "\nPid: ", inspect(pid)] ++
312        child_info(min_level, offender)
313
314    {:ok, msg}
315  end
316
317  defp report_supervisor(_min_level, _other), do: :none
318
319  # If start call raises reason will be of form {:EXIT, reason}
320  defp offender_reason({:EXIT, reason}, :start_error) do
321    Exception.format_exit(reason)
322  end
323
324  defp offender_reason(reason, _context) do
325    Exception.format_exit(reason)
326  end
327
328  defp sup_name({:local, name}), do: inspect(name)
329  defp sup_name({:global, name}), do: inspect(name)
330  defp sup_name({:via, _mod, name}), do: inspect(name)
331  defp sup_name({pid, mod}), do: [inspect(pid), " (", inspect(mod), ?)]
332  defp sup_name(unknown_name), do: inspect(unknown_name)
333
334  defp sup_context(:start_error), do: "failed to start"
335  defp sup_context(:child_terminated), do: "terminated"
336  defp sup_context(:shutdown), do: "caused shutdown"
337  defp sup_context(:shutdown_error), do: "shut down abnormally"
338
339  defp child_info(min_level, [{:mfargs, {mod, fun, args}} | debug]) do
340    ["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
341  end
342
343  # Comes from bridge with MFA
344  defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
345    ["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
346  end
347
348  # Comes from bridge with Mod
349  defp child_info(min_level, [{:mod, mod} | debug]) do
350    ["\nStart Module: ", inspect(mod) | child_debug(min_level, debug)]
351  end
352
353  defp child_info(_min_level, _child) do
354    []
355  end
356
357  defp child_debug(:debug, opts) do
358    for {key, value} <- opts do
359      child_debug_key(key, value)
360    end
361  end
362
363  defp child_debug(_min_level, _child) do
364    []
365  end
366
367  defp child_debug_key(:restart_type, value), do: ["\nRestart: " | inspect(value)]
368  defp child_debug_key(:shutdown, value), do: ["\nShutdown: " | inspect(value)]
369  defp child_debug_key(:child_type, value), do: ["\nType: " | inspect(value)]
370  defp child_debug_key(:significant, value), do: if(value, do: "\nSignificant: true", else: [])
371  defp child_debug_key(_, _), do: []
372
373  defp report_crash(min_level, [[{:initial_call, initial_call} | crashed], linked]) do
374    mfa = initial_call_to_mfa(initial_call)
375    report_crash(min_level, crashed, [{:initial_call, mfa}], linked)
376  end
377
378  defp report_crash(min_level, [crashed, linked]) do
379    report_crash(min_level, crashed, [], linked)
380  end
381
382  defp report_crash(min_level, crashed, extra, linked) do
383    [
384      {:pid, pid},
385      {:registered_name, name},
386      {:error_info, {kind, reason, stack}} | crashed
387    ] = crashed
388
389    dictionary = crashed[:dictionary]
390    reason = Exception.normalize(kind, reason, stack)
391
392    case Keyword.get(dictionary, :logger_enabled, true) do
393      false ->
394        :skip
395
396      true ->
397        user_metadata = Keyword.get(dictionary, :"$logger_metadata$", %{}) |> Map.to_list()
398
399        msg =
400          ["Process ", crash_name(pid, name), " terminating", format(kind, reason, stack)] ++
401            [crash_info(min_level, extra ++ crashed, [?\n]), crash_linked(min_level, linked)]
402
403        extra =
404          if ancestors = crashed[:ancestors], do: [{:ancestors, ancestors} | extra], else: extra
405
406        extra =
407          if callers = dictionary[:"$callers"], do: [{:callers, callers} | extra], else: extra
408
409        extra = [{:crash_reason, exit_reason(kind, reason, stack)} | extra]
410        {:ok, msg, registered_name(name) ++ extra ++ user_metadata}
411    end
412  end
413
414  defp initial_call_to_mfa({:supervisor, module, _}), do: {module, :init, 1}
415  defp initial_call_to_mfa({:supervisor_bridge, module, _}), do: {module, :init, 1}
416  defp initial_call_to_mfa({mod, fun, args}) when is_list(args), do: {mod, fun, length(args)}
417  defp initial_call_to_mfa(mfa), do: mfa
418
419  defp crash_name(pid, []), do: inspect(pid)
420  defp crash_name(pid, name), do: [inspect(name), " (", inspect(pid), ?)]
421
422  defp crash_info(min_level, [{:initial_call, {mod, fun, args}} | info], prefix) do
423    [prefix, "Initial Call: ", crash_call(mod, fun, args) | crash_info(min_level, info, prefix)]
424  end
425
426  defp crash_info(min_level, [{:current_function, {mod, fun, args}} | info], prefix) do
427    [prefix, "Current Call: ", crash_call(mod, fun, args) | crash_info(min_level, info, prefix)]
428  end
429
430  defp crash_info(min_level, [{:current_function, []} | info], prefix) do
431    crash_info(min_level, info, prefix)
432  end
433
434  defp crash_info(min_level, [{:ancestors, ancestors} | debug], prefix) do
435    [prefix, "Ancestors: ", inspect(ancestors) | crash_info(min_level, debug, prefix)]
436  end
437
438  defp crash_info(:debug, debug, prefix) do
439    for {key, value} <- debug do
440      crash_debug(key, value, prefix)
441    end
442  end
443
444  defp crash_info(_, _, _) do
445    []
446  end
447
448  defp crash_call(mod, fun, arity) when is_integer(arity) do
449    format_mfa(mod, fun, arity)
450  end
451
452  defp crash_call(mod, fun, args) do
453    format_mfa(mod, fun, length(args))
454  end
455
456  defp crash_debug(:current_stacktrace, stack, prefix) do
457    stack_prefix = [prefix | "    "]
458    stacktrace = Enum.map(stack, &[stack_prefix | Exception.format_stacktrace_entry(&1)])
459    [prefix, "Current Stacktrace:" | stacktrace]
460  end
461
462  defp crash_debug(key, value, prefix) do
463    [prefix, crash_debug_key(key), ?:, ?\s, inspect(value)]
464  end
465
466  defp crash_debug_key(key) do
467    case key do
468      :message_queue_len -> "Message Queue Length"
469      :messages -> "Messages"
470      :links -> "Links"
471      :dictionary -> "Dictionary"
472      :trap_exit -> "Trapping Exits"
473      :status -> "Status"
474      :heap_size -> "Heap Size"
475      :stack_size -> "Stack Size"
476      :reductions -> "Reductions"
477    end
478  end
479
480  defp crash_linked(_min_level, []), do: []
481
482  defp crash_linked(min_level, neighbours) do
483    Enum.reduce(neighbours, "\nNeighbours:", fn {:neighbour, info}, acc ->
484      [acc | crash_neighbour(min_level, info)]
485    end)
486  end
487
488  @indent "    "
489
490  defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, []} | info]) do
491    [?\n, @indent, inspect(pid) | crash_info(min_level, info, [?\n, @indent | @indent])]
492  end
493
494  defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, name} | info]) do
495    [?\n, @indent, inspect(name), " (", inspect(pid), ")"] ++
496      crash_info(min_level, info, [?\n, @indent | @indent])
497  end
498
499  defp format_last_message_from({_, {name, _}}), do: [" (from ", inspect(name), ")"]
500  defp format_last_message_from({from, _}), do: [" (from ", inspect(from), ")"]
501  defp format_last_message_from(_), do: []
502
503  defp format_client_info({from, :dead}),
504    do: ["\nClient ", inspect(from), " is dead"]
505
506  defp format_client_info({from, :remote}),
507    do: ["\nClient ", inspect(from), " is remote on node ", inspect(node(from))]
508
509  defp format_client_info({_, {name, stacktrace}}),
510    do: ["\nClient ", inspect(name), " is alive\n" | format_stacktrace(stacktrace)]
511
512  defp format_client_info(_),
513    do: []
514
515  defp format_reason({maybe_exception, [_ | _] = maybe_stacktrace} = reason) do
516    try do
517      format_stacktrace(maybe_stacktrace)
518    catch
519      :error, _ ->
520        {format_stop(reason), {reason, []}}
521    else
522      formatted_stacktrace ->
523        {formatted, reason} = maybe_normalize(maybe_exception, maybe_stacktrace)
524        {[formatted | formatted_stacktrace], {reason, maybe_stacktrace}}
525    end
526  end
527
528  defp format_reason(reason) do
529    {format_stop(reason), {reason, []}}
530  end
531
532  defp format_stop(reason) do
533    ["\n** (stop) " | Exception.format_exit(reason)]
534  end
535
536  # Erlang processes rewrite the :undef error to these reasons when logging
537  @gen_undef [:"module could not be loaded", :"function not exported"]
538
539  defp maybe_normalize(undef, [{mod, fun, args, _info} | _] = stacktrace)
540       when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
541    cond do
542      is_list(args) ->
543        format_undef(mod, fun, length(args), undef, stacktrace)
544
545      is_integer(args) ->
546        format_undef(mod, fun, args, undef, stacktrace)
547
548      true ->
549        {format_stop(undef), undef}
550    end
551  end
552
553  defp maybe_normalize(reason, stacktrace) do
554    # If this is already an exception (even an ErlangError), we format it as an
555    # exception. Otherwise, we try to normalize it, and if it's normalized as an
556    # ErlangError we instead format it as an exit.
557    if is_exception(reason) do
558      {[?\n | Exception.format_banner(:error, reason, stacktrace)], reason}
559    else
560      case Exception.normalize(:error, reason, stacktrace) do
561        %ErlangError{} ->
562          {format_stop(reason), reason}
563
564        exception ->
565          {[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
566      end
567    end
568  end
569
570  defp format(kind, payload, stacktrace) do
571    [?\n, Exception.format_banner(kind, payload, stacktrace) | format_stacktrace(stacktrace)]
572  end
573
574  defp format_stacktrace(stacktrace) do
575    for entry <- stacktrace do
576      ["\n    " | Exception.format_stacktrace_entry(entry)]
577    end
578  end
579
580  defp registered_name(name) when is_atom(name), do: [registered_name: name]
581  defp registered_name(_name), do: []
582
583  defp format_mfa(mod, fun, :undefined),
584    do: [inspect(mod), ?., Code.Identifier.inspect_as_function(fun) | "/?"]
585
586  defp format_mfa(mod, fun, args),
587    do: Exception.format_mfa(mod, fun, args)
588
589  defp exit_reason(:exit, reason, stack), do: {reason, stack}
590  defp exit_reason(:error, reason, stack), do: {reason, stack}
591  defp exit_reason(:throw, value, stack), do: {{:nocatch, value}, stack}
592
593  ## Deprecated helpers
594
595  defp format_from([]), do: ""
596  defp format_from([from]), do: " (from #{inspect(from)})"
597  defp format_from([from, stacktrace]) when is_list(stacktrace), do: " (from #{inspect(from)})"
598
599  defp format_from([from, node_name]) when is_atom(node_name),
600    do: " (from #{inspect(from)} on #{inspect(node_name)})"
601
602  defp format_client([from]) do
603    "\nClient #{inspect(from)} is dead"
604  end
605
606  defp format_client([from, stacktrace]) when is_list(stacktrace) do
607    ["\nClient #{inspect(from)} is alive\n" | format_stacktrace(stacktrace)]
608  end
609
610  defp format_client(_) do
611    []
612  end
613
614  defp format_undef(mod, fun, arity, undef, stacktrace) do
615    opts = [module: mod, function: fun, arity: arity, reason: undef]
616    exception = UndefinedFunctionError.exception(opts)
617    {[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
618  end
619end
620