Distributed Tracing for Ruby on Rails Microservices with OpenCensus/OpenTelemetry (part 2)

Yoshinori Kawasaki
May 17 · 8 min read
Photo by Matt Howard on Unsplash

Introducing OpenCensus

Data model

Architecture

OpenCensus architecture

Ruby/Rails integration

OpenCensus Ruby for Tracing

Configurations

# Gemfile
gem 'opencensus'
# When a process starts
OpenCensus.configure do |c|
c.trace.middleware_placement = :begin
c.trace.exporter = exporter
c.trace.default_sampler = \
OpenCensus::Trace::Samplers::Probability.new(0.01)
c.trace.default_max_attributes = 64
end

Exporter

# DataDog exporter
uri = URI.parse(ENV['DATADOG_APM_AGENT_URL'])
c.exporter = OpenCensus::Trace::Exporters::Datadog.new \
service: app_name,
agent_hostname: uri.host,
agent_port: uri.port
# StackDriver exporter
keyfile = Base64.strict_decode64(ENV['STACKDRIVER_JSON_KEY_BASE64'])
c.exporter = OpenCensus::Trace::Exporters::Stackdriver.new \
project_id: gcp_project_id,
credentials: JSON.parse(keyfile)
# multiple exporters
exporters = []
exporters << OpenCensus::Trace::Exporters::Datadog.new(...)
exporters << OpenCensus::Trace::Exporters::Stackdriver.new
c.exporter = OpenCensus::Trace::Exporters::Multi.new(*exporters)

Railtie

# application.rb 
require 'opencensus/trace/integrations/rails' # <- Rails::Railtie
module MyApp
class Application < Rails::Application
# the top-level config object is exposed as `config.opencensus`
config.opencensus.trace.default_max_attributes = 64
# ... end
end

Rack Middleware

class OpenCensus::Trace::Integrations::RackMiddleware
def call env
formatter = Formatters::TraceContext.new
context = formatter.deserialize env[formatter.rack_header_name]
Trace.start_request_trace \
trace_context: context,
same_process_as_parent: false do |span_context|
begin
Trace.in_span get_path(env) do |span|
start_request span, env
@app.call(env).tap do |response|
finish_request span, response
end
end
ensure
@exporter.export span_context.build_contained_spans
end
end
end
end

Rails Events

DEFAULT_NOTIFICATION_EVENTS = [
"sql.active_record",
"render_template.action_view",
"send_file.action_controller",
"send_data.action_controller",
"deliver.action_mailer"
].freeze
def setup_notifications
OpenCensus::Trace.configure.notifications.events.each do |type|
ActiveSupport::Notifications.subscribe(type) do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
handle_notification_event event
end
end
end
def handle_notification_event event
span_context = OpenCensus::Trace.span_context
if span_context
ns = OpenCensus::Trace.configure.notifications.attribute_namespace
span = span_context.start_span event.name, skip_frames: 2
span.start_time = event.time
span.end_time = event.end
event.payload.each do |k, v|
span.put_attribute "#{ns}#{k}", v.to_s
end
end
end

Farraday Middleware

class OpenCensus::Trace::IntegrationsFaradayMiddleware < ::Faraday::Middleware
def call request_env
span_context = request_env[:span_context]
span_name = extract_span_name(request_env)
span = span_context.start_span span_name, sampler: @sampler
start_request span, request_env
begin
@app.call(request_env).on_complete do |response_env|
finish_request span, response_env
end
rescue StandardError => e
span.set_status 2, e.message
raise
ensure
span_context.end_span span
end
end
end
conn = Faraday.new(url: api_base_url) do |c|
c.use OpenCensus::Trace::Integrations::FaradayMiddleware,
span_name: ->(env) { env[:url].path }
c.adapter Faraday.default_adapter
end
OpenCensus::Trace.in_span "long task" do
t = rand * 10
sleep t
end
def in_span name, kind: nil, skip_frames: 0, sampler: nil
span = start_span name, kind: kind, skip_frames: skip_frames + 1,
sampler: sampler
begin
yield span
ensure
end_span span
end
end

Summary



Wantedly Engineering

All about engineering & design at Wantedly

Thanks to Jiachi Kang, Camille Drapier, and Malvin Sutanto.

Yoshinori Kawasaki

Written by

Software Engineer, CTO at Wantedly, Inc.

Wantedly Engineering

All about engineering & design at Wantedly