TelemetryUI

repository·master·Indexed 42 days ago

https://github.com/mirego/telemetry_ui

A metrics dashboard for Elixir applications that displays telemetry data directly in a web page. It uses a Postgres backend via EctoPostgres to persist events, eliminating the need for external monitoring infrastructure. Features include support for custom metrics, hot-reloading configuration, and the ability to expose metrics via Phoenix routes.

Tokens
14K
Snippets
42
Records
50
Agent score
52%

What's inside TelemetryUI

  1. Use dynamic configuration for hot-reloading

    master

    By default, TelemetryUI configuration is read once at startup. To enable hot-reloading of metrics without restarting the application, pass a function (anonymous or module-based) instead of a static configuration map.

    # Using anonymous function
    {TelemetryUI, config: fn -> telemetry_config() end}
    
    # Using module and function tuple
    {TelemetryUI, config: {MyApp.Telemetry, :config}}
  2. Limitations of TelemetryUI hot-reloading

    master

    While hot-reloading allows for dynamic adjustments, there are several constraints to keep in mind:

    • Event Handlers: New telemetry event handlers will be attached, but existing handlers continue to collect data.
    • Data Persistence: Historical data is preserved; only the visualization configuration changes.
    • Schema Changes: Database schema changes (such as switching to a different backend or a different repo) require a full application restart and cannot be hot-reloaded.
  3. Configure Page and Metric UI Options

    master

    You can fine-tune the layout and behavior of specific pages or individual metrics.

    Page UI Options

    Passed via ui_options in a 3-tuple metric definition: { "Title", [metrics], ui_options: [...] }.

    • :metrics_class: TailwindCSS grid classes (e.g., "grid-cols-4 gap-4"). Supports responsive variants like md:. Default: "grid-cols-1 md:grid-cols-3 gap-4".
    • :hidden: Boolean to hide the page from navigation. Default: false.
    • :styles: Custom CSS string applied to the page container.
    # Page level
    {"HTTP", http_metrics(), ui_options: [metrics_class: "grid-cols-4 gap-4"]}
    
    # Metric level
    counter("my.metric", ui_options: [class: "col-span-2"])
  4. Run multiple TelemetryUI instances

    master

    You can run multiple isolated TelemetryUI instances (e.g., for different dashboards) by providing a unique :name to each instance in your supervisor and specifying the corresponding :telemetry_ui_name in your router.

    # lib/my_app/application.ex
    children = [
      {TelemetryUI, telemetry_config() ++ [name: :admin]},
      {TelemetryUI, user_config() ++ [name: :user_dashboard]}
    ]
    
    # lib/my_app_web/router.ex
    get("/admin/metrics", TelemetryUI.Web, [], [assigns: %{telemetry_ui_allowed: true, telemetry_ui_name: :admin}])
    get("/user/metrics", TelemetryUI.Web, [], [assigns: %{telemetry_ui_allowed: true, telemetry_ui_name: :user_dashboard}])
  5. Define a Complex TelemetryUI Configuration

    master

    A complete TelemetryUI configuration is a list containing three main keys: metrics, theme, and backend.

    Metrics

    metrics is a list of tuples. Each tuple represents a page (metrics group) and follows the format: {"Page Name", metrics_list, ui_options: [...]}.

    Each metrics_list is composed of individual metric definitions (e.g., counter/2, average/2, distribution/2) imported from TelemetryUI.Metrics.

    Theme

    A map defining the visual appearance of the UI, including header_color, primary_color, title, share_key, and logo (SVG string).

    Backend

    A map or struct defining how data is stored and managed (e.g., %TelemetryUI.Backend.EctoPostgres{...}).

    defmodule MyApp.TelemetryUI do
      import TelemetryUI.Metrics
    
      def config do
        ui_options = [metrics_class: "grid-cols-8 gap-4"]
    
        [
          metrics: [
            {"HTTP", http_metrics(), ui_options: ui_options},
            {"System", system_metrics()}
          ],
          theme: theme(),
          backend: backend()
        ]
      end
    
      # ... helper functions for metrics, theme, and backend
    end
  6. Configure Top-Level TelemetryUI Options

    master

    The configuration passed to {TelemetryUI, config} is a keyword list. Use these options to define your metrics, storage backend, and UI appearance.

    Required Options

    :metrics

    Defines the metrics to display. You can use three formats:

    1. Simple list: All metrics appear on one unnamed page.
    2. Named pages (2-tuple): { "Page Title", [metrics] } groups metrics into titled pages.
    3. Named pages with UI options (3-tuple): { "Page Title", [metrics], ui_options: [...] } allows page-level customization like grid layouts or visibility.

    Optional Options

    • :backend: Configures storage. If omitted, metrics are not persisted. Currently, only EctoPostgres is supported.
    • :theme: A map of customization options for the UI (see Theme Options).
    • :name: An atom identifier (e.g., :admin). Required if running multiple TelemetryUI instances.
    • :config: A function or module/function tuple used for dynamic configuration to enable hot-reloading.
    metrics: [
      counter("phoenix.router_dispatch.stop.duration"),
      {"HTTP", [counter("phoenix.router_dispatch.stop.duration")]},
      {"HTTP", http_metrics(), ui_options: [metrics_class: "grid-cols-4 gap-4"]}
    ]
    
    # Dynamic config
    config: {MyApp.Telemetry, :config}
    config: fn -> MyApp.Telemetry.config() end
  7. Customize dashboard layout with TailwindCSS

    master

    You can control the layout of metrics using TailwindCSS classes. There are two levels of layout control:

    1. Individual Metric Layout: Use the ui_options: [class: "..."] key within a metric definition (like distribution/2) to apply classes to that specific metric's container.
    2. Metrics Container Layout: Use the ui_options: [metrics_class: "..."] key within the metrics list definition to apply classes to the container holding the metrics (e.g., to define a grid structure).

    All Tailwind grid classes are supported, including sm and lg variants for responsive mobile and desktop views.

    metrics = [
      distribution("my_app.repo.query.total_time",
        reporter_options: [buckets: [0, 4, 10, 50]],
        ui_options: [class: "col-span-2"],
        unit: {:native, :millisecond}
      ),
      distribution("phoenix.router_dispatch.stop.duration",
        reporter_options: [buckets: [0, 40, 100, 500]],
        ui_options: [class: "col-span-2"],
        unit: {:native, :millisecond}
      ),
      distribution("phoenix.router_dispatch.stop.duration",
        tags: [:route],
        reporter_options: [buckets: [0, 40, 100, 500]],
        ui_options: [class: "col-span-full"],
        unit: {:native, :millisecond}
      )
    ]
    
    [
      metrics: [
        {"Metrics layout page", metrics, ui_options: [metrics_class: "grid-cols-4 gap-4"]}
      ],
      backend: #...
      theme: #...
    ]
  8. Install TelemetryUI

    master

    To install TelemetryUI, add it to your mix.exs dependencies. You should also configure it to be disabled during tests to prevent background processes from running in your test environment.

    1. Add the dependency to mix.exs.
    2. Set disabled: true in config/test.exs.
    3. Run mix deps.get.
    # mix.exs
    def deps do
      [
        {:telemetry_ui, "~> 5.0"}
      ]
    end
    
    # config/test.exs
    config :telemetry_ui, disabled: true
  9. Configure multiple metrics endpoints with different permissions

    master

    You can expose different sets of metrics at different URL paths by using the telemetry_ui_name key in your Plug.Conn assigns. This allows you to control access to specific metrics dashboards using your application's existing routing and authentication logic (e.g., via pipe_through).

    To implement this:

    1. Define multiple routes in your router, each passing a unique :telemetry_ui_name in the assigns option.
    2. Create corresponding configuration functions in your Telemetry module that return metric definitions matching those names.
    3. Start a separate TelemetryUI process for each configuration in your application supervisor.
    # 1. Router configuration
    scope "/admin" do
      pipe_through [EnsureAdmin]
      get("/metrics", TelemetryUI.Web, :index, assigns: %{telemetry_ui_name: :admin})
    end
    
    scope "/user_dashboard" do
      pipe_through [EnsureUser]
      get("/metrics", TelemetryUI.Web, :index, assigns: %{telemetry_ui_name: :user_dashboard})
    end
    
    # 2. Telemetry module definitions
    defmodule MyApp.Telemetry do
      import TelemetryUI.Metrics
    
      def admin do
        [
          name: :admin,
          metrics: [
            {"Memory", [value_over_time("vm.memory.total", unit: {:byte, :megabyte})]}
          ],
          backend: backend(),
          theme: %{header_color: "purple", title: "Admin metrics"}
        ]
      end
    
      def user_dashboard do
        [
          name: :user_dashboard,
          metrics: [
            {"Followers", [count_over_time("myapp.users.new_follower")]}
          ],
          backend: backend(),
          theme: %{header_color: "blue", title: "User dashboard metrics"}
        ]
      end
    end
  10. Start multiple TelemetryUI processes in your application

    master

    When using multiple metrics endpoints, you must start a TelemetryUI process for each unique configuration in your application's supervision tree. Each process should be initialized with the list returned by your telemetry configuration functions.

    defmodule MyApp.Application do
      use Application
    
      def start(_type, _args) do
        children = [
          # ...
          {TelemetryUI, MyApp.Telemetry.admin()},
          {TelemetryUI, MyApp.Telemetry.user_dashboard()}
        ]
    
        opts = [strategy: :one_for_one, name: MyApp.Supervisor]
        Supervisor.start_link(children, opts)
      end
    end
  11. Expose TelemetryUI metrics via a web route

    master

    To view the metrics dashboard, add a route to your Phoenix router. For security, you must assign :telemetry_ui_allowed to true in the connection. This allows you to integrate the dashboard with your existing authentication/authorization logic.

    # Basic route
    scope "/" do
      get("/metrics", TelemetryUI.Web, [], [assigns: %{telemetry_ui_allowed: true}])
    end
    
    # Example with existing auth pipeline
    pipeline :admin_protected do
      plug(MyAppWeb.EnsureCurrentUser)
      plug(MyAppWeb.EnsureRole, :admin)
      plug(:enable_telemetry_ui)
    end
    
    def enable_telemetry_ui(conn, _), do: assign(conn, :telemetry_ui_allowed, true)
  12. Display application-defined data using count_over_time/2

    master

    You can display existing application metrics (like database counts) in telemetry_ui without using the telemetry package by using the data_resolver option.

    To do this, provide a function to data_resolver that accepts an options argument (containing :from and :to timestamps) and returns {:ok, data}. The returned data should follow the same format used by telemetry_ui_events to ensure consistent graph layouts.

    For a count-over-time metric, the resolver should return a list of maps where each map contains a :date and a :count key.

    count_over_time(:data,
      description: "Users count",
      unit: " users",
      data_resolver: fn options ->
        query =
          from(
            users in "users",
            select: %{date: users.inserted_at, count: 1},
            where: users.inserted_at >= ^options.from and users.inserted_at <= ^options.to
          )
    
        {:ok, MyApp.Repo.all(query)}
      end
    )