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:
- A Phoenix LiveView app with the AppSignal package installed. If you have an app ready but haven't installed the AppSignal package, just follow this guide. Or you can clone the Phoenix app we'll be using throughout the tutorial.
- Basic experience working with Elixir and the Phoenix framework.
- An AppSignal account. Sign up for a free trial if you don't have one already.
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])
...
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
...
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
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
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:
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
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:
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
Using the instrument/2
function, we define a span called "Fetch games"
which results in this event trace:
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
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
This will create a transaction event, which you can visualize in your Events dashboard, as shown below:
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
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
And as you can see, the transaction times and throughput are now available for inspection on our dashboard:
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
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)