From efa568c90bc46eebe09d2e2f758c0a375c6b4550 Mon Sep 17 00:00:00 2001 From: Jeff Kreeftmeijer Date: Thu, 2 Oct 2025 12:45:04 +0200 Subject: [PATCH 1/6] Create span if none exists for Oban exceptions For situations where an error is raised from the main Oban process instead of the spawned process, create an ad-hoc span instead of using an already existing one. This produces an effect like our send_error helper, which doesn't have context but makes sure the error is still reported. --- lib/appsignal/oban.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/appsignal/oban.ex b/lib/appsignal/oban.ex index 959a723c..a62e0222 100644 --- a/lib/appsignal/oban.ex +++ b/lib/appsignal/oban.ex @@ -147,13 +147,15 @@ defmodule Appsignal.Oban do # If not present, assume the job failed. state = Map.get(metadata, :state, "failure") - span = - @tracer.current_span() - |> @span.set_attribute("state", to_string(state)) - - @span.add_error(span, kind, reason, stacktrace) + span = case @tracer.current_span() do + %Appsignal.Span{} = span -> span + _ -> @tracer.create_span("oban") + end - @tracer.close_span(span) + span + |> @span.set_attribute("state", to_string(state)) + |> @span.add_error(kind, reason, stacktrace) + |> @tracer.close_span() increment_job_stop_counter(worker, queue, state) From 84af625a0aaece8592d432230e94d3cd208bb0b1 Mon Sep 17 00:00:00 2001 From: Jeff Kreeftmeijer Date: Thu, 2 Oct 2025 12:51:41 +0200 Subject: [PATCH 2/6] Add context to ad-hoc exception spans Because ad-hoc exception spans in Oban don't have their context set when the span is created, set all context when handing the error. --- lib/appsignal/oban.ex | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/appsignal/oban.ex b/lib/appsignal/oban.ex index a62e0222..8c905a46 100644 --- a/lib/appsignal/oban.ex +++ b/lib/appsignal/oban.ex @@ -135,8 +135,11 @@ defmodule Appsignal.Oban do _event, %{duration: duration}, %{ - worker: worker, + id: id, + args: args, queue: queue, + worker: worker, + attempt: attempt, kind: kind, error: reason, stacktrace: stacktrace @@ -149,7 +152,15 @@ defmodule Appsignal.Oban do span = case @tracer.current_span() do %Appsignal.Span{} = span -> span - _ -> @tracer.create_span("oban") + _ -> + @tracer.create_span("oban") + |> @span.set_name("#{to_string(worker)}#perform") + |> @span.set_sample_data("params", args) + |> @span.set_attribute("id", id) + |> @span.set_attribute("queue", to_string(queue)) + |> @span.set_attribute("attempt", attempt) + |> @span.set_attribute("worker", to_string(worker)) + |> @span.set_attribute("appsignal:category", "job.oban") end span From 9e35f936968505dee2a906c9624a1cdd39dcea02 Mon Sep 17 00:00:00 2001 From: Jeff Kreeftmeijer Date: Fri, 3 Oct 2025 14:16:11 +0200 Subject: [PATCH 3/6] Check in tests for oban_job_exception/4 without span This test ensures oban_job_exception/4 creates a span when none exists, including the metadata that would have been added to the span if it existed before the exception was raised. --- test/appsignal/oban_test.exs | 84 ++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/test/appsignal/oban_test.exs b/test/appsignal/oban_test.exs index 1a280905..2faddd8c 100644 --- a/test/appsignal/oban_test.exs +++ b/test/appsignal/oban_test.exs @@ -332,6 +332,90 @@ defmodule Appsignal.ObanTest do end end + describe "oban_job_exception/4, when no span exists" do + setup do + fake_appsignal = start_supervised!(FakeAppsignal) + + with_config(%{}, &Appsignal.Oban.attach/0) + + execute_job_exception() + + [fake_appsignal: fake_appsignal] + end + + test "creates a span" do + assert {:ok, [{"oban"}]} = Test.Tracer.get(:create_span) + end + + test "closes the span" do + assert {:ok, [{%Span{}}]} = Test.Tracer.get(:close_span) + end + + test "sets the span's name" do + assert {:ok, [{%Span{}, "Test.Worker#perform"}]} = Test.Span.get(:set_name) + end + + test "sets the span's category" do + assert attribute?("appsignal:category", "job.oban") + end + + test "sets job arguments as span params" do + assert {:ok, [{%Span{}, "params", %{foo: "bar"}}]} = Test.Span.get(:set_sample_data) + end + + test "sets job attributes as span tags" do + assert attribute?("id", 123) + assert attribute?("worker", "Test.Worker") + assert attribute?("queue", "default") + assert attribute?("attempt", 1) + end + + test "sets the state attribute to failure" do + assert attribute?("state", "failure") + end + + test "adds the error to the span" do + assert {:ok, + [ + { + %Span{}, + :error, + %RuntimeError{message: "Exception!"}, + [{Appsignal.ObanTest, :execute_job_exception, _, _} | _] + } + ]} = Test.Span.get(:add_error) + end + + test "increments job stop counter", %{fake_appsignal: fake_appsignal} do + assert [ + %{key: _, value: 1, tags: %{state: "failure"}}, + %{key: _, value: 1, tags: %{state: "failure", queue: "default"}}, + %{key: _, value: 1, tags: %{state: "failure", worker: "Test.Worker"}}, + %{ + key: _, + value: 1, + tags: %{state: "failure", worker: "Test.Worker", queue: "default"} + } + ] = FakeAppsignal.get_counters(fake_appsignal, "oban_job_count") + end + + test "adds job duration distribution value", %{fake_appsignal: fake_appsignal} do + assert [ + %{key: _, value: 123, tags: %{worker: "Test.Worker"}}, + %{ + key: _, + value: 123, + tags: %{hostname: "Bobs-MBP.example.com", worker: "Test.Worker"} + }, + %{key: _, value: 123, tags: %{state: "failure", worker: "Test.Worker"}} + ] = FakeAppsignal.get_distribution_values(fake_appsignal, "oban_job_duration") + end + + test "does not detach the handler" do + assert attached?([:oban, :job, :exception]) + end + end + describe "oban_job_exception/4, with a :state metadata key (v2.4.0)" do setup do fake_appsignal = start_supervised!(FakeAppsignal) From 0835f4f16249e42f428109f7a2bb526180a2812e Mon Sep 17 00:00:00 2001 From: Jeff Kreeftmeijer Date: Fri, 3 Oct 2025 14:26:55 +0200 Subject: [PATCH 4/6] Add do_oban_job_start/1 to share span creation logic Because spans are now created from both oban_job_start/4 and oban_job_exeption/4, this patch adds do_oban_job_start/1, which is called from both functions. --- lib/appsignal/oban.ex | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/lib/appsignal/oban.ex b/lib/appsignal/oban.ex index 8c905a46..7ec3e524 100644 --- a/lib/appsignal/oban.ex +++ b/lib/appsignal/oban.ex @@ -50,15 +50,21 @@ defmodule Appsignal.Oban do def oban_job_start( _event, _measurements, - %{ - id: id, - args: args, - queue: queue, - worker: worker, - attempt: attempt - } = metadata, + metadata, _config ) do + do_oban_job_start(metadata) + end + + defp do_oban_job_start( + %{ + id: id, + args: args, + queue: queue, + worker: worker, + attempt: attempt + } = metadata + ) do span = @tracer.create_span("oban") span @@ -92,6 +98,8 @@ defmodule Appsignal.Oban do for tag <- Map.get(metadata, :tags, []) do @span.set_attribute(span, "job_tag_#{tag}", true) end + + span end def oban_job_stop( @@ -135,11 +143,8 @@ defmodule Appsignal.Oban do _event, %{duration: duration}, %{ - id: id, - args: args, queue: queue, worker: worker, - attempt: attempt, kind: kind, error: reason, stacktrace: stacktrace @@ -150,20 +155,7 @@ defmodule Appsignal.Oban do # If not present, assume the job failed. state = Map.get(metadata, :state, "failure") - span = case @tracer.current_span() do - %Appsignal.Span{} = span -> span - _ -> - @tracer.create_span("oban") - |> @span.set_name("#{to_string(worker)}#perform") - |> @span.set_sample_data("params", args) - |> @span.set_attribute("id", id) - |> @span.set_attribute("queue", to_string(queue)) - |> @span.set_attribute("attempt", attempt) - |> @span.set_attribute("worker", to_string(worker)) - |> @span.set_attribute("appsignal:category", "job.oban") - end - - span + (@tracer.current_span() || do_oban_job_start(metadata)) |> @span.set_attribute("state", to_string(state)) |> @span.add_error(kind, reason, stacktrace) |> @tracer.close_span() From 465a385806c7edddfe1188d40a52bb92cce365eb Mon Sep 17 00:00:00 2001 From: Jeff Kreeftmeijer Date: Tue, 7 Oct 2025 15:07:10 +0200 Subject: [PATCH 5/6] Add changeset to describe patch changes "Add support for Oban timeout exceptions" --- .changesets/add-support-for-oban-timeout-exceptions.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changesets/add-support-for-oban-timeout-exceptions.md diff --git a/.changesets/add-support-for-oban-timeout-exceptions.md b/.changesets/add-support-for-oban-timeout-exceptions.md new file mode 100644 index 00000000..18c3759f --- /dev/null +++ b/.changesets/add-support-for-oban-timeout-exceptions.md @@ -0,0 +1,6 @@ +--- +bump: minor +type: add +--- + +Add support for Oban timeout exceptions From ac4effed4e11096727dbb3740014b4672dd23f10 Mon Sep 17 00:00:00 2001 From: Jeff Kreeftmeijer Date: Wed, 8 Oct 2025 17:27:57 +0200 Subject: [PATCH 6/6] Update .changesets/add-support-for-oban-timeout-exceptions.md Co-authored-by: Noemi <45180344+unflxw@users.noreply.github.com> --- .changesets/add-support-for-oban-timeout-exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changesets/add-support-for-oban-timeout-exceptions.md b/.changesets/add-support-for-oban-timeout-exceptions.md index 18c3759f..f0675fd4 100644 --- a/.changesets/add-support-for-oban-timeout-exceptions.md +++ b/.changesets/add-support-for-oban-timeout-exceptions.md @@ -1,5 +1,5 @@ --- -bump: minor +bump: patch type: add ---