Skip to content
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
2 changes: 1 addition & 1 deletion lib/sentry/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ defmodule Sentry.Application do

defp start_integrations(config) do
if config[:oban][:cron][:enabled] do
Sentry.Integrations.Oban.Cron.attach_telemetry_handler()
Sentry.Integrations.Oban.Cron.attach_telemetry_handler(config[:oban][:cron])
end

if config[:oban][:capture_errors] do
Expand Down
9 changes: 9 additions & 0 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ defmodule Sentry.Config do
Whether to enable the Oban integration. When enabled, the Sentry SDK will
capture check-ins for Oban jobs. *Available since v10.2.0*.
"""
],
monitor_slug_generator: [
type: {:tuple, [:atom, :atom]},
type_doc: "`{module(), atom()}`",
doc: """
A `{module, function}` tuple that generates a monitor name based on the `Oban.Job` struct.
The function is called with the `Oban.Job` as its arguments and must return a string.
This can be used to customize monitor slugs. *Available since v10.8.0*.
"""
]
]
]
Expand Down
39 changes: 24 additions & 15 deletions lib/sentry/integrations/oban/cron.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ defmodule Sentry.Integrations.Oban.Cron do
[:oban, :job, :exception]
]

@spec attach_telemetry_handler() :: :ok
def attach_telemetry_handler do
_ = :telemetry.attach_many(__MODULE__, @events, &__MODULE__.handle_event/4, :no_config)
@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

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

def handle_event(
[:oban, :job, event],
measurements,
%{job: %mod{meta: %{"cron" => true, "cron_expr" => cron_expr}}} = metadata,
_config
config
)
when event in [:start, :stop, :exception] and mod == Oban.Job and is_binary(cron_expr) do
_ = handle_event(event, measurements, metadata)
_ = handle_oban_job_event(event, measurements, metadata, config)
:ok
end

Expand All @@ -35,16 +35,16 @@ defmodule Sentry.Integrations.Oban.Cron do

## Helpers

defp handle_event(:start, _measurements, metadata) do
if opts = job_to_check_in_opts(metadata.job) do
defp handle_oban_job_event(:start, _measurements, metadata, config) do
if opts = job_to_check_in_opts(metadata.job, config) do
opts
|> Keyword.merge(status: :in_progress)
|> Sentry.capture_check_in()
end
end

defp handle_event(:stop, measurements, metadata) do
if opts = job_to_check_in_opts(metadata.job) do
defp handle_oban_job_event(:stop, measurements, metadata, config) do
if opts = job_to_check_in_opts(metadata.job, config) do
status =
case metadata.state do
:success -> :ok
Expand All @@ -60,17 +60,26 @@ defmodule Sentry.Integrations.Oban.Cron do
end
end

defp handle_event(:exception, measurements, metadata) do
if opts = job_to_check_in_opts(metadata.job) do
defp handle_oban_job_event(:exception, measurements, metadata, config) do
if opts = job_to_check_in_opts(metadata.job, config) do
opts
|> Keyword.merge(status: :error, duration: duration_in_seconds(measurements))
|> Sentry.capture_check_in()
end
end

defp job_to_check_in_opts(job) when is_struct(job, Oban.Job) do
defp job_to_check_in_opts(job, config) when is_struct(job, Oban.Job) do
monitor_config_opts = Sentry.Config.integrations()[:monitor_config_defaults]

monitor_slug =
case config[:monitor_slug_generator] do
nil ->
slugify(job.worker)

{mod, fun} when is_atom(mod) and is_atom(fun) ->
mod |> apply(fun, [job]) |> slugify()
end

case Keyword.merge(monitor_config_opts, schedule_opts(job)) do
[] ->
nil
Expand All @@ -81,7 +90,7 @@ defmodule Sentry.Integrations.Oban.Cron do
[
check_in_id: id,
# This is already a binary.
monitor_slug: slugify(job.worker),
monitor_slug: monitor_slug,
monitor_config: monitor_config_opts
]
end
Expand Down
67 changes: 65 additions & 2 deletions test/sentry/integrations/oban/cron_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ defmodule Sentry.Integrations.Oban.CronTest do

import Sentry.TestHelpers

setup_all do
Sentry.Integrations.Oban.Cron.attach_telemetry_handler()
setup context do
opts = context[:attach_opts] || []

Sentry.Integrations.Oban.Cron.attach_telemetry_handler(opts)
on_exit(fn -> :telemetry.detach(Sentry.Integrations.Oban.Cron) end)
end

setup do
Expand Down Expand Up @@ -239,4 +242,64 @@ defmodule Sentry.Integrations.Oban.CronTest do

assert_receive {^ref, :done}, 1000
end

@tag attach_opts: [monitor_slug_generator: {__MODULE__, :custom_name_generator}]
test "monitor_slug is not affected if the custom monitor_name_generator does not target the worker",
%{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"] == "sentry-my-worker"
send(test_pid, {ref, :done})

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

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

assert_receive {^ref, :done}, 1000
end

@tag attach_opts: [monitor_slug_generator: {__MODULE__, :custom_name_generator}]
test "monitor_slug is set based on the custom monitor_name_generator if it targets the worker",
%{bypass: bypass} do
client_name = "my-client"
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"] == "sentry-client-worker-my-client"
send(test_pid, {ref, :done})

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

:telemetry.execute([:oban, :job, :start], %{}, %{
job: %Oban.Job{
worker: "Sentry.ClientWorker",
id: 123,
args: %{"client" => client_name},
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

def custom_name_generator(%Oban.Job{worker: worker}), do: worker
end
Loading