A repository of bitesize articles, tips & tricks
(in both English and French) curated by Mirego’s team.

Telemetry UI

TLDR; We use telemetry_ui to display telemetry based metrics in our Phoenix apps.

Screenshot of /metrics showcasing values and charts

At Mirego, we used to deploy a lot of web applications on Heroku. We then had access to the Heroku metrics page that could tell us, at a glance, if our app was doing okay, responding fast, consuming anormal amount of memory, etc.

Nowadays, we deploy our web applications on a lot of platforms: Azure, AWS, Google Cloud Platform… Seeing global metrics for our apps is now trickier since all platforms have their own way of dealing with telemetry. We can also rely on external tool like New Relic, Honeycomb or Sentry to get insights out of those events. That means paying for a product, adding the integration to your app, controlling who on your team has access to the tool and sending user data on another platform.

In Elixir, our most used packages come with a built-in integration of Erlang’s telemetry package. This means that they expose metrics through a uniform interface: telemetry. Phoenix, Absinthe, Ecto, Oban, Tesla etc. They emit useful events, with measurements and metadata that can be listened to by our system. The Phoenix documentation shows how simple it is to use telemetry. The Reporter part is where things gets interesting. We can implement our own reporter that will record those events to query them later. PhoenixLiveDashboard has this goal with the Metrics section. But it does not support time filtering, other graphs beside a line chart and the persistence is not baked in the package.

Enters Telemetry UI: an Elixir package that stores events in PostgreSQL and displays them inside our Phoenix application. The data stays on the same infrastructure and you can control how you display it. The configuration is not far from the official Phoenix example:

# application.ex
def start(_type, _args) do
  children = [
    MyApp.Repo,
    MyAppWeb.Endpoint,
    {Phoenix.PubSub, [name: MyApp.PubSub, adapter: Phoenix.PubSub.PG2]},
    {TelemetryUI, telemetry_config()}
  ]

  opts = [strategy: :one_for_one, name: MyApp.Supervisor]
  Supervisor.start_link(children, opts)
end

def telemetry_config do
  import Telemetry.Metrics

  metrics = [
    last_value("vm.memory.total"),
    counter("phoenix.router_dispatch.stop.duration", description: "Number of requests"),
    summary("phoenix.router_dispatch.stop.duration",
      description: "Average HTTP request duration",
      unit: {:native, :millisecond},
    ),
    summary("phoenix.router_dispatch.stop.duration",
      description: "HTTP request count",
      unit: {:native, :millisecond},
      reporter_options: [value_field: "count"]
    ),
    summary("phoenix.router_dispatch.stop.duration",
      tags: [:route],
      description: "HTTP requests count by route",
      reporter_options: [value_field: "count"],
      unit: {:native, :millisecond}
    )
  ]

  [
    metrics: metrics,
    backend: %TelemetryUI.Backend.EctoPostgres{
      repo: MyApp.Repo,
      pruner_threshold: [months: -1],
      pruner_interval: 84_000,
      max_buffer_size: 10_000,
      flush_interval_ms: 10_000
    }
  ]
end