lib/load/stats.ex

defmodule Stats do

  use GenServer

  require Logger

  @stats %{
    last_ms: 0, # last time stats were collected
    requests: 0,
    succeeded: 0,
    failed: 0
  }

  @impl true
  def init(args) do

    :pg.join(args.group, self())
    state = args
    |> Map.put(:stats_interval_ms, apply(:timer,
      Application.get_env(:load, :stats_timeunit, :seconds), [
      Application.get_env(:load, :stats_interval, 1)
    ]))
    |> Map.merge(Stats.empty())

    {:ok, state}
  end

  @impl true
  def handle_info({:update, stats}, state) do
    state = Map.merge(state, stats, fn _k, v1, v2 -> v1 + v2 end)

    state =
      case state.group do
        Local -> maybe_update(state, WS)
        Global -> maybe_update(state, nil)
      end

    {:noreply, state}
  end

  @impl true
  def handle_call(:get , _from, state) do
    {:reply, state |> Map.take([:history | Map.keys(Stats.empty())]), state}
  end

  def maybe_update(state, dest \\ Local) do
    now = DateTime.utc_now |> DateTime.to_unix(:millisecond)
    duration = now - state.last_ms
    if duration > state.stats_interval_ms do
      if Map.has_key?(state, :history) do
        %{state | history: [%{
          requests_rate: safe_div(state.requests, duration),
          succeeded_rate: safe_div(state.succeeded, duration),
          failed_rate: safe_div(state.failed, duration)} | state.history]}
      end
      :pg.get_local_members(dest)
      |> Enum.each(&send(&1, {:update, state |> Map.take(Map.keys(Stats.empty()))}))
      Map.merge(state, %{Stats.empty() | last_ms: now})
    else
      state
    end
  end

  @impl true
  def terminate(_reason, state) do
    Logger.info("terminated")
    :pg.leave(state.group, self())
    :ok
  end

  def empty, do: @stats

  def get do
    :pg.get_local_members(Global)
    |> Enum.map(&GenServer.call(&1, :get))
  end

  defp safe_div(count, duration_ms) do
    if duration_ms > 0 do
      count / duration_ms
    else
      0.0
    end
  end

end