Skip to content

Commit

Permalink
Support per-module custom options for check ins (#833)
Browse files Browse the repository at this point in the history
Co-authored-by: Savannah Manning <[email protected]>
Co-authored-by: Andrea Leopardi <[email protected]>
  • Loading branch information
3 people authored Feb 9, 2025
1 parent 2b5dc13 commit 8b97936
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 4 deletions.
64 changes: 62 additions & 2 deletions lib/sentry/integrations/oban/cron.ex
Original file line number Diff line number Diff line change
@@ -1,19 +1,38 @@
defmodule Sentry.Integrations.Oban.Cron do
@moduledoc false
@moduledoc """
This module provides built-in integration for cron jobs managed by Oban.
"""

@moduledoc since: "10.9.0"

alias Sentry.Integrations.CheckInIDMappings

@doc """
The Oban integration calls this callback (if present) to customize
the configuration options for the check-in.
This function must return options compatible with the ones passed to `Sentry.CheckIn.new/1`.
Options returned by this function overwrite any option inferred by the specific
integration for the check in. We perform *deep merging* of nested keyword options.
"""
@doc since: "10.9.0"
@callback sentry_check_in_configuration(oban_job :: struct()) :: options_to_merge :: keyword()

@events [
[:oban, :job, :start],
[:oban, :job, :stop],
[:oban, :job, :exception]
]

@doc false
@spec attach_telemetry_handler(keyword()) :: :ok
def attach_telemetry_handler(config) when is_list(config) do
_ = :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, config)
:ok
end

@doc false
@spec handle_event([atom()], term(), term(), keyword()) :: :ok
def handle_event(event, measurements, metadata, config)

Expand Down Expand Up @@ -87,15 +106,56 @@ defmodule Sentry.Integrations.Oban.Cron do
monitor_config_opts ->
id = CheckInIDMappings.lookup_or_insert_new(job.id)

[
opts = [
check_in_id: id,
# This is already a binary.
monitor_slug: monitor_slug,
monitor_config: monitor_config_opts
]

resolve_custom_opts(opts, job)
end
end

defp resolve_custom_opts(opts, %{worker: worker} = job)
when is_struct(job, Oban.Job) and is_binary(worker) do
job.worker |> String.split(".") |> Module.safe_concat()
rescue
ArgumentError -> opts
else
worker ->
if Code.ensure_loaded?(worker) do
resolve_custom_opts(opts, worker, job)
else
opts
end
end

defp resolve_custom_opts(opts, _job) do
opts
end

defp resolve_custom_opts(options, mod, per_integration_term) do
custom_opts =
if function_exported?(mod, :sentry_check_in_configuration, 1) do
mod.sentry_check_in_configuration(per_integration_term)
else
[]
end

deep_merge_keyword(options, custom_opts)
end

defp deep_merge_keyword(left, right) do
Keyword.merge(left, right, fn _key, left_val, right_val ->
if Keyword.keyword?(left_val) and Keyword.keyword?(right_val) do
deep_merge_keyword(left_val, right_val)
else
right_val
end
end)
end

defp schedule_opts(%{meta: meta} = job) when is_struct(job, Oban.Job) do
case meta["cron_expr"] do
"@hourly" -> [schedule: [type: :interval, value: 1, unit: :hour]]
Expand Down
53 changes: 51 additions & 2 deletions test/sentry/integrations/oban/cron_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,18 @@ defmodule Sentry.Integrations.Oban.CronTest do
Bypass.down(bypass)

:telemetry.execute([:oban, :job, unquote(event_type)], %{}, %{
job: %Oban.Job{meta: %{"cron" => true, "cron_expr" => "@reboot"}}
job: %Oban.Job{
worker: "Sentry.MyWorker",
meta: %{"cron" => true, "cron_expr" => "@reboot"}
}
})
end

test "ignores #{event_type} events with a cron expr that is not a string", %{bypass: bypass} do
Bypass.down(bypass)

:telemetry.execute([:oban, :job, unquote(event_type)], %{}, %{
job: %Oban.Job{meta: %{"cron" => true, "cron_expr" => 123}}
job: %Oban.Job{worker: "Sentry.MyWorker", meta: %{"cron" => true, "cron_expr" => 123}}
})
end
end
Expand Down Expand Up @@ -297,6 +300,52 @@ defmodule Sentry.Integrations.Oban.CronTest do
assert_receive {^ref, :done}, 1000
end

test "custom options", %{bypass: bypass} do
test_pid = self()
ref = make_ref()

Bypass.expect_once(bypass, "POST", "/api/1/envelope/", fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
assert [{_headers, check_in_body}] = decode_envelope!(body)

assert check_in_body["monitor_slug"] == "this-is-a-custom-slug-123"
assert check_in_body["monitor_config"]["schedule"]["type"] == "interval"
assert check_in_body["monitor_config"]["timezone"] == "Europe/Rome"

send(test_pid, {ref, :done})

Plug.Conn.send_resp(conn, 200, ~s<{"id": "1923"}>)
end)

defmodule WorkerWithCustomOptions do
use Oban.Worker

@behaviour Sentry.Integrations.Oban.Cron

@impl Oban.Worker
def perform(_job), do: :ok

@impl Sentry.Integrations.Oban.Cron
def sentry_check_in_configuration(job) do
[
monitor_slug: "this-is-a-custom-slug-#{job.id}",
monitor_config: [timezone: "Europe/Rome"]
]
end
end

:telemetry.execute([:oban, :job, :start], %{}, %{
job: %Oban.Job{
worker: inspect(WorkerWithCustomOptions),
id: 123,
args: %{},
meta: %{"cron" => true, "cron_expr" => "@daily"}
}
})

assert_receive {^ref, :done}, 1000
end

def custom_name_generator(%Oban.Job{worker: "Sentry.ClientWorker", args: %{"client" => client}}) do
"Sentry.ClientWorker.#{client}"
end
Expand Down

0 comments on commit 8b97936

Please sign in to comment.