[Ruby] Agent 7.0 is coming. Are you Ready?

For two years now, the Ruby agent has been shipping on the 6.x series. In that time, we’ve gathered a lot of deprecated changes to the API along with some exciting new features for Distributed Tracing, including implementing the open source W3C specs for Trace Context and Infinite Tracing for doing tail-based sampling. Last Summer, we reached a landmark juncture in the Ruby agent’s life by open-sourcing the agent with the Apache 2.0 license. It is time for the next major release that is 7.0. So, let’s dig in and talk about what’s coming your way.

Removing Support for Old Gems

The Ruby agent currently runs a test suite that includes a rather long history of gem versions across all the things we’re auto-instrumenting. In many cases, we’re testing gem versions going back the full 13 years of the agent’s lifespan! Typically, New Relic tries to be proactive about deprecating and sunsetting support for very old technologies. The Ruby space is different. Many, many customers are still on Rails 2.x and 3.x because of the pain of upgrading Rails from those early versions. We understand this pain, but also need to balance that pain point against focusing our attention on the newer libraries and evolutionary march of the Ruby ecosystem.

As such, we’re evaluating our entire auto-instrumented library to determine which versions of gems are needed for Rails 3.0 and discarding all but the latest that still supports Rails going that far back. For those gems that are Rails agnostic, we’re simply looking at the latest version to still support Ruby 2.0 and discarding any versions older in our test matrix. We’re not looking to change code that inherently breaks the older implementations, but we’ll no longer run automated tests on those deprecated versions. Our documentation and test matrix will be updated in tandem to accurately reflect what we’re testing and thus actively supporting.

Prepend becomes Default

With the advent of Ruby 3.0, it’s now it’s time to do a bit of house-keeping (i.e. clear the deprecated stuff off our plate) and adopt an instrumentation strategy that fully embraces the “Ruby Way” to extend libraries. I speak specifically of moving away from method-chaining as a monkey-patching strategy to using Ruby’s prepend method of introducing new functionality.

Our team learned through experience that simply swapping one out for the other is not always a smooth ride. This is because many other gems out there may patch the very same things we patch (Net::HTTP for example). As long as everyone’s using either method chaining or prepending, the whole thing works. But if there’s a mismatch where one gem uses method chaining and the other uses prepend, our users are all too often greeted with the dreaded “Stack level too deep” error message that leaves them scratching their heads, hunting through a huge pile of offenders and losing precious time in the process.

To alleviate this potential pain point, we’re implementing prepend auto-instrumentation strategy in the upcoming 7.0 release and documenting exactly what can possibly go wrong and how to work around it when conflicts between two gems inevitably occurs. Ruby 2.0 explicitly introduced prepend as the official way to patch existing libraries to add additional functionality. Rails long ago made the switch and we are now following suit to bring the Ruby agent in line with modern thinking on best practices for adding functionality to areas of code we don’t maintain or control directly ourselves.

Prepending will become the default method of auto-instrumenting starting with 7.0. Since we have no way to know what external gems are going to cause a ruckus in every customer’s environment, we’ll continue to carry the old-style method-chaining for auto-instrumentation. We will be providing a configuration flag for each library we currently auto-instrument with method-chaining to allow you to switch back if prepended instrumentation proves to be an issue in your environment.

An all new Method Tracer Implementation

One pain point the advent of Ruby 3.0 brings to our table is how to handle any combination of parameters on method signatures. The driver for this change, as described in this issue:

In Ruby 3.0, positional arguments and keyword arguments will be separated. Ruby 2.7 will warn for behaviors that will change in Ruby 3.0. If you see the following warnings, you need to update your code:

  • Using the last argument as keyword parameters is deprecated, or
  • Passing the keyword argument as the last hash parameter is deprecated, or
  • Splitting the last argument into positional and keyword parameters is deprecated

In most cases, you can avoid the incompatibility by adding the double splat operator. It explicitly specifies passing keyword arguments instead of a Hash object. Likewise, you may add braces {} to explicitly pass a Hash object, instead of keyword arguments. Read the section “Typical cases” below for more details.

While there, we might as well take advantage of all the goodness Ruby has brought us since the 1.8 days when the agent first came to light and refactor the method tracers using modern, less-brittle techniques that can cleanly support all the 2.x Rubies as well as Ruby 3.0+ with equal poise.

The refactor we have in mind will break some aspects of custom method tracers, namely that of referencing the method parameters by name (as currently written, those parameter names are going to change! As currently written, if you wanted a dynamic method name, you baked it into the tracer call with a string that become interpolated at run-time using the args array. For example:

def test_add_tracer_with_dynamic_metric
  metric_code = '#{args[0]}.#{args[1]}'. # <=== NOTE THIS LINE
  expected_metric = "1.2"                # <=== produces dynamic metric name!

  @metric_name = metric_code

  self.class.add_method_tracer :method_to_be_traced, metric_code

  in_transaction do
    method_to_be_traced 1,2,3,true,expected_metric
  assert_metrics_recorded expected_metric => {:call_count => 1, :total_call_time => 0.05}

That’s because when we wrap your method in a tracer with today’s implementation, we’re essentially redefining the original arguments of the method as *args, &block – this behavior will change and we expect the original argument names to be preserved. That means, if you do have code referencing args, that code will need to be refactored. While a pain point to push through in old code bases, the new implementation and approach will be “the Ruby Way” and also more intuitive to the application developer that is adding a custom method tracer within the application since the developer just uses original argument names of the original method signature going forward.

How we’re approaching the refactor effort for method tracers could use a community voice! We do not have a lot of insight into how our customers are using method tracers to add custom tracing to their apps. There’s some interesting bits in the definition as well like adding prefixed and postfixed code blocks that seem highly brittle and unintuitive. Do we preserve these? With the way Ruby has evolved, my intuition is to drop this support altogether, but I’d love to hear from our users if there are any strong opinions out there.

Symantec Certificate Bundle

Last, but not least, we’re removing the Symantec Certificate Bundle which was our solution the problem of poor support for SSL handshake support in Ruby way back in the day. Today, Ruby supports system installed certificates handily and we should no longer need this crutch. However, we know many customers are using bare-bones configuration for their containerized environments and may need to take some extra steps to ensure up-to-date SSL certificates are installed in their environment.

Feedback Welcome!

Today, I will be talking on The Relican Twitch TV channel about the 7.0 Release and would absolutely love to see you there!

Remember, the Ruby agent is open source and it’s long-term success depends on it’s community. We’ve had a lot of great contributions from the community since open sourcing and discussing what you want to see in the agent is just as important as those amazing pull requests we’re getting from those who use the Ruby agent in their daily lives.