Skip to content

Commit

Permalink
Merge pull request #957 from appsignal/rename-heartbeats-to-cron-chec…
Browse files Browse the repository at this point in the history
…kins

Rename heartbeats to cron check-ins
  • Loading branch information
unflxw authored Aug 14, 2024
2 parents dd322d1 + 6d91d54 commit 23e55a0
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 172 deletions.
6 changes: 6 additions & 0 deletions .changesets/deprecate-heartbeats.md
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions .changesets/rename-heartbeats-to-cron-check-ins.md
Original file line number Diff line number Diff line change
@@ -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)
```
9 changes: 7 additions & 2 deletions lib/appsignal.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
115 changes: 115 additions & 0 deletions lib/appsignal/check_in.ex
Original file line number Diff line number Diff line change
@@ -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
106 changes: 16 additions & 90 deletions lib/appsignal/heartbeat.ex
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions test/appsignal/check_in_test.exs
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 23e55a0

Please sign in to comment.