Skip to content

Commit

Permalink
Merge pull request #29 from CityBaseInc/set-session-from-logger-metadata
Browse files Browse the repository at this point in the history
Set session from Logger metadata
  • Loading branch information
Jeremy D. Frens authored Apr 2, 2024
2 parents eeefbb7 + b64271f commit c14c3ff
Show file tree
Hide file tree
Showing 15 changed files with 527 additions and 113 deletions.
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
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)],
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

0 comments on commit c14c3ff

Please sign in to comment.