elixir

Custom Instrumentation for a Phoenix App in Elixir with AppSignal

Aestimo Kirina

Aestimo Kirina on

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:

elixir
# lib/game_reviews_app/games.ex ... def get_game_with_reviews(id), do: Repo.get(Game, id) |> Repo.preload([:reviews]) ...

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

elixir
# 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 ...

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.

elixir
# 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

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

elixir
# 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

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:

elixir
# 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

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:

elixir
# 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

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:

elixir
# lib/game_reviews_app_web/controllers/game_controller.ex defmodule GameReviewsAppWeb.GameController do ... use Appsignal.Instrumentation.Decorators ... end

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

elixir
# 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

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:

elixir
# 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

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

elixir
# 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

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:

elixir
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

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!

Aestimo Kirina

Aestimo Kirina

Our guest author Aestimo is a full-stack developer, tech writer/author and SaaS entrepreneur.

All articles by Aestimo Kirina

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps