diff --git a/.changesets/deprecate-heartbeats.md b/.changesets/deprecate-heartbeats.md new file mode 100644 index 000000000..e6f2663d5 --- /dev/null +++ b/.changesets/deprecate-heartbeats.md @@ -0,0 +1,6 @@ +--- +bump: patch +type: deprecate +--- + +Calls to `Appsignal.heartbeat` and to methods in `Appsignal.Heartbeat` will emit a deprecation warning at compile-time. diff --git a/.changesets/rename-heartbeats-to-cron-check-ins.md b/.changesets/rename-heartbeats-to-cron-check-ins.md new file mode 100644 index 000000000..db3f0b31c --- /dev/null +++ b/.changesets/rename-heartbeats-to-cron-check-ins.md @@ -0,0 +1,18 @@ +--- +bump: patch +type: change +--- + +Rename heartbeats to cron check-ins. Calls to `Appsignal.heartbeat` and `Appsignal.Heartbeat` should be replaced with calls to `Appsignal.CheckIn.cron` and `Appsignal.CheckIn.Cron`, for example: + +```elixir +# Before +Appsignal.heartbeat("do_something", fn -> + do_something() +end) + +# After +Appsignal.CheckIn.cron("do_something", fn -> + do_something +end) +``` diff --git a/lib/appsignal.ex b/lib/appsignal.ex index 598809b6f..55a8c4ff3 100644 --- a/lib/appsignal.ex +++ b/lib/appsignal.ex @@ -165,8 +165,13 @@ defmodule Appsignal do defdelegate send_error(kind, reason, stacktrace), to: Appsignal.Instrumentation defdelegate send_error(kind, reason, stacktrace, fun), to: Appsignal.Instrumentation - defdelegate heartbeat(name), to: Appsignal.Heartbeat - defdelegate heartbeat(name, fun), to: Appsignal.Heartbeat + @spec heartbeat(String.t()) :: :ok + @deprecated "Use `Appsignal.CheckIn.cron/1` instead." + defdelegate heartbeat(name), to: Appsignal.CheckIn, as: :cron + + @spec heartbeat(String.t(), (-> out)) :: out when out: var + @deprecated "Use `Appsignal.CheckIn.cron/2` instead." + defdelegate heartbeat(name, fun), to: Appsignal.CheckIn, as: :cron defp log_nif_loading_error do arch = parse_architecture(to_string(:erlang.system_info(:system_architecture))) diff --git a/lib/appsignal/check_in.ex b/lib/appsignal/check_in.ex new file mode 100644 index 000000000..87584e4f6 --- /dev/null +++ b/lib/appsignal/check_in.ex @@ -0,0 +1,115 @@ +defmodule Appsignal.CheckIn do + alias Appsignal.CheckIn.Cron + + @spec cron(String.t()) :: :ok + def cron(identifier) do + Cron.finish(Cron.new(identifier)) + end + + @spec cron(String.t(), (-> out)) :: out when out: var + def cron(identifier, fun) do + cron = Cron.new(identifier) + + Cron.start(cron) + output = fun.() + Cron.finish(cron) + + output + end +end + +defmodule Appsignal.CheckIn.Cron do + alias __MODULE__ + alias Appsignal.CheckIn.Cron.Event + + @transmitter Application.compile_env( + :appsignal, + :appsignal_transmitter, + Appsignal.Transmitter + ) + @type t :: %Cron{identifier: String.t(), digest: String.t()} + + defstruct [:identifier, :digest] + + @spec new(String.t()) :: t + def new(identifier) do + %Cron{ + identifier: identifier, + digest: random_digest() + } + end + + defp random_digest do + Base.encode16(:crypto.strong_rand_bytes(8), case: :lower) + end + + @spec start(Cron.t()) :: :ok + def start(cron) do + transmit(Event.new(cron, :start)) + end + + @spec finish(Cron.t()) :: :ok + def finish(cron) do + transmit(Event.new(cron, :finish)) + end + + @spec transmit(Event.t()) :: :ok + defp transmit(event) do + if Appsignal.Config.active?() do + config = Appsignal.Config.config() + endpoint = "#{config[:logging_endpoint]}/check_ins/json" + + case @transmitter.transmit(endpoint, event, config) do + {:ok, status_code, _, _} when status_code in 200..299 -> + Appsignal.IntegrationLogger.trace( + "Transmitted cron check-in `#{event.identifier}` (#{event.digest}) #{event.kind} event" + ) + + {:ok, status_code, _, _} -> + Appsignal.IntegrationLogger.error( + "Failed to transmit cron check-in #{event.kind} event: status code was #{status_code}" + ) + + {:error, reason} -> + Appsignal.IntegrationLogger.error( + "Failed to transmit cron check-in #{event.kind} event: #{reason}" + ) + end + else + Appsignal.IntegrationLogger.debug( + "AppSignal not active, not transmitting cron check-in event" + ) + end + + :ok + end +end + +defmodule Appsignal.CheckIn.Cron.Event do + alias __MODULE__ + alias Appsignal.CheckIn.Cron + + @derive Jason.Encoder + + @type kind :: :start | :finish + @type t :: %Event{ + identifier: String.t(), + digest: String.t(), + kind: kind, + timestamp: integer, + check_in_type: :cron + } + + defstruct [:identifier, :digest, :kind, :timestamp, :check_in_type] + + @spec new(Cron.t(), kind) :: t + def new(%Cron{identifier: identifier, digest: digest}, kind) do + %Event{ + identifier: identifier, + digest: digest, + kind: kind, + timestamp: System.system_time(:second), + check_in_type: :cron + } + end +end diff --git a/lib/appsignal/heartbeat.ex b/lib/appsignal/heartbeat.ex index ffe02055a..29f59b099 100644 --- a/lib/appsignal/heartbeat.ex +++ b/lib/appsignal/heartbeat.ex @@ -1,100 +1,26 @@ defmodule Appsignal.Heartbeat do - alias __MODULE__ - alias Appsignal.Heartbeat.Event + alias Appsignal.CheckIn + alias Appsignal.CheckIn.Cron - @transmitter Application.compile_env( - :appsignal, - :appsignal_transmitter, - Appsignal.Transmitter - ) - @type t :: %Heartbeat{name: String.t(), id: String.t()} + @type t :: Cron.t() - defstruct [:name, :id] + @spec new(String.t()) :: Cron.t() + @deprecated "Use `Appsignal.CheckIn.Cron.new/1` instead." + defdelegate new(name), to: Cron - @spec new(String.t()) :: t - def new(name) do - %Appsignal.Heartbeat{ - name: name, - id: random_id() - } - end + @spec start(Cron.t()) :: :ok + @deprecated "Use `Appsignal.CheckIn.Cron.start/1` instead." + defdelegate start(cron), to: Cron - defp random_id do - Base.encode16(:crypto.strong_rand_bytes(8), case: :lower) - end - - @spec start(Heartbeat.t()) :: :ok - def start(heartbeat) do - transmit(Event.new(heartbeat, :start)) - end - - @spec finish(Heartbeat.t()) :: :ok - def finish(heartbeat) do - transmit(Event.new(heartbeat, :finish)) - end + @spec finish(Cron.t()) :: :ok + @deprecated "Use `Appsignal.CheckIn.Cron.finish/1` instead." + defdelegate finish(cron), to: Cron @spec heartbeat(String.t()) :: :ok - def heartbeat(name) do - finish(Heartbeat.new(name)) - end + @deprecated "Use `Appsignal.CheckIn.cron/1` instead." + defdelegate heartbeat(name), to: CheckIn, as: :cron @spec heartbeat(String.t(), (-> out)) :: out when out: var - def heartbeat(name, fun) do - heartbeat = Heartbeat.new(name) - - start(heartbeat) - output = fun.() - finish(heartbeat) - - output - end - - @spec transmit(Event.t()) :: :ok - defp transmit(event) do - if Appsignal.Config.active?() do - config = Appsignal.Config.config() - endpoint = "#{config[:logging_endpoint]}/heartbeats/json" - - case @transmitter.transmit(endpoint, event, config) do - {:ok, status_code, _, _} when status_code in 200..299 -> - Appsignal.IntegrationLogger.trace( - "Transmitted heartbeat `#{event.name}` (#{event.id}) #{event.kind} event" - ) - - {:ok, status_code, _, _} -> - Appsignal.IntegrationLogger.error( - "Failed to transmit heartbeat event: status code was #{status_code}" - ) - - {:error, reason} -> - Appsignal.IntegrationLogger.error("Failed to transmit heartbeat event: #{reason}") - end - else - Appsignal.IntegrationLogger.debug("AppSignal not active, not transmitting heartbeat event") - end - - :ok - end -end - -defmodule Appsignal.Heartbeat.Event do - alias __MODULE__ - alias Appsignal.Heartbeat - - @derive Jason.Encoder - - @type kind :: :start | :finish - @type t :: %Event{name: String.t(), id: String.t(), kind: kind, timestamp: integer} - - defstruct [:name, :id, :kind, :timestamp] - - @spec new(Heartbeat.t(), kind) :: t - def new(%Heartbeat{name: name, id: id}, kind) do - %Event{ - name: name, - id: id, - kind: kind, - timestamp: System.system_time(:second) - } - end + @deprecated "Use `Appsignal.CheckIn.cron/2` instead." + defdelegate heartbeat(name, fun), to: CheckIn, as: :cron end diff --git a/test/appsignal/check_in_test.exs b/test/appsignal/check_in_test.exs new file mode 100644 index 000000000..0a6571379 --- /dev/null +++ b/test/appsignal/check_in_test.exs @@ -0,0 +1,113 @@ +defmodule Appsignal.CheckInTest do + use ExUnit.Case + alias Appsignal.CheckIn + alias Appsignal.CheckIn.Cron + alias Appsignal.CheckIn.Cron.Event + alias Appsignal.FakeTransmitter + import AppsignalTest.Utils, only: [with_config: 2] + + setup do + start_supervised!(FakeTransmitter) + :ok + end + + describe "start/1 and finish/1, when AppSignal is not active" do + test "it does not transmit any events" do + cron = Cron.new("cron-checkin-name") + + with_config(%{active: false}, fn -> + Cron.start(cron) + Cron.finish(cron) + end) + + assert [] = FakeTransmitter.transmitted_payloads() + end + end + + describe "start/1" do + test "transmits a start event for the cron check-in" do + cron = Cron.new("cron-checkin-name") + Cron.start(cron) + + assert [ + %Event{identifier: "cron-checkin-name", kind: :start, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + end + end + + describe "finish/1" do + test "transmits a finish event for the cron check-in" do + cron = Cron.new("cron-checkin-name") + Cron.finish(cron) + + assert [ + %Event{identifier: "cron-checkin-name", kind: :finish, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + end + end + + describe "cron/2" do + test "transmits a start and finish event for the cron check-in" do + output = CheckIn.cron("cron-checkin-name", fn -> "output" end) + + assert [ + %Event{identifier: "cron-checkin-name", kind: :start, check_in_type: :cron}, + %Event{identifier: "cron-checkin-name", kind: :finish, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + + assert "output" == output + end + + test "does not transmit a finish event when the function throws an error" do + assert_raise RuntimeError, fn -> + CheckIn.cron("cron-checkin-name", fn -> raise "error" end) + end + + assert [ + %Event{identifier: "cron-checkin-name", kind: :start, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + end + end + + describe "cron/1" do + test "transmits a finish event for the cron check-in" do + CheckIn.cron("cron-checkin-name") + + assert [ + %Event{identifier: "cron-checkin-name", kind: :finish, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + end + end + + describe "deprecated heartbeat functions" do + test "forwards heartbeat/1 to CheckIn.cron/1" do + Appsignal.heartbeat("heartbeat-name") + + assert [ + %Event{identifier: "heartbeat-name", kind: :finish, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + end + + test "forwards heartbeat/2 to CheckIn.cron/2" do + output = Appsignal.heartbeat("heartbeat-name", fn -> "output" end) + + assert [ + %Event{identifier: "heartbeat-name", kind: :start, check_in_type: :cron}, + %Event{identifier: "heartbeat-name", kind: :finish, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + + assert "output" == output + end + + test "forwards new/1, start/1 and finish/1 to the CheckIn.Cron module" do + heartbeat = Appsignal.Heartbeat.new("heartbeat-name") + Appsignal.Heartbeat.start(heartbeat) + Appsignal.Heartbeat.finish(heartbeat) + + assert [ + %Event{identifier: "heartbeat-name", kind: :start, check_in_type: :cron}, + %Event{identifier: "heartbeat-name", kind: :finish, check_in_type: :cron} + ] = FakeTransmitter.transmitted_payloads() + end + end +end diff --git a/test/appsignal/heartbeat_test.exs b/test/appsignal/heartbeat_test.exs deleted file mode 100644 index fcd059b7e..000000000 --- a/test/appsignal/heartbeat_test.exs +++ /dev/null @@ -1,80 +0,0 @@ -defmodule Appsignal.HeartbeatTest do - use ExUnit.Case - alias Appsignal.FakeTransmitter - alias Appsignal.Heartbeat - alias Appsignal.Heartbeat.Event - import AppsignalTest.Utils, only: [with_config: 2] - - setup do - start_supervised!(FakeTransmitter) - :ok - end - - describe "start/1 and finish/1, when AppSignal is not active" do - test "it does not transmit any events" do - heartbeat = Heartbeat.new("heartbeat-name") - - with_config(%{active: false}, fn -> - Heartbeat.start(heartbeat) - Heartbeat.finish(heartbeat) - end) - - assert [] = FakeTransmitter.transmitted_payloads() - end - end - - describe "start/1" do - test "transmits a start event for the heartbeat" do - heartbeat = Heartbeat.new("heartbeat-name") - Heartbeat.start(heartbeat) - - assert [ - %Event{name: "heartbeat-name", kind: :start} - ] = FakeTransmitter.transmitted_payloads() - end - end - - describe "finish/1" do - test "transmits a finish event for the heartbeat" do - heartbeat = Heartbeat.new("heartbeat-name") - Heartbeat.finish(heartbeat) - - assert [ - %Event{name: "heartbeat-name", kind: :finish} - ] = FakeTransmitter.transmitted_payloads() - end - end - - describe "heartbeat/2" do - test "transmits a start and finish event for the heartbeat" do - output = Heartbeat.heartbeat("heartbeat-name", fn -> "output" end) - - assert [ - %Event{name: "heartbeat-name", kind: :start}, - %Event{name: "heartbeat-name", kind: :finish} - ] = FakeTransmitter.transmitted_payloads() - - assert "output" == output - end - - test "does not transmit a finish event when the function throws an error" do - assert_raise RuntimeError, fn -> - Heartbeat.heartbeat("heartbeat-name", fn -> raise "error" end) - end - - assert [ - %Event{name: "heartbeat-name", kind: :start} - ] = FakeTransmitter.transmitted_payloads() - end - end - - describe "heartbeat/1" do - test "transmits a finish event for the heartbeat" do - Heartbeat.heartbeat("heartbeat-name") - - assert [ - %Event{name: "heartbeat-name", kind: :finish} - ] = FakeTransmitter.transmitted_payloads() - end - end -end