DEV Community

Cover image for Custom Instrumentation for a Phoenix App in Elixir with AppSignal
Aestimo K. for AppSignal

Posted on • Originally published at blog.appsignal.com

Custom Instrumentation for a Phoenix App in Elixir with AppSignal

In the first part of this series, we saw that even if you just use AppSignal’s default application monitoring, you can get a lot of information about how your Phoenix application is running.

Even so, there are many ways in which a Phoenix application may exhibit performance issues, such as slow database queries, poorly engineered LiveView components, views that are too heavy, or non-optimized assets.

To get a grasp on such issues and peer even more closely into your app’s internals, you’ll need something that packs more punch than the default dashboards: custom instrumentation.

In this article, we'll go through a step-by-step process to build custom instrumentation using AppSignal for a Phoenix application.

Pre-requisites

Before we get into custom instrumentation, here's what you need to follow along:

What Is Custom Instrumentation, and Why Do We Need It?

Custom instrumentation means adding additional telemetry emission and monitoring functionality to your Phoenix app beyond what is provided by the default instrumentation. Specifically, it involves the use of functions and macros (provided through the AppSignal package) in specific places in your codebase that you want to investigate further. You can then view the collected data on a custom AppSignal dashboard.

If you have a complex or mission-critical Phoenix app, it might not be possible to identify all its potential performance issues using the default instrumentation. It's important to have deeper insights into what's going on inside your app.

With custom instrumentation, you can easily structure exactly what you need to identify and visualize these custom data collections within the AppSignal dashboard.

Getting Started with Custom Instrumentation in AppSignal

You can implement custom instrumentation using AppSignal in two ways: through function decorators or instrumentation helpers.

Function Decorators

A function decorator is a higher-order function that you wrap around a function from which you want to collect data. They give you more granular control than instrumentation helpers, but compared to the latter, they are not as flexible.

Instrumentation Helpers

AppSignal also provides instrumentation helper functions, which you can use to instrument specific parts of your code manually. These helper functions can start and stop custom measurements, help you track custom events, and add custom metadata to metrics reported via AppSignal dashboards. This approach gives you more flexibility in instrumenting your code but requires more manual intervention.

In the following sections, we'll use what we've learned to implement custom instrumentation for a Phoenix application.

How to Use an Instrumentation Helper

To implement the first custom instrumentation, we'll consider a controller action that calls a potentially slow function in a simple Phoenix app featuring games and reviews.

As you can see below, the games context has a method for fetching an individual game and preloading any reviews:

# lib/game_reviews_app/games.ex

...
def get_game_with_reviews(id), do: Repo.get(Game, id) |> Repo.preload([:reviews])
...
Enter fullscreen mode Exit fullscreen mode

And the subsequent action using this method in the game controller is:

# lib/game_reviews_app_web/controllers/game_controller.ex

...
def show(conn, %{"id" => id}) do
  game=Games.get_game_with_reviews(id)
  render(conn, :show, game: game)
end
...
Enter fullscreen mode Exit fullscreen mode

Now let's modify this controller action with a custom instrumentation helper to check how many times it is called and its response times. We can do this using AppSignal's instrument/2 function, which takes two parameters:

  • The function name
  • The function being instrumented

Note: If you use instrument/3 instead, it's possible to add an event group name as an optional parameter. Usually, whenever you use instrument/2, measurements will be collected and categorized under the "other" event group within the event groups section. But if you wanted another more descriptive name, instrument/3 allows you to pass in an additional parameter for the event group name.

# lib/game_reviews_app_web/controllers/game_controller.ex

defmodule GameReviewsAppWeb.GameController do
  use GameReviewsAppWeb, :controller

  ...

  def show(conn, %{"id"=>id}) do
    game = Games.get_game_with_reviews(id)
    am_i_slow() # add this line
    render(conn, :show, game: game)
  end
  ...
end
Enter fullscreen mode Exit fullscreen mode

Let's define the am_i_slow function as a private module:

# lib/game_reviews_app_web/controllers/game_controller.ex

defmodule GameReviewsAppWeb.GameController do
  use GameReviewsAppWeb, :controller

  ...
  defp am_i_slow do
    Appsignal.instrument("Check if am slow", fn->
      :timer.sleep(1000)
    end)
  end
end
Enter fullscreen mode Exit fullscreen mode

Specifically, we're defining a custom trace span with an event sample called "Check if am slow". This will show up in our dashboard under the other (background) event namespace, as you can see in the screenshots below:

Custom instrumentation using an instrumentation helper - 1

Custom instrumentation using an instrumentation helper - 2

Custom instrumentation using an instrumentation helper - 3

But what if you wanted a different event group name from "other" or "background" for the event namespace? Just use the instrument/3 function:

# lib/game_reviews_app_web/controllers/game_controller.ex

defmodule GameReviewsAppWeb.GameController do
  use GameReviewsAppWeb, :controller

  ...
  defp am_i_slow do
    Appsignal.instrument("Really Slow Queries","First Slow Query", fn->
      :timer.sleep(1000)
    end)
  end
end
Enter fullscreen mode Exit fullscreen mode

Here, we give the span a category name of "Really Slow Queries" and a span name of "First Slow Query", for a sample view like this:

Instrumentation helper with custom category name

Here's another example of using function helpers:

# lib/game_reviews_app_web/controllers/game_controller.ex

defmodule GameReviewsAppWeb.GameController do
  use GameReviewsAppWeb, :controller

  ...
  def index(conn,_params) do
    games = Appsignal.instrument("Fetch games", fn->
    Games.list_games()
  end)

  render(conn, :index, games: games)
  end
end
Enter fullscreen mode Exit fullscreen mode

Using the instrument/2 function, we define a span called "Fetch games" which results in this event trace:

Instrumentation helper - 5

With that, you can easily visualize the function's response time and throughput.

There are more options available to customize AppSignal's instrumentation helpers than what I've shown here. I highly encourage you to check out the possibilities.

Next, let's see how you can use function decorators.

Using Function Decorators

As you've probably noticed when using instrumentation helpers, you end up modifying existing functions with the helper code you add. If you don't want to do this, you can use function decorators instead.

Let's continue working with the game controller and instrument the index method using a decorator. First, we will add the decorator module Appsignal.Instrumentation.Decorators:

# lib/game_reviews_app_web/controllers/game_controller.ex

defmodule GameReviewsAppWeb.GameController do
  ...
  use Appsignal.Instrumentation.Decorators
  ...
end
Enter fullscreen mode Exit fullscreen mode

You now have access to the decorator's functions. Let's decorate the index method as shown below:

# lib/game_reviews_app_web/controllers/game_controller.ex

defmodule GameReviewsAppWeb.GameController do
  ...
  use Appsignal.Instrumentation.Decorators

  def show(conn, %{"id"=>id}) do
    am_i_slow()
    game = Games.get_game_with_reviews(id)
    render(conn,:show,game:game)
  end

  # add the decorator function
  @decorate transaction_event()
  defp am_i_slow do
    :timer.sleep(1000)
  end

end
Enter fullscreen mode Exit fullscreen mode

This will create a transaction event, which you can visualize in your Events dashboard, as shown below:

Function decorator - 1

You get all the information you need, namely:

  • a. The resource where the function decorator was called
  • b. A sample breakdown showing how long Ecto queries took, how long the templates took to load, and so forth.
  • c. An event timeline with a breakdown of the transaction time of everything involved in that function.

Finally, let's take a look at instrumenting Phoenix channels.

Instrumenting Phoenix LiveViews and Channels

In the example below, we have the welcome live view as shown:

# game_reviews_app_web/live/welcome_live.ex

defmodule GameReviewsAppWeb.WelcomeLive do
  use GameReviewsAppWeb, :live_view

  def mount(_params,_session,socket) do
    {:ok,assign(socket, current_time: DateTime.utc_now())}
  end

  def render(assigns) do
    ~H"""
      <div class="container">
        <h1>Welcome to my LiveView App</h1>
        <p>Current time: <%= @current_time %></p>
      </div>
    """
  end
end
Enter fullscreen mode Exit fullscreen mode

Let's use an instrumentation helper to see how this live view performs:

# game_reviews_app_web/live/welcome_live.ex

defmodule GameReviewsAppWeb.WelcomeLive do
  ...

  import Appsignal.Phoenix.LiveView,only:[instrument: 4]

  def mount(_params,_session,socket) do
    instrument(__MODULE__,"Liveview instrumentation", socket, fn->
      :timer.send_interval(1000,self(),:tick)
      {
        :ok,
        assign(socket,current_time:DateTime.utc_now())
      }
    end)
  end

  ...
end
Enter fullscreen mode Exit fullscreen mode

And as you can see, the transaction times and throughput are now available for inspection on our dashboard:

Instrumenting Liveviews

It's also worth noting that the AppSignal for Elixir package enables you to instrument Phoenix channels using a custom function decorator:

defmodule GameReviewsAppWeb.VideoChannel do
  use GameReviewsAppWeb, :channel

  # first add the instrumentation decorators module
  use Appsignal.Instrumentation.Decorators

  # then add the decorator function
  @decorate channel_action()
  def join("videos:" <> video_id, _params, socket) do
    {:ok,assign(socket, :video_id, String.to_integer(video_id))}
  end
end
Enter fullscreen mode Exit fullscreen mode

And that's it!

Wrapping Up

In part one of this series, we looked at how to set up AppSignal for an Elixir app and AppSignal's error tracking functionality.

In this article, we've seen how easy it is to use AppSignal's Elixir package to implement custom instrumentation for a Phoenix application. We've also learned how to use instrumentation helpers and function decorators.

With this information, you can now easily decide when to use a decorator versus an instrumentation helper in your next Phoenix app.

Happy coding!

P.S. If you'd like to read Elixir Alchemy posts as soon as they get off the press, subscribe to our Elixir Alchemy newsletter and never miss a single post!

Top comments (0)