# frozen_string_literal: true module PriceHubble module Instrumentation # Produce logs for requests. class LogSubscriber < ActiveSupport::LogSubscriber # Return the PriceHubble SDK configured logger when logging is enabled. # Otherwise +nil+ is returned and the subscriber is never started. # # @return [Logger, nil] the logger to use def logger return unless PriceHubble.configuration.request_logging PriceHubble.configuration.logger end # Log request statistics and debugging details. # # @param event [ActiveSupport::Notifications::Event] the subscribed event def request(event) log_action_summary(event) log_request_details(event) log_response_details(event) end private # Print some top-level request/action details. # # @param event [ActiveSupport::Notifications::Event] the subscribed event def log_action_summary(event) env = event.payload info do "[#{req_id(env)}] #{req_origin(env)} -> #{res_result(env)} " \ "(#{event.duration.round(1)}ms)" end end # Print details about the request. # # @param event [ActiveSupport::Notifications::Event] the subscribed event def log_request_details(event) env = event.payload debug do "[#{req_id(env)}] #{req_dest(env)} > " \ "#{env.request_headers.sort.to_h.to_json}" end end # When no response is available (due to timeout, DNS resolve issues, etc) # we just can log an error without details. Otherwise print details about # the response. # # @param event [ActiveSupport::Notifications::Event] the subscribed event def log_response_details(event) env = event.payload if env.response.nil? return error do "[#{req_id(env)}] #{req_dest(env)} < #{res_result(env)}" end end debug do "[#{req_id(env)}] #{req_dest(env)} < " \ "#{env.response_headers.sort.to_h.to_json}" end end # Format the request identifier. # # @param env [Faraday::Env] the request/response environment # @return [String] the request identifier def req_id(env) env.request.context[:request_id].to_s end # Format the request/action origin. # # @param env [Faraday::Env] the request/response environment # @return [String] the request identifier def req_origin(env) req = env.request.context action = req[:action] action = color(action, color_method(action), true) client = req[:client].to_s.gsub('PriceHubble::Client', 'PriceHubble') "#{client.underscore}##{action}" end # Format the request destination. # # @param env [Faraday::Env] the request/response environment # @return [String] the request identifier def req_dest(env) method = env[:method].to_s.upcase method = color(method, color_method(method), true) url = env[:url].to_s.gsub(/access_token=[^&]+/, 'access_token=[FILTERED]') "#{method} #{url}" end # Format the request result. # # @param env [Faraday::Env] the request/response environment # @return [String] the request identifier def res_result(env) return color('no response', RED, true).to_s if env.response.nil? status = env[:status] color("#{status}/#{env[:reason_phrase]}", color_status(status), true) end # Decide which color to use for the given HTTP status code. # # @param status [Integer] the HTTP status code # @return [String] the ANSI color code def color_status(status) case status when 0..199 then MAGENTA when 200..299 then GREEN when 300..399 then YELLOW when 400..599 then RED else WHITE end end # Decide which color to use for the given HTTP/client method. # # @param method [String] the method to inspect # @return [String] the ANSI color code def color_method(method) case method when /delete/i then RED when /get|search|reload|find/i then BLUE when /post|create/i then GREEN when /put|patch|update/i then YELLOW when /login|logout|download|query/i then CYAN else MAGENTA end end end end end