greenhat 0.1.5 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6b9e87957f5dfc919379519552041ce83b8daa5923b0e89fc64424e050d8981
4
- data.tar.gz: 412b2c4db6613c707c8aad3e39e41a3fb018d1834f5ef17000b11b3af582c639
3
+ metadata.gz: 79572c7389cb4cd519007f185a7fc249a771c08fa1fcbde8645d99786db42736
4
+ data.tar.gz: 6904a95f759a1dc0e866733f0a6220c62b47f3985998f82e2efd4619fdabe390
5
5
  SHA512:
6
- metadata.gz: da4b45b6433d2c4c786bca4b46dc0cfad9b879796e6040b9efceb5ba5181f329c7d3792aef458fde54698791f03830168aad17b9b7d31191aa2e717407444662
7
- data.tar.gz: 3d53ed5c45398acc3a317cc41a5573b82ab1c263e947c94cc419f235efd5bc527905f4484867968535f12aed15c3fbb79abe1d63093402f845c572557b7e288d
6
+ metadata.gz: d44e38727ce532ddbdcf8cc9aad1f5d7ee26a113ee589489bb5f15a99e616d06f66d304dc1e7735a679a707ed99323a45184234779066ca9f1f2cdea3e1b87e9
7
+ data.tar.gz: 6fb959d970482e244bea30f39ccd7357ce442fbc886df0084f425c3540b662a86972889213273ed5e734cace0f6b2b710c356c5585f98f76ffc4763ce2c68b10
data/lib/greenhat.rb CHANGED
@@ -76,5 +76,6 @@ require 'greenhat/pry_helpers'
76
76
  require 'greenhat/tty/line'
77
77
  require 'greenhat/tty/reader'
78
78
  require 'greenhat/tty/custom_line'
79
+ require 'greenhat/tty/columns'
79
80
 
80
81
  Warning.ignore(/The table size exceeds the currently set width/)
@@ -20,8 +20,64 @@ module GreenHat
20
20
  end
21
21
  end
22
22
 
23
- def self.df
24
- Thing.where(name: 'df_h')
23
+ def self.df(args = {})
24
+ things = Thing.where(name: 'df_h')
25
+
26
+ # Host / Archive
27
+ things.select! { |x| x.archive? args.archive } if args.archive
28
+
29
+ things
30
+ end
31
+
32
+ # Unified Output Handler
33
+ # rubocop:disable Metrics/MethodLength
34
+ def self.format_output(file, name = false, limit = nil, filter = %w[tmpfs loop])
35
+ output = []
36
+
37
+ output << file.friendly_name if name
38
+
39
+ # Reject TMPFS
40
+ disks = file.data.sort_by { |x| x.use.to_i }.reverse
41
+
42
+ # Filter
43
+ disks.reject! { |x| filter.any? { |y| x.filesystem.include? y } }
44
+
45
+ disks = disks[0..limit - 1] if limit
46
+
47
+ pad_mount, pad_size, pad_used, pad_avail = GreenHat::Disk.padding(disks)
48
+
49
+ # Headers
50
+ output << [
51
+ 'Mount'.ljust(pad_mount).colorize(:blue),
52
+ 'Size'.ljust(pad_size).colorize(:magenta),
53
+ 'Used'.ljust(pad_used).colorize(:cyan),
54
+ 'Avail'.ljust(pad_avail).colorize(:white),
55
+ '% Use'.ljust(pad_avail).colorize(:green)
56
+ ].join
57
+
58
+ # Table Summary
59
+ disks.map do |disk|
60
+ # Pretty Disk Use
61
+ use = [
62
+ disk.use.rjust(5).ljust(5).colorize(:green),
63
+ ' ['.colorize(:blue),
64
+ ('=' * (disk.use.to_i / 2)).colorize(:green),
65
+ ' ' * (50 - disk.use.to_i / 2),
66
+ ']'.colorize(:blue)
67
+ ].join
68
+
69
+ # Whole Thing
70
+ output << [
71
+ disk.mounted_on.ljust(pad_mount).colorize(:blue),
72
+ disk[:size].to_s.ljust(pad_size).colorize(:magenta),
73
+ disk.used.to_s.ljust(pad_used).colorize(:cyan),
74
+ disk.avail.to_s.ljust(pad_avail).colorize(:white),
75
+ use
76
+ ].join
77
+ end
78
+
79
+ output
25
80
  end
81
+ # rubocop:enable Metrics/MethodLength
26
82
  end
27
83
  end
@@ -1,8 +1,13 @@
1
1
  module GreenHat
2
2
  # Sidekiq Log Helpers
3
3
  module Ps
4
- def self.ps
5
- Thing.where(name: 'ps')
4
+ def self.ps(args)
5
+ things = Thing.where(name: 'ps')
6
+
7
+ # Host / Archive
8
+ things.select! { |x| x.archive? args.archive } if args.archive
9
+
10
+ things
6
11
  end
7
12
  end
8
13
  end
data/lib/greenhat/cli.rb CHANGED
@@ -379,11 +379,7 @@ module GreenHat
379
379
  next
380
380
  end
381
381
 
382
- if line =~ /^exit/i
383
- Settings.cmd_write
384
-
385
- break
386
- end
382
+ break if line =~ /^exit/i
387
383
 
388
384
  Settings.cmd_add(line) unless line.blank?
389
385
 
@@ -4,13 +4,41 @@ module GreenHat
4
4
  module Settings
5
5
  def self.settings
6
6
  @settings ||= {
7
- history: []
7
+ history: [],
8
+
9
+ # round: [2],
10
+ # page: [:true] Automatic,
11
+ truncate: [TTY::Screen.width * 4]
12
+
8
13
  }
9
14
  end
10
15
 
16
+ # Load User Settings and drop them into settings
17
+ def self.settings_load
18
+ return true unless File.exist?(settings_file)
19
+
20
+ Oj.load(File.read(settings_file)).each do |key, value|
21
+ settings[key] = value
22
+ end
23
+ end
24
+
25
+ def self.settings_file
26
+ "#{dir}/settings.json"
27
+ end
28
+
29
+ # Set any Log Arguments that weren't set otherwise
30
+ def self.default_log_args(args, skip_args)
31
+ args[:round] ||= settings.round unless skip_args.include?(:round)
32
+ args[:page] ||= settings.page unless skip_args.include?(:page)
33
+ args[:truncate] ||= settings.truncate unless skip_args.include?(:truncate)
34
+ end
35
+
11
36
  def self.start
12
37
  Dir.mkdir dir unless Dir.exist? dir
13
38
 
39
+ # Load User Settings
40
+ settings_load
41
+
14
42
  # CMD History Loading / Tracking
15
43
  File.write(cmd_file, "\n") unless File.exist? cmd_file
16
44
  end
@@ -33,8 +61,8 @@ module GreenHat
33
61
  File.read(cmd_file).split("\n")
34
62
  end
35
63
 
36
- def self.cmd_write
37
- File.write(cmd_file, cmd_history_clean.join("\n"))
64
+ def self.cmd_history_clear
65
+ File.write(cmd_file, "\n")
38
66
  end
39
67
 
40
68
  def self.cmd_add(line)
@@ -9,8 +9,8 @@ module GreenHat
9
9
  Disk.df
10
10
  end
11
11
 
12
- def self.ps
13
- Process.ps
12
+ def self.ps(raw = {})
13
+ Process.ps raw
14
14
  end
15
15
 
16
16
  def self.netstat
@@ -63,11 +63,11 @@ module GreenHat
63
63
  end
64
64
 
65
65
  def self.history_clear
66
- File.write(GreenHat::Cli.history_file, "\n")
66
+ Settings.cmd_history_clear
67
67
  end
68
68
 
69
69
  def self.history
70
- File.read(GreenHat::Cli.history_file).split("\n").each_with_index do |line, i|
70
+ Settings.cmd_history_clean.each_with_index do |line, i|
71
71
  puts "#{i.to_s.ljust(3).colorize(:magenta)} #{line}"
72
72
  end
73
73
  end
@@ -0,0 +1,43 @@
1
+ module GreenHat
2
+ module ShellHelper
3
+ # Helper to colorize and make outtput easier to read
4
+ module StringColor
5
+ def self.do(key, entry)
6
+ LogBot.debug('Unknown Format', entry.class) if ENV['DEBUG'] && !entry.instance_of?(String)
7
+
8
+ # Other Helpful colorizers
9
+ if colorize?(key)
10
+ colorize(key, entry)
11
+ else
12
+ entry.to_s
13
+ end
14
+ end
15
+
16
+ # Add Color?
17
+ def self.colorize?(key)
18
+ [:severity].any? key
19
+ end
20
+
21
+ # General Key/Value Coloring
22
+ def self.colorize(key, value)
23
+ case key
24
+ when :severity then severity(value)
25
+ else
26
+ value.to_s
27
+ end
28
+ end
29
+ # ----
30
+
31
+ def self.severity(value)
32
+ case value.to_s.downcase.to_sym
33
+ when :debug then value.colorize(:blue)
34
+ when :info then value.colorize(:cyan)
35
+ when :warn then value.colorize(:yellow)
36
+ when :fatal, :error then value.colorize(:light_red)
37
+ else
38
+ value.to_s
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -13,55 +13,16 @@ module GreenHat
13
13
  ShellHelper.file_output GreenHat::Disk.df
14
14
  end
15
15
 
16
- # rubocop:disable Metrics/MethodLength,Metrics/BlockLength
17
- def self.free
18
- GreenHat::Disk.df.each do |file|
19
- # Reject TMPFS
20
- puts file.friendly_name
21
- disks = file.data.sort_by { |x| x.use.to_i }.reverse.reject { |x| x.filesystem.include? 'tmpfs' }
16
+ def self.free(raw = {})
17
+ _log_list, _1opts, args = ShellHelper.param_parse(raw)
22
18
 
23
- # pad_mount = GreenHat::Disk.max_padding(disks, :mounted_on)
24
- # pad_size = GreenHat::Disk.max_padding(disks, :size)
25
- # pad_used = GreenHat::Disk.max_padding(disks, :used)
26
- # pad_avail = GreenHat::Disk.max_padding(disks, :avail)
27
-
28
- pad_mount, pad_size, pad_used, pad_avail = GreenHat::Disk.padding(disks)
29
-
30
- # Headers
31
- puts [
32
- 'Mount'.ljust(pad_mount).colorize(:blue),
33
- 'Size'.ljust(pad_size).colorize(:magenta),
34
- 'Used'.ljust(pad_used).colorize(:cyan),
35
- 'Avail'.ljust(pad_avail).colorize(:white),
36
- '% Use'.ljust(pad_avail).colorize(:green)
37
- ].join
38
-
39
- # Table Summary
40
- disks.map do |disk|
41
- # Pretty Disk Use
42
- use = [
43
- disk.use.rjust(5).ljust(5).colorize(:green),
44
- ' ['.colorize(:blue),
45
- ('=' * (disk.use.to_i / 2)).colorize(:green),
46
- ' ' * (50 - disk.use.to_i / 2),
47
- ']'.colorize(:blue)
48
- ].join
49
-
50
- # Whole Thing
51
- puts [
52
- disk.mounted_on.ljust(pad_mount).colorize(:blue),
53
- disk[:size].to_s.ljust(pad_size).colorize(:magenta),
54
- disk.used.to_s.ljust(pad_used).colorize(:cyan),
55
- disk.avail.to_s.ljust(pad_avail).colorize(:white),
56
- use
57
- ].join
58
- end
19
+ GreenHat::Disk.df(args).each do |file|
20
+ puts GreenHat::Disk.format_output(file, true)
59
21
 
60
22
  # File End Loop / Break
61
23
  puts
62
24
  end
63
25
  end
64
- # rubocop:enable Metrics/MethodLength,Metrics/BlockLength
65
26
  # ------------------------------------------------------------------------
66
27
  end
67
28
  end
@@ -111,11 +111,8 @@ module GreenHat
111
111
  # --sort=count,fail,max,median,min,p95,p99,rps,score
112
112
  # ========================================================================
113
113
  def self.top(log_list)
114
- # Extract Args
115
- log_list, opts, args = ShellHelper.param_parse(log_list)
116
-
117
- # Default to color output
118
- args['color-output'] ||= true
114
+ # Extract Args, No Round / Truncate for Fast Stats
115
+ log_list, opts, args = ShellHelper.param_parse(log_list, %i[page round truncate])
119
116
 
120
117
  cmd = opts.map { |opt| "--#{opt.field}=#{opt.value}" }.join(' ')
121
118
  cmd += args.keys.map { |arg| "--#{arg}" }.join(' ')
@@ -142,8 +139,8 @@ module GreenHat
142
139
  # ===== [ Fast Stats - Errors ] ====================
143
140
  # ========================================================================
144
141
  def self.errors(log_list)
145
- # Extract Args
146
- log_list, opts, args = ShellHelper.param_parse(log_list)
142
+ # Extract Args, No Round / Truncate for Fast Stats
143
+ log_list, opts, args = ShellHelper.param_parse(log_list, %i[page round truncate])
147
144
 
148
145
  # Default to color output
149
146
  args['color-output'] ||= true
@@ -157,6 +154,8 @@ module GreenHat
157
154
  # Convert to Things
158
155
  files = ShellHelper.find_things(log_list)
159
156
 
157
+ LogBot.debug('FastStats CMD', cmd) if ENV['DEBUG']
158
+
160
159
  results = ShellHelper.file_process(files) do |file|
161
160
  [
162
161
  file.friendly_name,
@@ -0,0 +1,128 @@
1
+ module GreenHat
2
+ # CLI Helper
3
+ module ShellHelper
4
+ # Unify Filter / Filter Help
5
+ module Filter
6
+ # rubocop:disable Metrics/MethodLength
7
+ def self.help
8
+ puts "\u2500".colorize(:cyan) * 20
9
+ puts 'Filter'.colorize(:yellow)
10
+ puts "\u2500".colorize(:cyan) * 20
11
+
12
+ puts 'Options'.colorize(:blue)
13
+ puts '--raw'.colorize(:green)
14
+ puts ' Do not use less/paging'
15
+ puts
16
+
17
+ puts '--page'.colorize(:green)
18
+ puts ' Specifically enable or disable paging'
19
+ puts ' E.g. --page (default to true), --page=true, --page=false'
20
+ puts
21
+
22
+ puts '--round'.colorize(:green)
23
+ puts ' Attempt to round all integers. Default: 2.'
24
+ puts ' E.g. --round, --round=3, --round=0'
25
+ puts
26
+
27
+ puts '--truncate'.colorize(:green)
28
+ puts ' Limit output values. Defaults to 4 lines. 0 to disable'
29
+ puts ' E.g. --truncate=100 or --truncate=0'
30
+ puts
31
+
32
+ puts '--json'.colorize(:green)
33
+ puts ' Print output back into JSON'
34
+ puts
35
+
36
+ puts '--or'.colorize(:green)
37
+ puts ' Filters will use OR instead of AND'
38
+ puts
39
+
40
+ puts '--total'.colorize(:green)
41
+ puts ' Print only total count of matching entries'
42
+ puts
43
+
44
+ puts '--slice'.colorize(:green)
45
+ puts ' Extract specific fields from entries (slice multiple with comma)'
46
+ puts ' Ex: --slice=path or --slice=path,params'
47
+ puts
48
+
49
+ puts '--except'.colorize(:green)
50
+ puts ' Exclude specific fields (except multiple with comma)'
51
+ puts ' Ex: --except=params --except=params,path'
52
+ puts
53
+
54
+ puts '--stats'.colorize(:green)
55
+ puts ' Order/Count occurrances by field'
56
+ puts ' Ex: --stats=params --except=params,path'
57
+ puts
58
+
59
+ puts '--uniq'.colorize(:green)
60
+ puts ' Show unique values only'
61
+ puts ' Ex: --uniq=params --uniq=params,path'
62
+ puts
63
+
64
+ puts '--pluck'.colorize(:green)
65
+ puts ' Extract values from entries'
66
+ puts ' Ex: --pluck=params --pluck=params,path'
67
+ puts
68
+
69
+ puts '--archive'.colorize(:green)
70
+ puts ' Limit to specific archvie name (inclusive). Matching SOS tar.gz name'
71
+ puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
72
+ puts
73
+
74
+ puts '--sort'.colorize(:green)
75
+ puts ' Sort by multiple fields'
76
+ puts ' Ex: --sort=duration_s,db_duration_s'
77
+ puts
78
+
79
+ puts '--reverse'.colorize(:green)
80
+ puts ' Reverse all results'
81
+ puts ' Ex: --reverse'
82
+ puts
83
+
84
+ puts '--combine'.colorize(:green)
85
+ puts ' Omit archive identifier dividers. Useful with sort or time filters'
86
+ puts ' Ex: --combine'
87
+ puts
88
+
89
+ puts '--start'.colorize(:green)
90
+ puts ' Show events after specified time. Filtered by the `time` field'
91
+ puts ' Use with `--end` for between selections'
92
+ puts ' Ex: log filter --start="2021-06-22 14:44 UTC" --end="2021-06-22 14:45 UTC"'
93
+ puts
94
+
95
+ puts '--end'.colorize(:green)
96
+ puts ' Show events before specified time. Filtered by the `time` field'
97
+ puts ' Use with `--start` for between selections'
98
+ puts ' Ex: log filter --end="2021-06-22"'
99
+ puts
100
+
101
+ puts '--truncate'.colorize(:green)
102
+ puts ' Truncate field length. On by default (4 rows). Performance issues!'
103
+ puts ' Disable with --truncate=0'.colorize(:light_red)
104
+ puts ' Ex: --truncate=200, --truncate=2048"'
105
+ puts
106
+
107
+ puts 'Field Searching'.colorize(:blue)
108
+ puts ' --[key]=[value]'
109
+ puts ' Search in key for value'
110
+ puts ' Example: --path=mirror/pull'
111
+ puts
112
+
113
+ puts 'Search specific logs'.colorize(:blue)
114
+ puts ' Any non dash parameters will be the log list to search from'
115
+ puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.colorize(:yellow)}` for log names"
116
+ puts
117
+
118
+ puts 'Example Queries'.colorize(:blue)
119
+ puts " Also see #{'examples'.colorize(:light_blue)} for even more examples"
120
+ puts ' log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message'
121
+ puts ' log filter gitlab-rails/api_json.log --slice=ua --uniq=ua --ua=gitlab-runner'
122
+
123
+ puts
124
+ end
125
+ # rubocop:enable Metrics/MethodLength
126
+ end
127
+ end
128
+ end
@@ -3,7 +3,7 @@ module GreenHat
3
3
  # rubocop:disable Metrics/ModuleLength
4
4
  module ShellHelper
5
5
  # Generic Parameter Parsing
6
- def self.param_parse(params)
6
+ def self.param_parse(params, skip_args = [])
7
7
  # Turn Params into Hash
8
8
  opts = params.flat_map { |param| param_opt_scan(param) }
9
9
 
@@ -23,6 +23,9 @@ module GreenHat
23
23
  opt_field_remove?(opts, param) || arg_field_remove?(args, param)
24
24
  end
25
25
 
26
+ # Update other user defaults
27
+ Settings.default_log_args(args, skip_args)
28
+
26
29
  [params, opts, args]
27
30
  end
28
31
 
@@ -57,7 +60,7 @@ module GreenHat
57
60
 
58
61
  def self.param_special_opts
59
62
  %i[
60
- slice except stats uniq pluck round archive start end sort limit
63
+ slice except stats uniq pluck round archive start end sort limit truncate page case
61
64
  ]
62
65
  end
63
66
 
@@ -75,8 +78,10 @@ module GreenHat
75
78
  # Arg Defaults
76
79
  def self.param_arg_defaults(field)
77
80
  case field
78
- when :round then 2
81
+ when :round then [2]
79
82
  when :limit then [TTY::Screen.height / 2]
83
+ when :truncate then [TTY::Screen.width * 4]
84
+ when :page, :case then [:true]
80
85
  when *param_special_opts then []
81
86
  else
82
87
  true
@@ -128,15 +133,15 @@ module GreenHat
128
133
  end
129
134
 
130
135
  # Check if content needs to paged, or if auto_height is off
131
- if !args.page && count_rows(data)
132
- puts data.map { |x| entry_show(x, args) }.compact.join("\n")
136
+ if Page.skip?(args, data)
137
+ puts data.map { |entry| entry_show(args, entry) }.compact.join("\n")
133
138
  return true
134
139
  end
135
140
 
136
141
  # Default Pager
137
142
  TTY::Pager.page do |pager|
138
143
  data.each do |entry|
139
- output = entry_show(entry, args)
144
+ output = entry_show(args, entry)
140
145
 
141
146
  next if output.blank?
142
147
 
@@ -145,63 +150,76 @@ module GreenHat
145
150
  end
146
151
  end
147
152
 
148
- # Array/Hash and String pagination check. Don't unncessarily loop through everything
149
- def self.count_rows(data)
150
- height = TTY::Screen.height
151
- size = 0
152
-
153
- data.each do |entry|
154
- size += case entry
155
- when Hash then entry.keys.count
156
- when Array then entry.count
157
- else
158
- 1
159
- end
160
-
161
- break if size > height
162
- end
163
-
164
- height > size
165
- end
166
-
167
- # Entry Shower
168
- def self.entry_show(entry, args)
153
+ # Entry Shower / Top Level
154
+ def self.entry_show(args, entry, key = nil)
155
+ LogBot.debug('Entry Show', entry.class) if ENV['DEBUG']
169
156
  case entry
170
157
  when Hash then render_table(entry, args)
171
- when String, Float, Integer then entry
172
- when Array
173
- entry.map { |x| x.ai(multiline: false) }.join("\n")
158
+ when Float, Integer, Array
159
+ format_table_entry(args, entry, key)
160
+ # Ignore Special Formatting for Strings / Usually already formatted
161
+ when String
162
+ entry
174
163
  else
175
164
  LogBot.warn('Shell Show', "Unknown #{entry.class}")
176
165
  nil
177
166
  end
178
167
  end
179
168
 
169
+ # Format Table Entries
170
+ def self.format_table_entry(args, entry, key = nil)
171
+ formatted_entry = case entry
172
+ # Rounding
173
+ when Float, Integer || entry.numeric?
174
+ args.round ? entry.to_f.round(args.round.first.to_s.to_i).ai : entry.ai
175
+
176
+ # General Inspecting
177
+ when Hash then entry.ai(ruby19_syntax: true)
178
+
179
+ # Arrays often contain Hashes. Dangerous Recursive?
180
+ when Array
181
+ entry.map { |x| format_table_entry(args, x) }.join("\n")
182
+
183
+ when Time
184
+ entry.to_s.colorize(:light_white)
185
+
186
+ # Default String Formatting
187
+ else
188
+ StringColor.do(key, entry)
189
+ end
190
+
191
+ if args[:truncate]
192
+ entry_truncate(formatted_entry, args[:truncate].join.to_i)
193
+ else
194
+ formatted_entry
195
+ end
196
+ rescue StandardError => e
197
+ binding.pry
198
+ end
199
+
180
200
  # Print the Table in a Nice way
181
201
  def self.render_table(entry, args)
202
+ entry = entry.map { |k, v| [k, format_table_entry(args, v, k)] }.to_h
203
+ # Pre-format Entry
204
+
182
205
  table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
183
206
 
184
- table.render(:unicode, padding: [0, 1, 0, 1]) do |renderer|
207
+ LogBot.debug('Rendering Entries') if ENV['DEBUG']
208
+ table.render(:unicode, padding: [0, 1, 0, 1], multiline: true) do |renderer|
185
209
  renderer.border.style = :cyan
186
210
 
187
- renderer.filter = lambda do |val, _row_index, col_index|
188
- if col_index == 1
189
- if val.numeric?
190
- # TODO: Better Casting?
191
- val = val.to_f.round(args.round.first.to_s.to_i) if args.round
192
- val.to_s.colorize(:blue)
193
- else
194
- val.to_s
195
- end
196
- else
197
- val.to_s
198
- end
199
- end
211
+ # renderer.filter = lambda do |val, _row_index, col_index|
212
+ # render_table_entry(val, col_index, args)
213
+ # end
200
214
  end
201
215
 
216
+ # LogBot.debug('Finish Render Table') if ENV['DEBUG']
202
217
  # Fall Back to Amazing Inspect
203
218
  rescue StandardError => e
204
- LogBot.warn('Table', message: e.message, backtrace: e.backtrace.first) if ENV['DEBUG']
219
+ if ENV['DEBUG']
220
+ LogBot.warn('Table', message: e.message)
221
+ ap e.backtrace
222
+ end
205
223
 
206
224
  [
207
225
  entry.ai,
@@ -210,6 +228,12 @@ module GreenHat
210
228
  ].join("\n")
211
229
  end
212
230
 
231
+ def self.render_table_entry(val, col_index, args)
232
+ return val.to_s unless col_index == 1
233
+
234
+ format_table_entry(args, val)
235
+ end
236
+
213
237
  # Main Entry Point for Filtering
214
238
  def self.filter_start(log_list, filter_type, args, opts)
215
239
  # Convert to Things
@@ -239,7 +263,7 @@ module GreenHat
239
263
  results = data.clone.flatten.compact
240
264
  results.select! do |row|
241
265
  opts.send(type) do |opt|
242
- filter_row_key(row, opt)
266
+ filter_row_key(row, opt, args)
243
267
  end
244
268
  end
245
269
 
@@ -273,9 +297,12 @@ module GreenHat
273
297
  # Pluck
274
298
  results = filter_pluck(results, args[:pluck]) if args.key?(:pluck)
275
299
 
300
+ # Truncate
301
+ # filter_truncate(results, args[:truncate].join.to_i) if args.key?(:truncate)
302
+
276
303
  # Limit
277
304
  if args[:limit]
278
- results[0..args[:limit].map(&:to_s).join.to_i]
305
+ results[0..args[:limit].map(&:to_s).join.to_i - 1]
279
306
  else
280
307
  results
281
308
  end
@@ -332,6 +359,17 @@ module GreenHat
332
359
  results.map { |row| row.except(*except) }
333
360
  end
334
361
 
362
+ def self.entry_truncate(entry, truncate)
363
+ # Ignore if Truncation Off
364
+ return entry if truncate.zero?
365
+
366
+ # Only truncate large strings
367
+ return entry unless entry.instance_of?(String) && entry.size > truncate
368
+
369
+ # Include '...' to indicate truncation
370
+ "#{entry.to_s[0..truncate]} #{'...'.colorize(:light_blue)}"
371
+ end
372
+
335
373
  def self.filter_slice(results, slice)
336
374
  # Avoid Empty Results
337
375
  if slice.empty?
@@ -397,13 +435,18 @@ module GreenHat
397
435
  end
398
436
 
399
437
  # Break out filter row logic into separate method
400
- def self.filter_row_key(row, param)
438
+ def self.filter_row_key(row, param, args)
401
439
  # Ignore Other Logic if Field isn't even included
402
440
  return false unless row.key? param.field
403
441
 
404
- # Not Included Param
405
- included = row[param.field].to_s.include? param.value.to_s
442
+ # Sensitivity Check / Check for Match
443
+ included = if args.key?(:case)
444
+ row[param.field].to_s.include? param.value.to_s
445
+ else
446
+ row[param.field].to_s.downcase.include? param.value.to_s.downcase
447
+ end
406
448
 
449
+ # Pivot of off include vs exclude
407
450
  if param.bang
408
451
  !included
409
452
  else
@@ -2,7 +2,6 @@ module GreenHat
2
2
  # CLI Helper
3
3
  module Shell
4
4
  # Logs
5
- # rubocop:disable Metrics/ModuleLength
6
5
  module Log
7
6
  def self.help
8
7
  puts "\u2500".colorize(:cyan) * 20
@@ -14,16 +13,17 @@ module GreenHat
14
13
  puts ' Just print selected logs'
15
14
  puts ' filter'.colorize(:green)
16
15
  puts ' Key/Field Filtering'
16
+ puts ' - See `filter_help`'
17
17
  puts ' search'.colorize(:green)
18
18
  puts ' General text by entry searching'
19
+ puts ' - See `search_help`'
19
20
  puts ' ls'.colorize(:green)
20
21
  puts ' List available files'
21
22
  puts
23
+ end
22
24
 
23
- filter_help
24
-
25
- puts
26
- search_help
25
+ def self.filter_help
26
+ ShellHelper::Filter.help
27
27
  end
28
28
 
29
29
  # List Files Helpers
@@ -113,122 +113,15 @@ module GreenHat
113
113
  end
114
114
 
115
115
  # log filter --path='cloud/gitlab-automation' --path='/pull' --all
116
- # cloud/gitlab-automation
117
116
  # log filter --project=thingy --other_filter=asdf *
118
117
  rescue StandardError => e
119
- binding.pry
120
118
  LogBot.fatal('Filter', message: e.message, backtrace: e.backtrace.first)
121
119
  end
122
120
  # ========================================================================
123
121
 
124
- # rubocop:disable Metrics/MethodLength
125
- def self.filter_help
126
- puts "\u2500".colorize(:cyan) * 20
127
- puts 'Log Filter'.colorize(:yellow)
128
- puts "\u2500".colorize(:cyan) * 20
129
-
130
- puts 'Options'.colorize(:blue)
131
- puts '--raw'.colorize(:green)
132
- puts ' Do not use less/paging'
133
- puts
134
-
135
- puts '--round'.colorize(:green)
136
- puts ' Attempt to round all integers. Default: 2.'
137
- puts ' E.g. --round, --round=3, --round=0'
138
- puts
139
-
140
- puts '--json'.colorize(:green)
141
- puts ' Print output back into JSON'
142
- puts
143
-
144
- puts '--or'.colorize(:green)
145
- puts ' Filters will use OR instead of AND'
146
- puts
147
-
148
- puts '--total'.colorize(:green)
149
- puts ' Print only total count of matching entries'
150
- puts
151
-
152
- puts '--slice'.colorize(:green)
153
- puts ' Extract specific fields from entries (slice multiple with comma)'
154
- puts ' Ex: --slice=path or --slice=path,params'
155
- puts
156
-
157
- puts '--except'.colorize(:green)
158
- puts ' Exclude specific fields (except multiple with comma)'
159
- puts ' Ex: --except=params --except=params,path'
160
- puts
161
-
162
- puts '--stats'.colorize(:green)
163
- puts ' Order/Count occurrances by field'
164
- puts ' Ex: --stats=params --except=params,path'
165
- puts
166
-
167
- puts '--uniq'.colorize(:green)
168
- puts ' Show unique values only'
169
- puts ' Ex: --uniq=params --uniq=params,path'
170
- puts
171
-
172
- puts '--pluck'.colorize(:green)
173
- puts ' Extract values from entries'
174
- puts ' Ex: --pluck=params --pluck=params,path'
175
- puts
176
-
177
- puts '--archive'.colorize(:green)
178
- puts ' Limit to specific archvie name (inclusive). Matching SOS tar.gz name'
179
- puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
180
- puts
181
-
182
- puts '--sort'.colorize(:green)
183
- puts ' Sort by multiple fields'
184
- puts ' Ex: --sort=duration_s,db_duration_s'
185
- puts
186
-
187
- puts '--reverse'.colorize(:green)
188
- puts ' Reverse all results'
189
- puts ' Ex: --reverse'
190
- puts
191
-
192
- puts '--combine'.colorize(:green)
193
- puts ' Omit archive identifier dividers. Useful with sort or time filters'
194
- puts ' Ex: --combine'
195
- puts
196
-
197
- puts '--start'.colorize(:green)
198
- puts ' Show events after specified time. Filtered by the `time` field'
199
- puts ' Use with `--end` for between selections'
200
- puts ' Ex: log filter --start="2021-06-22 14:44 UTC" --end="2021-06-22 14:45 UTC"'
201
- puts
202
-
203
- puts '--end'.colorize(:green)
204
- puts ' Show events before specified time. Filtered by the `time` field'
205
- puts ' Use with `--start` for between selections'
206
- puts ' Ex: log filter --end="2021-06-22"'
207
- puts
208
-
209
- puts 'Field Searching'.colorize(:blue)
210
- puts ' --[key]=[value]'
211
- puts ' Search in key for value'
212
- puts ' Example: --path=mirror/pull'
213
- puts
214
-
215
- puts 'Search specific logs'.colorize(:blue)
216
- puts ' Any non dash parameters will be the log list to search from'
217
- puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.colorize(:yellow)}` for log names"
218
- puts
219
-
220
- puts 'Example Queries'.colorize(:blue)
221
- puts " Also see #{'filter_examples'.colorize(:light_blue)} for even more examples"
222
- puts 'log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message'
223
- puts 'log filter gitlab-rails/api_json.log --slice=ua --uniq=ua --ua=gitlab-runner'
224
-
225
- puts
226
- end
227
- # rubocop:enable Metrics/MethodLength
228
-
229
122
  # rubocop:disable Layout/LineLength
230
123
  # TODO: Add a lot more examples
231
- def self.filter_examples
124
+ def self.examples
232
125
  puts 'Find `done` job for sidekiq, sort by duration, only duration, and show longest first'.colorize(:light_green)
233
126
  puts 'log filter sidekiq/current --job_status=done --sort=duration_s,db_duration_s --slice=duration_s,db_duration_s --reverse'
234
127
  puts
@@ -236,8 +129,8 @@ module GreenHat
236
129
  puts 'log filter --status=500 --slice=exception.message gitlab-rails/production_json.log'
237
130
  puts
238
131
  end
239
- # rubocop:enable Layout/LineLength
240
132
 
133
+ # rubocop:enable Layout/LineLength
241
134
  # ========================================================================
242
135
  # Search (Full Text / String Search)
243
136
  # ========================================================================
@@ -325,7 +218,7 @@ module GreenHat
325
218
 
326
219
  puts 'Search specific logs'.colorize(:blue)
327
220
  puts ' Any non dash parameters will be the log list to search from'
328
- puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.colorize(:yellow)}` for log names"
221
+ puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.colorize(:yellow)}` for log names)"
329
222
  puts
330
223
 
331
224
  puts 'Example Queries'.colorize(:blue)
@@ -337,7 +230,6 @@ module GreenHat
337
230
 
338
231
  # ------------------------------------------------------------------------
339
232
  end
340
- # rubocop:enable Metrics/ModuleLength
341
233
  end
342
234
  end
343
235
 
@@ -0,0 +1,33 @@
1
+ module GreenHat
2
+ module ShellHelper
3
+ # Helper to organize page check / methods
4
+ module Page
5
+ # Check if paging should be skipped
6
+ def self.skip?(args, data)
7
+ return count_rows(data) unless args[:page]
8
+
9
+ args[:page].first == :false
10
+ end
11
+
12
+ # Array/Hash and String pagination check. Don't unncessarily loop through everything
13
+ def self.count_rows(data)
14
+ height = TTY::Screen.height
15
+ size = 0
16
+
17
+ data.each do |entry|
18
+ size += case entry
19
+ when Hash then entry.keys.count
20
+ when Array then entry.count
21
+ else
22
+ 1
23
+ end
24
+
25
+ break if size > height
26
+ end
27
+
28
+ size < height
29
+ end
30
+ end
31
+ # ----
32
+ end
33
+ end
@@ -4,8 +4,51 @@ module GreenHat
4
4
  # Logs
5
5
  module Process
6
6
  # Easy Show All
7
- def self.ps
8
- ShellHelper.file_output GreenHat::Ps.ps
7
+ # Filter --archive
8
+
9
+ def self.ls
10
+ help
11
+ end
12
+
13
+ def self.help
14
+ puts "\u2500".colorize(:cyan) * 20
15
+ puts "#{'Process'.colorize(:yellow)} - ps helper"
16
+ puts "\u2500".colorize(:cyan) * 20
17
+
18
+ puts 'Command Summary'.colorize(:blue)
19
+ puts ' ps'.colorize(:green)
20
+ puts ' Raw `ps`'
21
+ puts ' filter'.colorize(:green)
22
+ puts ' Key/Field Filtering'
23
+ puts ' - See `filter_help`'
24
+ puts
25
+ end
26
+
27
+ def self.filter_help
28
+ ShellHelper::Filter.help
29
+ end
30
+
31
+ def self.ps(raw = {})
32
+ _log_list, _1opts, args = ShellHelper.param_parse(raw)
33
+ ShellHelper.file_output GreenHat::Ps.ps(args)
34
+ end
35
+
36
+ def self.filter(raw = {})
37
+ _log_list, opts, args = ShellHelper.param_parse(raw)
38
+
39
+ # AND / OR Filtering
40
+ filter_type = args.or ? :any? : :all?
41
+
42
+ # ShellHelper.file_output
43
+ results = ShellHelper.filter_start(GreenHat::Ps.ps(args).map(&:name), filter_type, args, opts)
44
+
45
+ # Check Search Results
46
+ if results.instance_of?(Hash) && results.values.flatten.empty?
47
+ puts 'No results'.colorize(:red)
48
+ else
49
+ # This causes the key 'colorized' output to also be included
50
+ ShellHelper.show(results.to_a.compact.flatten, args)
51
+ end
9
52
  end
10
53
  end
11
54
  end
@@ -19,7 +19,7 @@ module GreenHat
19
19
 
20
20
  attr_accessor :archive, :host, :os_release, :selinux_status, :cpu, :uname,
21
21
  :timedatectl, :uptime, :meminfo, :gitlab_manifest, :gitlab_status,
22
- :production_log, :api_log, :application_log, :sidekiq_log, :free_m
22
+ :production_log, :api_log, :application_log, :sidekiq_log, :free_m, :disk_free
23
23
 
24
24
  # Find Needed Files for Report
25
25
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
@@ -40,6 +40,7 @@ module GreenHat
40
40
  self.api_log = archive.things.find { |x| x.name == 'gitlab-rails/api_json.log' }
41
41
  self.application_log = archive.things.find { |x| x.name == 'gitlab-rails/application_json.log' }
42
42
  self.sidekiq_log = archive.things.find { |x| x.name == 'sidekiq/current' }
43
+ self.disk_free = archive.things.find { |x| x.name == 'df_h' }
43
44
  end
44
45
 
45
46
  def show
@@ -62,11 +63,17 @@ module GreenHat
62
63
  # Memory
63
64
  if meminfo || free_m
64
65
  output << 'Memory'.colorize(:light_yellow)
65
- output << memory if meminfo
66
+ output << memory_perc if meminfo
66
67
  output << memory_free if free_m
67
68
  output << ''
68
69
  end
69
70
 
71
+ # Disk
72
+ if disk_free
73
+ output << disks
74
+ output << ''
75
+ end
76
+
70
77
  # Gitlab
71
78
  output << 'GitLab'.colorize(:light_yellow) if gitlab_manifest
72
79
  output << gitlab_version if gitlab_manifest
@@ -209,6 +216,9 @@ module GreenHat
209
216
  end
210
217
 
211
218
  def sys_time
219
+ # Ignore if Empty
220
+ return false if timedatectl.data.nil?
221
+
212
222
  ntp_statuses = timedatectl.data.slice(*ntp_keys).values.compact
213
223
 
214
224
  enabled = %w[active yes] & ntp_statuses
@@ -260,26 +270,18 @@ module GreenHat
260
270
  ].join
261
271
  end
262
272
 
263
- def memory
273
+ def memory_perc
264
274
  total = ShellHelper.human_size_to_number(meminfo.data['MemTotal'])
265
275
  free = ShellHelper.human_size_to_number(meminfo.data['MemFree'])
266
276
  used = percent((total - free), total)
277
+
267
278
  [
268
- title('MemUsed'),
269
- '['.colorize(:light_black),
279
+ title('Usage'),
280
+ ' ['.colorize(:light_black),
270
281
  '='.colorize(:green) * (used / 2),
271
282
  ' ' * (50 - used / 2),
272
283
  ']'.colorize(:light_black),
273
- " #{100 - percent(free, total)}%".colorize(:green), # Inverse
274
- "\n",
275
- title('Total'),
276
- number_to_human_size(total).colorize(:light_white),
277
- "\n",
278
- title('Used'),
279
- number_to_human_size(total - free),
280
- "\n",
281
- title('Free'),
282
- number_to_human_size(free)
284
+ " #{100 - percent(free, total)}%".colorize(:green) # Inverse
283
285
  ].join
284
286
  end
285
287
 
@@ -288,6 +290,8 @@ module GreenHat
288
290
 
289
291
  return unless free
290
292
 
293
+ formatted_mem = free_m.data.map { |x| GreenHat::Memory.memory_row x }
294
+
291
295
  [
292
296
  title('Total', :cyan, 14),
293
297
  number_to_human_size(free.total.to_i * 1024**2),
@@ -299,7 +303,23 @@ module GreenHat
299
303
  number_to_human_size(free.free.to_i * 1024**2),
300
304
  "\n",
301
305
  title('Available', :green, 14),
302
- number_to_human_size(free.available.to_i * 1024**2)
306
+ number_to_human_size(free.available.to_i * 1024**2),
307
+ "\n\n",
308
+ formatted_mem.map { |x| x.prepend ' ' * 2 }.join("\n")
309
+ ].join
310
+ end
311
+
312
+ def disks
313
+ # GreenHat::Disk.df({archive: []})
314
+ file = GreenHat::Disk.df({ archive: [archive.name] })
315
+
316
+ disk_list = GreenHat::Disk.format_output(file.first, false, 3)
317
+
318
+ # Preapre / Indent List
319
+ [
320
+ 'Disks(Top % Usage)'.colorize(:light_yellow),
321
+ "\n",
322
+ disk_list.each { |x| x.prepend(' ' * 4) }.join("\n")
303
323
  ].join
304
324
  end
305
325
 
@@ -44,7 +44,7 @@ module GreenHat
44
44
  ]
45
45
  },
46
46
  'log/syslog' => {
47
- format: :bracket_log,
47
+ format: :syslog,
48
48
  log: true,
49
49
  pattern: [
50
50
  %r{log/syslog}
@@ -95,6 +95,13 @@ module GreenHat
95
95
  %r{consul/current}
96
96
  ]
97
97
  },
98
+ 'consul/failover_pgbouncer.log' => {
99
+ format: :raw,
100
+ log: true,
101
+ pattern: [
102
+ %r{consul/failover_pgbouncer.log}
103
+ ]
104
+ },
98
105
  'pgbouncer/current' => {
99
106
  format: :time_space,
100
107
  log: true,
@@ -179,6 +186,13 @@ module GreenHat
179
186
  %r{logrotate/current}
180
187
  ]
181
188
  },
189
+ 'gitlab-rails/grpc.log' => {
190
+ format: :raw,
191
+ log: true,
192
+ pattern: [
193
+ %r{gitlab-rails/grpc.log}
194
+ ]
195
+ },
182
196
  'gitlab-rails/api_json.log' => {
183
197
  format: :api_json,
184
198
  log: true,
@@ -301,6 +315,13 @@ module GreenHat
301
315
  /mount/
302
316
  ]
303
317
  },
318
+ 'nginx/current' => {
319
+ format: :time_space,
320
+ log: true,
321
+ pattern: [
322
+ %r{nginx/current}
323
+ ]
324
+ },
304
325
  'nginx/gitlab_pages_access.log' => {
305
326
  format: :raw,
306
327
  log: true,
@@ -671,7 +692,7 @@ module GreenHat
671
692
  ]
672
693
  },
673
694
  'log/messages' => {
674
- format: :raw,
695
+ format: :syslog,
675
696
  log: true,
676
697
  pattern: [
677
698
  %r{log/messages}
@@ -0,0 +1,39 @@
1
+ # Top
2
+ module GreenHat
3
+ # Log
4
+ module Formatters
5
+ # Formatters for bracket logs (dmesg, sos)
6
+ def format_syslog
7
+ self.result = raw.map do |row|
8
+ next if row.empty? || row == "\n"
9
+
10
+ format_syslog_row(row)
11
+ end
12
+
13
+ result.compact!
14
+ end
15
+
16
+ # Split / Parse Time
17
+ # TODO: Better sys log parsing? Cannot find ready-made regex/ruby parser
18
+ def format_syslog_row(row)
19
+ month, day, time, device, service, message = row.split(' ', 6)
20
+ service.gsub!(':', '')
21
+ pid = service.match(/\[(.*?)\]/)&.captures&.first
22
+
23
+ {
24
+ time: format_time_parse("#{month} #{day} #{time}"),
25
+ device: device,
26
+ service: service,
27
+ message: message,
28
+ pid: pid
29
+ }
30
+
31
+ # Return everything incase of error
32
+ rescue StandardError => e
33
+ LogBot.warn('SysLog Parse', e.message)
34
+ {
35
+ message: row
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TTY
4
+ class Table
5
+ # A module for calculating table data column widths
6
+ #
7
+ # Used by {Table} to manage column sizing.
8
+ #
9
+ # @api private
10
+ module Columns
11
+ # Converts column widths to array format or infers default widths
12
+ #
13
+ # @param [TTY::Table] table
14
+ #
15
+ # @param [Array, Numeric, NilClass] column_widths
16
+ #
17
+ # @return [Array[Integer]]
18
+ #
19
+ # @api public
20
+ def widths_from(table, column_widths = nil)
21
+ case column_widths
22
+ when Array
23
+ assert_widths(column_widths, table.columns_size)
24
+ Array(column_widths).map(&:to_i)
25
+ when Numeric
26
+ Array.new(table.columns_size, column_widths)
27
+ when NilClass
28
+ # THE SHIM! Strings that are too large fail to render correctly to do an empty result
29
+ # Set the maximum table width to half the screen size. Can't be full size due to the table headers
30
+ LogBot.debug('TTY Column Width') if ENV['DEBUG']
31
+ extract_widths(table.data).map { |x| x >= TTY::Screen.width ? (TTY::Screen.width * 3 / 4) : x }
32
+
33
+ else
34
+ raise TypeError, 'Invalid type for column widths'
35
+ end
36
+ end
37
+ module_function :widths_from
38
+ end
39
+ end
40
+ end
@@ -1,3 +1,3 @@
1
1
  module GreenHat
2
- VERSION = '0.1.5'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: greenhat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Davin Walker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-08 00:00:00.000000000 Z
11
+ date: 2021-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: amazing_print
@@ -399,14 +399,17 @@ files:
399
399
  - lib/greenhat/settings.rb
400
400
  - lib/greenhat/shell.rb
401
401
  - lib/greenhat/shell/cat.rb
402
+ - lib/greenhat/shell/color_string.rb
402
403
  - lib/greenhat/shell/disk.rb
403
404
  - lib/greenhat/shell/faststats.rb
405
+ - lib/greenhat/shell/filter.rb
404
406
  - lib/greenhat/shell/gitlab.rb
405
407
  - lib/greenhat/shell/help.rb
406
408
  - lib/greenhat/shell/helper.rb
407
409
  - lib/greenhat/shell/log.rb
408
410
  - lib/greenhat/shell/memory.rb
409
411
  - lib/greenhat/shell/network.rb
412
+ - lib/greenhat/shell/page.rb
410
413
  - lib/greenhat/shell/process.rb
411
414
  - lib/greenhat/shell/report.rb
412
415
  - lib/greenhat/thing.rb
@@ -425,6 +428,7 @@ files:
425
428
  - lib/greenhat/thing/formatters/multiline_json.rb
426
429
  - lib/greenhat/thing/formatters/raw.rb
427
430
  - lib/greenhat/thing/formatters/shellwords.rb
431
+ - lib/greenhat/thing/formatters/syslog.rb
428
432
  - lib/greenhat/thing/formatters/table.rb
429
433
  - lib/greenhat/thing/formatters/time_json.rb
430
434
  - lib/greenhat/thing/formatters/time_shellwords.rb
@@ -435,6 +439,7 @@ files:
435
439
  - lib/greenhat/thing/kind.rb
436
440
  - lib/greenhat/thing/spinner.rb
437
441
  - lib/greenhat/thing/super_log.rb
442
+ - lib/greenhat/tty/columns.rb
438
443
  - lib/greenhat/tty/custom_line.rb
439
444
  - lib/greenhat/tty/line.rb
440
445
  - lib/greenhat/tty/reader.rb