Telemetry

This guide aims to show you how to instrument and report on :telemetry events in your application for cache statistics.

This guide is not focused on explaining :telemetry, how it works, configure it, and so on. Instead, it is focused on how we can use :telemetry for reporting cache stats. For more information about :telemetry you can check the docs, or the Phoenix Telemetry guide is also recommended.

Instrumenting Nebulex Caches

The stats support is something each adapter is responsible for. However, Nebulex built-in adapters support the stats suggested and defined by Nebulex.Cache.Stats. Besides, when the :stats option is enabled, we can use Telemetry for emitting the current stat values.

First of all, let's configure the dependencies adding :telemetry_metrics and :telemetry_poller packages:

def deps do
  [
    {:nebulex, "~> 2.0"},
    {:shards, "~> 0.6"},   #=> For using :shards as backend
    {:decorator, "~> 1.3"} #=> For using Caching Annotations
    {:telemetry_metrics, "~> 0.5"},
    {:telemetry_poller, "~> 0.5"}
  ]
end

Then define the cache and add the configuration:

defmodule MyApp.Cache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Local

  # Use stats helpers
  use Nebulex.Cache.Stats
end

Could be configured:

config :my_app, MyApp.Cache,
  stats: true,
  backend: :shards,
  gc_interval: 86_400_000,
  max_size: 1_000_000,
  gc_cleanup_min_timeout: 10_000,
  gc_cleanup_max_timeout: 900_000

Create your Telemetry supervisor at lib/my_app/telemetry.ex:

# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
  use Supervisor
  import Telemetry.Metrics

  def start_link(arg) do
    Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
  end

  def init(_arg) do
    children = [
      # Configure `:telemetry_poller` for reporting the cache stats
      {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}

      # For example, we use the console reporter, but you can change it.
      # See `:telemetry_metrics` for for information.
      {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end

  defp metrics do
    [
      # Nebulex Stats Metrics
      last_value("nebulex.cache.stats.hits", tags: [:cache]),
      last_value("nebulex.cache.stats.misses", tags: [:cache]),
      last_value("nebulex.cache.stats.writes", tags: [:cache]),
      last_value("nebulex.cache.stats.evictions", tags: [:cache]),
      last_value("nebulex.cache.stats.expirations", tags: [:cache]),

      # VM Metrics
      summary("vm.memory.total", unit: {:byte, :kilobyte}),
      summary("vm.total_run_queue_lengths.total"),
      summary("vm.total_run_queue_lengths.cpu"),
      summary("vm.total_run_queue_lengths.io")
    ]
  end

  defp periodic_measurements do
    [
      {MyApp.Cache, :dispatch_stats, []}
    ]
  end
end

Make sure to replace MyApp by your actual application name.

Then add to your main application's supervision tree (usually in lib/my_app/application.ex):

children = [
  MyApp.Cache,
  MyAppWeb.Telemetry,
  ...
]

Now start an IEx session and call the server:

iex(1)> MyApp.Cache.get 1
nil
iex(2)> MyApp.Cache.put 1, 1, ttl: 10
:ok
iex(3)> MyApp.Cache.get 1
1
iex(4)> MyApp.Cache.put 2, 2
:ok
iex(5)> MyApp.Cache.delete 2
:ok
iex(6)> Process.sleep(20)
:ok
iex(7)> MyApp.Cache.get 1
nil

and you should see something like the following output:

[Telemetry.Metrics.ConsoleReporter] Got new event!
Event name: nebulex.cache.stats
All measurements: %{evictions: 2, expirations: 1, hits: 1, misses: 2, writes: 2}
All metadata: %{cache: MyApp.Cache}

Metric measurement: :hits (last_value)
With value: 1
Tag values: %{cache: MyApp.Cache}

Metric measurement: :misses (last_value)
With value: 2
Tag values: %{cache: MyApp.Cache}

Metric measurement: :writes (last_value)
With value: 2
Tag values: %{cache: MyApp.Cache}

Metric measurement: :evictions (last_value)
With value: 2
Tag values: %{cache: MyApp.Cache}

Metric measurement: :expirations (last_value)
With value: 1
Tag values: %{cache: MyApp.Cache}

Adding other custom metrics

In the same way, you can, for instance, add another periodic measurement for reporting the cache size.

Using our previous cache:

defmodule MyApp.Cache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Local

  # Use stats helpers
  use Nebulex.Cache.Stats

  def dispatch_cache_size do
    :telemetry.execute(
      [:nebulex, :cache, :size],
      %{value: size()},
      %{cache: __MODULE__}
    )
  end
end

And add it to the list of periodic measurements in our previously defined supervisor:

defp periodic_measurements do
  [
    {MyApp.Cache, :dispatch_stats, []},
    {MyApp.Cache, :dispatch_cache_size, []}
  ]
end

Metrics:

defp metrics do
  [
    # Nebulex Stats Metrics
    last_value("nebulex.cache.stats.hits", tags: [:cache]),
    last_value("nebulex.cache.stats.misses", tags: [:cache]),
    last_value("nebulex.cache.stats.writes", tags: [:cache]),
    last_value("nebulex.cache.stats.evictions", tags: [:cache]),
    last_value("nebulex.cache.stats.expirations", tags: [:cache]),

    # Nebulex custom Metrics
    last_value("nebulex.cache.size.value", tags: [:cache]),

    # VM Metrics
    summary("vm.memory.total", unit: {:byte, :kilobyte}),
    summary("vm.total_run_queue_lengths.total"),
    summary("vm.total_run_queue_lengths.cpu"),
    summary("vm.total_run_queue_lengths.io")
  ]
end

If you start an IEx session like previously, you should see the new metric too:

[Telemetry.Metrics.ConsoleReporter] Got new event!
Event name: nebulex.cache.size
All measurements: %{value: 0}
All metadata: %{cache: MyApp.Cache}

Metric measurement: :value (last_value)
With value: 0
Tag values: %{cache: MyApp.Cache}