Log Translation

View Source

Elixir and Erlang are different languages, but Elixir runs on the Erlang platform and it doesn't take much time until the platform starts to manifest itself. A lot of common errors originate in Erlang and if printed as-is, look foreign in the context of Elixir applications. To avoid this, Elixir performs something called log translation. This is one of the main functions of Logger applications Compare these two GenServer errors, with and without translation:

With Translation

13:43:32.538 [error] GenServer #PID<0.198.0> terminating
** (RuntimeError) oops
    (logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: anonymous fn/0 in LoggerHandlerKit.Act.genserver_crash/1
    (stdlib 6.2.2) gen_server.erl:2381: :gen_server.try_handle_call/4
    (stdlib 6.2.2) gen_server.erl:2410: :gen_server.handle_msg/6
    (stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3
Last message (from #PID<0.197.0>): {:run, #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>}
State: nil
Client #PID<0.197.0> is alive

    (stdlib 6.2.2) gen.erl:260: :gen.do_call/4
    (elixir 1.19.0-dev) lib/gen_server.ex:1139: GenServer.call/3
    (logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: LoggerHandlerKit.Act.genserver_crash/1
    (elixir 1.19.0-dev) src/elixir.erl:363: :elixir.eval_external_handler/3
    (stdlib 6.2.2) erl_eval.erl:919: :erl_eval.do_apply/7
    (elixir 1.19.0-dev) src/elixir.erl:341: :elixir.eval_forms/4
    (elixir 1.19.0-dev) lib/module/parallel_checker.ex:141: Module.ParallelChecker.verify/1
    (iex 1.19.0-dev) lib/iex/evaluator.ex:340: IEx.Evaluator.eval_and_inspect/3

Without Translation

13:44:53.011 [error] ** Generic server <0.199.0> terminating
** Last message in was {run,#Fun<Elixir.LoggerHandlerKit.Act.16.413966>}
** When Server state == nil
** Reason for termination ==
** {#{message => <<"oops">>,'__struct__' => 'Elixir.RuntimeError',
      '__exception__' => true},
    [{'Elixir.LoggerHandlerKit.Act','-genserver_crash/1-fun-0-',0,
         [{file,"lib/logger_handler_kit/act.ex"},
          {line,528},
          {error_info,#{module => 'Elixir.Exception'}}]},
     {gen_server,try_handle_call,4,[{file,"gen_server.erl"},{line,2381}]},
     {gen_server,handle_msg,6,[{file,"gen_server.erl"},{line,2410}]},
     {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,329}]}]}
** Client <0.197.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,260}]},
    {'Elixir.GenServer',call,3,[{file,"lib/gen_server.ex"},{line,1139}]},
    {'Elixir.LoggerHandlerKit.Act',genserver_crash,1,
                                   [{file,"lib/logger_handler_kit/act.ex"},
                                    {line,528}]},
    {elixir,eval_external_handler,3,[{file,"src/elixir.erl"},{line,363}]},
    {erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,919}]},
    {elixir,eval_forms,4,[{file,"src/elixir.erl"},{line,341}]},
    {'Elixir.Module.ParallelChecker',verify,1,
                                     [{file,"lib/module/parallel_checker.ex"},
                                      {line,141}]},
    {'Elixir.IEx.Evaluator',eval_and_inspect,3,
                            [{file,"lib/iex/evaluator.ex"},{line,340}]}]


13:44:53.016 [error]   crasher:
    initial call: 'Elixir.LoggerHandlerKit.GenServer':init/1
    pid: <0.199.0>
    registered_name: []
    exception error: #Elixir.RuntimeError
      in function  'Elixir.LoggerHandlerKit.Act':'-genserver_crash/1-fun-0-'/0 (lib/logger_handler_kit/act.ex, line 528)
         *** oops
      in call from gen_server:try_handle_call/4 (gen_server.erl, line 2381)
      in call from gen_server:handle_msg/6 (gen_server.erl, line 2410)
    ancestors: [<0.197.0>,<0.98.0>]
    message_queue_len: 0
    messages: []
    links: []
    dictionary: [{'$callers',[<0.197.0>]}]
    trap_exit: false
    status: running
    heap_size: 6772
    stack_size: 29
    reductions: 19158
  neighbours: []

It's very clear how important translation is for people who are fluent primarily in Elixir.

Translation is a Filter

Log translation is implemented as a primary filter, which means it will be called before handlers and any handler-specific filters. Remember that filters and logger configuration in general can be altered by users, although it's somewhat uncommon to remove or modify the logger_translator specifically.

To better visualize this, here is where the translation happens:

iex(1)> :logger.get_primary_config()
%{
  level: :debug,
  filter_default: :log,
  filters: [
    # Here it is! Translation filter
    logger_translator: {&Logger.Utils.translator/2,
     %{otp: true, sasl: false, translators: [{Logger.Translator, :translate}]}},
    logger_process_level: {&Logger.Utils.process_level/2, []}
  ],
  metadata: %{}
}

1.18 vs 1.19+

Before Elixir 1.19, log translation used to replace the original Erlang structured report with an unstructured log message. If a handler wanted to access some of the data in the original report, it had to parse the log message. Starting from Elixir 1.19, translation is inserted into the report, thus making the original report accessible to the handlers. Here is an example:

Before Translation

%{
  meta: %{
    error_logger: %{tag: :error_report, type: :crash_report},
    line: 950,
    pid: #PID<0.203.0>,
    time: 1746910751187661,
    file: ~c"proc_lib.erl",
    gl: #PID<0.70.0>,
    domain: [:otp, :sasl],
    logger_formatter: %{title: ~c"CRASH REPORT"},
    mfa: {:proc_lib, :crash_report, 4},
    report_cb: &:proc_lib.report_cb/2
  },
  msg: {:report,
   %{
     label: {:proc_lib, :crash},
     report: [
       [
         initial_call: {LoggerHandlerKit.GenServer, :init, [:Argument__1]},
         pid: #PID<0.203.0>,
         registered_name: [],
         process_label: :undefined,
         error_info: {:error, %RuntimeError{message: "oops"},
          [
            {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0,
             [
               file: ~c"lib/logger_handler_kit/act.ex",
               line: 528,
               error_info: %{module: Exception}
             ]},
            {:gen_server, :try_handle_call, 4,
             [file: ~c"gen_server.erl", line: 2381]},
            {:gen_server, :handle_msg, 6,
             [file: ~c"gen_server.erl", line: 2410]},
            {:proc_lib, :init_p_do_apply, 3,
             [file: ~c"proc_lib.erl", line: 329]}
          ]},
         ancestors: [#PID<0.197.0>, #PID<0.98.0>],
         message_queue_len: 0,
         messages: [],
         links: [],
         dictionary: ["$callers": [#PID<0.197.0>]],
         trap_exit: false,
         status: :running,
         heap_size: 1598,
         stack_size: 29,
         reductions: 28455
       ],
       []
     ]
   }},
  level: :error
}

1.18 Translation

%{
  meta: %{
    error_logger: %{tag: :error, report_cb: &:gen_server.format_log/1},
    line: 2646,
    pid: #PID<0.1105.0>,
    time: 1746911049539570,
    file: ~c"gen_server.erl",
    gl: #PID<0.70.0>,
    domain: [:otp],
    report_cb: &:gen_server.format_log/2,
    mfa: {:gen_server, :error_info, 8},
    crash_reason: {%RuntimeError{message: "oops"},
     [
       {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0,
        [
          file: ~c"lib/logger_handler_kit/act.ex",
          line: 528,
          error_info: %{module: Exception}
        ]},
       {:gen_server, :try_handle_call, 4,
        [file: ~c"gen_server.erl", line: 2381]},
       {:gen_server, :handle_msg, 6, [file: ~c"gen_server.erl", line: 2410]},
       {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 329]}
     ]}
  },
  msg: {:string,
   [
     [
       "GenServer ",
       "#PID<0.1105.0>",
       " terminating",
       [
         [10 | "** (RuntimeError) oops"],
         ["\n    " |
          "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: anonymous fn/0 in LoggerHandlerKit.Act.genserver_crash/1"],
         ["\n    " |
          "(stdlib 6.2.2) gen_server.erl:2381: :gen_server.try_handle_call/4"],
         ["\n    " |
          "(stdlib 6.2.2) gen_server.erl:2410: :gen_server.handle_msg/6"],
         ["\n    " |
          "(stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3"]
       ],
       [],
       "\nLast message",
       [" (from ", "#PID<0.1099.0>", ")"],
       ": ",
       "{:run, #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>}"
     ],
     "\nState: ",
     "nil",
     "\nClient ",
     "#PID<0.1099.0>",
     " is alive\n",
     ["\n    " | "(stdlib 6.2.2) gen.erl:260: :gen.do_call/4"],
     ["\n    " | "(elixir 1.18.3) lib/gen_server.ex:1125: GenServer.call/3"],
     ["\n    " |
      "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: LoggerHandlerKit.Act.genserver_crash/1"],
     ["\n    " |
      "(elixir 1.18.3) src/elixir.erl:386: :elixir.eval_external_handler/3"],
     ["\n    " | "(stdlib 6.2.2) erl_eval.erl:919: :erl_eval.do_apply/7"],
     ["\n    " | "(elixir 1.18.3) src/elixir.erl:364: :elixir.eval_forms/4"],
     ["\n    " |
      "(elixir 1.18.3) lib/module/parallel_checker.ex:120: Module.ParallelChecker.verify/1"],
     ["\n    " |
      "(iex 1.18.3) lib/iex/evaluator.ex:336: IEx.Evaluator.eval_and_inspect/3"]
   ]},
  level: :error
}

1.19 Translation

%{
  meta: %{
    error_logger: %{tag: :error, report_cb: &:gen_server.format_log/1},
    line: 2646,
    pid: #PID<0.205.0>,
    time: 1746910905177484,
    file: ~c"gen_server.erl",
    gl: #PID<0.70.0>,
    domain: [:otp],
    mfa: {:gen_server, :error_info, 8},
    report_cb: &Logger.Utils.translated_cb/1,
    crash_reason: {%RuntimeError{message: "oops"},
     [
       {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0,
        [
          file: ~c"lib/logger_handler_kit/act.ex",
          line: 528,
          error_info: %{module: Exception}
        ]},
       {:gen_server, :try_handle_call, 4,
        [file: ~c"gen_server.erl", line: 2381]},
       {:gen_server, :handle_msg, 6, [file: ~c"gen_server.erl", line: 2410]},
       {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 329]}
     ]}
  },
  msg: {:report,
   %{
     label: {:gen_server, :terminate},
     name: #PID<0.205.0>,
     reason: {%RuntimeError{message: "oops"},
      [
        {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0,
         [
           file: ~c"lib/logger_handler_kit/act.ex",
           line: 528,
           error_info: %{module: Exception}
         ]},
        {:gen_server, :try_handle_call, 4,
         [file: ~c"gen_server.erl", line: 2381]},
        {:gen_server, :handle_msg, 6, [file: ~c"gen_server.erl", line: 2410]},
        {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 329]}
      ]},
     log: [],
     state: nil,
     process_label: :undefined,
     last_message: {:run,
      #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>},
     client_info: {#PID<0.197.0>,
      {#PID<0.197.0>,
       [
         {:gen, :do_call, 4, [file: ~c"gen.erl", line: 260]},
         {GenServer, :call, 3, [file: ~c"lib/gen_server.ex", line: 1139]},
         {LoggerHandlerKit.Act, :genserver_crash, 1,
          [file: ~c"lib/logger_handler_kit/act.ex", line: 528]},
         {:elixir, :eval_external_handler, 3,
          [file: ~c"src/elixir.erl", line: 363]},
         {:erl_eval, :do_apply, 7, [file: ~c"erl_eval.erl", line: 919]},
         {:elixir, :eval_forms, 4, [file: ~c"src/elixir.erl", line: 341]},
         {Module.ParallelChecker, :verify, 1,
          [file: ~c"lib/module/parallel_checker.ex", line: 141]},
         {IEx.Evaluator, :eval_and_inspect, 3,
          [file: ~c"lib/iex/evaluator.ex", line: 340]}
       ]}},
     elixir_translation: [
       [
         "GenServer ",
         "#PID<0.205.0>",
         " terminating",
         [
           [10 | "** (RuntimeError) oops"],
           ["\n    " |
            "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: anonymous fn/0 in LoggerHandlerKit.Act.genserver_crash/1"],
           ["\n    " |
            "(stdlib 6.2.2) gen_server.erl:2381: :gen_server.try_handle_call/4"],
           ["\n    " |
            "(stdlib 6.2.2) gen_server.erl:2410: :gen_server.handle_msg/6"],
           ["\n    " |
            "(stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3"]
         ],
         [],
         "\nLast message",
         [" (from ", "#PID<0.197.0>", ")"],
         ": ",
         "{:run, #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>}"
       ],
       "\nState: ",
       "nil",
       "\nClient ",
       "#PID<0.197.0>",
       " is alive\n",
       ["\n    " | "(stdlib 6.2.2) gen.erl:260: :gen.do_call/4"],
       ["\n    " |
        "(elixir 1.19.0-dev) lib/gen_server.ex:1139: GenServer.call/3"],
       ["\n    " |
        "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: LoggerHandlerKit.Act.genserver_crash/1"],
       ["\n    " |
        "(elixir 1.19.0-dev) src/elixir.erl:363: :elixir.eval_external_handler/3"],
       ["\n    " | "(stdlib 6.2.2) erl_eval.erl:919: :erl_eval.do_apply/7"],
       ["\n    " |
        "(elixir 1.19.0-dev) src/elixir.erl:341: :elixir.eval_forms/4"],
       ["\n    " |
        "(elixir 1.19.0-dev) lib/module/parallel_checker.ex:141: Module.ParallelChecker.verify/1"],
       ["\n    " |
        "(iex 1.19.0-dev) lib/iex/evaluator.ex:340: IEx.Evaluator.eval_and_inspect/3"]
     ]
   }},
  level: :error
}

More Than Translation

In addition to translating logs, the logger_translator filter is also responsible for enforcing the handle_otp_reports and handle_sasl_reports configuration options. This is worth keeping in mind and may come up during debugging. Trying to understand which part of the system dropped an OTP log event may lead to the surprising discovery that the logger translator did it.