Next Level Ruby on Rails Application Monitoring with AppSignal
Tags: ruby, rails
Published: 2022-01-12 13:09:58

In the [first of this two-part series](https://blog.appsignal.com/2021/12/01/ruby-on-rails-application-monitoring-with-appsignal.html), we covered how to set up AppSignal in a Ruby on Rails application for many great insights out of the box. AppSignal can automatically track errors, monitor performance, and report metrics about some dependencies.

But, in many cases, each of our applications behaves in different ways, so we'll want more than just generic monitoring.

In this post, we will run through adding custom instrumentation and monitoring to a Ruby on Rails application. This will give you deeper insights into how your application is behaving.

**Prerequisites if you want to follow along with the code:**

- An account on [www.appsignal.com](https://www.appsignal.com/)
- [Docker](https://www.docker.com/products/docker-desktop) installed and running (to use `docker-compose`)

To follow along with this post, you will need to [set up AppSignal in the sample application with your own AppSignal account](https://blog.appsignal.com/2021/12/01/ruby-on-rails-application-monitoring-with-appsignal.html).

## Custom Instrumentation and Monitoring

When you need more than what AppSignal instruments out of the box, the AppSignal gem allows you to add custom instrumentation to your Rails application.

### Instrumenting Parts of the Code

Let's say you want to add a new feature to an application. When a user visits `/posts` to view all posts, they should be able to filter for posts where the title begins with a specific letter (or something a lot more complex 🪄).

This new search functionality has already been implemented in the `Post` model with the method `Post.where_title_starts_with`. Let's update the `PostsController#index` to use the new method if a specific query parameter is present:

```ruby
# app/controllers/posts_controller.rb
  def index
    starts_with = params[:starts_with]
    @posts = if starts_with.present?
               Post.where_title_starts_with(starts_with)
             else
               Post.all
             end
  end
```

This is such a core part of your application that you'll want to know how it performs and when that performance changes. AppSignal provides a few ways to do this.

First, we will instrument the contents of the `Post.where_title_starts_with` method. If you want to receive insights about any code blocks, you can use [instrumentation blocks](https://docs.appsignal.com/ruby/instrumentation/instrumentation.html) to wrap the code blocks. Update the method like this:

```ruby
# app/models/post.rb
def self.where_title_starts_with(letter)
  Appsignal.instrument('Post.where_title_starts_with', "Fetch posts that start with letter") do
    Analytics.track_post_title_search(letter.downcase)
    select('*, pg_sleep(0.01)').where("title ILIKE :letter", letter: "#{letter.downcase}%").load
  end
end
```

Secondly, we also want to instrument the `Analytics.track_post_title_search` method being called because `app/services/analytics.rb` is doing some heavy processing. In this case, we will use [method instrumentation](https://docs.appsignal.com/ruby/instrumentation/method-instrumentation.html) to instrument the entire method more accurately:

```ruby
# app/services/analytics.rb
require 'appsignal/integrations/object'

class Analytics
  def self.track_post_title_search(letter, sleep = sleep(1))
    # Some heavy processing
    sleep 1
  end
  appsignal_instrument_class_method :track_post_title_search
end
```

#### Insights

A few minutes after saving the above to the application, take a look at whatever new information is available on your AppSignal dashboard (if you don't see the information, you may need to restart the docker containers again). You can verify that the new feature works by visiting the posts index page with a search param:
[http://localhost:3000/posts?starts_with=f](http://localhost:3000/posts?starts_with=f)

Depending on the number of posts created in the database, the
`/posts` endpoint will have become a lot slower.

If you open up performance issues on AppSignal ('Performance' -> 'Issue list') and view the `PostsController#index` action, lower down on the page, you should be able to see an 'Event Timeline'. This gives you a breakdown of how much time is spent running specific code:

![Performance issue event timeline](https://blog.appsignal.com/images/blog/2022-01/event-timeline.png)

This timeline exists for all performance events, but here, we can also see the custom instrumentation events. It shows us that calling `Post.where_title_starts_with` took 8.84 seconds to run, with 2.01 seconds used up by the `Analytics.track_post_title_search` method, and the remaining time used up by an active record query. You can also click into individual events for further investigation and see more information about their performance — e.g. the `sql.active_record` event.

AppSignal's instrumentation helpers give you a more detailed breakdown of the application code, so it's easier to gain insights into particular pieces of code that you think could impact an application's performance. You can learn more about this in [AppSignal's instrumentation guide](https://docs.appsignal.com/ruby/instrumentation/instrumentation.html).

### Exception Handling

Besides monitoring the performance of your code, you also need to know when the application doesn't behave as expected and where things go wrong. We've already seen how AppSignal reports exceptions that have not been handled by our code. But there's always a bit more than what comes out of the box.

You can start by removing existing code that causes an intermittent error. We see where this error occurs in the backtrace when viewing the error on the dashboard. Inside of `app/controllers/pages_controller.rb` remove the `if` statement:

```ruby
class PagesController < ApplicationController
  def home
    CreateRandomPostsJob.perform_later
  end
end
```

Now, in the overview dashboard, the application's error rate will drop significantly.

Currently, when a user tries to view a post that doesn't exist, the application crashes — e.g, [http://localhost:3000/posts/doesnotexist](http://localhost:3000/posts/doesnotexist).
Instead, you might want to show them a message. Add a rescue to where this could happen inside the `PostsController`. Update the `#set_post` method:

```ruby
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
	.
	.
	.
	private
    def set_post
      @post = Post.find(params[:id])
    rescue ActiveRecord::RecordNotFound => e
      render json: { error: "Oops. That post isn't here" } , status: :not_found
    end
	.
	.
end
```

Because we are manually handling the exception, it won't automatically be reported to AppSignal. You can still track errors manually by using `Appsignal.set_error`.

The easiest way to track an error is to simply add it as the function's only argument like `Appsignal.set_error(e)`. We also want to take advantage of the ability to add more context to the request. AppSignal allows you to tag events with your own arbitrary information using `Appsignal.tag_request`:

```ruby
def set_post
  Appsignal.tag_request(user_id: 'user-from-params', post_id: params[:id])
  @post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
  Appsignal.set_error(e)
  render json: { error: "Oops. That post isn't here" }, status: :not_found
end
```

Now visit [http://localhost:3000/posts/doesnotexist](http://localhost:3000/posts/doesnotexist) to verify that you get back the JSON response as expected, instead of having the application crash.

#### Insights

After you try to view a post that does not exist, the added updates ensure that the errors are reported to AppSignal.
On the AppSignal dashboard, in *'Errors -> Issue list'*, find and view the new reported error (`ActiveRecord::RecordNotFound`).

The error detail page gives us useful context about the error, which by default, includes information about the request such as the HTTP method, parameters, and session data. You can see that the custom tags are also included, which gives you the ability to filter for all of the errors with a matching tag.

![Error tags](https://blog.appsignal.com/images/blog/2022-01/tags.png)

Because we tagged the request, it adds this information to errors and other instrumented events. If you view an individual post a few times, e.g., [http://localhost:3000/posts/1](http://localhost:3000/posts/posts/1), you will notice that the tags are also included when you look at the performance measurement ('Performance' -> 'Issue list' -> 'PostsController#show'). You can [read more about tagging transactions in the guides](https://docs.appsignal.com/guides/custom-data/tagging-request.html).

This ability to add custom metadata to the transaction opens up many opportunities to help diagnose issues in production. A great example of this is [adding Kubernetes metadata to your errors](https://blog.appsignal.com/2021/06/23/adding-kubernetes-metadata-to-your-appsignal-errors.html).

### Metrics

Now that there is some custom instrumentation and error monitoring in place, you might realize that sometimes, there are large spikes in posts searches. Whenever a user searches, `Analytics#track_post_title_search` is called, which does some calculations and makes an API call to a third-party service. This third party has rate limits on the API. We want to track how often it is called to keep an eye on how close the application is to its limits.

AppSignal allows you to track custom metrics throughout the application as you wish.

First, we will track how often we're calling our analytics service and with what data, using a counter and tags:

```ruby
#app/services/analytics.rb
require 'appsignal/integrations/object'

class Analytics
  def self.track_post_title_search(letter, sleep = sleep(1))
    Appsignal.increment_counter("track_post_search", 1, { letter: letter })
    # Some heavy processing
    sleep 1
  end
  appsignal_instrument_class_method :track_post_title_search
end
```

Secondly, we will also track the number of posts being returned in the `PostsController#index`, because this is a core part of the application's behavior, and we know it keeps growing:

```ruby
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
	.
	.
  def index
    .
		.
    Appsignal.set_gauge("posts_index", @posts.size, starts_with: params[:starts_with])
  end
end
```

The fake traffic script still running on the application will generate some data, but to add more variety, let's also search for posts starting with [f](http://localhost:3000/posts?starts_with=f), [l](http://localhost:3000/posts?starts_with=l), and [v](http://localhost:3000/posts?starts_with=v).

####  Insights

To view the custom metrics, you will need to create a dashboard with custom graphs on AppSignal. This can be done through the UI, but we will just import one for this example. Under the 'Dashboard' section, click on 'Add dashboard' and import a dashboard with the following:

```json
{
  "title": "Post Search",
  "description": "Sample dashboard about posts search activity",
  "visuals": [
    {
      "title": "Analytics",
      "line_label": "%name% %letter%",
      "display": "LINE",
      "format": "number",
      "draw_null_as_zero": true,
      "metrics": [
        {
          "name": "track_post_search",
          "fields": [
            {
              "field": "COUNTER"
            }
          ],
          "tags": [
            {
              "key": "letter",
              "value": "*"
            }
          ]
        }
      ],
      "type": "timeseries"
    },
    {
      "title": "Search",
      "line_label": "%name% %starts_with%",
      "display": "LINE",
      "format": "number",
      "draw_null_as_zero": true,
      "metrics": [
        {
          "name": "posts_index",
          "fields": [
            {
              "field": "GAUGE"
            }
          ],
          "tags": [
            {
              "key": "starts_with",
              "value": "*"
            }
          ]
        }
      ],
      "type": "timeseries"
    }
  ]
}
```

<video width="100%" loop muted autoplay controls>
  <source
    src="https://blog.appsignal.com/images/blog/2022-01/custom-dashboard.mp4"
    type="video/mp4"
  />
  <source
    src="https://blog.appsignal.com/images/blog/2022-01/custom-dashboard.ogv"
    type="video/ogg"
  />
  <source
    src="https://blog.appsignal.com/images/blog/2022-01/custom-dashboard.webm"
    type="video/webm"
  />
</video>

You should see data on your graphs within a few minutes. Hovering over the lines shows you a legend of the metrics collected within the timeframe you're viewing.

Notice that this shows different lines for each tag value. Currently, our fake traffic is only searching for the letter `e`, but because we manually searched for other letters, you will see a new line on the graph for each one to indicate another data point.

Thought that was enough? AppSignal has more custom instrumentation solutions to offer that we won't be covering here. One that's worth a quick mention is [breadcrumbs](https://docs.appsignal.com/ruby/instrumentation/breadcrumbs.html). Breadcrumbs allow you to track a list of actions in your application, which will then be reported in an error. You'll have even more specific and ordered information about what led up to an error.

[Read all about custom instrumentation in the guides](https://docs.appsignal.com/ruby/instrumentation/).

## Wrap-up: Custom Instrumentation and Monitoring for Ruby Apps with AppSignal

[Part 1 of this series](https://blog.appsignal.com/2021/12/01/ruby-on-rails-application-monitoring-with-appsignal.html) covered the basic setup and use of AppSignal for your Ruby applications.

In this part, we've taken an application that already has great out-of-the-box monitoring and made it even better using the AppSignal gem.

AppSignal's custom instrumentation, error tracking, and performance monitoring features give you the insights you need into how your applications are behaving. It gives your application a lot out of the box while letting you take control when needed.

It's time to let your code run free in the wild, as long as you keep an eye on how it's doing. Happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**