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

Add option for generating custom cron monitor slugs #803

Merged
Show file tree
Hide file tree
Changes from 5 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
8 changes: 8 additions & 0 deletions lib/sentry/config.ex
iautom8things marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ 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_name_generator: [
iautom8things marked this conversation as resolved.
Show resolved Hide resolved
type: :mfa,
iautom8things marked this conversation as resolved.
Show resolved Hide resolved
doc: """
A {module, function, arguments} tuple that generates a monitor name based on the Oban.Job struct.
This function should only accept a single argument, the Oban.Job struct, and return a string.
This can be used to create monitors specific to a Job's argument. *Available since v10.7.1*.
iautom8things marked this conversation as resolved.
Show resolved Hide resolved
"""
]
]
]
Expand Down
43 changes: 28 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,30 @@ 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_name_generator] do
nil ->
slugify(job.worker)

{mod, fun, args} when is_atom(mod) and is_atom(fun) and is_list(args) ->
if function_exported?(mod, fun, Enum.count(args) + 1) do
apply(mod, fun, [job | args]) |> slugify()
else
slugify(job.worker)
end
iautom8things marked this conversation as resolved.
Show resolved Hide resolved
end

case Keyword.merge(monitor_config_opts, schedule_opts(job)) do
[] ->
nil
Expand All @@ -81,7 +94,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
72 changes: 70 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,16 @@ defmodule Sentry.Integrations.Oban.CronTest do

import Sentry.TestHelpers

setup_all do
Sentry.Integrations.Oban.Cron.attach_telemetry_handler()
setup context do
opts =
if context[:custom_monitor_name_generator] do
[monitor_name_generator: {__MODULE__, :custom_name_generator, []}]
else
[]
end
iautom8things marked this conversation as resolved.
Show resolved Hide resolved

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 +247,64 @@ defmodule Sentry.Integrations.Oban.CronTest do

assert_receive {^ref, :done}, 1000
end

@tag :custom_monitor_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 :custom_monitor_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