-
Notifications
You must be signed in to change notification settings - Fork 82
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an `Appsignal.CheckIn.heartbeat` helper that emits a single heartbeat for the check-in identifier given. When called with `continuous: true` as the second argument, it starts and links a separate Elixir process that emits a heartbeat every thirty seconds. Unlike the equivalent functionality in the Ruby integration, which spawns a thread that will stay alive for the lifetime of the Ruby process, the Elixir process is linked to the process that spawned it, meaning it will be shut down when its parent process is shut down. This allows it to be used to track the lifetime of individual Elixir processes. Additionally, it is also possible to add `Appsignal.CheckIn.Heartbeat` as a child process to a supervisor, meaning its lifetime will be tied to that of the other processes supervised by it. Finally, the functionality seen in the Ruby integration could also be achieved by manually calling `Appsignal.CheckIn.Heartbeat.start/1`, keeping the process unlinked and therefore alive for the entirety of the Elixir node's lifetime, though this is unlikely to be useful in the Elixir process model.
- Loading branch information
Showing
11 changed files
with
406 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
--- | ||
bump: minor | ||
type: add | ||
--- | ||
|
||
Add support for heartbeat check-ins. | ||
|
||
Use the `Appsignal.CheckIn.heartbeat` method to send a single heartbeat check-in event from your application. This can be used, for example, in a `GenServer`'s callback: | ||
|
||
```elixir | ||
@impl true | ||
def handle_cast({:process_job, job}, jobs) do | ||
Appsignal.CheckIn.heartbeat("job_processor") | ||
{:noreply, [job | jobs], {:continue, :process_job}} | ||
end | ||
``` | ||
|
||
Heartbeats are deduplicated and sent asynchronously, without blocking the current thread. Regardless of how often the `.heartbeat` method is called, at most one heartbeat with the same identifier will be sent every ten seconds. | ||
|
||
Pass `continuous: true` as the second argument to send heartbeats continuously during the entire lifetime of the current process. This can be used, for example, during a `GenServer`'s initialisation: | ||
|
||
```elixir | ||
@impl true | ||
def init(_arg) do | ||
Appsignal.CheckIn.heartbeat("my_genserver", continuous: true) | ||
{:ok, nil} | ||
end | ||
``` | ||
|
||
You can also use `Appsignal.CheckIn.Heartbeat` as a supervisor's child process, in order for heartbeats to be sent continuously during the lifetime of the supervisor. This can be used, for example, during an `Application`'s start: | ||
|
||
```elixir | ||
@impl true | ||
def start(_type, _args) do | ||
Supervisor.start_link([ | ||
{Appsignal.CheckIn.Heartbeat, "my_application"} | ||
], strategy: :one_for_one, name: MyApplication.Supervisor) | ||
end | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
defmodule Appsignal.CheckIn.Event do | ||
alias __MODULE__ | ||
alias Appsignal.CheckIn.Cron | ||
|
||
@type kind :: :start | :finish | ||
@type check_in_type :: :cron | :heartbeat | ||
@type t :: %Event{ | ||
identifier: String.t(), | ||
digest: String.t() | nil, | ||
kind: kind | nil, | ||
timestamp: integer, | ||
check_in_type: check_in_type | ||
} | ||
|
||
defstruct [:identifier, :digest, :kind, :timestamp, :check_in_type] | ||
|
||
@spec cron(Cron.t(), kind) :: t | ||
def cron(%Cron{identifier: identifier, digest: digest}, kind) do | ||
%Event{ | ||
identifier: identifier, | ||
digest: digest, | ||
kind: kind, | ||
timestamp: System.system_time(:second), | ||
check_in_type: :cron | ||
} | ||
end | ||
|
||
@spec heartbeat(String.t()) :: t | ||
def heartbeat(identifier) do | ||
%Event{ | ||
identifier: identifier, | ||
timestamp: System.system_time(:second), | ||
check_in_type: :heartbeat | ||
} | ||
end | ||
|
||
@spec describe([t]) :: String.t() | ||
def describe([]) do | ||
# This shouldn't happen. | ||
"no check-in events" | ||
end | ||
|
||
def describe([%Event{check_in_type: :cron} = event]) do | ||
"cron check-in `#{event.identifier || "unknown"}` " <> | ||
"#{event.kind || "unknown"} event (digest #{event.digest || "unknown"})" | ||
end | ||
|
||
def describe([%Event{check_in_type: :heartbeat} = event]) do | ||
"heartbeat check-in `#{event.identifier || "unknown"}` event" | ||
end | ||
|
||
def describe([_event]) do | ||
# This shouldn't happen. | ||
"unknown check-in event" | ||
end | ||
|
||
def describe(events) do | ||
"#{Enum.count(events)} check-in events" | ||
end | ||
|
||
@spec redundant?(t, t) :: boolean | ||
def redundant?( | ||
%Event{check_in_type: :cron} = event, | ||
%Event{check_in_type: :cron} = new_event | ||
) do | ||
# Consider any existing cron check-in event redundant if it has the | ||
# same identifier, digest and kind as the one we're adding. | ||
event.identifier == new_event.identifier && | ||
event.kind == new_event.kind && | ||
event.digest == new_event.digest | ||
end | ||
|
||
def redundant?( | ||
%Event{check_in_type: :heartbeat} = event, | ||
%Event{check_in_type: :heartbeat} = new_event | ||
) do | ||
# Consider any existing heartbeat check-in event redundant if it has | ||
# the same identifier as the one we're adding. | ||
event.identifier == new_event.identifier | ||
end | ||
|
||
def redundant?(_event, _new_event), do: false | ||
end | ||
|
||
defimpl Jason.Encoder, for: Appsignal.CheckIn.Event do | ||
def encode(%Appsignal.CheckIn.Event{} = event, opts) do | ||
event | ||
|> Map.from_struct() | ||
|> Map.reject(&is_nil/1) | ||
|> Jason.Encode.map(opts) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
defmodule Appsignal.CheckIn.Heartbeat do | ||
use GenServer, shutdown: :brutal_kill | ||
|
||
@interval_milliseconds Application.compile_env( | ||
:appsignal, | ||
:appsignal_checkin_heartbeat_interval_milliseconds, | ||
30_000 | ||
) | ||
|
||
@impl true | ||
def init(identifier) do | ||
{:ok, identifier, {:continue, :heartbeat}} | ||
end | ||
|
||
def start(identifier) do | ||
GenServer.start(__MODULE__, identifier) | ||
end | ||
|
||
def start_link(identifier) do | ||
GenServer.start_link(__MODULE__, identifier) | ||
end | ||
|
||
def heartbeat(identifier) do | ||
GenServer.cast(__MODULE__, {:heartbeat, identifier}) | ||
:ok | ||
end | ||
|
||
@impl true | ||
def handle_continue(:heartbeat, identifier) do | ||
Appsignal.CheckIn.heartbeat(identifier) | ||
Process.send_after(self(), :heartbeat, @interval_milliseconds) | ||
{:noreply, identifier} | ||
end | ||
|
||
@impl true | ||
def handle_info(:heartbeat, identifier) do | ||
{:noreply, identifier, {:continue, :heartbeat}} | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.