The Elastic APM Ruby agent is surprisingly good at automatically capturing a huge amount of performance data with almost zero configuration.

Let’s see it in action. Imagine a Rails app with a controller action that queries a database and then makes an external HTTP request.

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all # Database query
    response = Net::HTTP.get(URI('http://example.com/data')) # External HTTP request
    @data = JSON.parse(response)
    render :index
  end
end

With the Elastic APM agent installed and configured, a request to GET /posts would automatically generate a trace in the APM UI. This trace would show:

  • The incoming web request (HTTP GET /posts).
  • The database query (SELECT "posts".* FROM "posts").
  • The external HTTP request (HTTP GET http://example.com/data).
  • The JSON parsing operation.

The agent stitches these together into a single trace, showing you the latency of each segment and how they relate to each other. You can drill down into each span to see its duration, and if it’s a database query, even the exact SQL executed.

The core problem the Elastic APM agent solves is making distributed tracing and performance monitoring in a complex, multi-service Ruby application feasible without manual instrumentation for every single operation. It achieves this by leveraging Ruby’s TracePoint API and Monkey Patching common libraries.

When a request comes into your Rails app, the agent starts a new trace. As code executes, it hooks into various points:

  • Controller Actions: It captures the start and end of ActionController::Base actions.
  • Database Queries: It intercepts calls to ActiveRecord::Base and records the SQL statements and their execution times.
  • External HTTP Requests: It patches Net::HTTP and HTTParty (and others) to track outgoing requests.
  • Background Jobs: It integrates with Sidekiq, Resque, and Delayed Job to trace background processing.
  • Rendering: It can even capture the time spent rendering ERB or Haml templates.

The agent uses a concept called "spans" to represent discrete units of work within a trace. A trace is a collection of spans. Each span has a name (e.g., ActiveRecord::Base#all), a type (e.g., db), a start time, and a duration. The agent automatically assigns parent-child relationships between spans, building the tree structure you see in the APM UI.

The configuration is typically done in an initializer file. You’ll need to set the service_name, server_url, and environment.

# config/initializers/elastic_apm.rb
ElasticAPM.configure do |config|
  config.service_name = 'my-rails-app'
  config.server_url = 'http://localhost:8200' # Or your APM Server URL
  config.environment = Rails.env
  config.secret_token = ENV['ELASTIC_APM_SECRET_TOKEN'] # Optional
  config.capture_exceptions = true # Capture uncaught exceptions
end

The agent sends data to the Elastic APM Server, which then processes, stores, and makes it available through Kibana. This allows you to visualize request flows, identify bottlenecks, and debug performance issues across your entire stack.

The agent’s ability to automatically instrument Rack middleware is a key mechanism for capturing the initial request and subsequent response cycle. It inserts itself into the Rack stack, and for every incoming request, it initiates a trace. The TracePoint API is then used to dynamically hook into method calls within your application’s execution path, creating spans for database queries, external HTTP calls, and other significant operations without you having to explicitly wrap them in ElasticAPM.trace blocks. The agent also uses monkey patching on popular gems like net-http and activerecord to achieve this automatic instrumentation.

The next thing you’ll want to understand is how to add custom instrumentation for parts of your code the agent doesn’t cover automatically, like complex business logic or specific library calls.

Want structured learning?

Take the full Elastic-apm course →