OpenTelemetry Ruby Tracing API
This document teaches you how to use the OpenTelemetry Ruby API. To learn how to install and configure the OpenTelemetry Ruby SDK, see Getting started with OpenTelemetry Ruby.
Installation
OpenTelemetry-Ruby is the Ruby implementation of OpenTelemetry. It provides the OpenTelemetry Ruby API, which you can use to instrument your application with OpenTelemetry traces.
Install the required packages:
gem install opentelemetry-api opentelemetry-sdk
Or add to your Gemfile:
gem 'opentelemetry-api'
gem 'opentelemetry-sdk'
Quickstart
Step 1. Let's instrument the following function:
def create_user(name:, email:)
User.create(name: name, email: email)
end
Step 2. Wrap the operation with a span:
require 'opentelemetry'
tracer = OpenTelemetry.tracer_provider.tracer('my_app_or_gem', '1.0.0')
def create_user(name:, email:)
tracer.in_span('create-user') do |span|
User.create(name: name, email: email)
end
end
Step 3. Record exceptions and set status code:
def create_user(name:, email:)
tracer.in_span('create-user') do |span|
begin
User.create(name: name, email: email)
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise # Re-raise the exception
end
end
end
Step 4. Record contextual information with attributes:
def create_user(name:, email:, user_type: 'regular')
tracer.in_span('create-user') do |span|
# Check if span is being recorded to avoid unnecessary work
if span.recording?
span.set_attribute('user.name', name)
span.set_attribute('user.email', email)
span.set_attribute('user.type', user_type)
end
begin
result = User.create(name: name, email: email, user_type: user_type)
if span.recording?
span.set_attribute('operation.result', 'success')
span.set_attribute('user.created_id', result.id)
end
result
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise
end
end
end
That's it! The operation is now fully instrumented with proper error handling and contextual information.
Tracer
To start creating spans, you need a tracer. You can create a tracer by providing the name and version of the library or application doing the instrumentation:
require 'opentelemetry'
tracer = OpenTelemetry.tracer_provider.tracer('my_app_or_gem', '1.0.0')
You can have as many tracers as you want, but usually you need only one tracer per application or library. Later, you can use tracer names to identify the instrumentation that produces the spans.
Tracer Configuration
When creating a tracer, you can provide additional metadata:
tracer = OpenTelemetry.tracer_provider.tracer(
'my_app_or_gem', # Instrumentation name
'1.0.0', # Instrumentation version
schema_url: 'https://example.com/schema' # Schema URL (optional)
)
Global Tracer Pattern
For convenience, you can create a global tracer instance:
# In your application initializer or main file
module MyApp
TRACER = OpenTelemetry.tracer_provider.tracer('my_app', '1.0.0')
end
# Use throughout your application
def some_operation
MyApp::TRACER.in_span('some-operation') do |span|
# Your logic here
end
end
Creating Spans
Once you have a tracer, creating spans is straightforward:
require 'opentelemetry'
# Create a span with name "operation-name" and kind="server"
tracer.in_span('operation-name', kind: :server) do |span|
begin
do_some_work
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise
end
end
Span Kinds
Specify the type of span using span kinds:
# For incoming requests (server-side)
tracer.in_span('handle-request', kind: :server) do |span|
# Handle incoming HTTP request
end
# For outgoing requests (client-side)
tracer.in_span('http-request', kind: :client) do |span|
# Make HTTP request to external service
end
# For async operations (producer/consumer)
tracer.in_span('publish-message', kind: :producer) do |span|
# Publish message to queue
end
tracer.in_span('consume-message', kind: :consumer) do |span|
# Process message from queue
end
# For internal operations (default)
tracer.in_span('internal-operation', kind: :internal) do |span|
# Internal business logic
end
Span Configuration Options
Configure spans with additional options:
# Create span with custom configuration
tracer.in_span(
'complex-operation',
kind: :internal,
attributes: { 'operation.type' => 'batch' }, # Set initial attributes
links: [span_link], # Link to other spans
start_timestamp: start_time # Custom start time (optional)
) do |span|
# Your operation logic
end
Manual Span Management
For advanced use cases, you can manually manage span lifecycle:
# Start span manually
span = tracer.start_span('manual-operation', kind: :client)
# Set span as current
OpenTelemetry::Trace.with_span(span) do
begin
# Your operation
do_work
span.set_status(OpenTelemetry::Trace::Status.ok)
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise
ensure
# Always finish the span
span.finish
end
end
Adding Span Attributes
To record contextual information, you can annotate spans with attributes. For example, an HTTP endpoint may have attributes such as http.method = GET and http.route = /projects/:id.
tracer.in_span('http-request') do |span|
# Check if span is being recorded to avoid expensive computations
if span.recording?
span.set_attribute('http.method', 'GET')
span.set_attribute('http.route', '/projects/:id')
span.set_attribute('http.status_code', 200)
span.set_attribute('user.id', user_id)
end
end
Setting Multiple Attributes
You can set multiple attributes efficiently:
span.add_attributes({
'http.method' => 'POST',
'http.route' => '/api/users',
'http.status_code' => 201,
'user.role' => 'admin',
'request.size' => request_body.length
})
Semantic Conventions
Use semantic conventions for consistent attribute naming:
require 'opentelemetry/semantic_conventions'
tracer.in_span('http-request') do |span|
if span.recording?
span.add_attributes({
OpenTelemetry::SemanticConventions::Trace::HTTP_METHOD => 'GET',
OpenTelemetry::SemanticConventions::Trace::HTTP_ROUTE => '/projects/:id',
OpenTelemetry::SemanticConventions::Trace::HTTP_STATUS_CODE => 200,
OpenTelemetry::SemanticConventions::Trace::USER_ID => user_id
})
end
end
Adding Span Events
You can annotate spans with events that represent significant moments during the span's lifetime:
tracer.in_span('file-processing') do |span|
# Simple event
span.add_event('cache.miss')
# Event with attributes
span.add_event('user.login', {
'user.id' => '12345',
'user.role' => 'admin',
'login.method' => 'oauth'
})
# Event with timestamp
span.add_event(
'database.query.start',
{
'db.statement' => 'SELECT * FROM users WHERE id = ?',
'db.operation' => 'SELECT'
},
timestamp: Time.now.to_f * 1_000_000_000 # nanoseconds
)
end
Logging with Events
Events are particularly useful for structured logging:
def process_order(order_id)
tracer.in_span('process-order') do |span|
span.add_event('log', {
'log.severity' => 'info',
'log.message' => 'Order processing started',
'order.id' => order_id
})
begin
order = Order.find(order_id)
span.add_event('log', {
'log.severity' => 'info',
'log.message' => 'Order found and validated',
'order.status' => order.status
})
# Process order...
rescue ActiveRecord::RecordNotFound => e
span.add_event('log', {
'log.severity' => 'error',
'log.message' => 'Order not found',
'order.id' => order_id,
'error.type' => 'RecordNotFound'
})
raise
end
end
end
Setting Span Status
Use status codes to indicate the outcome of operations:
tracer.in_span('database-operation') do |span|
begin
result = perform_database_operation
# Successful operation (default is UNSET)
span.set_status(OpenTelemetry::Trace::Status.ok)
result
rescue ActiveRecord::ConnectionTimeoutError => e
# Operation with error
span.set_status(OpenTelemetry::Trace::Status.error('Database connection timeout'))
span.record_exception(e)
raise
rescue StandardError => e
# Generic error
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
span.record_exception(e)
raise
end
end
Status Codes
OpenTelemetry defines three status codes:
# UNSET (default) - The operation completed without known error
span.set_status(OpenTelemetry::Trace::Status.unset)
# OK - The operation completed successfully
span.set_status(OpenTelemetry::Trace::Status.ok)
# ERROR - The operation failed
span.set_status(OpenTelemetry::Trace::Status.error('Something went wrong'))
Recording Exceptions
OpenTelemetry provides a convenient method to record exceptions:
def risky_operation
tracer.in_span('risky-operation') do |span|
begin
perform_risky_work
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise # Re-raise if needed
end
end
end
Exception with Additional Context
def connect_to_database(host, port)
tracer.in_span('database-connection') do |span|
retry_count = 0
begin
establish_connection(host, port)
rescue ConnectionError => e
retry_count += 1
span.record_exception(e, attributes: {
'db.host' => host,
'db.port' => port,
'retry.count' => retry_count,
'connection.timeout' => 30
})
span.set_status(OpenTelemetry::Trace::Status.error('Database connection failed'))
raise
end
end
end
Current Span and Context
OpenTelemetry stores the active span in a context that is automatically managed. You can access the current span from anywhere in your application.
Getting the Current Span
require 'opentelemetry'
def add_user_info_to_span(user)
# Get the currently active span
current_span = OpenTelemetry::Trace.current_span
if current_span.recording?
current_span.add_attributes({
'user.id' => user.id,
'user.email' => user.email,
'user.role' => user.role
})
end
end
# Usage within an existing span
tracer.in_span('user-operation') do |span|
user = User.find(user_id)
add_user_info_to_span(user) # This will add attributes to the current span
# perform user operation...
end
Context Nesting
Contexts are automatically nested when using in_span:
# Parent span
tracer.in_span('parent-operation') do |parent_span|
parent_span.set_attribute('operation.type', 'parent')
# Child span - automatically becomes a child of the parent
tracer.in_span('child-operation') do |child_span|
child_span.set_attribute('operation.type', 'child')
# Both spans are properly nested
puts "Parent: #{parent_span.context.hex_span_id}"
puts "Child: #{child_span.context.hex_span_id}"
puts "Child parent: #{child_span.context.hex_trace_id}"
end
end
Manual Context Management
For advanced scenarios, you can manually manage context:
# Create span without auto-activation
span = tracer.start_span('manual-span')
# Manually set as current span
OpenTelemetry::Trace.with_span(span) do
# Span is now active within this block
current = OpenTelemetry::Trace.current_span
current.set_attribute('manual', true)
do_work
end
# Always finish manually created spans
span.finish
Best Practices
Error Handling Pattern
Use consistent error handling patterns:
def service_operation(params)
tracer.in_span('service-operation') do |span|
if span.recording?
span.add_attributes({
'operation.type' => 'service_call',
'params.count' => params.length
})
end
begin
result = perform_operation(params)
span.set_status(OpenTelemetry::Trace::Status.ok)
if span.recording?
span.set_attribute('operation.result', 'success')
span.set_attribute('result.count', result.length)
end
result
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
if span.recording?
span.set_attribute('operation.result', 'failure')
span.set_attribute('error.type', e.class.name)
end
raise
end
end
end
Context Manager Helper
Create a helper module for automatic span management:
module TracingHelper
def with_traced_operation(name, **attributes)
tracer = OpenTelemetry.tracer_provider.tracer(self.class.name, '1.0.0')
tracer.in_span(name, attributes: attributes) do |span|
begin
result = yield(span)
span.set_status(OpenTelemetry::Trace::Status.ok)
result
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise
end
end
end
end
# Usage
class UserService
include TracingHelper
def create_user(params)
with_traced_operation('create-user', operation: 'create') do |span|
user = User.create!(params)
span.set_attribute('user.id', user.id) if span.recording?
user
end
end
end
Attribute Optimization
Only set attributes when the span is being recorded:
tracer.in_span('expensive-operation') do |span|
# Only do expensive operations if the span is being recorded
if span.recording?
expensive_attribute = calculate_expensive_value
span.set_attribute('expensive.attribute', expensive_attribute)
end
# Always do the main work
perform_operation
end
Span Naming
Use descriptive, low-cardinality span names:
# Good: Low cardinality
tracer.in_span('GET /users/:id') do |span|
span.set_attribute('http.route', '/users/:id')
span.set_attribute('user.id', user_id)
end
# Bad: High cardinality
tracer.in_span("GET /users/#{user_id}") do |span|
# This creates a new span name for each user_id
end
Integration Examples
HTTP Client Instrumentation
require 'net/http'
require 'opentelemetry/semantic_conventions'
class HttpClient
include OpenTelemetry::SemanticConventions::Trace
def initialize
@tracer = OpenTelemetry.tracer_provider.tracer('http_client', '1.0.0')
end
def get(url, headers = {})
uri = URI(url)
@tracer.in_span("HTTP GET", kind: :client) do |span|
if span.recording?
span.add_attributes({
HTTP_METHOD => 'GET',
HTTP_URL => url,
HTTP_SCHEME => uri.scheme,
HTTP_HOST => uri.host,
HTTP_TARGET => uri.path
})
end
begin
# Inject trace context into outgoing request
OpenTelemetry.propagation.inject(headers)
# Make HTTP request
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
request = Net::HTTP::Get.new(uri)
headers.each { |key, value| request[key] = value }
http.request(request)
end
if span.recording?
span.add_attributes({
HTTP_STATUS_CODE => response.code.to_i,
HTTP_RESPONSE_SIZE => response.body&.length || 0
})
end
# Set status based on HTTP status code
if response.code.to_i >= 400
span.set_status(OpenTelemetry::Trace::Status.error("HTTP #{response.code}"))
else
span.set_status(OpenTelemetry::Trace::Status.ok)
end
response
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error('HTTP request failed'))
raise
end
end
end
end
# Usage
client = HttpClient.new
response = client.get('https://api.example.com/users')
Database Query Instrumentation
require 'pg'
require 'opentelemetry/semantic_conventions'
class DatabaseClient
include OpenTelemetry::SemanticConventions::Trace
def initialize(connection_params)
@connection_params = connection_params
@tracer = OpenTelemetry.tracer_provider.tracer('database_client', '1.0.0')
end
def execute_query(sql, params = [])
@tracer.in_span('db.query', kind: :client) do |span|
if span.recording?
span.add_attributes({
DB_SYSTEM => 'postgresql',
DB_STATEMENT => sql,
DB_NAME => @connection_params[:dbname],
DB_USER => @connection_params[:user],
DB_CONNECTION_STRING => connection_string_for_tracing
})
end
start_time = Time.now
begin
conn = PG.connect(@connection_params)
span.add_event('db.connection.established')
result = if params.any?
conn.exec_params(sql, params)
else
conn.exec(sql)
end
duration = Time.now - start_time
if span.recording?
span.add_attributes({
DB_ROWS_AFFECTED => result.ntuples,
'db.duration' => duration
})
end
span.add_event('db.query.completed', {
'db.rows_returned' => result.ntuples
})
span.set_status(OpenTelemetry::Trace::Status.ok)
result
rescue PG::Error => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error("Database error: #{e.message}"))
raise
ensure
conn&.close
span.add_event('db.connection.closed')
end
end
end
private
def connection_string_for_tracing
# Return safe connection string without password
"postgresql://#{@connection_params[:user]}@#{@connection_params[:host]}:#{@connection_params[:port]}/#{@connection_params[:dbname]}"
end
end
# Usage
db = DatabaseClient.new({
host: 'localhost',
port: 5432,
dbname: 'myapp',
user: 'myuser',
password: 'mypassword'
})
result = db.execute_query('SELECT * FROM users WHERE id = $1', [123])
Background Job Instrumentation
# Sidekiq job with tracing
class UserNotificationJob
include Sidekiq::Job
def perform(user_id, notification_type)
tracer = OpenTelemetry.tracer_provider.tracer('background_jobs', '1.0.0')
tracer.in_span('process-notification', kind: :consumer) do |span|
if span.recording?
span.add_attributes({
'job.type' => 'user_notification',
'user.id' => user_id,
'notification.type' => notification_type,
'job.queue' => 'default'
})
end
begin
user = User.find(user_id)
span.add_event('user.found', {
'user.email' => user.email,
'user.status' => user.status
})
case notification_type
when 'email'
send_email_notification(user, span)
when 'sms'
send_sms_notification(user, span)
else
raise ArgumentError, "Unknown notification type: #{notification_type}"
end
span.set_status(OpenTelemetry::Trace::Status.ok)
rescue ActiveRecord::RecordNotFound => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error('User not found'))
raise
rescue StandardError => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error(e.message))
raise
end
end
end
private
def send_email_notification(user, parent_span)
tracer = OpenTelemetry.tracer_provider.tracer('email_service', '1.0.0')
tracer.in_span('send-email', kind: :client) do |span|
if span.recording?
span.add_attributes({
'email.to' => user.email,
'email.provider' => 'sendgrid'
})
end
# Send email logic
EmailService.send_notification(user)
span.add_event('email.sent')
span.set_status(OpenTelemetry::Trace::Status.ok)
end
end
end
# Usage
UserNotificationJob.perform_async(123, 'email')
Rails Controller Instrumentation
class UsersController < ApplicationController
before_action :setup_tracing
def show
@tracer.in_span('users#show', kind: :server) do |span|
if span.recording?
span.add_attributes({
'http.route' => '/users/:id',
'user.id' => params[:id],
'request.format' => request.format.to_s
})
end
begin
@user = User.find(params[:id])
span.add_event('user.found', {
'user.status' => @user.status,
'user.created_at' => @user.created_at.iso8601
})
render json: @user
rescue ActiveRecord::RecordNotFound => e
span.record_exception(e)
span.set_status(OpenTelemetry::Trace::Status.error('User not found'))
if span.recording?
span.set_attribute('http.status_code', 404)
end
render json: { error: 'User not found' }, status: :not_found
end
end
end
private
def setup_tracing
@tracer = OpenTelemetry.tracer_provider.tracer('rails_controllers', '1.0.0')
end
end
Performance Considerations
Conditional Instrumentation
For high-performance scenarios, consider conditional instrumentation:
class PerformanceCriticalService
def initialize(enable_tracing: true)
@enable_tracing = enable_tracing
@tracer = OpenTelemetry.tracer_provider.tracer('critical_service', '1.0.0') if enable_tracing
end
def perform_operation
if @enable_tracing
@tracer.in_span('critical-operation') do |span|
perform_work
end
else
perform_work
end
end
private
def perform_work
# Your actual business logic
end
end
# Usage with conditional tracing based on environment
service = PerformanceCriticalService.new(
enable_tracing: Rails.env.development? || Rails.env.staging?
)
Sampling Awareness
Check if spans are being sampled to avoid unnecessary work:
tracer.in_span('operation') do |span|
# Only do expensive operations if the span is being recorded
if span.recording?
expensive_data = calculate_expensive_metrics
span.set_attribute('expensive.data', expensive_data)
end
# Always do the main work
perform_main_operation
end
Batch Attribute Setting
Set multiple attributes efficiently:
# Efficient: Set all attributes at once
attributes = {
'http.method' => 'GET',
'http.route' => '/api/users',
'http.status_code' => 200,
'user.id' => user_id,
'user.role' => user_role
}
tracer.in_span('http-request') do |span|
if span.recording?
span.add_attributes(attributes)
end
# Process request...
end
Memory Management
For long-running applications, be mindful of memory usage:
# Avoid holding references to spans after they complete
def process_batch(items)
tracer.in_span('batch-processing') do |span|
span.set_attribute('batch.size', items.length) if span.recording?
items.each_with_index do |item, index|
# Create child spans for individual items
tracer.in_span('process-item') do |item_span|
if item_span.recording?
item_span.set_attribute('item.index', index)
item_span.set_attribute('item.id', item.id)
end
process_single_item(item)
end
# Span is automatically finished and can be garbage collected
end
end
end
Auto-instrumentation
When possible, you should prefer using explicit instrumentation for better control and understanding of your traces. Auto-instrumentation works well for common frameworks and libraries.
OpenTelemetry Ruby allows you to automatically instrument Ruby applications using various instrumentation gems.
Installation
First, you need to install the instrumentation gems for the libraries you want to automatically instrument:
# Install specific instrumentations
gem install opentelemetry-instrumentation-rails
gem install opentelemetry-instrumentation-net_http
gem install opentelemetry-instrumentation-faraday
gem install opentelemetry-instrumentation-redis
gem install opentelemetry-instrumentation-pg
# Or install all available instrumentations
gem install opentelemetry-instrumentation-all
Add to your Gemfile:
gem 'opentelemetry-instrumentation-all'
# or specific gems
gem 'opentelemetry-instrumentation-rails'
gem 'opentelemetry-instrumentation-net_http'
Supported Libraries
Auto-instrumentation is available for:
- Web frameworks: Rails, Sinatra, Rack
- HTTP clients: Net::HTTP, Faraday, RestClient, HTTPClient
- Databases: ActiveRecord, Redis, PG (PostgreSQL), MySQL2, MongoDB
- Background jobs: Sidekiq, DelayedJob
- Other libraries: GraphQL, Bunny (RabbitMQ), Elasticsearch
Enabling Auto-Instrumentation
Enable all available instrumentations:
require 'opentelemetry/sdk'
require 'opentelemetry/instrumentation/all'
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-ruby-app'
c.service_version = '1.0.0'
c.use_all() # enables all available instrumentation
end
Selective Auto-Instrumentation
Enable specific instrumentations with configuration:
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-app'
# Enable Rails instrumentation
c.use 'OpenTelemetry::Instrumentation::Rails'
# Enable HTTP client instrumentation
c.use 'OpenTelemetry::Instrumentation::Net::HTTP'
c.use 'OpenTelemetry::Instrumentation::Faraday'
# Enable database instrumentation with configuration
c.use 'OpenTelemetry::Instrumentation::PG', {
# Configure database statement handling
db_statement: :obfuscate # Options: :include, :omit, :obfuscate
}
# Enable Redis with custom configuration
c.use 'OpenTelemetry::Instrumentation::Redis', {
peer_service: 'redis-cache'
}
end
Environment Variable Control
Disable specific instrumentations using environment variables:
# Disable specific instrumentations
export OTEL_RUBY_INSTRUMENTATION_RAILS_ENABLED=false
export OTEL_RUBY_INSTRUMENTATION_REDIS_ENABLED=false
# Configure instrumentation options
export OTEL_RUBY_INSTRUMENTATION_PG_CONFIG_OPTS="db_statement=obfuscate"
How Auto-instrumentation Works
Auto-instrumentation works by:
- Method wrapping: Instrumentations wrap existing methods to add tracing
- Middleware injection: Web frameworks get middleware for request tracing
- Callback hooks: Libraries use existing callback mechanisms to inject spans
- Context propagation: Automatic injection/extraction of trace context
Example of what happens automatically:
# Your original code
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
render json: @user
end
end
# What auto-instrumentation adds (conceptually)
class UsersController < ApplicationController
def show
tracer.in_span('UsersController#show') do |span|
span.set_attribute('http.route', '/users/:id')
# Database query is also automatically traced
@user = User.find(params[:id]) # Creates a 'SELECT' span
render json: @user
end
end
end
OpenTelemetry APM
Uptrace is a OpenTelemetry APM that supports distributed tracing, metrics, and logs. You can use it to monitor applications and troubleshoot issues.

Uptrace comes with an intuitive query builder, rich dashboards, alerting rules with notifications, and integrations for most languages and frameworks.
Uptrace can process billions of spans and metrics on a single server and allows you to monitor your applications at 10x lower cost.
In just a few minutes, you can try Uptrace by visiting the cloud demo (no login required) or running it locally with Docker. The source code is available on GitHub.
What's Next?
Now that you understand the OpenTelemetry Ruby Tracing API, explore these related topics: