lib/nebulex/adapter/stats.ex

defmodule Nebulex.Adapter.Stats do
  @moduledoc """
  Specifies the stats API required from adapters.

  Each adapter is responsible for providing support for stats by implementing
  this behaviour. However, this module brings with a default implementation
  using [Erlang counters][https://erlang.org/doc/man/counters.html], with all
  callbacks overridable, which is supported by the built-in adapters.

  See `Nebulex.Adapters.Local` for more information about how this can be used
  from the adapter, and also [Nebulex Telemetry Guide][telemetry_guide] to learn
  how to use the Cache with Telemetry.

  [telemetry_guide]: http://hexdocs.pm/nebulex/telemetry.html
  """

  @doc """
  Returns `Nebulex.Stats.t()` with the current stats values.

  If the stats are disabled for the cache, then `nil` is returned.

  The adapter may also include additional custom measurements,
  as well as metadata.

  See `c:Nebulex.Cache.stats/0`.
  """
  @callback stats(Nebulex.Adapter.adapter_meta()) :: Nebulex.Stats.t() | nil

  @doc false
  defmacro __using__(_opts) do
    quote do
      @behaviour Nebulex.Adapter.Stats

      @impl true
      def stats(adapter_meta) do
        if counter_ref = adapter_meta[:stats_counter] do
          %Nebulex.Stats{
            measurements: %{
              hits: :counters.get(counter_ref, 1),
              misses: :counters.get(counter_ref, 2),
              writes: :counters.get(counter_ref, 3),
              updates: :counters.get(counter_ref, 4),
              evictions: :counters.get(counter_ref, 5),
              expirations: :counters.get(counter_ref, 6)
            },
            metadata: %{
              cache: adapter_meta[:name] || adapter_meta[:cache]
            }
          }
        end
      end

      defoverridable stats: 1
    end
  end

  import Nebulex.Helpers

  @doc """
  Initializes the Erlang's counter to be used by the adapter. See the module
  documentation for more information about the stats default implementation.

  Returns `nil` is the option `:stats` is set to `false` or it is not set at
  all; the stats will be skipped.

  ## Example

      Nebulex.Adapter.Stats.init(opts)

  > **NOTE:** This function is usually called by the adapter in case it uses
    the default implementation; the adapter should feed `Nebulex.Stats.t()`
    counters.

  See adapters documentation for more information about stats implementation.
  """
  @spec init(Keyword.t()) :: :counters.counters_ref() | nil
  def init(opts) do
    case get_boolean_option(opts, :stats, false) do
      true -> :counters.new(6, [:write_concurrency])
      false -> nil
    end
  end

  @doc """
  Increments the `counter`'s `stat_name` by the given `incr` value.

  ## Examples

      Nebulex.Adapter.Stats.incr(stats_counter, :hits)

      Nebulex.Adapter.Stats.incr(stats_counter, :writes, 10)

  > **NOTE:** This function is usually called by the adapter in case it uses
    the default implementation; the adapter should feed `Nebulex.Stats.t()`
    counters.

  See adapters documentation for more information about stats implementation.
  """
  @spec incr(:counters.counters_ref() | nil, atom, integer) :: :ok
  def incr(counter, stat_name, incr \\ 1)

  def incr(nil, _stat, _incr), do: :ok
  def incr(ref, :hits, incr), do: :counters.add(ref, 1, incr)
  def incr(ref, :misses, incr), do: :counters.add(ref, 2, incr)
  def incr(ref, :writes, incr), do: :counters.add(ref, 3, incr)
  def incr(ref, :updates, incr), do: :counters.add(ref, 4, incr)
  def incr(ref, :evictions, incr), do: :counters.add(ref, 5, incr)
  def incr(ref, :expirations, incr), do: :counters.add(ref, 6, incr)
end