lib/prom_ex/lifecycle_annotator.ex

defmodule PromEx.LifecycleAnnotator do
  @moduledoc """
  This GenServer is responsible to keeping track of the life cycle
  of the application and sending annotation requests to Grafana
  when the application starts and when it terminates. It will
  include things in the message like:

    - Hostname
    - OTP app name
    - App version
    - Git SHA of the last commit (if the GIT_SHA environment variable is present)
    - Git author of the last commit (if the GIT_AUTHOR environment variable is present)
  """

  use GenServer

  require Logger

  alias PromEx.GrafanaClient

  @doc """
  Used to start the `PromEx.LifecycleAnnotator` process.
  """
  @spec start_link(opts :: keyword()) :: GenServer.on_start()
  def start_link(opts) do
    {name, remaining_opts} = Keyword.pop(opts, :name)
    state = Map.new(remaining_opts)

    GenServer.start_link(__MODULE__, state, name: name)
  end

  @impl true
  def init(state) do
    # Trap exit so that the stop annotation can be published
    Process.flag(:trap_exit, true)

    {:ok, state, {:continue, :create_startup_annotation}}
  end

  @impl true
  def handle_continue(:create_startup_annotation, %{prom_ex_module: prom_ex_module, otp_app: otp_app} = state) do
    # Collect relevant info for application
    hostname =
      :inet.gethostname()
      |> elem(1)
      |> :erlang.list_to_binary()

    app_version =
      otp_app
      |> Application.spec(:vsn)
      |> to_string()

    git_sha =
      case System.fetch_env("GIT_SHA") do
        {:ok, git_sha} ->
          git_sha

        :error ->
          "Not available"
      end

    git_author =
      case System.fetch_env("GIT_AUTHOR") do
        {:ok, git_sha} ->
          git_sha

        :error ->
          "Not available"
      end

    state =
      state
      |> Map.put(:hostname, hostname)
      |> Map.put(:app_version, app_version)
      |> Map.put(:git_sha, git_sha)
      |> Map.put(:git_author, git_author)

    grafana_conn = GrafanaClient.build_conn(prom_ex_module)

    annotation_details = generate_annotation_details(state)
    annotation_text = ["#{to_string(otp_app)} is starting up\n" | annotation_details] |> Enum.join("\n")

    grafana_conn
    |> GrafanaClient.create_annotation(["prom_ex", to_string(otp_app), "start"], annotation_text)
    |> case do
      {:ok, _response_payload} ->
        Logger.info("PromEx.LifecycleAnnotator successfully created start annotation in Grafana.")

      {:error, reason} ->
        Logger.warning("PromEx.LifecycleAnnotator failed to create start annotation in Grafana: #{inspect(reason)}")
    end

    {:noreply, state}
  end

  @impl true
  def terminate(_reason, %{prom_ex_module: prom_ex_module, otp_app: otp_app} = state) do
    grafana_conn = GrafanaClient.build_conn(prom_ex_module)

    annotation_details = generate_annotation_details(state)
    annotation_text = ["#{to_string(otp_app)} is shutting down\n" | annotation_details] |> Enum.join("\n")

    grafana_conn
    |> GrafanaClient.create_annotation(["prom_ex", to_string(otp_app), "stop"], annotation_text)
    |> case do
      {:ok, _response_payload} ->
        Logger.info("PromEx.LifecycleAnnotator successfully created stop annotation in Grafana.")

      {:error, reason} ->
        Logger.warning("PromEx.LifecycleAnnotator failed to create stop annotation in Grafana: #{inspect(reason)}")
    end

    :ok
  end

  defp generate_annotation_details(state) do
    %{app_version: app_version, hostname: hostname, git_sha: git_sha, git_author: git_author} = state

    [
      "Hostname - #{hostname}",
      "App Version - #{app_version}",
      "Git SHA - #{git_sha}",
      "Git Author - #{git_author}"
    ]
  end
end