In the last few months, we on the New Relic Ruby agent team have been getting questions about the agent’s support for Module#prepend usage. However, the agent generally uses alias_method chains to instrument your code. These two metaprogramming techniques jam up when directly competing against each other—at least, in some setups.

In this post, I will cover what each of these techniques are and discuss when they might come into conflict. I’ll also go over our recommendations for customers encountering this issue and what we’re considering for future releases of the agent in order to resolve it.

Overview of how the agent works

The New Relic Ruby agent is designed to monitor your application and help you identify and solve performance issues. While there are public APIs for custom instrumentation, most customers use the agent’s auto-detected framework and library instrumentation. For example, if the agent detects that you’re using Rails 4 with Puma and Dalli, it automatically installs all the relevant instrumentation. You can see what instrumentation has been installed by tailing the newrelic_agent.log when starting up your app and grepping for lines that look like INFO : Installing ActiveRecord 4+ middleware instrumentation.

How does the agent accomplish this magic of automatic instrumentation? By reading through the source code of the libraries we want to support, we can determine commonly used public methods. We then use metaprogramming to modify those methods and insert New Relic instrumentation code. Your app calls and uses these methods without any changes, but behind the scenes the agent can see and instrument those code paths being used, while the library ends up evaluating your call just the same. This has the happy result of no additional work needed by our customers!

What are alias_method and Module#prepend?

alias_method makes a copy of an existing method, but with a new name. Using multiple alias_method calls is a metaprogramming technique known as “alias_method chaining”, which allows Rubyists to wrap an existing method with new code. In the Ruby agent, we define a “New Relic version” of library methods that include our instrumentation to measure your application’s performance. The New Relic version still ultimately calls the original method, thanks to this alias_method chaining technique. For example, the agent defines a log_with_newrelic_instrumentation method, which collects data and then calls a method referenced as log_without_newrelic_instrumentation. It then opens up ActiveRecord::ConnectionAdapters::AbstractAdapter and includes a New Relic module that adds in these lines:


alias_method :log_without_newrelic_instrumentation, :log
alias_method :log, :log_with_newrelic_instrumentation

With this chaining, any time your app calls this log method, it actually ends up calling log_with_newrelic_instrumentation, which in turn eventually calls log_without_newrelic_instrumentation, a copy of the original log method.

Using alias_method has been a standard practice in the Ruby community for many years to insert some extra code around an existing method. You can see it being used throughout Rails, for example. However, one downside is that it can be difficult to determine where this patching has been done, or who was responsible, if multiple players are metaprogramming. This is why the New Relic Ruby agent always tries to install itself last, after any other libraries that might be involved.

Module#prepend was introduced a few years ago in Ruby 2.0. Before having the Module#prepend option, you could only add module methods into a class by including the module into that class. When a module is included in a class, Ruby looks up method calls in the class definition first, then in each included module, and then continues up the inheritance chain through the class’ ancestors.


class A < B
  include C
end
thing = A.new
thing.mysterious_method

Order of looking for where mysterious_method is defined:

 

ruby agent chart

 

Module#prepend allows you to insert that module below the class in the ancestors chain so that any method calls will first look within the module for a definition. You can then call super to execute the original method.


class A < B
  prepend C
end
 
thing = A.new
thing.mysterious_method

Order of looking for where mysterious_method is defined:

 

ruby agent chart

 

When do these techniques conflict?

For alias_method and Module#prepend, you could certainly use both techniques in the same project, but generally not on the same methods. If you used both techniques to modify a particular method, such as prepending a module with a new definition for that method, but then included a different module that also has a new definition for that method via alias_method, you can end up triggering stack level too deep errors. Here’s how to reproduce that situation:


class Muffin
  def batter
     'muffin'
  end
end
 
module Blueberry
  def batter
    "blueberry #{super}"
  end
end
 
module Streusel
  def self.included base
    base.class_eval do
      alias_method :batter_without_topping, :batter
      alias_method :batter, :batter_with_topping
    end
  end
 
  def batter_with_topping
    "#{batter_without_topping} with streusel topping"
  end
end
 
Muffin.prepend Blueberry
Muffin.include Streusel
 
puts Muffin.new.batter

If you try to run this, you will get a stack level too deep error. When you call the batter method on a new Muffin instance, here’s what Ruby does:

1. Looks for a batter method first in Blueberry, which it finds and runs.

2. The Blueberry version of batter includes a call to super, which gets Ruby to look for the batter in the original Muffin class.

3. However, including Streusel means that calling batter on a Muffin instance actually calls batter_with_topping from Streusel.

4. Ruby runs batter_with_topping, which includes a call to batter_without_topping … but this goes back to step #1, calling the batter method from Blueberry. This is because Streusel was included last, so by the time the line alias_method :batter_without_topping, :batter was run, :batter was already pointing to the one on Blueberry, not the batter in Muffin.

How horrible to be kept from delicious muffins due to this infinite recursive loop!

If instead you first include Streusel and then prepend Blueberry, you’d receive blueberry muffin with streusel topping as your output. You would end up with the same Muffin.ancestors: [Blueberry, Muffin, Streusel, Object, Kernel, BasicObject], but you wouldn’t have methods calling themselves without terminating. This solves the stack level too deep error, but goes against how the Ruby agent is currently set to install its instrumentation last.

To play around with the different variations of metaprogramming here, see this gist written up by my teammate Kenichi Nakamura.

What should customers do currently?

So, we now know that this problem comes up if your app uses something that does Module#prepend on a method that the Ruby agent later puts an alias_method on. For example, this could be a gem that patches ActiveRecord methods that New Relic later does alias_method on.

In the case where it’s metaprogramming involving ActiveRecord 5, good news—the most recent version of the agent changes our ActiveRecord 5 instrumentation to use Module#prepend rather than alias_method! So if this is your situation, the next step would be to upgrade the version of the agent you’re using to 3.17.2.

Outside of ActiveRecord 5, if you run into this issue in your app, we recommend taking a look at how and in what order your gems are being installed. One common troubleshooting technique we use is to set everything up in a barebones test app and slowly add pieces to it until we have the simplest repro case possible. We’ve found that, at least occasionally, the way in which your gems are enabled can have an impact, and when fixed can help everyone get along after all.

The happy situations for wrapping the same method are:

  • only Module#prepend
  • only alias_method
  • have Module#prepend happen after alias_method

To achieve either of the first two situations, you could try to determine which particular instrumentation path this is happening on, then see if it would be possible to disable that particular path in one of the conflicting gems. The Ruby agent configuration documentation lists the keys for disabling installation of particular instrumentations.

Rather than having to make that choice, though, you could also try to ensure that the Module#prepend happens after alias_method after all. Once you’ve determined the gem that’s using Module#prepend, you can add :require => false so that your Gemfile looks like:


gem 'other_gem', :require => false
gem 'newrelic_rpm'

Then, add an after_initialize block to your app that manually requires that gem, so that any Module#prepend actions are taken after alias_method:


config.after_initialize do
  require 'other_gem'
end

Some possible paths forward under consideration

We’ve been hearing from more customers that are running into this challenge, and we definitely want to resolve it! The core of the issue is that alias_method and Module#prepend are examples of two different metaprogramming techniques that work together only in particular situations. If we were to convert the Ruby agent over to using Module#prepend entirely, it would end up being unusable on other stacks relying on alias_method.

We ultimately need an agent that is capable of installing instrumentation with one method or the other. This could be configurable, or it could attempt to auto-detect based on what we know about particular libraries. We would want to target the most popular Module#prepend-using libraries for our customers. If this is something that you anticipate needing, please do let us know by posting in the “Feature Ideas: Ruby Agent” category of the New Relic Online Technical Community.

 

KWu is a Ruby agent software engineer at New Relic. Before that, she was in tech support and product operations at Google. She likes to "strategically maximize learning efficiency," aka, only work a little to learn a lot. When not at a computer, she makes pickles, knits, and lifts weights. View posts by .

Interested in writing for New Relic Blog? Send us a pitch!