Skip to content

Commit c0297a6

Browse files
Merge pull request #403 from getsentry/logger-backend-two
Logger Backend improvements
2 parents a0a0c5e + dd943d9 commit c0297a6

File tree

10 files changed

+333
-222
lines changed

10 files changed

+333
-222
lines changed

CHANGELOG.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* Enhancements
66
* Cache environment config in application config (#393)
7+
* Allow configuring LoggerBackend to send all messages, not just exceptions (e.g. `Logger.error("I am an error message")`)
78

89
* Bug Fixes
910
* fix request url port in payloads for HTTPS requests (#391)
@@ -12,10 +13,12 @@
1213
* Change default `included_environments` to only include `:prod` by default (#370)
1314
* Change default event send type to :none instead of :async (#341)
1415
* Make hackney an optional dependency, and simplify Sentry.HTTPClient behaviour (#400)
15-
* Use Logger.metadata for Sentry.Context, no longer return metadata values on set_* functions, and rename `set_http_context` to `set_request_context`
16-
* Move excluded exceptions from Sentry.Plug to Sentry.DefaultEventFilter
17-
* Remove Sentry.Plug and Sentry.Phoenix.Endpoint in favor of Sentry.PlugContext and Sentry.PlugCapture
18-
* Remove feedback form rendering and configuration
16+
* Use Logger.metadata for Sentry.Context, no longer return metadata values on set_* functions, and rename `set_http_context` to `set_request_context` (#401)
17+
* Move excluded exceptions from Sentry.Plug to Sentry.DefaultEventFilter (#402)
18+
* Remove Sentry.Plug and Sentry.Phoenix.Endpoint in favor of Sentry.PlugContext and Sentry.PlugCapture (#402)
19+
* Remove feedback form rendering and configuration (#402)
20+
* Logger metadata is now specified by key in LoggerBackend instead of enabled/disabled
21+
* `Sentry.capture_exception/1` now only accepts exceptions
1922

2023
## 7.2.4 (2020-03-09)
2124

lib/sentry.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ defmodule Sentry do
112112
Parses and submits an exception to Sentry if current environment is in included_environments.
113113
`opts` argument is passed as the second argument to `Sentry.send_event/2`.
114114
"""
115-
@spec capture_exception(Exception.t() | atom() | {atom(), atom()}, Keyword.t()) :: send_result
115+
@spec capture_exception(Exception.t(), Keyword.t()) :: send_result
116116
def capture_exception(exception, opts \\ []) do
117117
filter_module = Config.filter()
118118
event_source = Keyword.get(opts, :event_source)

lib/sentry/event.ex

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ defmodule Sentry.Event do
7373
@doc """
7474
Creates an Event struct out of context collected and options
7575
## Options
76-
* `:exception` - Sentry-formatted exception
77-
* `:original_exception` - Original exception
76+
* `:exception` - Sentry-structured exception
77+
* `:original_exception` - Original Elixir exception struct
7878
* `:message` - message
7979
* `:stacktrace` - a list of Exception.stacktrace()
8080
* `:extra` - map of extra context
@@ -175,34 +175,18 @@ defmodule Sentry.Event do
175175
176176
"""
177177
@spec transform_exception(Exception.t(), keyword()) :: Event.t()
178-
def transform_exception(exception, opts) do
179-
error_type = Keyword.get(opts, :error_type) || :error
180-
normalized = Exception.normalize(:error, exception, Keyword.get(opts, :stacktrace, nil))
181-
178+
def transform_exception(%_{} = exception, opts) do
182179
type =
183-
if error_type == :error do
184-
normalized.__struct__
185-
|> to_string()
186-
|> String.trim_leading("Elixir.")
187-
else
188-
error_type
189-
end
180+
exception.__struct__
181+
|> to_string()
182+
|> String.trim_leading("Elixir.")
190183

191-
value =
192-
if error_type == :error do
193-
Exception.message(normalized)
194-
else
195-
Exception.format_banner(error_type, exception)
196-
end
184+
value = Exception.message(exception)
197185

198186
module = Keyword.get(opts, :module)
199187
transformed_exception = [%{type: type, value: value, module: module}]
200188

201-
message =
202-
:error
203-
|> Exception.format_banner(normalized)
204-
|> String.trim("*")
205-
|> String.trim()
189+
message = "(#{type} #{value})"
206190

207191
opts
208192
|> Keyword.put(:exception, transformed_exception)

lib/sentry/logger_backend.ex

Lines changed: 87 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,62 @@
11
defmodule Sentry.LoggerBackend do
22
@moduledoc """
3-
This module makes use of Elixir 1.7's new Logger metadata to report
4-
crashed processes. It replaces the previous `Sentry.Logger` sytem.
5-
6-
To include the backend in your application, the backend can be added in your
7-
configuration file:
3+
Report Logger events like crashed processes to Sentry. To include in your
4+
application, add this module to your Logger backends:
85
96
config :logger,
107
backends: [:console, Sentry.LoggerBackend]
118
12-
If you are on OTP 21+ and would like to configure the backend to include metadata from
13-
`Logger.metadata/0` in reported events, it can be enabled:
9+
Sentry context will be included in metadata in reported events. Example:
1410
15-
config :logger, Sentry.LoggerBackend,
16-
include_logger_metadata: true
11+
Sentry.Context.set_user_context(%{
12+
user_id: current_user.id
13+
})
14+
15+
## Configuration
1716
18-
It is important to be aware of whether this will include sensitive information
19-
in Sentry events before enabling it.
17+
* `:excluded_domains` - Any messages with a domain in the configured
18+
list will not be sent. Defaults to `[:cowboy]` to avoid double reporting
19+
events from `Sentry.PlugCapture`.
2020
21-
## Options
21+
* `:metadata` - To include non-Sentry Logger metadata in reports, the
22+
`:metadata` key can be set to a list of keys. Metadata under those keys will
23+
be added in the `:extra` context under the `:logger_metadata` key. Defaults
24+
to `[]`.
2225
23-
The supported options are:
26+
* `:level` - The minimum [Logger level](https://hexdocs.pm/logger/Logger.html#module-levels) to send events for.
27+
Defaults to `:error`.
2428
25-
* `:include_logger_metadata` - Enabling this option will read any key/value
26-
pairs with with binary, atom or number values from `Logger.metadata/0`
27-
and include that dictionary under the `:logger_metadata` key in an
28-
event's `:extra` metadata. This option defaults to `false`.
29-
* `:ignore_plug` - Enabling this option will ignore any events that
30-
appear to be from a Plug process crashing. This is to prevent
31-
duplicate errors being reported to Sentry alongside `Sentry.Plug`.
29+
* `:capture_log_messages` - When `true`, this module will send all Logger
30+
messages. Defaults to `false`, which will only send messages with metadata
31+
that has the shape of an exception and stacktrace.
32+
33+
Example:
34+
35+
config :logger, Sentry.LoggerBackend,
36+
# Also send warn messages
37+
level: :warn,
38+
# Send messages from Plug/Cowboy
39+
excluded_domains: [],
40+
# Include metadata added with `Logger.metadata([foo_bar: "value"])`
41+
metadata: [:foo_bar],
42+
# Send messages like `Logger.error("error")` to Sentry
43+
capture_log_messages: true
3244
"""
3345
@behaviour :gen_event
3446

35-
defstruct level: nil, include_logger_metadata: false, ignore_plug: true
47+
defstruct level: :error, metadata: [], excluded_domains: [:cowboy], capture_log_messages: false
3648

3749
def init(__MODULE__) do
3850
config = Application.get_env(:logger, __MODULE__, [])
39-
{:ok, init(config, %__MODULE__{})}
51+
{:ok, struct(%__MODULE__{}, config)}
4052
end
4153

4254
def init({__MODULE__, opts}) when is_list(opts) do
4355
config =
4456
Application.get_env(:logger, __MODULE__, [])
4557
|> Keyword.merge(opts)
4658

47-
{:ok, init(config, %__MODULE__{})}
59+
{:ok, struct(%__MODULE__{}, config)}
4860
end
4961

5062
def handle_call({:configure, options}, state) do
@@ -53,65 +65,22 @@ defmodule Sentry.LoggerBackend do
5365
|> Keyword.merge(options)
5466

5567
Application.put_env(:logger, __MODULE__, config)
56-
state = init(config, state)
57-
{:ok, :ok, state}
68+
{:ok, :ok, struct(state, config)}
5869
end
5970

6071
def handle_event({_level, gl, {Logger, _, _, _}}, state) when node(gl) != node() do
6172
{:ok, state}
6273
end
6374

64-
def handle_event({_level, _gl, {Logger, _msg, _ts, meta}}, state) do
65-
%{include_logger_metadata: include_logger_metadata, ignore_plug: ignore_plug} = state
66-
67-
opts =
68-
if include_logger_metadata do
69-
[
70-
extra: %{
71-
logger_metadata: build_logger_metadata(meta)
72-
}
73-
]
74-
else
75-
[]
76-
end
77-
78-
case Keyword.get(meta, :crash_reason) do
79-
{reason, stacktrace} when is_list(stacktrace) ->
80-
if ignore_plug &&
81-
Enum.any?(stacktrace, fn {module, function, arity, _file_line} ->
82-
match?({^module, ^function, ^arity}, {Plug.Cowboy.Handler, :init, 2}) ||
83-
match?({^module, ^function, ^arity}, {Phoenix.Endpoint.Cowboy2Handler, :init, 2}) ||
84-
match?({^module, ^function, ^arity}, {Phoenix.Endpoint.Cowboy2Handler, :init, 4})
85-
end) do
86-
:ok
87-
else
88-
opts =
89-
opts
90-
|> Keyword.put(:event_source, :logger)
91-
|> Keyword.put(:stacktrace, stacktrace)
92-
93-
Sentry.capture_exception(reason, opts)
94-
end
95-
96-
{{_reason, stacktrace}, {_m, _f, args}} when is_list(stacktrace) and is_list(args) ->
97-
# Cowboy stuff
98-
# https://github.com/ninenines/cowboy/blob/master/src/cowboy_stream_h.erl#L148-L151
99-
:ok
100-
101-
reason when is_atom(reason) and not is_nil(reason) ->
102-
Sentry.capture_exception(reason, [{:event_source, :logger} | opts])
103-
104-
_ ->
105-
:ok
75+
def handle_event({level, _gl, {Logger, msg, _ts, meta}}, state) do
76+
if Logger.compare_levels(level, state.level) != :lt and
77+
not excluded_domain?(meta[:domain], state) do
78+
log(level, msg, meta, state)
10679
end
10780

10881
{:ok, state}
10982
end
11083

111-
def handle_event(:flush, state) do
112-
{:ok, state}
113-
end
114-
11584
def handle_event(_, state) do
11685
{:ok, state}
11786
end
@@ -128,27 +97,57 @@ defmodule Sentry.LoggerBackend do
12897
:ok
12998
end
13099

131-
defp init(config, %__MODULE__{} = state) do
132-
level = Keyword.get(config, :level, state.level)
100+
defp log(level, msg, meta, state) do
101+
sentry = meta[:sentry] || get_sentry_from_callers(meta[:callers] || [])
133102

134-
include_logger_metadata =
135-
Keyword.get(config, :include_logger_metadata, state.include_logger_metadata)
103+
opts =
104+
[
105+
event_source: :logger,
106+
extra: %{logger_metadata: logger_metadata(meta, state), logger_level: level},
107+
result: :none
108+
] ++ Map.to_list(sentry)
109+
110+
case meta[:crash_reason] do
111+
{%_{__exception__: true} = exception, stacktrace} when is_list(stacktrace) ->
112+
Sentry.capture_exception(exception, [stacktrace: stacktrace] ++ opts)
113+
114+
{other, stacktrace} when is_list(stacktrace) ->
115+
Sentry.capture_exception(
116+
Sentry.CrashError.exception(other),
117+
[stacktrace: stacktrace] ++ opts
118+
)
136119

137-
ignore_plug = Keyword.get(config, :ignore_plug, state.ignore_plug)
120+
_ ->
121+
if state.capture_log_messages do
122+
try do
123+
if is_binary(msg), do: msg, else: :unicode.characters_to_binary(msg)
124+
rescue
125+
_ -> :ok
126+
else
127+
msg -> Sentry.capture_message(msg, opts)
128+
end
129+
end
130+
end
131+
end
138132

139-
%{
140-
state
141-
| level: level,
142-
include_logger_metadata: include_logger_metadata,
143-
ignore_plug: ignore_plug
144-
}
133+
defp get_sentry_from_callers([head | tail]) when is_pid(head) do
134+
with {:dictionary, [_ | _] = dictionary} <- :erlang.process_info(head, :dictionary),
135+
%{sentry: sentry} <- dictionary[:"$logger_metadata$"] do
136+
sentry
137+
else
138+
_ -> get_sentry_from_callers(tail)
139+
end
145140
end
146141

147-
defp build_logger_metadata(meta) do
148-
meta
149-
|> Enum.filter(fn {_key, value} ->
150-
is_binary(value) || is_atom(value) || is_number(value)
151-
end)
152-
|> Enum.into(%{})
142+
defp get_sentry_from_callers(_), do: %{}
143+
144+
defp excluded_domain?([head | _], state), do: head in state.excluded_domains
145+
defp excluded_domain?(_, _), do: false
146+
147+
defp logger_metadata(meta, state) do
148+
for key <- state.metadata,
149+
value = meta[key],
150+
do: {key, value},
151+
into: %{}
153152
end
154153
end

mix.exs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule Sentry.Mixfile do
55
[
66
app: :sentry,
77
version: "7.2.4",
8-
elixir: "~> 1.7",
8+
elixir: "~> 1.10",
99
description: "The Official Elixir client for Sentry",
1010
package: package(),
1111
deps: deps(),
@@ -31,7 +31,7 @@ defmodule Sentry.Mixfile do
3131
{:hackney, "~> 1.8 or 1.6.5", optional: true},
3232
{:jason, "~> 1.1", optional: true},
3333
{:plug, "~> 1.6", optional: true},
34-
{:plug_cowboy, "~> 1.0 or ~> 2.0", optional: true},
34+
{:plug_cowboy, "~> 2.3", optional: true},
3535
{:dialyxir, "~> 1.0", only: [:dev], runtime: false},
3636
{:ex_doc, "~> 0.21.0", only: :dev},
3737
{:bypass, "~> 1.0", only: [:test]},

test/event_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ defmodule Sentry.EventTest do
3232
assert event.level == "error"
3333

3434
assert event.message ==
35-
"(UndefinedFunctionError) function Sentry.Event.not_a_function/3 is undefined or private"
35+
"(UndefinedFunctionError function Sentry.Event.not_a_function/3 is undefined or private)"
3636

3737
assert is_binary(event.server_name)
3838

0 commit comments

Comments
 (0)