module HttpLog
Constants
- LOG_PREFIX
- PARAM_MASK
- VERSION
Attributes
configuration[W]
Public Class Methods
call(options = {})
click to toggle source
# File lib/httplog/http_log.rb, line 31 def call(options = {}) parse_request(options) if config.json_log log_json(options) elsif config.graylog_formatter log_graylog(options) elsif config.compact_log log_compact(options[:method], options[:url], options[:response_code], options[:benchmark]) else HttpLog.log_request(options[:method], options[:url]) HttpLog.log_headers(options[:request_headers]) HttpLog.log_data(options[:request_body]) HttpLog.log_status(options[:response_code]) HttpLog.log_benchmark(options[:benchmark]) HttpLog.log_headers(options[:response_headers]) HttpLog.log_body(options[:response_body], options[:mask_body], options[:encoding], options[:content_type]) end end
colorize(msg)
click to toggle source
# File lib/httplog/http_log.rb, line 176 def colorize(msg) return msg unless config.color if config.color.is_a?(Hash) msg = Rainbow(msg).color(config.color[:color]) if config.color[:color] msg = Rainbow(msg).bg(config.color[:background]) if config.color[:background] else msg = Rainbow(msg).color(config.color) end msg rescue StandardError warn "HTTPLOG CONFIGURATION ERROR: #{config.color} is not a valid color" msg end
configuration()
click to toggle source
# File lib/httplog/http_log.rb, line 18 def configuration @configuration ||= Configuration.new end
Also aliased as: config
configure() { |configuration| ... }
click to toggle source
# File lib/httplog/http_log.rb, line 27 def configure yield(configuration) end
log(msg)
click to toggle source
# File lib/httplog/http_log.rb, line 60 def log(msg) return unless config.enabled config.logger.public_send(config.logger_method, config.severity, colorize(prefix + msg.to_s)) end
log_benchmark(seconds)
click to toggle source
# File lib/httplog/http_log.rb, line 93 def log_benchmark(seconds) return unless config.log_benchmark log("Benchmark: #{seconds.to_f.round(6)} seconds") end
log_body(body, mask_body, encoding = nil, content_type = nil)
click to toggle source
# File lib/httplog/http_log.rb, line 99 def log_body(body, mask_body, encoding = nil, content_type = nil) return unless config.log_response data = parse_body(body, mask_body, encoding, content_type) if config.prefix_response_lines log('Response:') log_data_lines(data) else log("Response:\n#{data}") end rescue BodyParsingError => e log("Response: #{e.message}") end
log_compact(method, uri, status, seconds)
click to toggle source
# File lib/httplog/http_log.rb, line 166 def log_compact(method, uri, status, seconds) return unless config.compact_log status = Rack::Utils.status_code(status) unless status == /\d{3}/ log("#{method.to_s.upcase} #{masked(uri)} completed with status code #{status} in #{seconds.to_f.round(6)} seconds") end
log_connection(host, port = nil)
click to toggle source
# File lib/httplog/http_log.rb, line 66 def log_connection(host, port = nil) return if config.json_log || config.compact_log || !config.log_connect log("Connecting: #{[host, port].compact.join(':')}") end
log_data(data)
click to toggle source
# File lib/httplog/http_log.rb, line 155 def log_data(data) return unless config.log_data if config.prefix_data_lines log('Data:') log_data_lines(data) else log("Data: #{data}") end end
log_headers(headers = {})
click to toggle source
# File lib/httplog/http_log.rb, line 78 def log_headers(headers = {}) return unless config.log_headers masked(headers).each do |key, value| log("Header: #{key}: #{value}") end end
log_request(method, uri)
click to toggle source
# File lib/httplog/http_log.rb, line 72 def log_request(method, uri) return unless config.log_request log("Sending: #{method.to_s.upcase} #{masked(uri)}") end
log_status(status)
click to toggle source
# File lib/httplog/http_log.rb, line 86 def log_status(status) return unless config.log_status status = Rack::Utils.status_code(status) unless status == /\d{3}/ log("Status: #{status}") end
masked_body_url?(url)
click to toggle source
# File lib/httplog/http_log.rb, line 56 def masked_body_url?(url) config.filter_parameters.any? && config.url_masked_body_pattern && url.to_s.match(config.url_masked_body_pattern) end
parse_body(body, mask_body, encoding, content_type)
click to toggle source
# File lib/httplog/http_log.rb, line 114 def parse_body(body, mask_body, encoding, content_type) raise BodyParsingError, "(not showing binary data)" unless text_based?(content_type) if body.is_a?(Net::ReadAdapter) # open-uri wraps the response in a Net::ReadAdapter that defers reading # the content, so the response body is not available here. raise BodyParsingError, '(not available yet)' end body_copy = body.dup body_copy = body.to_s if defined?(HTTP::Response::Body) && body.is_a?(HTTP::Response::Body) return nil if body_copy.nil? || body_copy.empty? if encoding =~ /gzip/ begin sio = StringIO.new(body_copy.to_s) gz = Zlib::GzipReader.new(sio) body_copy = gz.read rescue Zlib::GzipFile::Error log("(gzip decompression failed)") end end result = utf_encoded(body_copy.to_s, content_type) if mask_body if content_type =~ /json/ result = begin masked_data config.json_parser.load(result) rescue => e 'Failed to mask response body: ' + e.message end else result = masked(result) end end result end
reset!()
click to toggle source
# File lib/httplog/http_log.rb, line 23 def reset! @configuration = nil end
transform_response_code(response_code_name)
click to toggle source
# File lib/httplog/http_log.rb, line 172 def transform_response_code(response_code_name) Rack::Utils::HTTP_STATUS_CODES.detect { |_k, v| v.to_s.casecmp(response_code_name.to_s).zero? }.first end
url_approved?(url)
click to toggle source
# File lib/httplog/http_log.rb, line 50 def url_approved?(url) return false if config.url_blacklist_pattern && url.to_s.match(config.url_blacklist_pattern) !config.url_whitelist_pattern || url.to_s.match(config.url_whitelist_pattern) end
Private Class Methods
dump_json(data)
click to toggle source
# File lib/httplog/http_log.rb, line 206 def dump_json(data) config.json_parser.dump(json_payload(data)) end
hash_classes()
click to toggle source
# File lib/httplog/http_log.rb, line 325 def hash_classes @hash_classes ||= begin hash_classes = [Hash, Enumerator] hash_classes << HTTP::Headers if defined?(HTTP::Headers) hash_classes end end
json_payload(data = {})
click to toggle source
# File lib/httplog/http_log.rb, line 226 def json_payload(data = {}) data[:response_code] = transform_response_code(data[:response_code]) if data[:response_code].is_a?(Symbol) parsed_body = begin parse_body(data[:response_body], data[:mask_body], data[:encoding], data[:content_type]) rescue BodyParsingError => e e.message end if config.compact_log { method: data[:method].to_s.upcase, url: masked(data[:url]), response_code: data[:response_code].to_i, benchmark: data[:benchmark] } else { method: data[:method].to_s.upcase, url: masked(data[:url]), request_body: data[:request_body], request_headers: masked(data[:request_headers].to_h), response_code: data[:response_code].to_i, response_body: parsed_body, response_headers: data[:response_headers].to_h, benchmark: data[:benchmark] } end end
log_data_lines(data)
click to toggle source
# File lib/httplog/http_log.rb, line 351 def log_data_lines(data) data.each_line.with_index do |line, row| if config.prefix_line_numbers log("#{row + 1}: #{line.chomp}") else log(line.strip) end end end
log_graylog(data)
click to toggle source
# File lib/httplog/http_log.rb, line 210 def log_graylog(data) result = json_payload(data) begin send_to_graylog result rescue result[:response_body] = 'Graylog JSON dump failed' result[:request_body] = 'Graylog JSON dump failed' send_to_graylog result end end
log_json(data = {})
click to toggle source
# File lib/httplog/http_log.rb, line 192 def log_json(data = {}) return unless config.json_log log( begin dump_json(data) rescue data[:response_body] = "#{config.json_parser} dump failed" data[:request_body] = "#{config.json_parser} dump failed" dump_json(data) end ) end
masked(msg, key=nil)
click to toggle source
# File lib/httplog/http_log.rb, line 256 def masked(msg, key=nil) return msg if config.filter_parameters.empty? return msg if msg.nil? # If a key is given, msg is just the value and can be replaced # in its entirety. return (config.filter_parameters.include?(key.downcase) ? PARAM_MASK : msg) if key # Otherwise, we'll parse Strings for key=value pairs, # for name="key"\n value... case msg when *string_classes config.filter_parameters.reduce(msg) do |m,key| scrubbed = m.to_s.encode('UTF-8', invalid: :replace, undef: :replace) scrubbed.to_s.gsub(/(#{key})=[^&]+/i, "#{key}=#{PARAM_MASK}") .gsub(/name="#{key}"\s+\K[\s\w]+/, "#{PARAM_MASK}\r\n") # multi-part Faraday end # ...and recurse over hashes when *hash_classes Hash[msg.map {|k,v| [k, masked(v, k)]}] else log "*** FILTERING NOT APPLIED BECAUSE #{msg.class} IS UNEXPECTED ***" msg end end
masked_data(msg)
click to toggle source
# File lib/httplog/http_log.rb, line 303 def masked_data msg case msg when Hash Hash[msg.map { |k, v| [k, config.filter_parameters.include?(k.downcase) ? PARAM_MASK : masked_data(v)] }] when Array msg.map { |element| masked_data(element) } else msg end end
parse_request(options)
click to toggle source
# File lib/httplog/http_log.rb, line 282 def parse_request(options) return if options[:request_body].nil? # Downcase content-type and content-encoding because ::HTTP returns "Content-Type" and "Content-Encoding" headers = options[:request_headers].find_all do |header, _| %w[content-type Content-Type content-encoding Content-Encoding].include? header end.to_h.each_with_object({}) { |(k, v), h| h[k.downcase] = v } copy = options[:request_body].dup options[:request_body] = if text_based?(headers['content-type']) && options[:mask_body] begin parse_body(copy, options[:mask_body], headers['content-encoding'], headers['content-type']) rescue BodyParsingError => e log(e.message) end else masked(copy).to_s end end
prefix()
click to toggle source
# File lib/httplog/http_log.rb, line 361 def prefix if config.prefix.respond_to?(:call) config.prefix.call else config.prefix.to_s end end
send_to_graylog(data)
click to toggle source
# File lib/httplog/http_log.rb, line 221 def send_to_graylog data data.compact! config.logger.public_send(config.logger_method, config.severity, config.graylog_formatter.call(data)) end
string_classes()
click to toggle source
# File lib/httplog/http_log.rb, line 314 def string_classes @string_classes ||= begin string_classes = [String] string_classes << HTTP::Response::Body if defined?(HTTP::Response::Body) string_classes << HTTP::URI if defined?(HTTP::URI) string_classes << URI::HTTP if defined?(URI::HTTP) string_classes << HTTP::FormData::Urlencoded if defined?(HTTP::FormData::Urlencoded) string_classes end end
text_based?(content_type)
click to toggle source
# File lib/httplog/http_log.rb, line 343 def text_based?(content_type) # This is a very naive way of determining if the content type is text-based; but # it will allow application/json and the like without having to resort to more # heavy-handed checks. content_type =~ /^text/ || content_type =~ /^application/ && !['application/octet-stream', 'application/pdf'].include?(content_type) end
utf_encoded(data, content_type = nil)
click to toggle source
# File lib/httplog/http_log.rb, line 333 def utf_encoded(data, content_type = nil) charset = content_type.to_s.scan(/; charset=(\S+)/).flatten.first || 'UTF-8' begin data.force_encoding(charset) rescue StandardError data.force_encoding('UTF-8') end data.encode('UTF-8', invalid: :replace, undef: :replace) end