-
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.
Merge pull request #962 from appsignal/implement-heartbeat-checkins
Implement heartbeat check-ins
- Loading branch information
Showing
11 changed files
with
435 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,93 @@ | ||
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() | ||
|> Enum.reject(fn {_k, v} -> is_nil(v) end) | ||
|> Enum.into(%{}) | ||
|> 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.