Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
230: Distributed Tracing for Ruby Microservices
When you're dealing with a monolith, a stack trace is your best friend. But once you move to microservices, that stack trace stops at the edge of the first service. If a request fails in the fourth service down the chain, you're left grepping logs across four different machines, trying to match timestamps and hoping your request_id was passed along correctly. That's why we use distributed tracing.
Today, we're going to build a tiny version of this. We'll create an OrderService that calls a PaymentService. We'll use OpenTelemetry (OTel), which is the industry standard right now, to make sure we can see the entire lifecycle of a request across both processes.
Setting the Stage with an Order and Payment Flow
I'm going to keep this simple. We aren't going to boot up full Rails apps; instead, we'll use two small Ruby scripts. One will act as the "frontend" (Order) and the other as the "backend" (Payment). For HTTP calls, I'm using Faraday because it's flexible and plays well with OTel middleware.
# Gemfile
gem 'opentelemetry-sdk'
gem 'opentelemetry-exporter-otlp'
gem 'faraday'
gem 'sinatra'
In a real production app, you'd send these traces to Jaeger or Honeycomb. For this lesson, I'm configuring the SDK to print to the console. It's not pretty, but it lets us verify the Trace IDs without needing to spin up a whole observability stack.
Wiring up the OpenTelemetry SDK
First, we need to initialize the tracer. I usually wrap this in a helper method so I don't clutter my main application logic. Notice how I'm naming the service—this is crucial because, in a sea of microservices, "ruby-app" is a useless name.
require 'opentelemetry/sdk'
OpenTelemetry::SDK.configure do |c|
c.service_name = 'order-service'
# In a real world scenario, we'd use the OTLP exporter here.
# For now, we'll just log to stdout to see the magic happen.
end
tracer = OpenTelemetry.tracer_provider.tracer('order-tracer')
Now, let's wrap our "place order" logic in a span. A span represents a single unit of work. If the whole request is a "Trace," the span is a "Step" within that trace.
tracer.in_span('process_order') do |span|
span.set_attribute('order.id', '12345')
puts "Order service is processing..."
# Call the Payment service
Faraday.get('http://localhost:4567/pay')
end
The "Silent Failure" of Broken Traces
Here is where I almost tripped up. I wrote the PaymentService with the exact same SDK configuration as the OrderService. I ran both, made a request, and looked at the output. I saw a span for process_order and a span for process_payment, but they had different Trace IDs.
This is a classic mistake. I had instrumented both services, but I hadn't propagated the context. The PaymentService had no idea it was being called as part of an existing trace, so it just started a brand new one. In a production environment, this means your distributed trace is broken, and you're back to square one—searching logs manually.
Connecting the Dots with Context Propagation
To fix this, we need to inject the trace context into the HTTP headers of the request and then extract it on the receiving end. OpenTelemetry provides "propagators" to handle this automatically.
In the OrderService, I need to make sure the headers are injected. While the OTel Faraday instrumentation usually handles this, doing it manually once helps you understand what's actually happening under the hood:
# Injecting the current span context into the headers
headers = {}
OpenTelemetry.propagation.inject(headers)
Faraday.get('http://localhost:4567/pay', nil, headers)
Now, on the PaymentService side (which I'm running via a simple Sinatra app), I need to extract that context before starting the span. If I don't extract it, the PaymentService won't know it's a child of the OrderService span.
# Payment Service (Sinatra)
get '/pay' do
# Extract the context from the request headers
parent_context = OpenTelemetry.propagation.extract(request.env)
# Wrap the work in a span, explicitly using the extracted context
OpenTelemetry::Context.with_current(parent_context) do
tracer.in_span('process_payment') do |span|
span.set_attribute('payment.amount', 100)
"Payment Successful"
end
end
end
Now, when you run this, both spans will share the same trace_id. You can finally see the request travel from Order to Payment as one continuous timeline. It's a small change in code, but it's the difference between knowing exactly why a request took 5 seconds and guessing which service was the bottleneck.
📋 Practical Task
Implement Trace Propagation for a Shipping Service
You have a system where OrderService calls PaymentService, and PaymentService then calls a ShippingService to arrange delivery. You have successfully implemented tracing between Order and Payment, but the ShippingService is currently "dark"—it creates its own independent traces, making it impossible to track a single order from start to finish.
Your task: Modify the ShippingService (a Sinatra application) to extract the trace context from the incoming request headers and wrap its shipping logic in a span that is linked to the parent trace from the PaymentService.
Requirements:
- Use
OpenTelemetry.propagation.extractto retrieve the context from the request. - Ensure the
in_span('arrange_shipping')block is executed within the extracted context. - Add a span attribute
shipping.carrierwith the value"FedEx"to the shipping span.
There are no comments for now.