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

Switch HTTP client from hackney to finch #758

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/_build
/.elixir_ls
/deps
erl_crash.dump
*.ez
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ This is the official Sentry SDK for [Sentry].

To use Sentry in your project, add it as a dependency in your `mix.exs` file.

Sentry does not install a JSON library nor an HTTP client by itself. Sentry will default to the [built-in `JSON`](https://hexdocs.pm/elixir/JSON.html) for JSON and [Hackney] for HTTP requests, but can be configured to use other ones. To use the default ones, do:
Sentry does not install a JSON library nor an HTTP client by itself. Sentry will default to the [built-in `JSON`](https://hexdocs.pm/elixir/JSON.html) for JSON and [Finch] for HTTP requests, but can be configured to use other ones. To use the default ones, do:

```elixir
defp deps do
[
# ...

{:sentry, "~> 10.8"},
{:hackney, "~> 1.20"}
{:sentry, "~> 10.0"},
{:jason, "~> 1.4"},
{:finch, "~> 0.18"}
]
end
```
Expand Down Expand Up @@ -203,7 +204,7 @@ Licensed under the MIT license, see [`LICENSE`](./LICENSE).

[Sentry]: http://sentry.io/
[Jason]: https://github.com/michalmuskala/jason
[Hackney]: https://github.com/benoitc/hackney
[Finch]: https://github.com/sneako/finch
[Bypass]: https://github.com/PSPDFKit-labs/bypass
[docs]: https://hexdocs.pm/sentry/readme.html
[logger-handlers]: https://www.erlang.org/doc/apps/kernel/logger_chapter#handlers
Expand Down
5 changes: 4 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ if config_env() == :test do
tags: %{},
enable_source_code_context: true,
root_source_code_paths: [File.cwd!()],
hackney_opts: [recv_timeout: 50, pool: :sentry_pool],
finch_opts: [recv_timeout: 50],

Choose a reason for hiding this comment

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

Suggested change
finch_opts: [recv_timeout: 50],
finch_opts: [receive_timeout: 50],

According to t:Finch.request_opt(), this should be :receive_timeout

send_result: :sync,
send_max_attempts: 1,
dedup_events: false,
Expand All @@ -16,3 +16,6 @@ if config_env() == :test do
end

config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason)

config :sentry,
client: Sentry.FinchClient
2 changes: 1 addition & 1 deletion lib/mix/tasks/sentry.send_test_event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defmodule Mix.Tasks.Sentry.SendTestEvent do
end

Mix.shell().info("current environment_name: #{inspect(to_string(Config.environment_name()))}")
Mix.shell().info("hackney_opts: #{inspect(Config.hackney_opts())}\n")
Mix.shell().info("Finch options: #{inspect(Config.finch_opts())}\n")
end

defp send_event(opts) do
Expand Down
66 changes: 49 additions & 17 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -284,11 +284,11 @@ defmodule Sentry.Config do
client: [
type: :atom,
type_doc: "`t:module/0`",
default: Sentry.HackneyClient,
default: Sentry.FinchClient,
doc: """
A module that implements the `Sentry.HTTPClient`
behaviour. Defaults to `Sentry.HackneyClient`, which uses
[hackney](https://github.com/benoitc/hackney) as the HTTP client.
behaviour. Defaults to `Sentry.FinchClient`, which uses
[Finch](https://github.com/sneako/finch) as the HTTP client.
"""
],
send_max_attempts: [
Expand All @@ -298,32 +298,64 @@ defmodule Sentry.Config do
The maximum number of attempts to send an event to Sentry.
"""
],
hackney_opts: [
Copy link
Collaborator

Choose a reason for hiding this comment

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

We need to keep hackney_opts and deprecate them if we want this to not be a breaking change. NimbleOptions supports deprecating options, check out the docs for that.

This also applies to the options below.

Copy link
Collaborator Author

@savhappy savhappy Jul 30, 2024

Choose a reason for hiding this comment

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

Does this mean we need to keep the option:

    client: [
      type: :atom,
      type_doc: "`t:module/0`",
      default: Sentry.HackneyClient,
      doc: """
      """
    ]

and the module HackneyClient?? @whatyouhide

finch_opts: [
type: :keyword_list,
default: [pool: :sentry_pool],

Choose a reason for hiding this comment

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

I don't believe pool: :sentry_pool is not a valid option for Finch? But it also looks like finch_opts is not used. Maybe it is best to shift to allowing a separation between Finch request_opts vs start_link opts? Sentry may not need to do intense validation since Finch also uses nimble_options under the hood

Copy link
Collaborator Author

@savhappy savhappy Mar 3, 2025

Choose a reason for hiding this comment

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

Thanks @jjcarstens for the review 💜

You're right! :sentry_pool isn't valid. The Finch config options were just placeholder until we decide what options to expose. Which of the pool options do we want to support? @whatyouhide or @jjcarstens thoughts?
Maybe it makes sense to have a catch all like you (@jjcarstens) mentioned configuration option without the need to handle validation.

    finch_pool_opts: [
      type: :keyword_list,
      default: [size: 50, conn_max_idle_time: 5000],
      doc: """
      Start link pool ptions to be passed to `finch`....
      """
    ]

doc: """
Options to be passed to `hackney`. Only
applied if `:client` is set to `Sentry.HackneyClient`.
Options to be passed to `finch`. Only
applied if `:client` is set to `Sentry.FinchClient`.
"""
],
hackney_pool_timeout: [
finch_pool_timeout: [
type: :timeout,
default: 5000,
doc: """
The maximum time to wait for a
connection to become available. Only applied if `:client` is set to
`Sentry.HackneyClient`.
`Sentry.FinchClient`.
"""
],
hackney_pool_max_connections: [
finch_pool_max_connections: [
type: :pos_integer,
default: 50,
doc: """
The maximum number of
connections to keep in the pool. Only applied if `:client` is set to
`Sentry.HackneyClient`.
`Sentry.FinchClient`.
"""
]
],
hackney_opts:
[
type: :keyword_list,
default: [pool: :sentry_pool],
doc: """
Options to be passed to `hackney`. Only
applied if `:client` is set to `Sentry.HackneyClient`.
"""
] ++
if(Mix.env() == :test, do: [], else: [deprecated: "Use Finch instead as default client."]),
hackney_pool_timeout:
[
type: :timeout,
default: 5000,
doc: """
The maximum time to wait for a
connection to become available. Only applied if `:client` is set to
`Sentry.HackneyClient`.
"""
] ++
if(Mix.env() == :test, do: [], else: [deprecated: "Use Finch instead as default client."]),
hackney_pool_max_connections:
[
type: :pos_integer,
default: 50,
doc: """
The maximum number of
connections to keep in the pool. Only applied if `:client` is set to
`Sentry.HackneyClient`.
"""
] ++
if(Mix.env() == :test, do: [], else: [deprecated: "Use Finch instead as default client."])
]

source_code_context_opts_schema = [
Expand Down Expand Up @@ -545,11 +577,11 @@ defmodule Sentry.Config do
@spec environment_name() :: String.t() | nil
def environment_name, do: fetch!(:environment_name)

@spec max_hackney_connections() :: pos_integer()
def max_hackney_connections, do: fetch!(:hackney_pool_max_connections)
@spec max_finch_connections() :: pos_integer()
def max_finch_connections, do: fetch!(:finch_pool_max_connections)

@spec hackney_timeout() :: timeout()
def hackney_timeout, do: fetch!(:hackney_pool_timeout)
@spec finch_timeout() :: timeout()
def finch_timeout, do: fetch!(:finch_pool_timeout)

@spec tags() :: map()
def tags, do: fetch!(:tags)
Expand Down Expand Up @@ -590,8 +622,8 @@ defmodule Sentry.Config do
@spec sample_rate() :: float()
def sample_rate, do: fetch!(:sample_rate)

@spec hackney_opts() :: keyword()
def hackney_opts, do: fetch!(:hackney_opts)
@spec finch_opts() :: keyword()
def finch_opts, do: fetch!(:finch_opts)
Comment on lines +625 to +626
Copy link
Collaborator

Choose a reason for hiding this comment

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

Where do we use these?


@spec before_send() :: (Sentry.Event.t() -> Sentry.Event.t()) | {module(), atom()} | nil
def before_send, do: get(:before_send)
Expand Down
1 change: 0 additions & 1 deletion lib/sentry/envelope.ex
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@ defmodule Sentry.Envelope do
end

items_iodata = Enum.map(envelope.items, &item_to_binary(json_library, &1))

{:ok, IO.iodata_to_binary([headers_iodata, items_iodata])}
catch
{:error, _reason} = error -> error
Expand Down
56 changes: 56 additions & 0 deletions lib/sentry/finch_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule Sentry.FinchClient do
@behaviour Sentry.HTTPClient

@moduledoc """
The built-in HTTP client.

This client implements the `Sentry.HTTPClient` behaviour.

It's based on the [Finch](https://github.com/sneako/finch) Elixir HTTP client,
which is an *optional dependency* of this library. If you wish to use another
HTTP client, you'll have to implement your own `Sentry.HTTPClient`. See the
documentation for `Sentry.HTTPClient` for more information.

Finch is built on top of [NimblePool](https://github.com/dashbitco/nimble_pool). If you need to set other pool configuration options,
see "Pool Configuration Options" in the Finch documentation for details on the possible map values.
[finch configuration options](https://hexdocs.pm/finch/Finch.html#start_link/1-pool-configuration-options)
"""
@impl true
def child_spec do
if Code.ensure_loaded?(Finch) do
case Application.ensure_all_started(:finch) do
{:ok, _apps} -> :ok
{:error, reason} -> raise "failed to start the :finch application: #{inspect(reason)}"
end

Finch.child_spec(
name: __MODULE__,
pools: %{
:default => [
size: Sentry.Config.max_finch_connections(),
conn_max_idle_time: Sentry.Config.finch_timeout()
]
}
)
else
raise """
cannot start the :sentry application because the HTTP client is set to \
Sentry.FinchClient (which is the default), but the :finch library is not loaded. \
Add :finch to your dependencies to fix this.
"""
end
end

@impl true
def post(url, headers, body) do
request = Finch.build(:post, url, headers, body)

case Finch.request(request, __MODULE__) do
{:ok, %Finch.Response{status: status, headers: headers, body: body}} ->
{:ok, status, headers, body}

{:error, error} ->
{:error, error}
end
end
end
56 changes: 0 additions & 56 deletions lib/sentry/hackney_client.ex

Choose a reason for hiding this comment

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

The :hackney_opts was switched to soft deprecation, but this Hackney client was totally removed which I assume is equally as breaking. Is there a specific reason not to leave it in until hackney is hard deprecated?

This file was deleted.

45 changes: 22 additions & 23 deletions lib/sentry/http_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Sentry.HTTPClient do
@moduledoc """
A behaviour for HTTP clients that Sentry can use.

The default HTTP client is `Sentry.HackneyClient`.
The default HTTP client is `Sentry.FinchClient`.

To configure a different HTTP client, implement the `Sentry.HTTPClient` behaviour and
change the `:client` configuration:
Expand All @@ -25,46 +25,45 @@ defmodule Sentry.HTTPClient do
## Alternative Clients

Let's look at an example of using an alternative HTTP client. In this example, we'll
use [Finch](https://github.com/sneako/finch), a lightweight HTTP client for Elixir.
use [Hackney](https://github.com/benoitc/hackney), a lightweight HTTP client for Elixir.

First, we need to add Finch to our dependencies:
First, we need to add Hackney to our dependencies:

# In mix.exs
defp deps do
[
# ...
{:finch, "~> 0.16"}
{:hackney, "~> 1.8"}
]
end

Then, we need to define a module that implements the `Sentry.HTTPClient` behaviour:

defmodule MyApp.SentryFinchHTTPClient do
defmodule MyApp.SentryHackneyHTTPClient do
@behaviour Sentry.HTTPClient

@impl true
def child_spec do
Supervisor.child_spec({Finch, name: __MODULE__}, id: __MODULE__)
# @impl true
# def child_spec do
# :hackney_pool.child_spec(
# @hackney_pool_name,
# timeout: Sentry.Config.hackney_timeout(),
# max_connections: Sentry.Config.max_hackney_connections()
# )
# end

# @impl true
# def post(url, headers, body) do
# case :hackney.request(:post, url, headers, body) do
# {:ok, _status, _headers, _body} = result -> result
# {:error, _reason} = error -> error
# end
# end
end

@impl true
def post(url, headers, body) do
request = Finch.build(:post, url, headers, body)

case Finch.request(request, __MODULE__) do
{:ok, %Finch.Response{status: status, headers: headers, body: body}} ->
{:ok, status, headers, body}

{:error, error} ->
{:error, error}
end
end
end

Last, we need to configure Sentry to use our new HTTP client:

config :sentry,
client: MyApp.SentryFinchHTTPClient
client: Sentry.HackneyClient

### Umbrella Apps

Expand Down
Loading
Loading