# frozen_string_literal: true require 'opentracing/instrumentation/faraday/response_logger' module OpenTracing module Instrumentation module Faraday # TraceMiddleware inject tracing header into request and trace request # # Usage with default config: # Faraday.new(url) do |connection| # connection.use \ # OpenTracing::Instrumentation::Faraday::TraceMiddleware # end # # Usage with config block: # Faraday.new(url) do |connection| # connection.use \ # OpenTracing::Instrumentation::Faraday::TraceMiddleware do |c| # # c is instance of Config # c.tracer = tracer # end # end class TraceMiddleware extend Forwardable # Config for TraceMiddleware class Config DEFAULT_COMMAND_NAME = 'faraday_request' DEFAULT_COMPONENT = 'faraday' DEFAULT_EXPECTED_ERRORS = [StandardError].freeze def initialize @tracer = OpenTracing.global_tracer @operation_name = DEFAULT_COMMAND_NAME @component = DEFAULT_COMPONENT @expected_errors = DEFAULT_EXPECTED_ERRORS @service_name = nil @inject = true @response_logger = ResponseLogger.new end # Instance of tracer. Should implement OpenTracing::Tracer API. # # @return [OpenTracing::Tracer] attr_accessor :tracer # Operation name of tracing span. # # @return [String] attr_accessor :operation_name # List of handled errors. # # @return [Array] attr_accessor :expected_errors # Value for component tag # # @return [String] attr_accessor :component # Value for service_name tag. # # @return [String] attr_accessor :service_name # Instance of response logger # # @return [ResponseLogger] attr_accessor :response_logger # Inject trace headers to response # # @return [Boolean] attr_accessor :inject end # @param config [Config] # # @yieldparam config [Config] def initialize( app, config = Config.new ) @app = app @config = config yield(config) if block_given? end # Wrap Faraday request to trace it with OpenTracing # @param env [Faraday::Env] # @return [Faraday::Response] def call(env) trace_request(env) do |span| inject_tracing(span, env) if inject response_logger&.log_request(span, env.request_headers) @app.call(env).on_complete do |response| set_response_tags(span, response) response_logger&.log_response(span, response.response_headers) end end end private def_delegators :@config, :tracer, :operation_name, :component, :expected_errors, :inject, :response_logger, :service_name def trace_request(env) scope = build_scope(env) span = scope.span yield(span) rescue *expected_errors => e set_exception_tags(span, e) raise ensure scope.close end def build_scope(env) tracer.start_active_span( operation_name, tags: request_tags(env), ) end def inject_tracing(span, env) tracer.inject( span.context, OpenTracing::FORMAT_RACK, env[:request_headers], ) end def request_tags(env) base_tags .merge(http_tags(env)) .merge(faraday_tags(env)) end def base_tags { 'span.kind' => 'client', 'component' => component, 'peer.service_name' => service_name, }.compact end def http_tags(env) { 'http.method' => env[:method], 'http.url' => env[:url].to_s, } end def faraday_tags(env) { 'faraday.adapter' => @app.class.to_s, 'faraday.parallel' => env.parallel?, 'faraday.parse_body' => env.parse_body?, 'faraday.ssl_verify' => env.ssl.verify?, } end def set_response_tags(span, response) span.set_tag('http.status_code', response.status) return if response.success? set_http_error_tags(span, response) end def set_http_error_tags(span, response) span.set_tag('error', true) span.log_kv( event: 'error', message: response.body.to_s, 'error.kind': 'http', ) end def set_exception_tags(span, error) span.set_tag('error', true) span.log_kv( event: 'error', message: error.message, 'error.kind': error.class.to_s, stack: error.backtrace, ) end end end end end