Skip to content

Commit 6b2d514

Browse files
bvobartBart van Oort
andauthored
fix(elastic): better support for logging raised and caught exceptions and fix formatter crash (#120)
* fix(elastic): better support for logging raised and caught exceptions and fix formatter crash Logging caught exceptions can be done with crash_reason arg on logger methods, e.g. Logger.error("something went wrong", crash_reason: {e, __STACKTRACE__}) Formatter crash would sometimes appear on exception logs because `message` would be an array instead of a regular string * docs(logger_json): add crash_reason to well-known metadata keys --------- Co-authored-by: Bart van Oort <bart.van.oort@opinity.nl>
1 parent 2432081 commit 6b2d514

File tree

4 files changed

+85
-16
lines changed

4 files changed

+85
-16
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,8 @@ Any custom metadata fields will be added to the root of the message, so that you
287287
> Note that this also allows you to produce messages that do not strictly adhere to the ECS specification.
288288
289289
```json
290-
// with Logger.metadata(:"device.model.name": "My Awesome Device")
290+
// Logger.info("Hello") with Logger.metadata(:"device.model.name": "My Awesome Device")
291+
// or Logger.info("Hello", "device.model.name": "My Awesome Device")
291292
{
292293
"@timestamp": "2024-05-21T15:17:35.374Z",
293294
"ecs.version": "8.11.0",

lib/logger_json.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ defmodule LoggerJSON do
7070
7171
* `:conn` - the `Plug.Conn` struct. This is useful when logging HTTP requests and responses,
7272
each formatter may use it differently.
73+
* `:crash_reason` - accepts a tuple where the first element is the exception struct
74+
and the second is the stacktrace. For example: `Logger.error("Exception!", crash_reason: {e, __STACKTRACE__})`.
75+
Each formatter may encode it differently.
7376
"""
7477
@log_levels [:error, :info, :debug, :emergency, :alert, :critical, :warning, :notice]
7578
@log_level_strings Enum.map(@log_levels, &to_string/1)

lib/logger_json/formatters/elastic.ex

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ defmodule LoggerJSON.Formatters.Elastic do
2525
"log.level" => "info",
2626
"log.logger" => "Elixir.LoggerJSON.Formatters.ElasticTest",
2727
"log.origin" => %{
28-
"file.name" => ~c"/app/logger_json/test/formatters/elastic_test.exs",
28+
"file.name" => "/app/logger_json/test/formatters/elastic_test.exs",
2929
"file.line" => 18,
3030
"function" => "test logs an LogEntry of every level/1"
3131
},
@@ -46,8 +46,9 @@ defmodule LoggerJSON.Formatters.Elastic do
4646
"log.logger" => "Elixir.LoggerJSON.Formatters.ElasticTest",
4747
"log.origin" => %{
4848
"file.line" => 68,
49-
"file.name" => ~c"/app/logger_json/test/formatters/elastic_test.exs",
50-
"function" => "test logs an LogEntry with a map payload containing message/1"},
49+
"file.name" => "/app/logger_json/test/formatters/elastic_test.exs",
50+
"function" => "test logs an LogEntry with a map payload containing message/1"
51+
},
5152
"message" => "Hello"
5253
}
5354
```
@@ -59,10 +60,10 @@ defmodule LoggerJSON.Formatters.Elastic do
5960
"@timestamp" => "2024-05-17T16:20:00.000Z",
6061
"ecs.version" => "8.11.0",
6162
"error.message" => "runtime error",
62-
"error.stack_trace" => "** (RuntimeError) runtime error\\n Elixir.LoggerJSON.Formatters.ElasticTest.erl:159: anonymous fn/4 in LoggerJSON.Formatters.ElasticTest.\\"test logs exceptions\\"/1\\n",
63+
"error.stack_trace" => "** (RuntimeError) runtime error\n test/logger_json/formatters/elastic_test.exs:191: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\"test logs exceptions\"/1\n",
6364
"error.type" => "Elixir.RuntimeError",
6465
"log.level" => "error",
65-
"message" => "runtime error"
66+
"message" => "Process #PID<0.322.0> raised an exception\n** (RuntimeError) runtime error\n test/logger_json/formatters/elastic_test.exs:191: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\"test logs exceptions\"/1"
6667
}
6768
```
6869
@@ -89,12 +90,44 @@ defmodule LoggerJSON.Formatters.Elastic do
8990
"error.code" => 42,
9091
"error.id" => "oops_id",
9192
"error.message" => "oops!",
92-
"error.stack_trace" => "** (LoggerJSON.Formatters.ElasticTest.TestException) oops!\\n test/formatters/elastic_test.exs:190: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\\"test logs exceptions with id and code\\"/1\\n",
93+
"error.stack_trace" => "** (LoggerJSON.Formatters.ElasticTest.TestException) oops!\n test/logger_json/formatters/elastic_test.exs:223: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\"test logs exceptions with id and code\"/1\n",
9394
"error.type" => "Elixir.LoggerJSON.Formatters.ElasticTest.TestException",
9495
"log.level" => "error",
95-
"message" => "oops!"
96+
"message" => "Process #PID<0.325.0> raised an exception\n** (LoggerJSON.Formatters.ElasticTest.TestException) oops!\n test/logger_json/formatters/elastic_test.exs:223: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\"test logs exceptions with id and code\"/1"
9697
}
9798
```
99+
100+
You can also choose to log caught exceptions with a custom message.
101+
For example, after catching an exception with `try`/`rescue`:
102+
103+
```elixir
104+
try do
105+
raise "oops"
106+
rescue
107+
e in RuntimeError -> Logger.error("Something went wrong", crash_reason: {e, __STACKTRACE__})
108+
end
109+
```
110+
111+
then you'll get a message like:
112+
113+
```elixir
114+
%{
115+
"@timestamp" => "2024-05-17T16:20:00.000Z",
116+
"ecs.version" => "8.11.0",
117+
"error.message" => "oops",
118+
"error.stack_trace" => "** (RuntimeError) oops\n test/logger_json/formatters/elastic_test.exs:421: anonymous fn/0 in LoggerJSON.Formatters.ElasticTest.\"test logs caught errors\"/1\n (logger_json 6.0.2) test/support/logger_case.ex:16: anonymous fn/1 in LoggerJSON.Case.capture_log/2\n (ex_unit 1.16.3) lib/ex_unit/capture_io.ex:258: ExUnit.CaptureIO.do_with_io/3\n (ex_unit 1.16.3) lib/ex_unit/capture_io.ex:134: ExUnit.CaptureIO.capture_io/2\n (logger_json 6.0.2) test/support/logger_case.ex:15: LoggerJSON.Case.capture_log/2\n test/logger_json/formatters/elastic_test.exs:419: LoggerJSON.Formatters.ElasticTest.\"test logs caught errors\"/1\n (ex_unit 1.16.3) lib/ex_unit/runner.ex:472: ExUnit.Runner.exec_test/2\n (stdlib 5.2.3) timer.erl:270: :timer.tc/2\n (ex_unit 1.16.3) lib/ex_unit/runner.ex:394: anonymous fn/6 in ExUnit.Runner.spawn_test_monitor/4\n",
119+
"error.type" => "Elixir.RuntimeError",
120+
"log.level" => "error",
121+
"log.logger" => "Elixir.LoggerJSON.Formatters.ElasticTest",
122+
"log.origin" => %{
123+
"file.line" => 423,
124+
"file.name" => "/app/logger_json/test/logger_json/formatters/elastic_test.exs",
125+
"function" => "test logs caught errors/1"
126+
},
127+
"message" => "Something went wrong"
128+
}
129+
```
130+
98131
"""
99132
import LoggerJSON.Formatter.{MapBuilder, DateTime, Message, Metadata, RedactorEncoder}
100133
require Jason.Helpers
@@ -171,17 +204,15 @@ defmodule LoggerJSON.Formatters.Elastic do
171204
format_error_fields(message, error_message, stacktrace, "throw")
172205
end
173206

174-
def format_crash_reason(_message, {%type{} = exception, stacktrace}, _meta) do
175-
message = Exception.message(exception)
176-
207+
def format_crash_reason(message, {%type{} = exception, stacktrace}, _meta) do
177208
formatted_stacktrace =
178209
[
179210
Exception.format_banner(:error, exception, stacktrace),
180211
Exception.format_stacktrace(stacktrace)
181212
]
182213
|> Enum.join("\n")
183214

184-
format_error_fields(message, message, formatted_stacktrace, type)
215+
format_error_fields(message, Exception.message(exception), formatted_stacktrace, type)
185216
|> maybe_put(:"error.id", get_exception_id(exception))
186217
|> maybe_put(:"error.code", get_exception_code(exception))
187218
end
@@ -201,9 +232,9 @@ defmodule LoggerJSON.Formatters.Elastic do
201232
# Formats the error fields as specified in https://www.elastic.co/guide/en/ecs/8.11/ecs-error.html
202233
defp format_error_fields(message, error_message, stacktrace, type) do
203234
%{
204-
message: message,
205-
"error.message": error_message,
235+
message: safe_chardata_to_string(message),
206236
"error.stack_trace": stacktrace,
237+
"error.message": error_message,
207238
"error.type": type
208239
}
209240
end

test/logger_json/formatters/elastic_test.exs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,13 @@ defmodule LoggerJSON.Formatters.ElasticTest do
198198
|> decode_or_print_error()
199199

200200
assert %{
201-
"message" => "runtime error",
201+
"message" => message,
202202
"error.message" => "runtime error",
203203
"error.stack_trace" => stacktrace,
204204
"error.type" => "Elixir.RuntimeError"
205205
} = log_entry
206206

207+
assert message =~ ~r/Process #PID<\d.\d+.\d> raised an exception/
207208
assert stacktrace =~ "** (RuntimeError) runtime error"
208209
assert stacktrace =~ ~r/test\/logger_json\/formatters\/elastic_test.exs:\d+: anonymous fn\/0/
209210
assert stacktrace =~ "in LoggerJSON.Formatters.ElasticTest.\"test logs exceptions\"/1"
@@ -229,13 +230,15 @@ defmodule LoggerJSON.Formatters.ElasticTest do
229230
|> decode_or_print_error()
230231

231232
assert %{
232-
"message" => "oops!",
233+
"message" => message,
233234
"error.message" => "oops!",
234235
"error.stack_trace" => _,
235236
"error.type" => "Elixir.LoggerJSON.Formatters.ElasticTest.TestException",
236237
"error.id" => "oops_id",
237238
"error.code" => 42
238239
} = log_entry
240+
241+
assert message =~ ~r/Process #PID<\d.\d+.\d> raised an exception/
239242
end
240243

241244
test "logged exception stacktrace is in default Elixir format" do
@@ -410,4 +413,35 @@ defmodule LoggerJSON.Formatters.ElasticTest do
410413
"user_agent.original" => "Mozilla/5.0"
411414
} = log_entry
412415
end
416+
417+
test "logs caught errors" do
418+
log_entry =
419+
capture_log(fn ->
420+
try do
421+
raise "oops"
422+
rescue
423+
e in RuntimeError -> Logger.error("Something went wrong", crash_reason: {e, __STACKTRACE__})
424+
end
425+
end)
426+
|> decode_or_print_error()
427+
428+
assert %{
429+
"message" => "Something went wrong",
430+
"error.message" => "oops",
431+
"error.type" => "Elixir.RuntimeError",
432+
"error.stack_trace" => stacktrace,
433+
"log.level" => "error",
434+
"log.logger" => "Elixir.LoggerJSON.Formatters.ElasticTest",
435+
"log.origin" => %{
436+
"file.name" => origin_file,
437+
"file.line" => origin_line,
438+
"function" => origin_function
439+
}
440+
} = log_entry
441+
442+
assert origin_line > 0
443+
assert String.ends_with?(origin_file, "test/logger_json/formatters/elastic_test.exs")
444+
assert String.starts_with?(origin_function, "test logs caught errors/1")
445+
assert String.starts_with?(stacktrace, "** (RuntimeError) oops")
446+
end
413447
end

0 commit comments

Comments
 (0)