lib/smartsheet/api/request_logger.rb in smartsheet-1.0.0 vs lib/smartsheet/api/request_logger.rb in smartsheet-1.1.0

- old
+ new

@@ -1,183 +1,183 @@ -require 'logger' - -module Smartsheet - module API - # Censors strings and hash values for select blacklisted keys - class Censor - EXPOSED_CHARS = 4 - KEY_TO_STRING = ->(k){ k.to_s } - KEY_TO_DOWNCASE_STRING = ->(k){ k.to_s.downcase } - - def initialize(*blacklist) - @blacklist = Set.new(blacklist) - end - - def censor_hash(h, case_insensitive: false) - if case_insensitive - _censor_hash(h, KEY_TO_DOWNCASE_STRING, downcased_blacklist) - else - _censor_hash(h, KEY_TO_STRING, blacklist) - end - end - - def censor(str) - total_length = str.length - censored_length = [total_length - EXPOSED_CHARS, 0].max - ('*' * censored_length) + str[censored_length...total_length] - end - - private - - def _censor_hash(h, key_transform, cased_blacklist) - h.collect do |(k, v)| - new_v = - cased_blacklist.include?(key_transform.call(k)) ? - censor(v) : - v - - [k, new_v] - end.to_h - end - - def downcased_blacklist - blacklist.collect { |x| x.downcase } - end - - attr_reader :blacklist - end - - # Logs request and response information, while censoring OAuth-relevant keys - class RequestLogger - QUERY_PARAM_CENSOR = Censor.new 'code', 'client_id', 'hash', 'refresh_token' - HEADER_CENSOR = Censor.new 'authorization' - PAYLOAD_CENSOR = Censor.new 'access_token', 'refresh_token' - - TRUNCATED_BODY_LENGTH = 1024 - - def initialize(logger, log_full_body:) - @logger = logger - @log_full_body = log_full_body - end - - def log_request(request) - log_request_basics(Logger::INFO, request) - log_headers('Request', request) - log_body('Request', request.body) - end - - def log_retry_attempt(request, response, attempt_num) - logger.warn { "Request attempt #{attempt_num} failed" } - log_request_basics(Logger::WARN, request) - log_api_error(Logger::WARN, response) - end - - def log_retry_failure(num_tries) - try_word = num_tries == 1 ? 'try' : 'tries' - logger.error { "Request failed after #{num_tries} #{try_word}" } - end - - def log_successful_response(response) - log_status(Logger::INFO, response) - log_headers('Response', response) - log_body('Response', response.result) - end - - def log_api_error_response(request, error) - log_request_basics(Logger::ERROR, request) - log_api_error(Logger::ERROR, error) - end - - def log_http_error_response(request, error) - log_request_basics(Logger::ERROR, request) - log_http_error(Logger::ERROR, error) - end - - private - - attr_reader :logger, :log_full_body - - def log_request_basics(level, request) - logger.log(level) { "Request: #{request.method.upcase} #{build_logging_url(request)}" } - end - - def build_logging_url(request) - query_params = QUERY_PARAM_CENSOR.censor_hash(request.params) - query_param_str = - if query_params.empty? - '' - else - '?' + query_params.collect { |(k, v)| "#{k}=#{v}" }.join('&') # TODO: URI Encoding - end - request.url + query_param_str - end - - def log_api_error(level, response) - log_status(level, response) - logger.log(level) do - "#{response.error_code}: #{response.message} - Ref ID: #{response.ref_id}" - end - log_headers('Response', response) - end - - def log_http_error(level, response) - log_status(level, response) - log_headers('Response', response) - end - - def log_status(level, response) - logger.log(level) { "Response: #{response.status_code} #{response.reason_phrase}" } - end - - def log_headers(context, req_or_resp) - censored_hash = HEADER_CENSOR.censor_hash(req_or_resp.headers, case_insensitive: true) - logger.debug { "#{context} Headers: #{censored_hash}" } - end - - def log_body(context, body) - return unless body - - body_str = - if body.is_a? String - body - elsif body.is_a? Hash - PAYLOAD_CENSOR.censor_hash(body).to_s - else - '<Binary body>' - end - - body_str = truncate_body(body_str) unless log_full_body - - logger.debug "#{context} Body: #{body_str}" - end - - def truncate_body(body_str) - if body_str.length > TRUNCATED_BODY_LENGTH - body_str[0...TRUNCATED_BODY_LENGTH] + '...' - else - body_str - end - end - end - - # Stubs all request logging methods by doing nothing (see {RequestLogger}) - class MuteRequestLogger - def log_request(request) - end - - def log_retry_attempt(request, response, attempt_num) - end - - def log_retry_failure(num_retries) - end - - def log_successful_response(response) - end - - def log_api_error_response(request, error) - end - - def log_http_error_response(request, error) - end - end - end +require 'logger' + +module Smartsheet + module API + # Censors strings and hash values for select blacklisted keys + class Censor + EXPOSED_CHARS = 4 + KEY_TO_STRING = ->(k){ k.to_s } + KEY_TO_DOWNCASE_STRING = ->(k){ k.to_s.downcase } + + def initialize(*blacklist) + @blacklist = Set.new(blacklist) + end + + def censor_hash(h, case_insensitive: false) + if case_insensitive + _censor_hash(h, KEY_TO_DOWNCASE_STRING, downcased_blacklist) + else + _censor_hash(h, KEY_TO_STRING, blacklist) + end + end + + def censor(str) + total_length = str.length + censored_length = [total_length - EXPOSED_CHARS, 0].max + ('*' * censored_length) + str[censored_length...total_length] + end + + private + + def _censor_hash(h, key_transform, cased_blacklist) + h.collect do |(k, v)| + new_v = + cased_blacklist.include?(key_transform.call(k)) ? + censor(v) : + v + + [k, new_v] + end.to_h + end + + def downcased_blacklist + blacklist.collect { |x| x.downcase } + end + + attr_reader :blacklist + end + + # Logs request and response information, while censoring OAuth-relevant keys + class RequestLogger + QUERY_PARAM_CENSOR = Censor.new 'code', 'client_id', 'hash', 'refresh_token' + HEADER_CENSOR = Censor.new 'authorization' + PAYLOAD_CENSOR = Censor.new 'access_token', 'refresh_token' + + TRUNCATED_BODY_LENGTH = 1024 + + def initialize(logger, log_full_body:) + @logger = logger + @log_full_body = log_full_body + end + + def log_request(request) + log_request_basics(Logger::INFO, request) + log_headers('Request', request) + log_body('Request', request.body) + end + + def log_retry_attempt(request, response, attempt_num) + logger.warn { "Request attempt #{attempt_num} failed" } + log_request_basics(Logger::WARN, request) + log_api_error(Logger::WARN, response) + end + + def log_retry_failure(num_tries) + try_word = num_tries == 1 ? 'try' : 'tries' + logger.error { "Request failed after #{num_tries} #{try_word}" } + end + + def log_successful_response(response) + log_status(Logger::INFO, response) + log_headers('Response', response) + log_body('Response', response.result) + end + + def log_api_error_response(request, error) + log_request_basics(Logger::ERROR, request) + log_api_error(Logger::ERROR, error) + end + + def log_http_error_response(request, error) + log_request_basics(Logger::ERROR, request) + log_http_error(Logger::ERROR, error) + end + + private + + attr_reader :logger, :log_full_body + + def log_request_basics(level, request) + logger.log(level) { "Request: #{request.method.upcase} #{build_logging_url(request)}" } + end + + def build_logging_url(request) + query_params = QUERY_PARAM_CENSOR.censor_hash(request.params) + query_param_str = + if query_params.empty? + '' + else + '?' + query_params.collect { |(k, v)| "#{k}=#{v}" }.join('&') # TODO: URI Encoding + end + request.url + query_param_str + end + + def log_api_error(level, response) + log_status(level, response) + logger.log(level) do + "#{response.error_code}: #{response.message} - Ref ID: #{response.ref_id}" + end + log_headers('Response', response) + end + + def log_http_error(level, response) + log_status(level, response) + log_headers('Response', response) + end + + def log_status(level, response) + logger.log(level) { "Response: #{response.status_code} #{response.reason_phrase}" } + end + + def log_headers(context, req_or_resp) + censored_hash = HEADER_CENSOR.censor_hash(req_or_resp.headers, case_insensitive: true) + logger.debug { "#{context} Headers: #{censored_hash}" } + end + + def log_body(context, body) + return unless body + + body_str = + if body.is_a? String + body + elsif body.is_a? Hash + PAYLOAD_CENSOR.censor_hash(body).to_s + else + '<Binary body>' + end + + body_str = truncate_body(body_str) unless log_full_body + + logger.debug "#{context} Body: #{body_str}" + end + + def truncate_body(body_str) + if body_str.length > TRUNCATED_BODY_LENGTH + body_str[0...TRUNCATED_BODY_LENGTH] + '...' + else + body_str + end + end + end + + # Stubs all request logging methods by doing nothing (see {RequestLogger}) + class MuteRequestLogger + def log_request(request) + end + + def log_retry_attempt(request, response, attempt_num) + end + + def log_retry_failure(num_retries) + end + + def log_successful_response(response) + end + + def log_api_error_response(request, error) + end + + def log_http_error_response(request, error) + end + end + end end \ No newline at end of file