Skip to content

Commit

Permalink
Add GoogleErrorReporter to format errors compatible with Google Error…
Browse files Browse the repository at this point in the history
… Reporting (sneako#58)

* Add GoogleErrorReporter to format errors compatible with Google Error Reporting

* :undef error contains an extra line of context

* mix format

* Add optional metadata

* Automatically format errors for google when google_error_reporter config is present

* Revert "Automatically format errors for google when google_error_reporter config is present"

This reverts commit 472e16ca0f755e7fa3cc75b8617e0b74289e285b.

* Add google_error_reporter config

* Handle case where stack trace includes arg data

* Reformat Elixir's stacktrace format

Use regular expressions to reformat the lines, instead of building the 
stacktraces manually. This ensures that the stacktrace includes all of 
the original information. The previous approach could throw away lines 
it didn't understand.

* Format the message name

Google skips over this with Elixir's default format

* Specify the reason

This allows for pass-through from try/catch

* Revert "Format the message name"

This reverts commit 99ffdce64d81528ab15815cc7158f6fae92fe19a.

* Add a Context section after the stacktrace

Google Error Reporter names the error message after last non-file line 
in the message appearing before the first file line. This leads to 
errors being named "Foo.Bar(123, 456)" rather than 
"(UndefinedFunctionError) function Foo.bar/2 is undefined"

Putting the context at the end keeps the original error message name, 
and still includes the contextual information.
  • Loading branch information
patmaddox authored Jan 20, 2021
1 parent d6b7edb commit 8e4290a
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 0 deletions.
61 changes: 61 additions & 0 deletions lib/logger_json/formatters/google_error_reporter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule LoggerJSON.Formatters.GoogleErrorReporter do
require Logger
@googleErrorType "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"

def report(kind, reason, stacktrace, metadata \\ []) do
[format_banner(kind, reason, stacktrace) | format_stacktrace(stacktrace)]
|> Enum.join("\n")
|> Logger.error(Keyword.merge(build_metadata(), metadata))
end

defp format_banner(kind, reason, stacktrace) do
Exception.format_banner(kind, reason, stacktrace)
end

defp format_stacktrace(stacktrace) do
lines =
Exception.format_stacktrace(stacktrace)
|> String.trim_trailing()
|> String.split("\n")
|> Enum.map(&format_line/1)
|> Enum.group_by(fn {kind, _line} -> kind end)

format_lines(:trace, lines[:trace]) ++ format_lines(:context, lines[:context]) ++ [""]
end

defp format_line(line) do
case Regex.run(~r/(.+)\:(\d+)\: (.*)/, line) do
[_, file, line, function] -> {:trace, "#{file}:#{line}:in `#{function}'"}
_ -> {:context, line}
end
end

defp format_lines(_kind, nil) do
[]
end

defp format_lines(:trace, lines) do
Enum.map(lines, fn {:trace, line} -> line end)
end

defp format_lines(:context, lines) do
["Context:" | Enum.map(lines, fn {:context, line} -> line end)]
end

defp build_metadata() do
["@type": @googleErrorType]
|> with_service_context()
end

defp with_service_context(metadata) do
if service_context = config()[:service_context] do
Keyword.merge(metadata, serviceContext: service_context)
else
metadata
end
end

defp config do
Application.get_env(:logger_json, :google_error_reporter, [])
end
end
92 changes: 92 additions & 0 deletions test/unit/logger_json_google_error_reporter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
defmodule LoggerJSONGoogleErrorReporterTest do
use Logger.Case, async: false
alias LoggerJSON.Formatters.GoogleCloudLogger
alias LoggerJSON.Formatters.GoogleErrorReporter

setup do
:ok =
Logger.configure_backend(
LoggerJSON,
device: :user,
level: nil,
metadata: :all,
json_encoder: Jason,
on_init: :disabled,
formatter: GoogleCloudLogger
)

:ok = Logger.reset_metadata([])
end

test "metadata" do
log =
capture_log(fn -> GoogleErrorReporter.report(:error, %RuntimeError{message: "oops"}, []) end)
|> Jason.decode!()

assert log["severity"] == "ERROR"
assert log["@type"] == "type.googleapis.com/google.devtools.clouderrorreporting.v1beta1.ReportedErrorEvent"
end

test "google_error_reporter metadata" do
:ok = Application.put_env(:logger_json, :google_error_reporter, service_context: [service: "myapp", version: "abc123"])
log =
capture_log(fn -> GoogleErrorReporter.report(:error, %RuntimeError{message: "oops"}, []) end)
|> Jason.decode!()

assert log["serviceContext"]["service"] == "myapp"
assert log["serviceContext"]["version"] == "abc123"
after
Application.delete_env(:logger_json, :google_error_reporter)
end

test "optional metadata" do
log =
capture_log(fn -> GoogleErrorReporter.report(:error, %RuntimeError{message: "oops"}, [], foo: "bar") end)
|> Jason.decode!()

assert log["foo"] == "bar"
end

test "logs elixir error" do
error = %RuntimeError{message: "oops"}

stacktrace = [
{Foo, :bar, 0, [file: 'foo/bar.ex', line: 123]},
{Foo.Bar, :baz, 1, [file: 'foo/bar/baz.ex', line: 456]}
]

log =
capture_log(fn -> GoogleErrorReporter.report(:error, error, stacktrace) end)
|> Jason.decode!()

assert log["message"] ==
"""
** (RuntimeError) oops
foo/bar.ex:123:in `Foo.bar/0'
foo/bar/baz.ex:456:in `Foo.Bar.baz/1'
"""
end

test "logs erlang error" do
error = :undef

stacktrace = [
{Foo, :bar, [123, 456], []},
{Foo, :bar, 2, [file: 'foo/bar.ex', line: 123]},
{Foo.Bar, :baz, 1, [file: 'foo/bar/baz.ex', line: 456]}
]

log =
capture_log(fn -> GoogleErrorReporter.report(:error, error, stacktrace) end)
|> Jason.decode!()

assert log["message"] ==
"""
** (UndefinedFunctionError) function Foo.bar/2 is undefined (module Foo is not available)
foo/bar.ex:123:in `Foo.bar/2'
foo/bar/baz.ex:456:in `Foo.Bar.baz/1'
Context:
Foo.bar(123, 456)
"""
end
end

0 comments on commit 8e4290a

Please sign in to comment.