Using Elixometer With Phoenix

Elixometer metrics graphs

Exometer is a great tool for collecting metrics within the Erlang ecosystem. It allows you to easily capture metrics in your application and push those metrics to an aggregator for you to view with pretty graphs. Recently Pinterest released Elixometer, a thin wrapper around Exometer, to make collecting metrics easier within the Elixir ecosystem.

If you're interested in learning how to create a basic metric collection setup with Exometer, Michael Schäfermeyer has a wonderful guide to Monitoring Phoenix. My post extends what he has already provided and uses Elixometer instead of Exometer.

Configuration

Modify your mix.exs config file to add Elixometer as a dependency:

defmodule MyApp.Mixfile do
  ...

  def application do
    [mod: {MyApp, []},
     applications: [
       ...,
       :elixometer,
       :exometer
     ]
    ]
  end

  defp deps do
    [
     ...,
      {:elixometer, github: "pinterest/elixometer"},
      {:exometer, github: "Feuerlabs/exometer"},
      {:exometer_core, "~>1.4.0", override: true},
      {:lager, "~> 3.2.1", override: true}
    ]
  end

As of Sept 13, 2016, these deps should work thanks to Andrew Summers.

Update your config.ex and add Elixometer reporting settings and metrics collection:

memory_stats = ~w(atom binary ets processes total)a

config :exometer,
   predefined: [
     {
       ~w(erlang memory)a,
       {:function, :erlang, :memory, [], :proplist, memory_stats},
       []
     }
   ],
   report: [
     reporters: [{:exometer_report_statsd, []}],
     subscribers: [
       {
         :exometer_report_statsd,
         [:erlang, :memory], memory_stats, 1_000, true
       }
     ]
   ]

config :elixometer,
  reporter: :exometer_report_statsd,
    env: Mix.env,
    metric_prefix: "myapp"

You're configured to start collecting metrics including Erlang VM memory usage. You can add other predefined metrics here that you can't easily capture with Elixometer.

Measuring Ecto

If you're using Ecto version 1.1x, update your repo.ex to use Elixometer:

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :myapp
  use Elixometer

  def log(entry) do
    update_histogram("query_exec_time", (entry.query_time + entry.queue_time || 0) / 1000)
    update_histogram("query_queue_time", (entry.queue_time || 0) / 1000)
    update_spiral("query_count", 1)

    super log_entry
  end
end

Query execution time, query queue time, and query counts are now being measured.

If you're using Ecto 2.0.x there's a little more work involved since Repo.log/1 is no longer overridable. Create a file repo_metrics.ex and copy this:

defmodule MyApp.Repo.Metrics do
  use Elixometer

  def record_metric(entry) do
    update_histogram("query_exec_time", entry.query_time + (entry.queue_time || 0))
    update_histogram("query_queue_time", (entry.queue_time || 0))
    update_spiral("query_count", 1)
  end
end

Update your config.ex to use this module for Ecto logging.

config :myapp, MyApp.Repo,
  loggers: [{Ecto.LogEntry, :log, []}, {MyApp.Repo.Metrics, :record_metric, []}]

Now you're set to collect Ecto metrics in microseconds.

Measuring Channels

You might have to do a some more code modifications to measuring channels, but here something to get you started.

defmodule MyApp.SomeChannel
  use MyApp.Web, :channel
  use Elixometer

  # Have all channel messages go to a single point
  @timed(key: "channel_resp_time", units: :millis)
  def handle_in(event, params, socket) do

    # Use a different named function so we can measure messages
    response = handle_event(event, params, socket)
   
    # update event count
    update_spiral("channel_event_count", 1)
    
    response
  end

  # Methods that will handle logic
  def handle_event("some:topic", _params, socket) do
     ...
  end
 
  ...

end

Measuring Controllers

Create a new file elixometer_plug.ex and copy this code (thanks Renan Ranelli for the fix):

defmodule ElixometerPlug do
  @behaviour Plug
  import Plug.Conn, only: [register_before_send: 2]
  use Elixometer

  def init(opts), do: opts

  def call(conn, _config) do
    req_start_time = :os.timestamp

    register_before_send conn, fn conn ->
      # increment count
      update_spiral("resp_count", 1)

      # log response time in microseconds
      req_end_time = :os.timestamp
      duration = :timer.now_diff(req_end_time, req_start_time)
      update_histogram("resp_time", duration)

      conn
    end
  end
end

You can add the plug individually to your controllers or you could apply them to all of your controllers automatically by updating the web.ex file and updating the :controller section:

defmodule MyApp.Web do
  ...

  def controller do
    quote do
      ...

      # Add the Elixometer plug
      plug ElixometerPlug
    end
  end

  ...

end

Wrap Up

Now you're setup to collect metrics in your Phoenix application to give you or your team more insight on your application's performance with very little code change. Hook up your reporter to an application like graphite and see the data flow!

If you have any suggestions or questions, please let me know!

#elixir   •   #phoenix   •   #exometer   •   #elixometer