Skip to content

Instantly share code, notes, and snippets.

@arika arika/select_rails_log.rb
Last active Aug 21, 2019

Embed
What would you like to do?
#!/usr/bin/env ruby
require 'time'
require 'pp'
require 'optparse'
require 'ostruct'
request_id_regexp = /\h{8}-\h{4}-\h{4}-\h{4}-\h{12}/ # Rails 5.x
LOG_REGEXP = /\A., \[(?<time>\S+) #(?<pid>\d+)\] *(?<severity>\S+) -- :(?: \[(?<reqid>#{request_id_regexp})\])? (?<log>.*)/
ANSI_ESCAPE_SEQ_REGEXP = /\e\[(?:\d{1,2}(?:;\d{1,2})?)?[mK]/
qstr_regexp = /"(?>(?:\\\\)*\\.|[^"\\]*)*"/
tempfile_regexp = /#<Tempfile:.*?>/
uploaded_file_ivar_regexp = /@\w+=(?:#{qstr_regexp}|#{tempfile_regexp})/
uploaded_file_class_regexp = /ActionDispatch::Http::UploadedFile|Rack::Test::UploadedFile/
UPLOADED_FILE_REGEXP = /#<#{uploaded_file_class_regexp}:0x\h+ (?:#{uploaded_file_ivar_regexp}, )*#{uploaded_file_ivar_regexp}>/
PARAMATERS_REGEXP = /[{}\[\],]|=>|\s+|true|false|nil|\d+(?:\.\d+)?|#{qstr_regexp}|#{UPLOADED_FILE_REGEXP}/
def process(info, options)
if options.raw_output
puts info[:orig_logs]
return
end
puts "time: #{info[:begin_time]} .. #{info[:end_time]}"
puts "id: #{info[:request_id]}" if info[:request_id]
puts "pid: #{info[:pid]}"
puts "status: #{info[:status]}"
if options.show_parsed_params
parsed_params_str = pp_params(info[:parameters])
puts "params:\n #{parsed_params_str.gsub(/\n/, "\n ")}" if parsed_params_str
end
logs = info[:logs]
logs = logs.reject {|severity, _| severity == 'DEBUG' } unless options.show_debug_logs
logs_text = logs.map {|_, log| log }.join("\n ").gsub(ANSI_ESCAPE_SEQ_REGEXP, '')
puts "logs:\n #{logs_text}"
puts
end
def pp_params(parameters)
return if parameters.nil?
parsed = decode_parameters(parameters)
return unless parsed
out = ''
PP.pp(parsed, out)
out.chomp
end
def decode_parameters(parameters)
tokens = parameters.scan(PARAMATERS_REGEXP)
return unless parameters == tokens.join('')
eval tokens.map {|token| UPLOADED_FILE_REGEXP.match(token) ? token.dump : token }.join('')
rescue SyntaxError
nil
end
def apply_filters(info, filters, invert)
if filters.empty?
result = true
else
result = filters.all? {|filter| filter.call(info) }
result = !result if invert
end
return yield if result && block_given?
result
end
options = OpenStruct.new(
pre_filters: [],
filters: [],
include_debug_log: false,
show_parsed_params: false,
invert: false,
raw_output: false
)
OptionParser.new do |o|
o.banner = "usage: #{$0} [options] [log-files]"
o.separator ''
o.separator 'Options:'
o.on('-a=NAMES', '--action-names=NAMES', Array,
'Filter by controller and action names; ex: "FooController#index,BarController,..."') do |list|
list = list.select {|name| /\A\S+(?:#\S+)?\z/ }.map {|name| name.split(/#/, 2) }
options.pre_filters << lambda do |info|
list.any? do |controller, action|
info[:controller] == controller &&
(action.nil? || info[:action] == action)
end
end unless list.empty?
end
o.on('-s=STATUSES', '--statuses=STATUSES',
'Filter by statuses; ex: "3,20,!201,..."') do |list|
m = /\A([,\d]+)?(?:!([,\d]+))?\z/.match(list)
unless m
raise OptionParser::InvalidArgument,
'Invalid statuses format (expected "[include_status,...][!exclude_status,...]")'
end
includes = m[1] ? m[1].split(/,/) : ['']
excludes = m[2] ? m[2].split(/,/) : []
options.filters << lambda do |info|
!excludes.any? {|exc| info[:status].index(exc) == 0 } &&
includes.any? {|inc| info[:status].index(inc) == 0 }
end
end
o.on('-t=TIME_RANGE', '--time-range=TIME_RANGE', String,
'Filter by time range; ex: "2018-01-02 12:00..2018-02-01 12:00", "1/2 12:00...2/2 12:00", or "3/5,60"') do |range_str|
range = nil
if /\.\.\.?/ =~ range_str
range_begin = Time.parse($`)
range_end = Time.parse($') + ($& == '...' ? 0 : 1)
range = Range.new(range_begin, range_end, true)
elsif /,(\d+)\z/ =~ range_str
time = Time.parse($`)
s = $1.to_i
range = Range.new(time - s, time + s + 1, true)
end
options.filters << lambda do |info|
range.cover?(info[:begin_time]) ||
range.cover?(info[:end_time])
end if range
end
o.on('-m=METHOD', '--method=METHOD', 'Filter by HTTP method name') do |method|
method = method.upcase
options.pre_filters << lambda do |info|
info[:method] == method
end
end
o.on('-p=REGEXP', '--params-regexp=REGEXP', Regexp,
%q(Filter by parameters pattern; ex: '"foo"=>"ba[rz]"')) do |regexp|
options.filters << lambda do |info|
regexp =~ info[:parameters]
end
end
o.on('-r=REGEXP', '--regexp=REGEXP', Regexp,
%q(Filter by pattern; ex: '"^ Rendering .*\.json"')) do |regexp|
options.filters << lambda do |info|
info[:logs].any? {|_, log| regexp =~ log }
end
end
o.on('-v', '--invert-match', 'Invert match result') do
options.invert = true
end
o.on('-D', '--[no-]show-debug-logs', 'Show DEBUG logs') do |value|
options.show_debug_logs = value
end
o.on('-P', '--[no-]show-parsed-params', 'Prityprint parameters (NOTE: use eval internally)') do |value|
options.show_parsed_params = value
end
o.on('--raw', 'Output as raw form') do
options.raw_output = true
end
o.on('-h', '--help', 'Show help') do
puts o
exit
end
end.parse!
buff = {}
ARGF.each_line do |line|
m = LOG_REGEXP.match(line)
next unless m
pid = m[:pid]
reqid = m[:reqid]
sev_log = [m[:severity], m[:log]]
ident = reqid || pid
info = buff[ident] if buff.key?(ident)
case m[:log]
when /\AStarted (\S+) "([^"]*)" for (\S+)/
buff.delete(ident)
info = { begin_time: m[:time], pid: pid, request_id: reqid, method: $1, path: $2, client: $3, logs: [sev_log], orig_logs: [line] }
buff[ident] = info
when /\AProcessing by ([^\s#]+)#(\S+)/
next unless info
info[:controller] = $1
info[:action] = $2
info[:logs] << sev_log
info[:orig_logs] << line
unless apply_filters(info, options.pre_filters, options.invert)
buff.delete(ident)
next
end
when /\A Parameters: (.*)/
next unless info
info[:parameters] = $1
info[:logs] << sev_log
info[:orig_logs] << line
when /\ACompleted (\d+) /
next unless info
info[:status] = $1
info[:begin_time] = Time.parse(info[:begin_time])
info[:end_time] = Time.parse(m[:time])
info[:logs] << sev_log
info[:orig_logs] << line
apply_filters(info, options.filters, options.invert) do
process(info, options)
end
buff.delete(ident)
else
next unless info
info[:logs] << sev_log
info[:orig_logs] << line
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.