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

Set session from Logger metadata #29

Merged
merged 10 commits into from
Apr 2, 2024
Merged
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: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog for v0.x

## v2.1.0 (??????????)

### Enhancements

* New config option: if `:session` is set to `:include_logger_metadata`, the
Logger metadata from `Logger.metadata/0` is added to the `session` field of
the report. (If the option is not set, the metadata is not included.)

## v2.0.0 (2024-03-11)

### Enhancements
Expand All @@ -10,7 +18,7 @@
### Breaking Change

* Drop support for Elixir <1.12

## v1.0.0 (2023-10-12)

### Enhancements
Expand Down Expand Up @@ -43,7 +51,7 @@

* [Airbrake] Add `:filter_headers` option to filter HTTP headers included in `:environment`.
* [Airbrake.Payload] Conditionally derive `Jason.Encoder` if `Jason.Encoder` is defined (i.e., `jason` is a dependency).
* [Airbrake.Payload] Add fields `context`, `environment`, `params`, and `session` to `Airbrake.Payload`.

Check warning on line 54 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / all_tests (1.15.6, 26.1.1)

documentation references module "Airbrake.Payload" but it is hidden

Check warning on line 54 in CHANGELOG.md

View workflow job for this annotation

GitHub Actions / all_tests (1.15.6, 26.1.1)

documentation references module "Airbrake.Payload" but it is hidden
* [Airbrake.Worker] Generate a useable stacktrace when one isn't provided in the options.

## v0.9.0 (2021-06-04)
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,16 @@ config :airbrake_client,
environment: Mix.env(),
filter_parameters: ["password"],
filter_headers: ["authorization"],
session: :include_logger_metadata,
host: "https://api.airbrake.io" # or your Errbit host

config :logger,
backends: [{Airbrake.LoggerBackend, :error}, :console]
```

Split this config across your `config/*.exs` files (especially the runtime
setting in `config/runtime.exs`).

Required configuration arguments:

* `:api_key` - (binary) the token needed to access the [Airbrake
Expand All @@ -74,6 +78,24 @@ Optional configuration arguments:
to ignore some or all exceptions. See examples below.
* `:options` - (keyword list or function returning keyword list) values that
are included in all reports to Airbrake.io. See examples below.
* `:session` - can be set to `:include_logger_metadata` to include Logger
metadata in the `session` field of the report; omit this option if you do
not want Logger metadata. See below for more information.

### Logger metadata in the `session`

If you set the `:session` config to `:include_logger_metadata`, the Logger
metadata from the process that invokes `Airbrake.report/2` will be the initial
session data for the `session` field. The values passed as `:session` in the
`options` parameter of `Airbrake.report/2` are _added_ to the session value,
overwriting any Logger metadata values.

If you do not set the `:session` config, only the `:session` value passed as the
options to `Airbrake.report/2` will be used for the `session` field in the
report.

If the `session` turns out to be empty (for whatever reason), it is instead set
to `nil` (and should not show up in the report).

### Ignoring some exceptions

Expand Down
5 changes: 2 additions & 3 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Config

# These settings can be used on the iex console.
# More are set in `config/runtime.exs`.
config :airbrake_client,
api_key: {:system, "AIRBRAKE_API_KEY"},
project_id: {:system, "AIRBRAKE_PROJECT_ID"},
host: {:system, "AIRBRAKE_HOST", "https://api.airbrake.io"},
session: :include_logger_metadata,
private: [http_adapter: HTTPoison]
15 changes: 15 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Config

case config_env() do
:dev ->
config :airbrake_client,
api_key: System.get_env("AIRBRAKE_API_KEY"),
project_id: System.get_env("AIRBRAKE_PROJECT_ID"),
host: System.get_env("AIRBRAKE_HOST", "https://api.airbrake.io")

:test ->
nil

:prod ->
nil
end
3 changes: 2 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import Config
config :airbrake_client,
api_key: "TESTING_API_KEY",
project_id: 8_675_309,
private: [http_adapter: Airbrake.HTTPMock]
private: [http_adapter: Airbrake.HTTPMock],
filter_parameters: ["password"]
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ defmodule JasonOnlyAppTest do
"notifier" => %{
"name" => "Airbrake Client",
"url" => "https://github.com/CityBaseInc/airbrake_client",
"version" => "2.0.0"
"version" => "2.1.0"
},
"params" => nil,
"session" => nil
Expand Down Expand Up @@ -88,7 +88,7 @@ defmodule JasonOnlyAppTest do
"notifier" => %{
"name" => "Airbrake Client",
"url" => "https://github.com/CityBaseInc/airbrake_client",
"version" => "2.0.0"
"version" => "2.1.0"
},
"params" => %{"foo" => 55},
"session" => %{"foo" => 555}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ defmodule PoisonOnlyAppTest do
"notifier" => %{
"name" => "Airbrake Client",
"url" => "https://github.com/CityBaseInc/airbrake_client",
"version" => "2.0.0"
"version" => "2.1.0"
},
"params" => nil,
"session" => nil
Expand Down Expand Up @@ -89,7 +89,7 @@ defmodule PoisonOnlyAppTest do
"notifier" => %{
"name" => "Airbrake Client",
"url" => "https://github.com/CityBaseInc/airbrake_client",
"version" => "2.0.0"
"version" => "2.1.0"
},
"params" => %{"foo" => 55},
"session" => %{"foo" => 555}
Expand Down
47 changes: 47 additions & 0 deletions lib/airbrake/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule Airbrake.Config do
@moduledoc false

defmodule Behaviour do
@moduledoc false

@callback get(atom()) :: any()

@callback get(atom(), any()) :: any()

@callback env :: String.t()

@callback hostname :: String.t()
end

@behaviour Airbrake.Config.Behaviour

# Gets a value from the `:airbrake_client` config.
@impl Airbrake.Config.Behaviour
def get(key, default \\ nil) do
:airbrake_client
|> Application.get_env(key, default)
|> resolve()
end

# Returns the name of the environment.
@impl Airbrake.Config.Behaviour
def env do
case Application.get_env(:airbrake_client, :environment) do
nil -> hostname()
{:system, var} -> System.get_env(var, hostname())
atom_env when is_atom(atom_env) -> to_string(atom_env)
str_env when is_binary(str_env) -> str_env
fun_env when is_function(fun_env) -> fun_env.()
end
end

# Returns a hostname.
@impl Airbrake.Config.Behaviour
def hostname do
System.get_env("HOST") || to_string(elem(:inet.gethostname(), 1))
end

defp resolve({:system, key, default}), do: System.get_env(key) || default
defp resolve({:system, key}), do: System.get_env(key)
defp resolve(value), do: value
end
86 changes: 12 additions & 74 deletions lib/airbrake/payload.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
defmodule Airbrake.Payload do
@moduledoc false

alias Airbrake.Payload.Builder

@notifier_info %{
name: "Airbrake Client",
version: Airbrake.Mixfile.project()[:version],
Expand All @@ -18,83 +20,19 @@ defmodule Airbrake.Payload do
params: nil,
session: nil

alias Airbrake.Payload.Backtrace
alias Airbrake.Utils

def new(exception, stacktrace, options \\ [])

def new(%{__exception__: true} = exception, stacktrace, options) do
new(Airbrake.Worker.exception_info(exception), stacktrace, options)
end

def new(exception, stacktrace, options) when is_list(exception) do
%__MODULE__{}
|> add_error(
exception,
stacktrace,
Keyword.get(options, :context),
Keyword.get(options, :env),
Keyword.get(options, :params),
Keyword.get(options, :session)
)
end
def new(exception, stacktrace, opts \\ [])

defp add_error(payload, exception, stacktrace, context, env, params, session) do
payload
|> add_exception_info(exception, stacktrace)
|> add_context(context)
|> add_env(env)
|> add_params(params)
|> add_session(session)
def new(%{__exception__: true} = exception, stacktrace, opts) do
new(Airbrake.Worker.exception_info(exception), stacktrace, opts)
end

defp add_exception_info(payload, exception, stacktrace) do
error = %{
type: exception[:type],
message: exception[:message],
backtrace: Backtrace.from_stacktrace(stacktrace)
def new(exception, stacktrace, opts) when is_list(exception) do
%__MODULE__{
errors: [Builder.build_error(exception, stacktrace)],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much cleaner 👍 👍

context: Builder.build(:context, opts),
environment: Builder.build(:environment, opts),
params: Builder.build(:params, opts),
session: Builder.build(:session, opts)
}

Map.put(payload, :errors, [error])
end

defp env do
case Application.get_env(:airbrake_client, :environment) do
nil -> hostname()
{:system, var} -> System.get_env(var) || hostname()
atom_env when is_atom(atom_env) -> to_string(atom_env)
str_env when is_binary(str_env) -> str_env
fun_env when is_function(fun_env) -> fun_env.()
end
end

def hostname do
System.get_env("HOST") || to_string(elem(:inet.gethostname(), 1))
end

defp add_context(payload, context) do
context = Map.merge(%{environment: env(), hostname: hostname()}, context || %{})
Map.put(payload, :context, context)
end

defp add_env(payload, nil), do: payload
defp add_env(payload, env), do: Map.put(payload, :environment, filter_environment(env))

defp add_params(payload, nil), do: payload
defp add_params(payload, params), do: Map.put(payload, :params, filter_parameters(params))

defp add_session(payload, nil), do: payload
defp add_session(payload, session), do: Map.put(payload, :session, session)

defp filter_parameters(params), do: filter(params, :filter_parameters)

defp filter_environment(env) do
if Map.has_key?(env, "headers"),
do: Map.update!(env, "headers", &filter(&1, :filter_headers)),
else: env
end

defp filter(map, attributes_key) do
Utils.filter(map, Airbrake.Worker.get_env(attributes_key))
end
end
86 changes: 86 additions & 0 deletions lib/airbrake/payload/builder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule Airbrake.Payload.Builder do
@moduledoc false

alias Airbrake.Payload.Backtrace
alias Airbrake.Utils

def build_error(exception, stacktrace) do
%{
type: exception[:type],
message: exception[:message],
backtrace: Backtrace.from_stacktrace(stacktrace)
}
end

def build(:context, opts) do
config = get_config(opts)

Map.merge(
%{environment: config.env(), hostname: config.hostname()},
opts |> Keyword.get(:context, %{}) |> Enum.into(%{})
)
end

def build(:environment, opts) do
environment =
Keyword.get_lazy(opts, :environment, fn ->
Keyword.get(opts, :env)
end)

case environment do
nil -> nil
env -> env |> Enum.into(%{}) |> filter_environment(opts)
end
end

def build(:params, opts) do
case Keyword.get(opts, :params) do
nil -> nil
params -> params |> Enum.into(%{}) |> filter_parameters(opts)
end
end

def build(:session, opts) do
config = get_config(opts)

logger_metadata =
if config.get(:session) == :include_logger_metadata,
do: Keyword.get(opts, :logger_metadata, []),
else: []

opts_session = opts |> Keyword.get(:session, %{}) |> Enum.into(%{})
full_session = logger_metadata |> Enum.into(%{}) |> Map.merge(opts_session)

if full_session == %{},
do: nil,
else: full_session
end

def filter_parameters(params, opts) do
filter_parameters = get_config(opts).get(:filter_parameters, [])

Utils.filter(params, filter_parameters)
end

def filter_environment(nil) do
nil
end

def filter_environment(environment, opts) do
filter_headers = get_config(opts).get(:filter_headers, [])

cond do
Map.has_key?(environment, "headers") ->
Map.update!(environment, "headers", &Utils.filter(&1, filter_headers))

Map.has_key?(environment, :headers) ->
Map.update!(environment, :headers, &Utils.filter(&1, filter_headers))

true ->
environment
end
end

defp get_config(opts),
do: Keyword.get(opts, :config, Airbrake.Config)
end
Loading
Loading