Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] add crash report to error log #5

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# gRPC Elixir

[![GitHub CI](https://github.com/elixir-grpc/grpc/actions/workflows/ci.yml/badge.svg)](https://github.com/elixir-grpc/grpc/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/grpc.svg)](https://hex.pm/packages/grpc)
[![Travis Status](https://app.travis-ci.com/elixir-grpc/grpc.svg?branch=master)](https://app.travis-ci.com/elixir-grpc/grpc)
[![GitHub actions Status](https://github.com/elixir-grpc/grpc/workflows/CI/badge.svg)](https://github.com/elixir-grpc/grpc/actions)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/grpc/)
[![License](https://img.shields.io/hexpm/l/grpc.svg)](https://github.com/elixir-grpc/grpc/blob/master/LICENSE.md)
[![Last Updated](https://img.shields.io/github/last-commit/elixir-grpc/grpc.svg)](https://github.com/elixir-grpc/grpc/commits/master)
[![Total Download](https://img.shields.io/hexpm/dt/grpc.svg)](https://hex.pm/packages/elixir-grpc/grpc)
[![Last Updated](https://img.shields.io/github/last-commit/elixir-grpc/grpc.svg)](https://github.com/elixir-grpc/grpc/commits/master)

An Elixir implementation of [gRPC](http://www.grpc.io/).

Expand Down Expand Up @@ -100,7 +100,7 @@ We will use this module [in the gRPC server startup section](#start-application)

### **HTTP Transcoding**

1. Adding [grpc-gateway annotations](https://cloud.google.com/endpoints/docs/grpc/transcoding) to your protobuf file definition:
1. Adding [grpc-gateway annotations](https://cloud.google.com/endpoints/docs/grpc/transcoding) to your protobuf file definition:

```protobuf
import "google/api/annotations.proto";
Expand Down Expand Up @@ -154,7 +154,7 @@ mix protobuf.generate \
3. Enable http_transcode option in your Server module
```elixir
defmodule Helloworld.Greeter.Server do
use GRPC.Server,
use GRPC.Server,
service: Helloworld.Greeter.Service,
http_transcode: true

Expand Down
9 changes: 9 additions & 0 deletions lib/grpc/logger.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule GRPC.Logger do
@doc """
Normalizes the exception and stacktrace inputs by its kind to match the format specified for `crash_report` metadata
in [Logger](https://hexdocs.pm/logger/main/Logger.html#module-metadata)
"""
def crash_reason(:throw, reason, stacktrace), do: {{:nocatch, reason}, stacktrace}
def crash_reason(:error, reason, stack), do: {Exception.normalize(:error, reason, stack), stack}
def crash_reason(:exit, reason, stacktrace), do: {reason, stacktrace}
end
45 changes: 32 additions & 13 deletions lib/grpc/server/adapters/cowboy/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
A cowboy handler accepting all requests and calls corresponding functions defined by users.
"""

alias GRPC.Server.Adapters.ReportException
alias GRPC.Transport.HTTP2
alias GRPC.RPCError
require Logger
Expand Down Expand Up @@ -72,7 +73,7 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
http_transcode: transcode?(req)
}

server_rpc_pid = spawn_link(__MODULE__, :call_rpc, [server, route, stream])
server_rpc_pid = :proc_lib.spawn_link(__MODULE__, :call_rpc, [server, route, stream])
Process.flag(:trap_exit, true)

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

# expected error raised from user to return error immediately
def info({:EXIT, pid, {%RPCError{} = error, _stacktrace}}, req, state = %{pid: pid}) do
def info({:EXIT, pid, {%RPCError{} = error, stacktrace}}, req, state = %{pid: pid}) do
req = send_error(req, error, state, :rpc_error)

[req: req]
|> ReportException.new(error, stacktrace)
|> log_error(stacktrace)

{:stop, req, state}
end

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

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

{:stop, req, state}
end

def info({:EXIT, pid, {reason, stacktrace}}, req, state = %{pid: pid}) do
Logger.error(Exception.format(:error, reason, stacktrace))

error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"}
req = send_error(req, error, state, reason)

[req: req]
|> ReportException.new(reason, stacktrace)
|> log_error(stacktrace)

{:stop, req, state}
end

Expand All @@ -500,17 +511,17 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do
end
catch
kind, e ->
Logger.error(Exception.format(kind, e, __STACKTRACE__))
reason = Exception.normalize(kind, e, __STACKTRACE__)

exit({:handle_error, kind})
{:error, %{kind: kind, reason: reason, stack: __STACKTRACE__}}
end

case result do
{:error, %GRPC.RPCError{} = e} ->
exit({e, ""})
exit({e, _stacktrace = []})

{:error, %{kind: kind}} ->
exit({:handle_error, kind})
{:error, %{kind: _kind, reason: _reason, stack: _stack} = e} ->
exit({:handle_error, e})

other ->
other
Expand Down Expand Up @@ -648,4 +659,12 @@ defmodule GRPC.Server.Adapters.Cowboy.Handler do

{:wait, ref}
end

defp log_error(%ReportException{kind: kind} = exception, stacktrace) do
crash_reason = GRPC.Logger.crash_reason(kind, exception, stacktrace)

kind
|> Exception.format(exception, stacktrace)
|> Logger.error(crash_reason: crash_reason)
end
end
17 changes: 17 additions & 0 deletions lib/grpc/server/adapters/report_exception.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule GRPC.Server.Adapters.ReportException do
@moduledoc """
Exception raised by server adapter, meant to be used as part of metadata `crash_report`
"""

defexception [:kind, :reason, :stack, :adapter_extra]

def new(adapter_extra, %{__exception__: _} = exception, stack \\ [], kind \\ :error) do
exception(kind: kind, reason: exception, stack: stack, adapter_extra: adapter_extra)
end

def message(%__MODULE__{adapter_extra: [{:req, req}], kind: kind, reason: reason, stack: stack}) do
# Cowboy adapter message builder
path = :cowboy_req.path(req)
"Exception raised while handling #{path}:\n" <> Exception.format_banner(kind, reason, stack)
end
end
42 changes: 26 additions & 16 deletions test/grpc/integration/server_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -224,27 +224,37 @@ defmodule GRPC.Integration.ServerTest do
end

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

assert %GRPC.RPCError{
status: GRPC.Status.unauthenticated(),
message: "Please authenticate"
} == reply
end)
assert %GRPC.RPCError{
status: GRPC.Status.unauthenticated(),
message: "Please authenticate"
} == reply
end)
end)

assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
end

test "return errors for unknown errors" do
run_server([HelloErrorServer], fn port ->
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
req = %Helloworld.HelloRequest{name: "unknown error"}
logs =
ExUnit.CaptureLog.capture_log(fn ->
run_server([HelloErrorServer], fn port ->
{:ok, channel} = GRPC.Stub.connect("localhost:#{port}")
req = %Helloworld.HelloRequest{name: "unknown error"}

assert {:error,
%GRPC.RPCError{message: "Internal Server Error", status: GRPC.Status.unknown()}} ==
channel |> Helloworld.Greeter.Stub.say_hello(req)
end)
assert {:error,
%GRPC.RPCError{message: "Internal Server Error", status: GRPC.Status.unknown()}} ==
channel |> Helloworld.Greeter.Stub.say_hello(req)
end)
end)

assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello"
end

test "returns appropriate error for stream requests" do
Expand Down
Loading