greenhat 0.1.4 → 0.3.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -7
  3. data/lib/greenhat/accessors/disk.rb +58 -2
  4. data/lib/greenhat/accessors/gitlab.rb +75 -0
  5. data/lib/greenhat/accessors/memory.rb +10 -10
  6. data/lib/greenhat/accessors/process.rb +10 -1
  7. data/lib/greenhat/cli.rb +128 -57
  8. data/lib/greenhat/color.rb +27 -0
  9. data/lib/greenhat/logbot.rb +9 -9
  10. data/lib/greenhat/settings.rb +51 -3
  11. data/lib/greenhat/shell/args.rb +146 -0
  12. data/lib/greenhat/shell/cat.rb +25 -73
  13. data/lib/greenhat/shell/color_string.rb +43 -0
  14. data/lib/greenhat/shell/disk.rb +30 -42
  15. data/lib/greenhat/shell/faststats.rb +80 -61
  16. data/lib/greenhat/shell/filter_help.rb +143 -0
  17. data/lib/greenhat/shell/gitlab.rb +61 -2
  18. data/lib/greenhat/shell/help.rb +98 -15
  19. data/lib/greenhat/shell/list.rb +46 -0
  20. data/lib/greenhat/shell/log.rb +78 -203
  21. data/lib/greenhat/shell/page.rb +39 -0
  22. data/lib/greenhat/shell/process.rb +57 -2
  23. data/lib/greenhat/shell/report.rb +70 -60
  24. data/lib/greenhat/shell/shell_helper.rb +601 -0
  25. data/lib/greenhat/shell.rb +27 -13
  26. data/lib/greenhat/thing/file_types.rb +76 -8
  27. data/lib/greenhat/thing/formatters/json_shellwords.rb +0 -3
  28. data/lib/greenhat/thing/formatters/nginx.rb +44 -0
  29. data/lib/greenhat/thing/formatters/syslog.rb +39 -0
  30. data/lib/greenhat/thing/helpers.rb +4 -4
  31. data/lib/greenhat/thing/kind.rb +9 -2
  32. data/lib/greenhat/thing/spinner.rb +3 -3
  33. data/lib/greenhat/thing.rb +3 -3
  34. data/lib/greenhat/tty/columns.rb +44 -0
  35. data/lib/greenhat/version.rb +1 -1
  36. data/lib/greenhat.rb +15 -14
  37. metadata +30 -20
  38. data/lib/greenhat/shell/helper.rb +0 -514
@@ -0,0 +1,39 @@
1
+ module GreenHat
2
+ module ShellHelper
3
+ # Helper to organize page check / methods
4
+ module Page
5
+ # Check if paging should be skipped
6
+ # True / False / Not Set
7
+ def self.skip?(flags, data)
8
+ # Pass if Explicitly Set / Inverse for skip
9
+ return !flags[:page] if flags.key? :page
10
+
11
+ LogBot.debug('Page', count_rows(data, flags)) if ENV['DEBUG']
12
+
13
+ count_rows(data, flags)
14
+ end
15
+
16
+ # Array/Hash and String pagination check. Don't unncessarily loop through everything
17
+ def self.count_rows(data, flags)
18
+ height = TTY::Screen.height
19
+ size = 0
20
+
21
+ data.each do |entry|
22
+ size += case entry
23
+ when Hash then entry.keys.count
24
+ when Array then entry.count
25
+ else
26
+ # Each Boxed Entry is 3 Lines
27
+ flags.key?(:row_size) ? flags[:row_size] : 1
28
+
29
+ end
30
+
31
+ break if size > height
32
+ end
33
+
34
+ size < height
35
+ end
36
+ end
37
+ # ----
38
+ end
39
+ end
@@ -4,8 +4,63 @@ 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".pastel(:cyan) * 22
15
+ puts "#{'Process'.pastel(:yellow)} - ps helper"
16
+ puts "\u2500".pastel(:cyan) * 22
17
+
18
+ ShellHelper.common_opts
19
+
20
+ puts 'Command Summary'.pastel(:blue)
21
+ puts ' ps'.pastel(:green)
22
+ puts ' Raw `ps`'
23
+ puts
24
+ puts ' filter'.pastel(:green)
25
+ puts " Key/Field Filtering. See #{'filter_help'.pastel(:blue)}"
26
+ puts ' Examples'
27
+ puts ' filter --sort=mem --reverse'.pastel(:green)
28
+ puts ' filter --user=gitlab'.pastel(:green)
29
+
30
+ puts
31
+ end
32
+
33
+ def self.filter_help
34
+ ShellHelper::Filter.help
35
+ end
36
+
37
+ def self.ps(raw = {})
38
+ # Extract Args
39
+ files_list, flags, _args = Args.parse(raw)
40
+
41
+ # Collect Files
42
+ files = ShellHelper.files(files_list, GreenHat::Ps.things, flags)
43
+
44
+ # Output
45
+ ShellHelper.file_output(files, flags)
46
+ end
47
+
48
+ def self.filter(raw = {})
49
+ # Argument Parsing
50
+ files, flags, args = Args.parse(raw)
51
+
52
+ # Prepare Log List
53
+ file_list = ShellHelper.prepare_list(files, GreenHat::Ps.things)
54
+
55
+ results = ShellHelper.filter_start(file_list, flags, args)
56
+
57
+ # Check Search Results
58
+ if results.instance_of?(Hash) && results.values.flatten.empty?
59
+ puts 'No results'.pastel(:red)
60
+ else
61
+ # This causes the key 'colorized' output to also be included
62
+ ShellHelper.show(results.to_a.compact.flatten, flags)
63
+ end
9
64
  end
10
65
  end
11
66
  end
@@ -2,11 +2,17 @@ module GreenHat
2
2
  # Root Level Shell / Report Helper
3
3
  module Shell
4
4
  def self.report(raw)
5
- args = {
6
- raw: raw.include?('--raw')
7
- }
5
+ _files, flags, _args = Args.parse(raw)
8
6
 
9
- ShellHelper.show(Archive.all.map(&:report).map(&:show).flatten, args)
7
+ archives = if flags.archive
8
+ Archive.all.select do |archive|
9
+ flags.archive.any? { |x| archive.name.include? x.to_s }
10
+ end
11
+ else
12
+ Archive.all
13
+ end
14
+
15
+ ShellHelper.show(archives.map(&:report).map(&:show).flatten, flags)
10
16
  end
11
17
  end
12
18
  end
@@ -19,7 +25,7 @@ module GreenHat
19
25
 
20
26
  attr_accessor :archive, :host, :os_release, :selinux_status, :cpu, :uname,
21
27
  :timedatectl, :uptime, :meminfo, :gitlab_manifest, :gitlab_status,
22
- :production_log, :api_log, :application_log, :sidekiq_log, :free_m
28
+ :production_log, :api_log, :application_log, :sidekiq_log, :free_m, :disk_free
23
29
 
24
30
  # Find Needed Files for Report
25
31
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
@@ -40,15 +46,16 @@ module GreenHat
40
46
  self.api_log = archive.things.find { |x| x.name == 'gitlab-rails/api_json.log' }
41
47
  self.application_log = archive.things.find { |x| x.name == 'gitlab-rails/application_json.log' }
42
48
  self.sidekiq_log = archive.things.find { |x| x.name == 'sidekiq/current' }
49
+ self.disk_free = archive.things.find { |x| x.name == 'df_h' }
43
50
  end
44
51
 
45
52
  def show
46
53
  output = [
47
- archive.friendly_name.colorize(:blue)
54
+ archive.friendly_name.pastel(:blue)
48
55
  ]
49
56
 
50
57
  # OS
51
- output << 'OS'.colorize(:light_yellow)
58
+ output << 'OS'.pastel(:bright_yellow)
52
59
  output << hostname if host
53
60
  output << distro if os_release
54
61
  output << selinux if selinux_status
@@ -61,14 +68,20 @@ module GreenHat
61
68
 
62
69
  # Memory
63
70
  if meminfo || free_m
64
- output << 'Memory'.colorize(:light_yellow)
65
- output << memory if meminfo
71
+ output << 'Memory'.pastel(:bright_yellow)
72
+ output << memory_perc if meminfo
66
73
  output << memory_free if free_m
67
74
  output << ''
68
75
  end
69
76
 
77
+ # Disk
78
+ if disk_free
79
+ output << disks
80
+ output << ''
81
+ end
82
+
70
83
  # Gitlab
71
- output << 'GitLab'.colorize(:light_yellow) if gitlab_manifest
84
+ output << 'GitLab'.pastel(:bright_yellow) if gitlab_manifest
72
85
  output << gitlab_version if gitlab_manifest
73
86
  output << gitlab_services if gitlab_status
74
87
  output << title('Errors') if production_log || api_log || application_log || sidekiq_log
@@ -88,8 +101,8 @@ module GreenHat
88
101
  color = count.zero? ? :green : :red
89
102
 
90
103
  [
91
- title(' Production', :light_red, 18),
92
- count.to_s.colorize(color)
104
+ title(' Production', :bright_red, 18),
105
+ count.to_s.pastel(color)
93
106
  ].join
94
107
  end
95
108
 
@@ -98,8 +111,8 @@ module GreenHat
98
111
  color = count.zero? ? :green : :red
99
112
 
100
113
  [
101
- title(' API', :light_red, 18),
102
- count.to_s.colorize(color)
114
+ title(' API', :bright_red, 18),
115
+ count.to_s.pastel(color)
103
116
  ].join
104
117
  end
105
118
 
@@ -108,8 +121,8 @@ module GreenHat
108
121
  color = count.zero? ? :green : :red
109
122
 
110
123
  [
111
- title(' Application', :light_red, 18),
112
- count.to_s.colorize(color)
124
+ title(' Application', :bright_red, 18),
125
+ count.to_s.pastel(color)
113
126
  ].join
114
127
  end
115
128
 
@@ -118,32 +131,16 @@ module GreenHat
118
131
  color = count.zero? ? :green : :red
119
132
 
120
133
  [
121
- title(' Sidekiq', :light_red, 18),
122
- count.to_s.colorize(color)
134
+ title(' Sidekiq', :bright_red, 18),
135
+ count.to_s.pastel(color)
123
136
  ].join
124
137
  end
125
138
 
126
139
  def gitlab_services
127
- list = gitlab_status.data.keys.sort.map do |service|
128
- color = gitlab_status.data.dig(service, 0, :status) == 'run' ? :green : :red
129
- service.colorize(color)
130
- end
131
-
132
- # Keep Alphabetical Sort
133
- groups = list.each_slice((list.size / 3.to_f).round).to_a
134
-
135
- table = TTY::Table.new do |t|
136
- loop do
137
- break if groups.all?(&:empty?)
138
-
139
- t << groups.map(&:shift)
140
- end
141
- end
142
-
143
140
  [
144
141
  title('Services'),
145
142
  "\n ",
146
- table.render(:unicode, padding: [0, 1, 0, 1], indent: 3)
143
+ GitLab.services(archive, 3)
147
144
  ].join
148
145
  rescue StandardError => e
149
146
  LogBot.fatal('GitLab Services', message: e.message, backtrace: e.backtrace.first)
@@ -166,7 +163,7 @@ module GreenHat
166
163
  def distro
167
164
  [
168
165
  title('Distro'),
169
- "[#{os_release.data.ID}] ".colorize(:light_black),
166
+ "[#{os_release.data.ID}] ".pastel(:bright_black),
170
167
  os_release.data.PRETTY_NAME
171
168
  ].join
172
169
  end
@@ -177,7 +174,7 @@ module GreenHat
177
174
 
178
175
  [
179
176
  title('SeLinux'),
180
- status.colorize(status_color),
177
+ status.pastel(status_color),
181
178
  ' (',
182
179
  selinux_status.data['Current mode'],
183
180
  ')'
@@ -197,7 +194,7 @@ module GreenHat
197
194
  [
198
195
  title('Kernel'),
199
196
  value,
200
- " (#{build})".colorize(:light_black)
197
+ " (#{build})".pastel(:bright_black)
201
198
  ].join
202
199
  end
203
200
 
@@ -209,6 +206,9 @@ module GreenHat
209
206
  end
210
207
 
211
208
  def sys_time
209
+ # Ignore if Empty
210
+ return false if timedatectl.data.nil?
211
+
212
212
  ntp_statuses = timedatectl.data.slice(*ntp_keys).values.compact
213
213
 
214
214
  enabled = %w[active yes] & ntp_statuses
@@ -221,9 +221,9 @@ module GreenHat
221
221
  [
222
222
  title('Sys Time'),
223
223
  timedatectl.data['Local time'],
224
- ' (ntp: '.colorize(:light_black),
225
- ntp_status.colorize(ntp_color),
226
- ')'.colorize(:light_black)
224
+ ' (ntp: '.pastel(:bright_black),
225
+ ntp_status.pastel(ntp_color),
226
+ ')'.pastel(:bright_black)
227
227
  ].join
228
228
  end
229
229
 
@@ -248,38 +248,30 @@ module GreenHat
248
248
  [
249
249
  interval,
250
250
  ' (',
251
- "#{value}%".colorize(color),
251
+ "#{value}%".pastel(color),
252
252
  ')'
253
253
  ].join
254
254
  end
255
255
 
256
256
  [
257
257
  title('LoadAvg'),
258
- "[CPU #{cpu_count}] ".colorize(:light_white),
258
+ "[CPU #{cpu_count}] ".pastel(:bright_white),
259
259
  intervals_text.join(', ')
260
260
  ].join
261
261
  end
262
262
 
263
- def memory
263
+ def memory_perc
264
264
  total = ShellHelper.human_size_to_number(meminfo.data['MemTotal'])
265
265
  free = ShellHelper.human_size_to_number(meminfo.data['MemFree'])
266
266
  used = percent((total - free), total)
267
+
267
268
  [
268
- title('MemUsed'),
269
- '['.colorize(:light_black),
270
- '='.colorize(:green) * (used / 2),
269
+ title('Usage'),
270
+ ' ['.pastel(:bright_black),
271
+ '='.pastel(:green) * (used / 2),
271
272
  ' ' * (50 - used / 2),
272
- ']'.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)
273
+ ']'.pastel(:bright_black),
274
+ " #{100 - percent(free, total)}%".pastel(:green) # Inverse
283
275
  ].join
284
276
  end
285
277
 
@@ -288,6 +280,8 @@ module GreenHat
288
280
 
289
281
  return unless free
290
282
 
283
+ formatted_mem = free_m.data.map { |x| GreenHat::Memory.memory_row x }
284
+
291
285
  [
292
286
  title('Total', :cyan, 14),
293
287
  number_to_human_size(free.total.to_i * 1024**2),
@@ -299,7 +293,23 @@ module GreenHat
299
293
  number_to_human_size(free.free.to_i * 1024**2),
300
294
  "\n",
301
295
  title('Available', :green, 14),
302
- number_to_human_size(free.available.to_i * 1024**2)
296
+ number_to_human_size(free.available.to_i * 1024**2),
297
+ "\n\n",
298
+ formatted_mem.map { |x| x.prepend ' ' * 2 }.join("\n")
299
+ ].join
300
+ end
301
+
302
+ def disks
303
+ # GreenHat::Disk.df({archive: []})
304
+ file = GreenHat::Disk.df({ archive: [archive.name] })
305
+
306
+ disk_list = GreenHat::Disk.format_output(file.first, false, 3)
307
+
308
+ # Preapre / Indent List
309
+ [
310
+ 'Disks'.pastel(:bright_yellow) + ' (Top % Usage)'.pastel(:bright_black),
311
+ "\n",
312
+ disk_list.each { |x| x.prepend(' ' * 4) }.join("\n")
303
313
  ].join
304
314
  end
305
315
 
@@ -312,7 +322,7 @@ module GreenHat
312
322
 
313
323
  # Helper to Make Cyan Titles
314
324
  def title(name, color = :cyan, ljust = 12)
315
- " #{name}:".ljust(ljust).colorize(color)
325
+ " #{name}:".ljust(ljust).pastel(color)
316
326
  end
317
327
  end
318
328
  # rubocop:enable Metrics/ClassLength
@@ -0,0 +1,601 @@
1
+ module GreenHat
2
+ # Common Helpers
3
+ # rubocop:disable Metrics/ModuleLength
4
+ module ShellHelper
5
+ # Use File Process for Output
6
+ def self.file_output(files, flags = {})
7
+ results = file_process(files) do |file|
8
+ [
9
+ file.friendly_name,
10
+ file.output(false),
11
+ "\n"
12
+ ]
13
+ end
14
+
15
+ ShellHelper.show(results.flatten, flags)
16
+ end
17
+
18
+ def self.file_process(files, &block)
19
+ files.map do |file|
20
+ next if file.output(false).empty?
21
+
22
+ block.call(file)
23
+ end.flatten
24
+ end
25
+
26
+ # Pagination Helper
27
+ def self.page(data)
28
+ TTY::Pager.page do |pager|
29
+ data.flatten.each do |output|
30
+ pager.write("\n#{output}") # write line to the pager
31
+ end
32
+ end
33
+ end
34
+
35
+ # Show Data / Auto Paginate Helper
36
+ def self.show(data, flags = {})
37
+ # If Block of String
38
+ if data.instance_of?(String)
39
+ TTY::Pager.page data
40
+ return true
41
+ end
42
+
43
+ # If raw just print out
44
+ if flags[:raw]
45
+ puts data.join("\n")
46
+ return true
47
+ end
48
+
49
+ # Check if content needs to paged, or if auto_height is off
50
+ if Page.skip?(flags, data)
51
+ puts data.map { |entry| entry_show(flags, entry) }.compact.join("\n")
52
+ return true
53
+ end
54
+
55
+ # Default Pager
56
+ TTY::Pager.page do |pager|
57
+ data.each do |entry|
58
+ output = entry_show(flags, entry)
59
+
60
+ # Breaks any intentional spaces
61
+ # next if output.blank?
62
+
63
+ pager.write("\n#{output}") # write line to the pager
64
+ end
65
+ end
66
+ end
67
+
68
+ # Entry Shower / Top Level
69
+ def self.entry_show(flags, entry, key = nil)
70
+ LogBot.debug('Entry Show', entry.class) if ENV['DEBUG']
71
+ case entry
72
+ when Hash then render_table(entry, flags)
73
+ when Float, Integer, Array
74
+ format_table_entry(flags, entry, key)
75
+ # Ignore Special Formatting for Strings / Usually already formatted
76
+ when String
77
+ entry
78
+ else
79
+ LogBot.warn('Shell Show', "Unknown #{entry.class}")
80
+ nil
81
+ end
82
+ end
83
+
84
+ # Format Table Entries
85
+ def self.format_table_entry(flags, entry, key = nil)
86
+ formatted_entry = case entry
87
+ # Rounding
88
+ when Float, Integer || entry.numeric?
89
+ flags.key?(:round) ? entry.to_f.round(flags.round).ai : entry.ai
90
+
91
+ # General Inspecting
92
+ when Hash then entry.ai(ruby19_syntax: true)
93
+
94
+ # Arrays often contain Hashes. Dangerous Recursive?
95
+ when Array
96
+ entry.map { |x| format_table_entry(flags, x) }.join("\n")
97
+
98
+ when Time
99
+ entry.to_s.pastel(:bright_white)
100
+
101
+ # Default String Formatting
102
+ else
103
+ StringColor.do(key, entry)
104
+ end
105
+
106
+ if flags[:truncate]
107
+ entry_truncate(formatted_entry, flags[:truncate])
108
+ else
109
+ formatted_entry
110
+ end
111
+ rescue StandardError => e
112
+ if ENV['DEBUG']
113
+ LogBot.warn('Table Format Entry', message: e.message)
114
+ ap e.backtrace
115
+ end
116
+ end
117
+
118
+ # Print the Table in a Nice way
119
+ def self.render_table(entry, flags)
120
+ entry = entry.map { |k, v| [k, format_table_entry(flags, v, k)] }.to_h
121
+ # Pre-format Entry
122
+
123
+ table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
124
+
125
+ LogBot.debug('Rendering Entries') if ENV['DEBUG']
126
+ table.render(:unicode, padding: [0, 1, 0, 1], multiline: true) do |renderer|
127
+ renderer.border.style = :cyan
128
+ end
129
+
130
+ # LogBot.debug('Finish Render Table') if ENV['DEBUG']
131
+ # Fall Back to Amazing Inspect
132
+ rescue StandardError => e
133
+ if ENV['DEBUG']
134
+ LogBot.warn('Table', message: e.message)
135
+ ap e.backtrace
136
+ end
137
+
138
+ [
139
+ entry.ai,
140
+ ('_' * (TTY::Screen.width / 3)).pastel(:cyan),
141
+ "\n"
142
+ ].join("\n")
143
+ end
144
+
145
+ def self.render_table_entry(val, col_index, flags)
146
+ return val.to_s unless col_index == 1
147
+
148
+ format_table_entry(flags, val)
149
+ end
150
+
151
+ # Main Entry Point for Filtering
152
+ def self.filter_start(files, flags, args)
153
+ # Convert to Things
154
+ logs = ShellHelper.find_things(files, flags).select(&:processed?)
155
+
156
+ # Ignore Archive/Host Dividers
157
+ if flags[:combine]
158
+ results = logs.reject(&:blank?).map(&:data).flatten.compact
159
+ ShellHelper.filter(results, flags, args)
160
+ else
161
+ # Iterate and Preserve Archive/Host Index
162
+ logs.each_with_object({}) do |log, obj|
163
+ # Ignore Empty Results / No Thing
164
+ next if log&.blank?
165
+
166
+ # Include Total Count in Name
167
+ results = ShellHelper.filter(log.data, flags, args)
168
+ title = [
169
+ log.friendly_name,
170
+ " #{results.count}".pastel(:bright_black)
171
+ ]
172
+
173
+ # Save unless empty
174
+ obj[title.join] = results unless results.count.zero?
175
+
176
+ obj
177
+ end
178
+ end
179
+ end
180
+
181
+ # Filter Logic
182
+ # TODO: Simplify
183
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
184
+ def self.filter(data, flags = {}, args = {})
185
+ results = data.clone.flatten.compact
186
+ results.select! do |row|
187
+ args.send(flags.logic) do |arg|
188
+ filter_row_key(row, arg, flags)
189
+ end
190
+ end
191
+
192
+ # Ensure presecense of a specific field
193
+ results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
194
+
195
+ # Time Filtering
196
+ results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
197
+
198
+ # Strip Results if Slice is defined
199
+ results = filter_slice(results, flags[:slice]) if flags.key?(:slice)
200
+
201
+ # Strip Results if Except is defined
202
+ results = filter_except(results, flags[:except]) if flags.key?(:except)
203
+
204
+ # Remove Blank from either slice or except
205
+ results.reject!(&:empty?)
206
+
207
+ # Sort
208
+ results.sort_by! { |x| x.slice(*flags[:sort]).values } if flags.key?(:sort)
209
+
210
+ # JSON Formatting
211
+ results = results.map { |x| Oj.dump(x) } if flags.key?(:json)
212
+
213
+ # Show Unique Only
214
+ results = filter_uniq(results, flags[:uniq]) if flags.key?(:uniq)
215
+
216
+ # Reverse
217
+ results.reverse! if flags[:reverse]
218
+
219
+ # Count occurrences / Skip Results
220
+ return filter_stats(results, flags[:stats]) if flags.key?(:stats)
221
+
222
+ # Pluck
223
+ results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
224
+
225
+ # Limit / Ensure Exists and Valid Number
226
+ if flags.key?(:limit) && flags[:limit]
227
+ # Old
228
+ # results[0..flags[:limit].map(&:to_s).join.to_i - 1]
229
+
230
+ # New
231
+ results.shift flags[:limit]
232
+
233
+ else
234
+ results
235
+ end
236
+ end
237
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
238
+
239
+ # Filter Start and End Times
240
+ # rubocop:disable Metrics/MethodLength
241
+ # TODO: This is a bit icky, simplify/dry
242
+ def self.filter_time(results, flags)
243
+ if flags.key?(:start)
244
+ begin
245
+ time_start = Time.parse(flags[:start])
246
+
247
+ results.select! do |x|
248
+ if x.time
249
+ time_start < x.time
250
+ else
251
+ true
252
+ end
253
+ end
254
+ rescue StandardError
255
+ puts 'Unable to Process Start Time Filter'.pastel(:red)
256
+ end
257
+ end
258
+
259
+ if flags.key?(:end)
260
+ begin
261
+ time_start = Time.parse(flags[:end])
262
+
263
+ results.select! do |x|
264
+ if x.time
265
+ time_start > x.time
266
+ else
267
+ true
268
+ end
269
+ end
270
+ rescue StandardError
271
+ puts 'Unable to Process End Time Filter'.pastel(:red)
272
+ end
273
+ end
274
+
275
+ results
276
+ end
277
+ # rubocop:enable Metrics/MethodLength
278
+
279
+ def self.filter_except(results, except)
280
+ # Avoid Empty Results
281
+ if except.empty?
282
+ filter_empty_arg('except')
283
+ return results
284
+ end
285
+
286
+ results.map { |row| row.except(*except) }
287
+ end
288
+
289
+ def self.filter_exists(results, exists)
290
+ # Avoid Empty Results
291
+ if exists.empty?
292
+ filter_empty_arg('exists')
293
+ return results
294
+ end
295
+
296
+ results.select { |row| (exists - row.keys).empty? }
297
+ end
298
+
299
+ def self.entry_truncate(entry, truncate)
300
+ # Ignore if Truncation Off
301
+ return entry if truncate.zero?
302
+
303
+ # Only truncate large strings
304
+ return entry unless entry.instance_of?(String) && entry.size > truncate
305
+
306
+ # Include '...' to indicate truncation
307
+ "#{entry.to_s[0..truncate]} #{'...'.pastel(:bright_blue)}"
308
+ end
309
+
310
+ def self.filter_slice(results, slice)
311
+ # Avoid Empty Results
312
+ if slice.empty?
313
+ filter_empty_arg('slice')
314
+ return results
315
+ end
316
+
317
+ results.map { |row| row.slice(*slice) }
318
+ end
319
+
320
+ def self.filter_pluck(results, pluck)
321
+ # Avoid Empty Results
322
+ if pluck.empty?
323
+ filter_empty_arg('pluck')
324
+ return results
325
+ end
326
+
327
+ results.map { |x| x.slice(*pluck).values }.flatten
328
+ end
329
+
330
+ def self.filter_uniq(results, unique)
331
+ # Avoid Empty Results
332
+ if unique.empty?
333
+ filter_empty_arg('uniq')
334
+ return results
335
+ end
336
+
337
+ unique.map do |field|
338
+ results.uniq { |x| x[field] }
339
+ end.inject(:&)
340
+ end
341
+
342
+ def self.filter_stats(results, stats)
343
+ # Avoid Empty Results
344
+ if stats.empty?
345
+ filter_empty_arg('stats')
346
+ return results
347
+ end
348
+
349
+ # Loop through Stats, Separate Hash/Tables
350
+ stats.map do |field|
351
+ occurrences = filter_count_occurrences(results, field)
352
+
353
+ # Total Occurences
354
+ total = occurrences.values.sum
355
+
356
+ # Percs
357
+ occurrences.transform_values! do |count|
358
+ [
359
+ count,
360
+ " #{percent(count, total)}%".pastel(:bright_black)
361
+ ]
362
+ end
363
+
364
+ # Sort by total occurances / New Variable for Total
365
+ output = occurrences.sort_by(&:last).to_h.transform_values!(&:join).to_a
366
+
367
+ # Append Header / Total with field name
368
+ output.unshift([field.to_s.pastel(:bright_black), total])
369
+
370
+ # Format
371
+ output.to_h
372
+ end
373
+ end
374
+
375
+ # Percent Helper
376
+ def self.percent(value, total)
377
+ ((value / total.to_f) * 100).round
378
+ end
379
+
380
+ # Helper to Count occurrences
381
+ def self.filter_count_occurrences(results, field)
382
+ results.each_with_object(Hash.new(0)) do |entry, counts|
383
+ if entry.key? field
384
+ counts[entry[field]] += 1
385
+ else
386
+ counts['None'.pastel(:bright_black)] += 1
387
+ end
388
+
389
+ counts
390
+ end
391
+ end
392
+
393
+ def self.filter_empty_arg(arg)
394
+ puts [
395
+ 'Ignoring'.pastel(:bright_yellow),
396
+ "--#{arg}".pastel(:cyan),
397
+ 'it requires an argument'.pastel(:red)
398
+ ].join(' ')
399
+ end
400
+
401
+ # Break out filter row logic into separate method
402
+
403
+ def self.filter_row_key(row, arg, flags)
404
+ # Ignore Other Logic if Field isn't even included / Full Text Searching
405
+ return false unless row.key?(arg[:field]) || arg[:field] == :text
406
+
407
+ # Sensitivity Check / Check for Match / Full Text Searching
408
+ included = if arg[:field] == :text
409
+ filter_row_entry(row.to_s, arg, flags)
410
+ else
411
+ filter_row_entry(row[arg.field].to_s, arg, flags)
412
+ end
413
+
414
+ # Pivot of off include vs exclude
415
+ if arg.bang
416
+ !included
417
+ else
418
+ included
419
+ end
420
+ end
421
+
422
+ # Field Partial / Case / Exact search
423
+ def self.filter_row_entry(entry, arg, flags)
424
+ # Exact Matching / Unless doing full text search
425
+ return entry.to_s == arg.value.to_s if flags.key?(:exact) && arg.field != :text
426
+
427
+ if flags.key?(:case)
428
+ entry.include? arg.value.to_s
429
+ else
430
+ entry.downcase.include? arg.value.to_s.downcase
431
+ end
432
+ end
433
+
434
+ # Total Count Helper
435
+ def self.total_count(results)
436
+ results.each do |k, v|
437
+ puts k
438
+ puts "Total: #{v.count.to_s.pastel(:blue)}"
439
+ puts
440
+ end
441
+ end
442
+
443
+ # Unified Files Interface
444
+ def self.files(file_list, base_list = nil, flags = {})
445
+ base_list ||= Thing.all
446
+
447
+ # Prepare Log List
448
+ file_list = prepare_list(file_list, base_list)
449
+
450
+ # Convert to Things
451
+ find_things(file_list, flags)
452
+ end
453
+
454
+ # Total Log List Manipulator
455
+ def self.prepare_list(log_list, base_list = nil, _flags = {})
456
+ base_list ||= GreenHat::ShellHelper::Log.list
457
+
458
+ # Assume all
459
+ log_list.push '*' if log_list.empty?
460
+
461
+ # Map for All
462
+ log_list = base_list.map(&:name) if log_list == ['*']
463
+
464
+ log_list
465
+ end
466
+
467
+ # Fuzzy match for things
468
+ def self.thing_list
469
+ @thing_list ||= Thing.all.map(&:name)
470
+
471
+ @thing_list
472
+ end
473
+
474
+ # Shortcut find things
475
+ def self.find_things(files, flags = {})
476
+ things = files.uniq.flat_map do |file|
477
+ # If Thing, Return Thing
478
+ return file if file.instance_of?(Thing)
479
+
480
+ if flags.fuzzy_file_match
481
+ Thing.all.select { |x| x.name.include? file }
482
+ else
483
+ Thing.where name: file
484
+ end
485
+ end.uniq
486
+
487
+ # Host / Archive
488
+ things.select! { |x| x.archive? flags.archive } if flags.key?(:archive)
489
+
490
+ things
491
+ end
492
+
493
+ # Main Entry Point for Searching
494
+ # def self.search_start(log_list, filter_type, args, opts)
495
+ def self.search_start(files, flags, args)
496
+ # Convert to Things
497
+ logs = ShellHelper.find_things(files, flags)
498
+
499
+ logs.each_with_object({}) do |log, obj|
500
+ # Ignore Empty Results / No Thing
501
+ next if log&.data.blank?
502
+
503
+ obj[log.friendly_name] = ShellHelper.search(log.data, flags, args)
504
+
505
+ obj
506
+ end
507
+ end
508
+
509
+ # Generic Search Helper / String/Regex
510
+ def self.search(data, flags = {}, args = {})
511
+ results = data.clone.flatten.compact
512
+ results.select! do |row|
513
+ args.send(flags.logic) do |arg|
514
+ search_row(row, arg, flags)
515
+ end
516
+ end
517
+
518
+ # Strip Results if Slice is defined
519
+ results.map! { |row| row.slice(*flags[:slice]) } if flags[:slice]
520
+
521
+ # Strip Results if Except is defined
522
+ results.map! { |row| row.except(*flags[:except]) } if flags[:except]
523
+
524
+ # Remove Blank from either slice or except
525
+ results.reject!(&:empty?)
526
+
527
+ results
528
+ end
529
+
530
+ # Break out filter row logic into separate method
531
+ def self.search_row(row, arg, flags)
532
+ # Sensitivity Check / Check for Match
533
+ included = filter_row_entry(row.to_s, arg, flags)
534
+
535
+ # Pivot of off include vs exclude
536
+ if arg.bang
537
+ !included
538
+ else
539
+ included
540
+ end
541
+ end
542
+
543
+ # TODO: Remove?
544
+ # Color Reader Helper
545
+ # def self.pastel
546
+ # @pastel ||= Pastel.new
547
+ # end
548
+
549
+ # Number Helper
550
+ # https://gitlab.com/zedtux/human_size_to_number/-/blob/master/lib/human_size_to_number/helper.rb
551
+ def self.human_size_to_number(string)
552
+ size, unit = string.scan(/(\d*\.?\d+)\s?(Bytes?|KB|MB|GB|TB)/i).first
553
+ number = size.to_f
554
+
555
+ number = case unit.downcase
556
+ when 'byte', 'bytes'
557
+ number
558
+ when 'kb'
559
+ number * 1024
560
+ when 'mb'
561
+ number * 1024 * 1024
562
+ when 'gb'
563
+ number * 1024 * 1024 * 1024
564
+ when 'tb'
565
+ number * 1024 * 1024 * 1024 * 1024
566
+ end
567
+ number.round
568
+ end
569
+
570
+ # TODO: Needed?
571
+ def self.filter_and(data, params = {})
572
+ result = data.clone.flatten.compact
573
+ params.each do |k, v|
574
+ result.select! do |row|
575
+ if row.key? k.to_sym
576
+ row[k.to_sym].include? v
577
+ else
578
+ false
579
+ end
580
+ end
581
+ next
582
+ end
583
+
584
+ result
585
+ end
586
+
587
+ # General Helper for `show`
588
+ def self.common_opts
589
+ puts 'Common Options'.pastel(:blue)
590
+ puts ' --raw'.pastel(:green)
591
+ puts ' Do not use less/paging'
592
+ puts
593
+
594
+ puts ' --archive'.pastel(:green)
595
+ puts ' Limit to specific archive name (inclusive). Matching SOS tar.gz name'
596
+ puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
597
+ puts
598
+ end
599
+ end
600
+ # rubocop:enable Metrics/ModuleLength
601
+ end