Skip to content

Commit dce082c

Browse files
authored
Merge pull request #381 from beligante/add-crash-report-to-error-log
[ 377 ] Add `crash_report` logger metadata for exceptions captured by `Cowboy.Handler`
2 parents 56e3126 + 2d36c38 commit dce082c

File tree

4 files changed

+84
-29
lines changed

4 files changed

+84
-29
lines changed

lib/grpc/logger.ex

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule GRPC.Logger do
2+
@doc """
3+
Normalizes the exception and stacktrace inputs by its kind to match the format specified for `crash_report` metadata
4+
in [Logger](https://hexdocs.pm/logger/main/Logger.html#module-metadata)
5+
"""
6+
def crash_reason(:throw, reason, stacktrace), do: {{:nocatch, reason}, stacktrace}
7+
def crash_reason(:error, reason, stack), do: {Exception.normalize(:error, reason, stack), stack}
8+
def crash_reason(:exit, reason, stacktrace), do: {reason, stacktrace}
9+
end

lib/grpc/server/adapters/cowboy/handler.ex

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
33
A cowboy handler accepting all requests and calls corresponding functions defined by users.
44
"""
55

6+
alias GRPC.Server.Adapters.ReportException
67
alias GRPC.Transport.HTTP2
78
alias GRPC.RPCError
89
require Logger
@@ -72,7 +73,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
7273
http_transcode: transcode?(req)
7374
}
7475

75-
server_rpc_pid = spawn_link(__MODULE__, :call_rpc, [server, route, stream])
76+
server_rpc_pid = :proc_lib.spawn_link(__MODULE__, :call_rpc, [server, route, stream])
7677
Process.flag(:trap_exit, true)
7778

7879
req = :cowboy_req.set_resp_headers(HTTP2.server_headers(stream), req)
@@ -455,27 +456,37 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
455456
end
456457

457458
# expected error raised from user to return error immediately
458-
def info({:EXIT, pid, {%RPCError{} = error, _stacktrace}}, req, state = %{pid: pid}) do
459+
def info({:EXIT, pid, {%RPCError{} = error, stacktrace}}, req, state = %{pid: pid}) do
459460
req = send_error(req, error, state, :rpc_error)
461+
462+
[req: req]
463+
|> ReportException.new(error, stacktrace)
464+
|> log_error(stacktrace)
465+
460466
{:stop, req, state}
461467
end
462468

463469
# unknown error raised from rpc
464-
def info({:EXIT, pid, {:handle_error, _kind}} = err, req, state = %{pid: pid}) do
465-
Logger.warning("3. #{inspect(state)} #{inspect(err)}")
470+
def info({:EXIT, pid, {:handle_error, error}}, req, state = %{pid: pid}) do
471+
%{kind: kind, reason: reason, stack: stack} = error
472+
rpc_error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"}
473+
req = send_error(req, rpc_error, state, :error)
466474

467-
error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"}
468-
req = send_error(req, error, state, :error)
475+
[req: req]
476+
|> ReportException.new(reason, stack, kind)
477+
|> log_error(stack)
469478

470479
{:stop, req, state}
471480
end
472481

473482
def info({:EXIT, pid, {reason, stacktrace}}, req, state = %{pid: pid}) do
474-
Logger.error(Exception.format(:error, reason, stacktrace))
475-
476483
error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"}
477484
req = send_error(req, error, state, reason)
478485

486+
[req: req]
487+
|> ReportException.new(reason, stacktrace)
488+
|> log_error(stacktrace)
489+
479490
{:stop, req, state}
480491
end
481492

@@ -500,17 +511,17 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
500511
end
501512
catch
502513
kind, e ->
503-
Logger.error(Exception.format(kind, e, __STACKTRACE__))
514+
reason = Exception.normalize(kind, e, __STACKTRACE__)
504515

505-
exit({:handle_error, kind})
516+
{:error, %{kind: kind, reason: reason, stack: __STACKTRACE__}}
506517
end
507518

508519
case result do
509520
{:error, %GRPC.RPCError{} = e} ->
510-
exit({e, ""})
521+
exit({e, _stacktrace = []})
511522

512-
{:error, %{kind: kind}} ->
513-
exit({:handle_error, kind})
523+
{:error, %{kind: _kind, reason: _reason, stack: _stack} = e} ->
524+
exit({:handle_error, e})
514525

515526
other ->
516527
other
@@ -648,4 +659,12 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
648659

649660
{:wait, ref}
650661
end
662+
663+
defp log_error(%ReportException{kind: kind} = exception, stacktrace) do
664+
crash_reason = GRPC.Logger.crash_reason(kind, exception, stacktrace)
665+
666+
kind
667+
|> Exception.format(exception, stacktrace)
668+
|> Logger.error(crash_reason: crash_reason)
669+
end
651670
end
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule GRPC.Server.Adapters.ReportException do
2+
@moduledoc """
3+
Exception raised by server adapter, meant to be used as part of metadata `crash_report`
4+
"""
5+
6+
defexception [:kind, :reason, :stack, :adapter_extra]
7+
8+
def new(adapter_extra, %{__exception__: _} = exception, stack \\ [], kind \\ :error) do
9+
exception(kind: kind, reason: exception, stack: stack, adapter_extra: adapter_extra)
10+
end
11+
12+
def message(%__MODULE__{adapter_extra: [{:req, req}], kind: kind, reason: reason, stack: stack}) do
13+
# Cowboy adapter message builder
14+
path = :cowboy_req.path(req)
15+
"Exception raised while handling #{path}:\n" <> Exception.format_banner(kind, reason, stack)
16+
end
17+
end

test/grpc/integration/server_test.exs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -224,27 +224,37 @@ defmodule GRPC.Integration.ServerTest do
224224
end
225225

226226
test "returns appropriate error for unary requests" do
227-
run_server([HelloErrorServer], fn port ->
228-
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
229-
req = %Helloworld.HelloRequest{name: "Elixir"}
230-
{:error, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req)
227+
logs =
228+
ExUnit.CaptureLog.capture_log(fn ->
229+
run_server([HelloErrorServer], fn port ->
230+
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
231+
req = %Helloworld.HelloRequest{name: "Elixir"}
232+
{:error, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req)
231233

232-
assert %GRPC.RPCError{
233-
status: GRPC.Status.unauthenticated(),
234-
message: "Please authenticate"
235-
} == reply
236-
end)
234+
assert %GRPC.RPCError{
235+
status: GRPC.Status.unauthenticated(),
236+
message: "Please authenticate"
237+
} == reply
238+
end)
239+
end)
240+
241+
assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
237242
end
238243

239244
test "return errors for unknown errors" do
240-
run_server([HelloErrorServer], fn port ->
241-
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
242-
req = %Helloworld.HelloRequest{name: "unknown error"}
245+
logs =
246+
ExUnit.CaptureLog.capture_log(fn ->
247+
run_server([HelloErrorServer], fn port ->
248+
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
249+
req = %Helloworld.HelloRequest{name: "unknown error"}
243250

244-
assert {:error,
245-
%GRPC.RPCError{message: "Internal Server Error", status: GRPC.Status.unknown()}} ==
246-
channel |> Helloworld.Greeter.Stub.say_hello(req)
247-
end)
251+
assert {:error,
252+
%GRPC.RPCError{message: "Internal Server Error", status: GRPC.Status.unknown()}} ==
253+
channel |> Helloworld.Greeter.Stub.say_hello(req)
254+
end)
255+
end)
256+
257+
assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
248258
end
249259

250260
test "returns appropriate error for stream requests" do

0 commit comments

Comments
 (0)