Skip to content

Instantly share code, notes, and snippets.

@fzakaria
Last active May 13, 2020 02:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fzakaria/b09b124359583a98c50eb5aa01d93d45 to your computer and use it in GitHub Desktop.
Save fzakaria/b09b124359583a98c50eb5aa01d93d45 to your computer and use it in GitHub Desktop.
Zipkin JRuby
class RequestFilter
def initialize(app, tracer)
@app = app
@tracer = tracer
end
EXCLUDED_PATH_PREFIXES = %w[
...
].freeze
def call(env)
# if the current request is one we should exclude, then immediately return
# so we can avoid any cost associated with setting up a trace.
path = RequestFilter.get_path(env)
if path.start_with?(*EXCLUDED_PATH_PREFIXES)
return @app.call(env)
end
req = Rack::Request.new(env)
# Collect just a few HTTP headers from the request that might affect Tracing
# X-B3-TraceId, X-B3-SpanId or X-B3-Sampled
# https://github.com/openzipkin/b3-propagation/blob/master/README.md
#
# 1. Rack prefixes the headers in the env variable with HTTP_ so lets strip it
# 2. Rack annoyingly converts hyphens to underscores; so lets revert that.
# 3. Let's also capitalize for good measure
headers = Hash[*env.select {|k, _| k.start_with? 'HTTP_'}
.map {|k, v| [k.sub(/^HTTP_/, ''), v]}
.map {|k, v| [k.split('_').map(&:capitalize).join('-'), v]}
.flatten]
span = @tracer.extract_from_headers(headers)
# returns the path of the request without the query string, domain and any relative root
span.name(path)
# We will now set a bunch of common labels
# similar to https://github.com/googleapis/google-cloud-ruby/blob/master/google-cloud-trace/lib/google/cloud/trace/middleware.rb
span.tag(Label::HTTP_HOST, RequestFilter.get_host(env))
span.tag(Label::HTTP_METHOD, req.request_method)
span.tag(Label::HTTP_CLIENT_PROTOCOL, env["SERVER_PROTOCOL"])
span.tag(Label::HTTP_USER_AGENT, req.user_agent)
span.tag(Label::HTTP_URL, RequestFilter.get_url(env))
span.tag(Label::PID, ::Process.pid.to_s)
span.tag(Label::TID, ::Thread.current.object_id.to_s)
span.tag(Label::HTTP_REQUEST_SIZE, req.content_length) unless req.content_length.nil?
span.tag("developer", ENV["USER"]) if development? && ENV["USER"]
span.start
# At this point we need to mark the span as active so it's in thread-local storage
status, headers, body = @tracer.with_span_in_scope(span) do
@app.call(env)
end
# to reduce the cardinality of the span names; lets prefer the generic routes.
# for instance: /users/:user_id
# we have to set this after the call, because it's only present once the sinatra router handles the request
if env['sinatra.route'].present?
raw_sinatra_route = env['sinatra.route']
span.name(raw_sinatra_route)
end
# Add the propagation information
@tracer.inject_into_headers(span, headers)
span.tag(Label::HTTP_STATUS_CODE, status.to_s)
# We cannot take the content length of a stream; otherwise it will consume the body!
# This is a potential gotcha; so be wary changing this if condition.
unless body.is_a?(Sinatra::Helpers::Stream)
response = Rack::Response.new(body, status, headers)
span.tag(HLabel::HTTP_RESPONSE_SIZE, response.content_length) if response.content_length.present?
end
span.finish
[status, headers, body]
end
# https://sequel.jeremyevans.net/rdoc/classes/Sequel/Plugins.html
# https://github.com/jeremyevans/sequel/blob/master/doc/model_plugins.rdoc
module Sequel
module Plugins
# Sequel::Model instrumentation for Zipkin
module ZipkinTracing
#
# Meta-programming for creating method tracers for the Sequel::Model plugin.
module MethodWrapping
# Install a method named +method_name+ that will trace execution
# with a metric name derived from +operation_name+ (or +method_name+ if +operation_name+
# isn't specified).
def wrap_sequel_method(method_name, operation_name = method_name)
define_method(method_name) do |*args, &block|
klass = self.is_a?(Class) ? self : self.class
model = self.try(:model)&.name || klass.name
sql = args.first
# In 4.37.0, sql was converted to a prepared statement object
sql = sql.is_a?(String) ? sql : sql.prepared_sql
# We don't want the sequel methods as the root spans.
# We prefer those that have a parent
parent_span = @tracer.current_span
if parent_span.nil?
return super(*args, &block)
end
# lets turn the operation name from `sequel.query` into something like
# `sequel.query(User)`
# This also removes any module at the prefix
model = model.split('::').last
operation_name = "#{operation_name}(#{model})" if model.present?
@tracer.trace(operation_name, parent: parent_span) do |span|
span.tag("/sql", sql)
super(*args, &block)
end
end
end
end
# This is the catch all for all other dataset methods that are accessed
# Inspiration from:
# https://github.com/newrelic/rpm/blob/abfff6456e/lib/sequel/plugins/newrelic_instrumentation.rb
# https://github.com/lightstep/ls-trace-rb/blob/master/lib/ddtrace/contrib/sequel/dataset.rb
# https://github.com/foodiefm/sequel-opentracing/blob/master/lib/sequel/opentracing/dataset.rb
module DatasetMethods
extend Sequel::Plugins::ZipkinTracing::MethodWrapping
wrap_sequel_method :execute, "sequel.query"
wrap_sequel_method :execute_ddl, "sequel.ddl"
wrap_sequel_method :execute_dui, "sequel.dui"
wrap_sequel_method :execute_insert, "sequel.insert"
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment