select_rails_log 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +13 -0
  3. data/CHANGELOG.md +22 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +93 -0
  7. data/Rakefile +16 -0
  8. data/exe/select_rails_log +5 -0
  9. data/images/boxplot.png +0 -0
  10. data/images/histgram.png +0 -0
  11. data/images/text.png +0 -0
  12. data/lib/select_rails_log/command_line_options.rb +71 -0
  13. data/lib/select_rails_log/constants.rb +36 -0
  14. data/lib/select_rails_log/extension.rb +59 -0
  15. data/lib/select_rails_log/filter/base_filter.rb +35 -0
  16. data/lib/select_rails_log/filter/controller_action_filter.rb +50 -0
  17. data/lib/select_rails_log/filter/duration_range_filter.rb +40 -0
  18. data/lib/select_rails_log/filter/http_method_filter.rb +26 -0
  19. data/lib/select_rails_log/filter/http_status_filter.rb +39 -0
  20. data/lib/select_rails_log/filter/logs_regexp_filter.rb +27 -0
  21. data/lib/select_rails_log/filter/params_regexp_filter.rb +27 -0
  22. data/lib/select_rails_log/filter/range_pattern.rb +32 -0
  23. data/lib/select_rails_log/filter/request_id_filter.rb +28 -0
  24. data/lib/select_rails_log/filter/time_range_filter.rb +43 -0
  25. data/lib/select_rails_log/filter.rb +16 -0
  26. data/lib/select_rails_log/options.rb +20 -0
  27. data/lib/select_rails_log/printer/base_printer.rb +117 -0
  28. data/lib/select_rails_log/printer/boxplot_printer.rb +83 -0
  29. data/lib/select_rails_log/printer/data_serializable.rb +43 -0
  30. data/lib/select_rails_log/printer/histgram_printer.rb +48 -0
  31. data/lib/select_rails_log/printer/json_printer.rb +43 -0
  32. data/lib/select_rails_log/printer/jsonl_printer.rb +27 -0
  33. data/lib/select_rails_log/printer/null_printer.rb +19 -0
  34. data/lib/select_rails_log/printer/raw_printer.rb +45 -0
  35. data/lib/select_rails_log/printer/statistics_printer.rb +84 -0
  36. data/lib/select_rails_log/printer/text_printer.rb +51 -0
  37. data/lib/select_rails_log/printer/tsv_printer.rb +54 -0
  38. data/lib/select_rails_log/printer.rb +17 -0
  39. data/lib/select_rails_log/runner.rb +151 -0
  40. data/lib/select_rails_log/scanner.rb +142 -0
  41. data/lib/select_rails_log/selector.rb +32 -0
  42. data/lib/select_rails_log/version.rb +5 -0
  43. data/lib/select_rails_log.rb +22 -0
  44. data/sig/select_rails_log.rbs +4 -0
  45. metadata +131 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelectRailsLog
4
+ module Filter
5
+ class LogsRegexpFilter < BaseFilter
6
+ define_options :logs_regexp_filter do
7
+ option :regexp,
8
+ "--logs-regexp REGEXP", "-L", Regexp,
9
+ "Filter by log messages",
10
+ %q( ex: '"^ Rendering .*\.json"')
11
+ end
12
+
13
+ def initialize(...)
14
+ super
15
+ @regexp = options[:regexp]
16
+ end
17
+
18
+ def runnable?
19
+ !!@regexp
20
+ end
21
+
22
+ def run(data)
23
+ data[LOGS].any? { |log| @regexp.match?(log[MESSAGE]) }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelectRailsLog
4
+ module Filter
5
+ class ParamsRegexpFilter < BaseFilter
6
+ define_options :params_regexp_filter do
7
+ option :regexp,
8
+ "--params-regexp REGEXP", "-P", Regexp,
9
+ "Filter by parameters",
10
+ %q( ex: '"foo"=>"ba[rz]"')
11
+ end
12
+
13
+ def initialize(...)
14
+ super
15
+ @regexp = options[:regexp]
16
+ end
17
+
18
+ def runnable?
19
+ !!@regexp
20
+ end
21
+
22
+ def run(data)
23
+ @regexp.match?(data[PARAMETERS])
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelectRailsLog
4
+ module Filter
5
+ module RangePattern
6
+ private
7
+
8
+ def parse_range_pattern(pattern)
9
+ if /\A(?<range_begin>.*?[^.])?\.\.(?<exclude_end>\.)?(?<range_end>[^.].*)?\z/ =~ pattern
10
+ range_begin = yield(range_begin) if range_begin
11
+ range_end = yield(range_end) if range_end
12
+ exclude_end = !exclude_end.nil?
13
+ range_by_begin_end(range_begin, range_end, exclude_end)
14
+ elsif /\A(?<base>.+),(?<delta>[^,]+)\z/ =~ pattern
15
+ delta = delta.to_f
16
+ base = yield(base)
17
+ range_by_base_delta(base, delta)
18
+ else
19
+ raise ArgumentError
20
+ end
21
+ end
22
+
23
+ def range_by_begin_end(begin_time, end_time, exclude_end)
24
+ Range.new(begin_time, end_time, exclude_end)
25
+ end
26
+
27
+ def range_by_base_delta(base, delta)
28
+ range_by_begin_end(base - delta, base + delta, true)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelectRailsLog
4
+ module Filter
5
+ class RequestIdFilter < BaseFilter
6
+ filter_type :request
7
+
8
+ define_options :request_id_filter do
9
+ option :request_ids, "--request-ids IDs", "-I", Array, "Filter by request-id"
10
+ end
11
+
12
+ def initialize(...)
13
+ super
14
+ @request_ids = options[:request_ids].dup
15
+ end
16
+
17
+ def runnable?
18
+ !!@request_ids&.any?
19
+ end
20
+
21
+ def run(data)
22
+ raise StopIteration if @request_ids.empty?
23
+
24
+ !!@request_ids.delete(data[REQUEST_ID])
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "range_pattern"
4
+
5
+ module SelectRailsLog
6
+ module Filter
7
+ class TimeRangeFilter < BaseFilter
8
+ include RangePattern
9
+
10
+ define_options :time_range_filter do
11
+ option :pattern,
12
+ "--time-range RANGE", "-T", String,
13
+ "Filter by time range",
14
+ " range format is 'time1..time2', 'time1...time2', or 'time,seconds'.",
15
+ " ex: '2018-01-02 12:00..2018-02-01 12:00', '1/2 12:00...2/2 12:00', '3/5 12:00,30'"
16
+ end
17
+
18
+ def initialize(...)
19
+ super
20
+
21
+ pattern = options[:pattern]
22
+ return unless pattern
23
+
24
+ begin
25
+ @range = parse_range_pattern(pattern) { |time_str| Time.parse(time_str) }
26
+ rescue ArgumentError
27
+ raise CommandLineOptionError, "invalid time range pattern `#{pattern}`"
28
+ end
29
+ end
30
+
31
+ def runnable?
32
+ !!@range
33
+ end
34
+
35
+ def run(data)
36
+ return true if @range.cover?(data[STARTED]) || @range.cover?(data[COMPLETED])
37
+
38
+ range = data[STARTED]..data[COMPLETED]
39
+ range.cover?(@range.begin) || range.cover?(@range.end)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "filter/base_filter"
4
+ require_relative "filter/request_id_filter"
5
+ require_relative "filter/controller_action_filter"
6
+ require_relative "filter/http_method_filter"
7
+ require_relative "filter/http_status_filter"
8
+ require_relative "filter/time_range_filter"
9
+ require_relative "filter/duration_range_filter"
10
+ require_relative "filter/params_regexp_filter"
11
+ require_relative "filter/logs_regexp_filter"
12
+
13
+ module SelectRailsLog
14
+ module Filter
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module SelectRailsLog
6
+ class Options
7
+ extend Forwardable
8
+
9
+ def_delegators :@options, :key?, :[]=
10
+
11
+ def initialize
12
+ @options = {}
13
+ end
14
+
15
+ def fetch(key)
16
+ @options.fetch(key)
17
+ end
18
+ alias [] fetch
19
+ end
20
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module SelectRailsLog
6
+ module Printer
7
+ class BasePrinter < Extension
8
+ include Constants
9
+
10
+ define_options :base_printer do
11
+ separator ""
12
+ separator "printer options:"
13
+
14
+ option :default_output, "--default-output PATH", "-O", "Output to file or directory"
15
+ option :exclude_debug_logs, "--exclude-debug-logs", "-x", "Exclude debug logs"
16
+ end
17
+
18
+ OUTPUT_FILE_DATETIME_FORMAT = "%Y%m%d-%H%M%S.%6N"
19
+ private_constant :OUTPUT_FILE_DATETIME_FORMAT
20
+
21
+ def initialize(options, standard_output)
22
+ super(options)
23
+
24
+ @common_options = @whole_options[:base_printer]
25
+ @output_file = @standard_output = standard_output
26
+ @output_filename = @output_directory = nil
27
+ init_output_destination
28
+
29
+ @prepared = false
30
+ end
31
+
32
+ def close
33
+ @output_file&.close unless output_directory? || output_stdout?
34
+ end
35
+
36
+ def print(data)
37
+ unless @prepared
38
+ prepare
39
+ @prepared = true
40
+ end
41
+
42
+ with_output(data) do |io|
43
+ print_data(io, data)
44
+ end
45
+ end
46
+
47
+ def runnable?
48
+ !!output_option
49
+ end
50
+
51
+ def output_stdout?
52
+ !output_directory? && @output_file == @standard_output
53
+ end
54
+
55
+ def output_directory?
56
+ !!@output_directory
57
+ end
58
+
59
+ private
60
+
61
+ def init_output_destination
62
+ dest = output_option
63
+ dest = @common_options[:default_output] if dest == DEFAULT_OUTPUT
64
+ return if !dest || dest == "-"
65
+
66
+ if dest.end_with?("/")
67
+ @output_directory = dest.chomp("/")
68
+ elsif File.directory?(dest)
69
+ @output_directory = dest
70
+ else
71
+ @output_filename = dest
72
+ end
73
+ end
74
+
75
+ def output_option
76
+ options.key?(:output) && options[:output]
77
+ end
78
+
79
+ def prepare
80
+ return unless @output_filename
81
+
82
+ @output_file = File.open(@output_filename, "w")
83
+ end
84
+
85
+ def with_output(data, &)
86
+ return yield(@output_file) unless output_directory?
87
+
88
+ FileUtils.mkdir_p(@output_directory)
89
+ File.open("#{@output_directory}/#{output_filename(data)}",
90
+ File::CREAT | File::TRUNC | File::WRONLY, &)
91
+ end
92
+
93
+ def output_filename(data)
94
+ timestr = data[STARTED].strftime(OUTPUT_FILE_DATETIME_FORMAT)
95
+ "#{timestr}_#{data[ID]}#{self.class::SUFFIX}"
96
+ end
97
+
98
+ def print_data(_output, _data)
99
+ raise NotImplementedError
100
+ end
101
+
102
+ def each_log_with_index(data)
103
+ data[LOGS].each_with_index do |log, i|
104
+ next if @common_options[:exclude_debug_logs] && log[SEVERITY] == DEBUG
105
+
106
+ yield(log, i)
107
+ end
108
+ end
109
+
110
+ def each_log(data)
111
+ each_log_with_index(data) do |log, _i|
112
+ yield(log)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unicode_plot"
4
+ require "io/console"
5
+
6
+ module SelectRailsLog
7
+ module Printer
8
+ class BoxplotPrinter < BasePrinter
9
+ PLOT_TOTAL_DURATION = "Total duration"
10
+
11
+ define_options :boxplot_printer do
12
+ option :output, "--boxplot [FILE]", "-B", "Output statistics boxplot", default: DEFAULT_OUTPUT
13
+ option :min, "--boxplot-min MIN", " Minimum value for boxplot", Float
14
+ option :max, "--boxplot-max MAX", " Maximum value for boxplot", Float
15
+ option :width, "--boxplot-width NUM", " Width of boxplot column", Integer
16
+ end
17
+
18
+ def initialize(*)
19
+ super
20
+
21
+ @plot_data = Hash.new { |h, k| h[k] = [] }
22
+ @controller_actions = Hash.new { |h, k| h[k] = {} }
23
+ end
24
+
25
+ def close
26
+ print_plot
27
+ super
28
+ end
29
+
30
+ private
31
+
32
+ def init_output_destination
33
+ super
34
+ return unless output_directory?
35
+
36
+ raise CommandLineOptionError, "output to directory is not supported for plot"
37
+ end
38
+
39
+ def print_plot
40
+ return if @plot_data.empty?
41
+
42
+ boxplot.render(@output_file)
43
+ end
44
+
45
+ def boxplot
46
+ opts = {
47
+ title: PLOT_TOTAL_DURATION,
48
+ data: @plot_data.keys.sort.each_with_object({}) { |k, h| h[k] = @plot_data[k] },
49
+ width: boxplot_width,
50
+ xlim: boxplot_xlim
51
+ }.compact
52
+ UnicodePlot.boxplot(**opts)
53
+ end
54
+
55
+ def boxplot_width
56
+ return options[:width] if options[:width]
57
+
58
+ begin
59
+ _rows, cols = @output_file.winsize
60
+ rescue Errno::ENOTTY, Errno::ENODEV, NoMethodError
61
+ return nil
62
+ end
63
+
64
+ cols - @plot_data.keys.map(&:size).max - 8
65
+ end
66
+
67
+ def boxplot_xlim
68
+ return unless options[:min] || options[:max]
69
+
70
+ [
71
+ options[:min] || 0,
72
+ options[:max] || 0
73
+ ]
74
+ end
75
+
76
+ def print_data(_output, data)
77
+ controller, action = data.values_at(CONTROLLER, ACTION)
78
+ controller_action = @controller_actions[controller][action] ||= "#{controller}##{action}"
79
+ @plot_data[controller_action] << data[DURATION]
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module SelectRailsLog
6
+ module Printer
7
+ module DataSerializable
8
+ include Constants
9
+
10
+ DATETIME_FORMAT = "%FT%T.%6N%:z"
11
+
12
+ private
13
+
14
+ def serialize_data(data)
15
+ serialized = data.slice(
16
+ REQUEST_ID, CONTROLLER, ACTION,
17
+ HTTP_STATUS, HTTP_METHOD, PATH, PARAMETERS, CLIENT,
18
+ DURATION, PERFORMANCE
19
+ )
20
+
21
+ serialized[STARTED] = strftime(data[STARTED])
22
+ serialized[COMPLETED] = strftime(data[COMPLETED])
23
+ serialized[PID] = data[PID].to_i
24
+ serialized[LOGS] = collect_logs(data)
25
+ serialized
26
+ end
27
+
28
+ def collect_logs(data)
29
+ logs = []
30
+
31
+ each_log(data) do |log|
32
+ logs << log.merge(TIME => strftime(log[TIME]))
33
+ end
34
+
35
+ logs
36
+ end
37
+
38
+ def strftime(time)
39
+ time&.strftime(DATETIME_FORMAT)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unicode_plot"
4
+
5
+ module SelectRailsLog
6
+ module Printer
7
+ class HistgramPrinter < BasePrinter
8
+ PLOT_TOTAL_DURATION = "Total duration"
9
+
10
+ define_options :histgram_printer do
11
+ option :output, "--histgram [FILE]", "-H", "Output statistics histgram", default: DEFAULT_OUTPUT
12
+ option :nbins, "--histgram-nbins NUM", " Number of bins for histgram", Integer
13
+ end
14
+
15
+ def initialize(*)
16
+ super
17
+
18
+ @plot_data = []
19
+ end
20
+
21
+ def close
22
+ print_plot
23
+ super
24
+ end
25
+
26
+ private
27
+
28
+ def init_output_destination
29
+ super
30
+ return unless output_directory?
31
+
32
+ raise CommandLineOptionError, "output to directory is not supported for plot"
33
+ end
34
+
35
+ def print_plot
36
+ return if @plot_data.empty?
37
+
38
+ UnicodePlot
39
+ .histogram(@plot_data, title: PLOT_TOTAL_DURATION, nbins: options[:nbins])
40
+ .render(@output_file)
41
+ end
42
+
43
+ def print_data(_output, data)
44
+ @plot_data << data[DURATION]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "data_serializable"
4
+
5
+ module SelectRailsLog
6
+ module Printer
7
+ class JsonPrinter < BasePrinter
8
+ include DataSerializable
9
+
10
+ SUFFIX = ".json"
11
+
12
+ define_options :json_printer do
13
+ option :output, "--json [PATH]", "-j", "Output in JSON format", default: DEFAULT_OUTPUT
14
+ end
15
+
16
+ def initialize(...)
17
+ super
18
+ @no_output = true
19
+ end
20
+
21
+ def close
22
+ @output_file.puts "]" unless output_directory?
23
+ super
24
+ end
25
+
26
+ private
27
+
28
+ def print_data(output, data)
29
+ unless output_directory?
30
+ if @no_output
31
+ output.print "["
32
+ @no_output = false
33
+ else
34
+ output.print ","
35
+ end
36
+ end
37
+
38
+ output.print JSON.fast_generate(serialize_data(data))
39
+ output.puts if output_directory?
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "data_serializable"
4
+
5
+ module SelectRailsLog
6
+ module Printer
7
+ class JsonlPrinter < BasePrinter
8
+ include DataSerializable
9
+
10
+ SUFFIX = ".jsonl"
11
+
12
+ define_options :jsonl_printer do
13
+ option :output, "--jsonl [PATH]", "-J", "Output in JSON Lines format", default: DEFAULT_OUTPUT
14
+ end
15
+
16
+ def runnable?
17
+ !!@options[:output]
18
+ end
19
+
20
+ private
21
+
22
+ def print_data(output, data)
23
+ output.puts JSON.fast_generate(serialize_data(data))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelectRailsLog
4
+ module Printer
5
+ class NullPrinter < BasePrinter
6
+ define_options :null_printer do
7
+ option :enabled, "--no-output", "-n", "No output", TrueClass
8
+ end
9
+
10
+ def runnable?
11
+ !!options[:enabled]
12
+ end
13
+
14
+ private
15
+
16
+ def print_data(...); end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SelectRailsLog
4
+ module Printer
5
+ class RawPrinter < BasePrinter
6
+ SUFFIX = ".log"
7
+ DATETIME_FORMAT = "%FT%T.%6N"
8
+
9
+ define_options :raw_printer do
10
+ option :output, "--raw [PATH]", "-r", "Output in raw format", default: DEFAULT_OUTPUT
11
+ end
12
+
13
+ private
14
+
15
+ def print_data(output, data)
16
+ return print_logs(output, data) unless data.key?(RAW_LOGS)
17
+
18
+ each_log_with_index(data) do |_log, index|
19
+ output.puts data[RAW_LOGS][index]
20
+ end
21
+ end
22
+
23
+ def print_logs(output, data)
24
+ pid, request_id = data.values_at(PID, REQUEST_ID)
25
+ reqid = "[#{request_id}] " if request_id
26
+ each_log(data) do |log|
27
+ print_log_line(output, pid, reqid, log)
28
+ end
29
+ end
30
+
31
+ def print_log_line(output, pid, reqid, log)
32
+ severity = log[SEVERITY]
33
+ output.printf(
34
+ "%<sev>s, [%<time>s #%<pid>d] %<severity>5s -- : %<reqid>s%<message>s\n",
35
+ sev: severity[0],
36
+ severity:,
37
+ pid:,
38
+ reqid:,
39
+ time: log[TIME].strftime(DATETIME_FORMAT),
40
+ message: log[MESSAGE]
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end