select_rails_log 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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