|
| 1 | +# Log Translation |
| 2 | + |
| 3 | +Elixir and Erlang are different languages, but Elixir runs on the Erlang |
| 4 | +platform and it doesn't take much time until the platform starts to manifest |
| 5 | +itself. A lot of common errors originate in Erlang and if printed as-is, look |
| 6 | +foreign in the context of Elixir applications. To avoid this, Elixir performs |
| 7 | +something called log translation. This is one of the main functions of `Logger` |
| 8 | +applications Compare these two GenServer errors, with and without translation: |
| 9 | + |
| 10 | +<!-- tabs-open --> |
| 11 | + |
| 12 | +### With Translation |
| 13 | + |
| 14 | +```text |
| 15 | +13:43:32.538 [error] GenServer #PID<0.198.0> terminating |
| 16 | +** (RuntimeError) oops |
| 17 | + (logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: anonymous fn/0 in LoggerHandlerKit.Act.genserver_crash/1 |
| 18 | + (stdlib 6.2.2) gen_server.erl:2381: :gen_server.try_handle_call/4 |
| 19 | + (stdlib 6.2.2) gen_server.erl:2410: :gen_server.handle_msg/6 |
| 20 | + (stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3 |
| 21 | +Last message (from #PID<0.197.0>): {:run, #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>} |
| 22 | +State: nil |
| 23 | +Client #PID<0.197.0> is alive |
| 24 | +
|
| 25 | + (stdlib 6.2.2) gen.erl:260: :gen.do_call/4 |
| 26 | + (elixir 1.19.0-dev) lib/gen_server.ex:1139: GenServer.call/3 |
| 27 | + (logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: LoggerHandlerKit.Act.genserver_crash/1 |
| 28 | + (elixir 1.19.0-dev) src/elixir.erl:363: :elixir.eval_external_handler/3 |
| 29 | + (stdlib 6.2.2) erl_eval.erl:919: :erl_eval.do_apply/7 |
| 30 | + (elixir 1.19.0-dev) src/elixir.erl:341: :elixir.eval_forms/4 |
| 31 | + (elixir 1.19.0-dev) lib/module/parallel_checker.ex:141: Module.ParallelChecker.verify/1 |
| 32 | + (iex 1.19.0-dev) lib/iex/evaluator.ex:340: IEx.Evaluator.eval_and_inspect/3 |
| 33 | +``` |
| 34 | + |
| 35 | +### Without Translation |
| 36 | + |
| 37 | +```text |
| 38 | +13:44:53.011 [error] ** Generic server <0.199.0> terminating |
| 39 | +** Last message in was {run,#Fun<Elixir.LoggerHandlerKit.Act.16.413966>} |
| 40 | +** When Server state == nil |
| 41 | +** Reason for termination == |
| 42 | +** {#{message => <<"oops">>,'__struct__' => 'Elixir.RuntimeError', |
| 43 | + '__exception__' => true}, |
| 44 | + [{'Elixir.LoggerHandlerKit.Act','-genserver_crash/1-fun-0-',0, |
| 45 | + [{file,"lib/logger_handler_kit/act.ex"}, |
| 46 | + {line,528}, |
| 47 | + {error_info,#{module => 'Elixir.Exception'}}]}, |
| 48 | + {gen_server,try_handle_call,4,[{file,"gen_server.erl"},{line,2381}]}, |
| 49 | + {gen_server,handle_msg,6,[{file,"gen_server.erl"},{line,2410}]}, |
| 50 | + {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,329}]}]} |
| 51 | +** Client <0.197.0> stacktrace |
| 52 | +** [{gen,do_call,4,[{file,"gen.erl"},{line,260}]}, |
| 53 | + {'Elixir.GenServer',call,3,[{file,"lib/gen_server.ex"},{line,1139}]}, |
| 54 | + {'Elixir.LoggerHandlerKit.Act',genserver_crash,1, |
| 55 | + [{file,"lib/logger_handler_kit/act.ex"}, |
| 56 | + {line,528}]}, |
| 57 | + {elixir,eval_external_handler,3,[{file,"src/elixir.erl"},{line,363}]}, |
| 58 | + {erl_eval,do_apply,7,[{file,"erl_eval.erl"},{line,919}]}, |
| 59 | + {elixir,eval_forms,4,[{file,"src/elixir.erl"},{line,341}]}, |
| 60 | + {'Elixir.Module.ParallelChecker',verify,1, |
| 61 | + [{file,"lib/module/parallel_checker.ex"}, |
| 62 | + {line,141}]}, |
| 63 | + {'Elixir.IEx.Evaluator',eval_and_inspect,3, |
| 64 | + [{file,"lib/iex/evaluator.ex"},{line,340}]}] |
| 65 | +
|
| 66 | +
|
| 67 | +13:44:53.016 [error] crasher: |
| 68 | + initial call: 'Elixir.LoggerHandlerKit.GenServer':init/1 |
| 69 | + pid: <0.199.0> |
| 70 | + registered_name: [] |
| 71 | + exception error: #Elixir.RuntimeError |
| 72 | + in function 'Elixir.LoggerHandlerKit.Act':'-genserver_crash/1-fun-0-'/0 (lib/logger_handler_kit/act.ex, line 528) |
| 73 | + *** oops |
| 74 | + in call from gen_server:try_handle_call/4 (gen_server.erl, line 2381) |
| 75 | + in call from gen_server:handle_msg/6 (gen_server.erl, line 2410) |
| 76 | + ancestors: [<0.197.0>,<0.98.0>] |
| 77 | + message_queue_len: 0 |
| 78 | + messages: [] |
| 79 | + links: [] |
| 80 | + dictionary: [{'$callers',[<0.197.0>]}] |
| 81 | + trap_exit: false |
| 82 | + status: running |
| 83 | + heap_size: 6772 |
| 84 | + stack_size: 29 |
| 85 | + reductions: 19158 |
| 86 | + neighbours: [] |
| 87 | +``` |
| 88 | + |
| 89 | +<!-- tabs-close --> |
| 90 | + |
| 91 | +It's very clear how important translation is for people who are fluent primarily |
| 92 | +in Elixir. |
| 93 | + |
| 94 | +## Translation is a Filter |
| 95 | + |
| 96 | +Log translation is implemented as a primary filter, which means it will be |
| 97 | +called before handlers and any handler-specific filters. Remember that filters |
| 98 | +and logger configuration in general can be altered by users, although it's |
| 99 | +somewhat uncommon to remove or modify the `logger_translator` specifically. |
| 100 | + |
| 101 | +To better visualize this, here is where the translation happens: |
| 102 | + |
| 103 | +```elixir |
| 104 | +iex(1)> :logger.get_primary_config() |
| 105 | +%{ |
| 106 | + level: :debug, |
| 107 | + filter_default: :log, |
| 108 | + filters: [ |
| 109 | + # Here it is! Translation filter |
| 110 | + logger_translator: {&Logger.Utils.translator/2, |
| 111 | + %{otp: true, sasl: false, translators: [{Logger.Translator, :translate}]}}, |
| 112 | + logger_process_level: {&Logger.Utils.process_level/2, []} |
| 113 | + ], |
| 114 | + metadata: %{} |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## 1.18 vs 1.19+ |
| 119 | + |
| 120 | +Before Elixir 1.19, log translation used to _replace_ the original Erlang |
| 121 | +structured report with an unstructured log message. If a handler wanted to access |
| 122 | +some of the data in the original report, it had to parse the log message. |
| 123 | +Starting from Elixir 1.19, translation is _inserted into_ the report, thus |
| 124 | +making the original report accessible to the handlers. Here is an example: |
| 125 | + |
| 126 | +<!-- tabs-open --> |
| 127 | + |
| 128 | +### Before Translation |
| 129 | + |
| 130 | +```elixir |
| 131 | +%{ |
| 132 | + meta: %{ |
| 133 | + error_logger: %{tag: :error_report, type: :crash_report}, |
| 134 | + line: 950, |
| 135 | + pid: #PID<0.203.0>, |
| 136 | + time: 1746910751187661, |
| 137 | + file: ~c"proc_lib.erl", |
| 138 | + gl: #PID<0.70.0>, |
| 139 | + domain: [:otp, :sasl], |
| 140 | + logger_formatter: %{title: ~c"CRASH REPORT"}, |
| 141 | + mfa: {:proc_lib, :crash_report, 4}, |
| 142 | + report_cb: &:proc_lib.report_cb/2 |
| 143 | + }, |
| 144 | + msg: {:report, |
| 145 | + %{ |
| 146 | + label: {:proc_lib, :crash}, |
| 147 | + report: [ |
| 148 | + [ |
| 149 | + initial_call: {LoggerHandlerKit.GenServer, :init, [:Argument__1]}, |
| 150 | + pid: #PID<0.203.0>, |
| 151 | + registered_name: [], |
| 152 | + process_label: :undefined, |
| 153 | + error_info: {:error, %RuntimeError{message: "oops"}, |
| 154 | + [ |
| 155 | + {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0, |
| 156 | + [ |
| 157 | + file: ~c"lib/logger_handler_kit/act.ex", |
| 158 | + line: 528, |
| 159 | + error_info: %{module: Exception} |
| 160 | + ]}, |
| 161 | + {:gen_server, :try_handle_call, 4, |
| 162 | + [file: ~c"gen_server.erl", line: 2381]}, |
| 163 | + {:gen_server, :handle_msg, 6, |
| 164 | + [file: ~c"gen_server.erl", line: 2410]}, |
| 165 | + {:proc_lib, :init_p_do_apply, 3, |
| 166 | + [file: ~c"proc_lib.erl", line: 329]} |
| 167 | + ]}, |
| 168 | + ancestors: [#PID<0.197.0>, #PID<0.98.0>], |
| 169 | + message_queue_len: 0, |
| 170 | + messages: [], |
| 171 | + links: [], |
| 172 | + dictionary: ["$callers": [#PID<0.197.0>]], |
| 173 | + trap_exit: false, |
| 174 | + status: :running, |
| 175 | + heap_size: 1598, |
| 176 | + stack_size: 29, |
| 177 | + reductions: 28455 |
| 178 | + ], |
| 179 | + [] |
| 180 | + ] |
| 181 | + }}, |
| 182 | + level: :error |
| 183 | +} |
| 184 | +``` |
| 185 | + |
| 186 | +### 1.18 Translation |
| 187 | + |
| 188 | +```elixir |
| 189 | +%{ |
| 190 | + meta: %{ |
| 191 | + error_logger: %{tag: :error, report_cb: &:gen_server.format_log/1}, |
| 192 | + line: 2646, |
| 193 | + pid: #PID<0.1105.0>, |
| 194 | + time: 1746911049539570, |
| 195 | + file: ~c"gen_server.erl", |
| 196 | + gl: #PID<0.70.0>, |
| 197 | + domain: [:otp], |
| 198 | + report_cb: &:gen_server.format_log/2, |
| 199 | + mfa: {:gen_server, :error_info, 8}, |
| 200 | + crash_reason: {%RuntimeError{message: "oops"}, |
| 201 | + [ |
| 202 | + {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0, |
| 203 | + [ |
| 204 | + file: ~c"lib/logger_handler_kit/act.ex", |
| 205 | + line: 528, |
| 206 | + error_info: %{module: Exception} |
| 207 | + ]}, |
| 208 | + {:gen_server, :try_handle_call, 4, |
| 209 | + [file: ~c"gen_server.erl", line: 2381]}, |
| 210 | + {:gen_server, :handle_msg, 6, [file: ~c"gen_server.erl", line: 2410]}, |
| 211 | + {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 329]} |
| 212 | + ]} |
| 213 | + }, |
| 214 | + msg: {:string, |
| 215 | + [ |
| 216 | + [ |
| 217 | + "GenServer ", |
| 218 | + "#PID<0.1105.0>", |
| 219 | + " terminating", |
| 220 | + [ |
| 221 | + [10 | "** (RuntimeError) oops"], |
| 222 | + ["\n " | |
| 223 | + "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: anonymous fn/0 in LoggerHandlerKit.Act.genserver_crash/1"], |
| 224 | + ["\n " | |
| 225 | + "(stdlib 6.2.2) gen_server.erl:2381: :gen_server.try_handle_call/4"], |
| 226 | + ["\n " | |
| 227 | + "(stdlib 6.2.2) gen_server.erl:2410: :gen_server.handle_msg/6"], |
| 228 | + ["\n " | |
| 229 | + "(stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3"] |
| 230 | + ], |
| 231 | + [], |
| 232 | + "\nLast message", |
| 233 | + [" (from ", "#PID<0.1099.0>", ")"], |
| 234 | + ": ", |
| 235 | + "{:run, #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>}" |
| 236 | + ], |
| 237 | + "\nState: ", |
| 238 | + "nil", |
| 239 | + "\nClient ", |
| 240 | + "#PID<0.1099.0>", |
| 241 | + " is alive\n", |
| 242 | + ["\n " | "(stdlib 6.2.2) gen.erl:260: :gen.do_call/4"], |
| 243 | + ["\n " | "(elixir 1.18.3) lib/gen_server.ex:1125: GenServer.call/3"], |
| 244 | + ["\n " | |
| 245 | + "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: LoggerHandlerKit.Act.genserver_crash/1"], |
| 246 | + ["\n " | |
| 247 | + "(elixir 1.18.3) src/elixir.erl:386: :elixir.eval_external_handler/3"], |
| 248 | + ["\n " | "(stdlib 6.2.2) erl_eval.erl:919: :erl_eval.do_apply/7"], |
| 249 | + ["\n " | "(elixir 1.18.3) src/elixir.erl:364: :elixir.eval_forms/4"], |
| 250 | + ["\n " | |
| 251 | + "(elixir 1.18.3) lib/module/parallel_checker.ex:120: Module.ParallelChecker.verify/1"], |
| 252 | + ["\n " | |
| 253 | + "(iex 1.18.3) lib/iex/evaluator.ex:336: IEx.Evaluator.eval_and_inspect/3"] |
| 254 | + ]}, |
| 255 | + level: :error |
| 256 | +} |
| 257 | +``` |
| 258 | + |
| 259 | +### 1.19 Translation |
| 260 | + |
| 261 | +```elixir |
| 262 | +%{ |
| 263 | + meta: %{ |
| 264 | + error_logger: %{tag: :error, report_cb: &:gen_server.format_log/1}, |
| 265 | + line: 2646, |
| 266 | + pid: #PID<0.205.0>, |
| 267 | + time: 1746910905177484, |
| 268 | + file: ~c"gen_server.erl", |
| 269 | + gl: #PID<0.70.0>, |
| 270 | + domain: [:otp], |
| 271 | + mfa: {:gen_server, :error_info, 8}, |
| 272 | + report_cb: &Logger.Utils.translated_cb/1, |
| 273 | + crash_reason: {%RuntimeError{message: "oops"}, |
| 274 | + [ |
| 275 | + {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0, |
| 276 | + [ |
| 277 | + file: ~c"lib/logger_handler_kit/act.ex", |
| 278 | + line: 528, |
| 279 | + error_info: %{module: Exception} |
| 280 | + ]}, |
| 281 | + {:gen_server, :try_handle_call, 4, |
| 282 | + [file: ~c"gen_server.erl", line: 2381]}, |
| 283 | + {:gen_server, :handle_msg, 6, [file: ~c"gen_server.erl", line: 2410]}, |
| 284 | + {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 329]} |
| 285 | + ]} |
| 286 | + }, |
| 287 | + msg: {:report, |
| 288 | + %{ |
| 289 | + label: {:gen_server, :terminate}, |
| 290 | + name: #PID<0.205.0>, |
| 291 | + reason: {%RuntimeError{message: "oops"}, |
| 292 | + [ |
| 293 | + {LoggerHandlerKit.Act, :"-genserver_crash/1-fun-0-", 0, |
| 294 | + [ |
| 295 | + file: ~c"lib/logger_handler_kit/act.ex", |
| 296 | + line: 528, |
| 297 | + error_info: %{module: Exception} |
| 298 | + ]}, |
| 299 | + {:gen_server, :try_handle_call, 4, |
| 300 | + [file: ~c"gen_server.erl", line: 2381]}, |
| 301 | + {:gen_server, :handle_msg, 6, [file: ~c"gen_server.erl", line: 2410]}, |
| 302 | + {:proc_lib, :init_p_do_apply, 3, [file: ~c"proc_lib.erl", line: 329]} |
| 303 | + ]}, |
| 304 | + log: [], |
| 305 | + state: nil, |
| 306 | + process_label: :undefined, |
| 307 | + last_message: {:run, |
| 308 | + #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>}, |
| 309 | + client_info: {#PID<0.197.0>, |
| 310 | + {#PID<0.197.0>, |
| 311 | + [ |
| 312 | + {:gen, :do_call, 4, [file: ~c"gen.erl", line: 260]}, |
| 313 | + {GenServer, :call, 3, [file: ~c"lib/gen_server.ex", line: 1139]}, |
| 314 | + {LoggerHandlerKit.Act, :genserver_crash, 1, |
| 315 | + [file: ~c"lib/logger_handler_kit/act.ex", line: 528]}, |
| 316 | + {:elixir, :eval_external_handler, 3, |
| 317 | + [file: ~c"src/elixir.erl", line: 363]}, |
| 318 | + {:erl_eval, :do_apply, 7, [file: ~c"erl_eval.erl", line: 919]}, |
| 319 | + {:elixir, :eval_forms, 4, [file: ~c"src/elixir.erl", line: 341]}, |
| 320 | + {Module.ParallelChecker, :verify, 1, |
| 321 | + [file: ~c"lib/module/parallel_checker.ex", line: 141]}, |
| 322 | + {IEx.Evaluator, :eval_and_inspect, 3, |
| 323 | + [file: ~c"lib/iex/evaluator.ex", line: 340]} |
| 324 | + ]}}, |
| 325 | + elixir_translation: [ |
| 326 | + [ |
| 327 | + "GenServer ", |
| 328 | + "#PID<0.205.0>", |
| 329 | + " terminating", |
| 330 | + [ |
| 331 | + [10 | "** (RuntimeError) oops"], |
| 332 | + ["\n " | |
| 333 | + "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: anonymous fn/0 in LoggerHandlerKit.Act.genserver_crash/1"], |
| 334 | + ["\n " | |
| 335 | + "(stdlib 6.2.2) gen_server.erl:2381: :gen_server.try_handle_call/4"], |
| 336 | + ["\n " | |
| 337 | + "(stdlib 6.2.2) gen_server.erl:2410: :gen_server.handle_msg/6"], |
| 338 | + ["\n " | |
| 339 | + "(stdlib 6.2.2) proc_lib.erl:329: :proc_lib.init_p_do_apply/3"] |
| 340 | + ], |
| 341 | + [], |
| 342 | + "\nLast message", |
| 343 | + [" (from ", "#PID<0.197.0>", ")"], |
| 344 | + ": ", |
| 345 | + "{:run, #Function<16.413966/0 in LoggerHandlerKit.Act.genserver_crash/1>}" |
| 346 | + ], |
| 347 | + "\nState: ", |
| 348 | + "nil", |
| 349 | + "\nClient ", |
| 350 | + "#PID<0.197.0>", |
| 351 | + " is alive\n", |
| 352 | + ["\n " | "(stdlib 6.2.2) gen.erl:260: :gen.do_call/4"], |
| 353 | + ["\n " | |
| 354 | + "(elixir 1.19.0-dev) lib/gen_server.ex:1139: GenServer.call/3"], |
| 355 | + ["\n " | |
| 356 | + "(logger_handler_kit 0.1.0) lib/logger_handler_kit/act.ex:528: LoggerHandlerKit.Act.genserver_crash/1"], |
| 357 | + ["\n " | |
| 358 | + "(elixir 1.19.0-dev) src/elixir.erl:363: :elixir.eval_external_handler/3"], |
| 359 | + ["\n " | "(stdlib 6.2.2) erl_eval.erl:919: :erl_eval.do_apply/7"], |
| 360 | + ["\n " | |
| 361 | + "(elixir 1.19.0-dev) src/elixir.erl:341: :elixir.eval_forms/4"], |
| 362 | + ["\n " | |
| 363 | + "(elixir 1.19.0-dev) lib/module/parallel_checker.ex:141: Module.ParallelChecker.verify/1"], |
| 364 | + ["\n " | |
| 365 | + "(iex 1.19.0-dev) lib/iex/evaluator.ex:340: IEx.Evaluator.eval_and_inspect/3"] |
| 366 | + ] |
| 367 | + }}, |
| 368 | + level: :error |
| 369 | +} |
| 370 | +``` |
| 371 | + |
| 372 | +<!-- tabs-close --> |
| 373 | + |
| 374 | +## More Than Translation |
| 375 | + |
| 376 | +In addition to translating logs, the `logger_translator` filter is also responsible |
| 377 | +for enforcing the `handle_otp_reports` and `handle_sasl_reports` [configuration |
| 378 | +options](https://hexdocs.pm/logger/Logger.html#module-boot-configuration). This |
| 379 | +is worth keeping in mind and may come up during debugging. Trying to understand |
| 380 | +which part of the system dropped an OTP log event may lead to the surprising |
| 381 | +discovery that the logger translator did it. |
0 commit comments