greenhat 0.2.0 → 0.3.3

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/lib/greenhat/accessors/disk.rb +13 -13
  3. data/lib/greenhat/accessors/gitlab.rb +75 -0
  4. data/lib/greenhat/accessors/memory.rb +10 -10
  5. data/lib/greenhat/accessors/process.rb +4 -0
  6. data/lib/greenhat/cli.rb +147 -58
  7. data/lib/greenhat/color.rb +27 -0
  8. data/lib/greenhat/logbot.rb +9 -9
  9. data/lib/greenhat/settings.rb +27 -7
  10. data/lib/greenhat/shell/args.rb +146 -0
  11. data/lib/greenhat/shell/cat.rb +25 -73
  12. data/lib/greenhat/shell/color_string.rb +8 -8
  13. data/lib/greenhat/shell/disk.rb +31 -4
  14. data/lib/greenhat/shell/faststats.rb +103 -56
  15. data/lib/greenhat/shell/field_helper.rb +75 -0
  16. data/lib/greenhat/shell/filter_help.rb +162 -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 +118 -104
  21. data/lib/greenhat/shell/page.rb +11 -5
  22. data/lib/greenhat/shell/process.rb +29 -17
  23. data/lib/greenhat/shell/report.rb +37 -47
  24. data/lib/greenhat/shell/shell_helper.rb +661 -0
  25. data/lib/greenhat/shell.rb +23 -9
  26. data/lib/greenhat/thing/file_types.rb +31 -5
  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/helpers.rb +4 -4
  30. data/lib/greenhat/thing/kind.rb +9 -2
  31. data/lib/greenhat/thing/spinner.rb +3 -3
  32. data/lib/greenhat/thing.rb +25 -3
  33. data/lib/greenhat/tty/columns.rb +4 -0
  34. data/lib/greenhat/version.rb +1 -1
  35. data/lib/greenhat.rb +15 -14
  36. metadata +38 -18
  37. data/lib/greenhat/shell/filter.rb +0 -128
  38. data/lib/greenhat/shell/helper.rb +0 -584
@@ -0,0 +1,661 @@
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_style = flags[:table_style]&.to_sym || :unicode
124
+
125
+ table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
126
+
127
+ LogBot.debug('Rendering Entries') if ENV['DEBUG']
128
+ table.render(table_style, padding: [0, 1, 0, 1], multiline: true) do |renderer|
129
+ renderer.border.style = :cyan
130
+ end
131
+
132
+ # LogBot.debug('Finish Render Table') if ENV['DEBUG']
133
+ # Fall Back to Amazing Inspect
134
+ rescue StandardError => e
135
+ if ENV['DEBUG']
136
+ LogBot.warn('Table', message: e.message)
137
+ ap e.backtrace
138
+ end
139
+
140
+ [
141
+ entry.ai,
142
+ ('_' * (TTY::Screen.width / 3)).pastel(:cyan),
143
+ "\n"
144
+ ].join("\n")
145
+ end
146
+
147
+ def self.render_table_entry(val, col_index, flags)
148
+ return val.to_s unless col_index == 1
149
+
150
+ format_table_entry(flags, val)
151
+ end
152
+
153
+ # Main Entry Point for Filtering
154
+ def self.filter_start(files, flags, args)
155
+ # Convert to Things
156
+ logs = ShellHelper.find_things(files, flags).select(&:processed?)
157
+
158
+ # Ignore Archive/Host Dividers
159
+ if flags[:combine]
160
+ results = logs.reject(&:blank?).map(&:data).flatten.compact
161
+ ShellHelper.filter(results, flags, args)
162
+ else
163
+ # Iterate and Preserve Archive/Host Index
164
+ logs.each_with_object({}) do |log, obj|
165
+ # Ignore Empty Results / No Thing
166
+ next if log&.blank?
167
+
168
+ # Include Total Count in Name
169
+ results = ShellHelper.filter(log.data, flags, args)
170
+ title = [
171
+ log.friendly_name,
172
+ " #{results.count}".pastel(:bright_black)
173
+ ]
174
+
175
+ # Save unless empty
176
+ obj[title.join] = results unless results.count.zero?
177
+
178
+ obj
179
+ end
180
+ end
181
+ end
182
+
183
+ # Filter Logic
184
+ # TODO: Simplify
185
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
186
+ def self.filter(data, flags = {}, args = {})
187
+ # results = data.clone.flatten.compact
188
+
189
+ # Experimenting with deep clone
190
+ results = Marshal.load(Marshal.dump(data))
191
+ results.select! do |row|
192
+ args.send(flags.logic) do |arg|
193
+ filter_row_key(row, arg, flags)
194
+ end
195
+ end
196
+
197
+ # Ensure presecense of a specific field
198
+ results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
199
+
200
+ # Time Zone
201
+ results = filter_modify_timezone(results, flags[:time_zone]) if flags.key?(:time_zone)
202
+
203
+ # Time Filtering
204
+ results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
205
+
206
+ # Strip Results if Slice is defined
207
+ results = filter_slice(results, flags[:slice]) if flags.key?(:slice)
208
+
209
+ # Strip Results if Except is defined
210
+ results = filter_except(results, flags[:except]) if flags.key?(:except)
211
+
212
+ # Remove Blank from either slice or except
213
+ results.reject!(&:empty?)
214
+
215
+ # Sort
216
+ results.sort_by! { |x| x.slice(*flags[:sort]).values } if flags.key?(:sort)
217
+
218
+ # JSON Formatting
219
+ results = results.map { |x| Oj.dump(x) } if flags.key?(:json)
220
+
221
+ # Show Unique Only
222
+ results = filter_uniq(results, flags[:uniq]) if flags.key?(:uniq)
223
+
224
+ # Reverse
225
+ results.reverse! if flags[:reverse]
226
+
227
+ # Count occurrences / Skip Results
228
+ return filter_stats(results, flags) if flags.key?(:stats)
229
+
230
+ # Limit before Pluck / Flattening
231
+ results = filter_limit(results, flags[:limit]) if flags.key?(:limit)
232
+
233
+ # Pluck
234
+ results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
235
+
236
+ results
237
+ end
238
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
239
+
240
+ # Limit / Ensure Exists and Valid Number
241
+ def self.filter_limit(results, limit)
242
+ return results unless limit.integer? && limit.positive?
243
+
244
+ results.shift limit
245
+ end
246
+
247
+ def self.filter_modify_timezone(results, time_zone)
248
+ results.each do |x|
249
+ next unless x.key? :time
250
+
251
+ x[:time] = x[:time].in_time_zone time_zone
252
+ end
253
+
254
+ results
255
+ end
256
+
257
+ # Filter Start and End Times
258
+ # rubocop:disable Metrics/MethodLength
259
+ # TODO: This is a bit icky, simplify/dry
260
+ def self.filter_time(results, flags)
261
+ if flags.key?(:start)
262
+ begin
263
+ time_start = Time.parse(flags[:start])
264
+
265
+ results.select! do |x|
266
+ if x.time
267
+ time_start < x.time
268
+ else
269
+ true
270
+ end
271
+ end
272
+ rescue StandardError
273
+ puts 'Unable to Process Start Time Filter'.pastel(:red)
274
+ end
275
+ end
276
+
277
+ if flags.key?(:end)
278
+ begin
279
+ time_start = Time.parse(flags[:end])
280
+
281
+ results.select! do |x|
282
+ if x.time
283
+ time_start > x.time
284
+ else
285
+ true
286
+ end
287
+ end
288
+ rescue StandardError
289
+ puts 'Unable to Process End Time Filter'.pastel(:red)
290
+ end
291
+ end
292
+
293
+ results
294
+ end
295
+ # rubocop:enable Metrics/MethodLength
296
+
297
+ def self.filter_except(results, except)
298
+ # Avoid Empty Results
299
+ if except.empty?
300
+ filter_empty_arg('except')
301
+ return results
302
+ end
303
+
304
+ results.map { |row| row.except(*except) }
305
+ end
306
+
307
+ def self.filter_exists(results, exists)
308
+ # Avoid Empty Results
309
+ if exists.empty?
310
+ filter_empty_arg('exists')
311
+ return results
312
+ end
313
+
314
+ results.select { |row| (exists - row.keys).empty? }
315
+ end
316
+
317
+ def self.entry_truncate(entry, truncate)
318
+ # Ignore if Truncation Off
319
+ return entry if truncate.zero?
320
+
321
+ # Only truncate large strings
322
+ return entry unless entry.instance_of?(String) && entry.size > truncate
323
+
324
+ # Include '...' to indicate truncation
325
+ "#{entry.to_s[0..truncate]} #{'...'.pastel(:bright_blue)}"
326
+ end
327
+
328
+ def self.filter_slice(results, slice)
329
+ # Avoid Empty Results
330
+ if slice.empty?
331
+ filter_empty_arg('slice')
332
+ return results
333
+ end
334
+
335
+ results.map { |row| row.slice(*slice) }
336
+ end
337
+
338
+ def self.filter_pluck(results, pluck)
339
+ # Avoid Empty Results
340
+ if pluck.empty?
341
+ filter_empty_arg('pluck')
342
+ return results
343
+ end
344
+
345
+ results.map { |x| x.slice(*pluck).values }.flatten
346
+ end
347
+
348
+ def self.filter_uniq(results, unique)
349
+ # Avoid Empty Results
350
+ if unique.empty?
351
+ filter_empty_arg('uniq')
352
+ return results
353
+ end
354
+
355
+ unique.map do |field|
356
+ results.uniq { |x| x[field] }
357
+ end.inject(:&)
358
+ end
359
+
360
+ def self.filter_stats(results, flags)
361
+ stats = flags[:stats]
362
+
363
+ # Avoid Empty Results
364
+ if stats.empty?
365
+ filter_empty_arg('stats')
366
+ return results
367
+ end
368
+
369
+ # Loop through Stats, Separate Hash/Tables
370
+ stats.map do |field|
371
+ occurrences = filter_count_occurrences(results, field, flags)
372
+
373
+ # Total Occurences
374
+ total = occurrences.values.sum
375
+
376
+ # Percs
377
+ occurrences.transform_values! do |count|
378
+ [
379
+ count,
380
+ " #{percent(count, total)}%".pastel(:bright_black)
381
+ ]
382
+ end
383
+
384
+ # Sort by total occurances / New Variable for Total
385
+ output = occurrences.sort_by(&:last).to_h.transform_values!(&:join).to_a
386
+
387
+ # Append Header / Total with field name
388
+ output.unshift([field.to_s.pastel(:bright_black), total])
389
+
390
+ # Use Truncate For Long Keys
391
+ if flags[:truncate]
392
+ output.map! do |key, value|
393
+ [key.to_s[0..flags[:truncate]], value]
394
+ end
395
+ end
396
+
397
+ # Format
398
+ output.to_h
399
+ end
400
+ end
401
+
402
+ # Percent Helper
403
+ def self.percent(value, total)
404
+ ((value / total.to_f) * 100).round
405
+ end
406
+
407
+ # Helper to Count occurrences
408
+ def self.filter_count_occurrences(results, field, flags = {})
409
+ results.each_with_object(Hash.new(0)) do |entry, counts|
410
+ if entry.key? field
411
+ # Rounding in pagination breaks stats
412
+ key = if flags.key?(:round) && entry[field].numeric?
413
+ entry[field].to_f.round(flags.round)
414
+ else
415
+ entry[field]
416
+ end
417
+
418
+ counts[key] += 1
419
+ else
420
+ counts['None'.pastel(:bright_black)] += 1
421
+ end
422
+
423
+ counts
424
+ end
425
+ end
426
+
427
+ def self.filter_empty_arg(arg)
428
+ puts [
429
+ 'Ignoring'.pastel(:bright_yellow),
430
+ "--#{arg}".pastel(:cyan),
431
+ 'it requires an argument'.pastel(:red)
432
+ ].join(' ')
433
+ end
434
+
435
+ # Break out filter row logic into separate method
436
+
437
+ def self.filter_row_key(row, arg, flags)
438
+ # Ignore Other Logic if Field isn't even included / Full Text Searching
439
+ return false unless row.key?(arg[:field]) || arg[:field] == :text
440
+
441
+ # Sensitivity Check / Check for Match / Full Text Searching
442
+ included = if arg[:field] == :text
443
+ filter_row_entry(row.to_s, arg, flags)
444
+ else
445
+ filter_row_entry(row[arg.field].to_s, arg, flags)
446
+ end
447
+
448
+ # Pivot of off include vs exclude
449
+ if arg.bang
450
+ !included
451
+ else
452
+ included
453
+ end
454
+ end
455
+
456
+ # Field Partial / Case / Exact search
457
+ def self.filter_row_entry(entry, arg, flags)
458
+ # Exact Matching / Unless doing full text search
459
+ return entry.to_s == arg.value.to_s if flags.key?(:exact) && arg.field != :text
460
+
461
+ if flags.key?(:case)
462
+ entry.include? arg.value.to_s
463
+ else
464
+ entry.downcase.include? arg.value.to_s.downcase
465
+ end
466
+ end
467
+
468
+ # Total Count Helper
469
+ def self.total_count(results)
470
+ results.each do |k, v|
471
+ puts k
472
+ puts "Total: #{v.count.to_s.pastel(:blue)}"
473
+ puts
474
+ end
475
+ end
476
+
477
+ # Total Count Helper
478
+ def self.fields_print(results)
479
+ results.each do |k, v|
480
+ puts k
481
+ puts field_table(v.map(&:keys).flatten.uniq.sort)
482
+ puts
483
+ end
484
+ end
485
+
486
+ def self.field_table(list, columns = 4)
487
+ return nil if list.size.zero?
488
+
489
+ # Keep Alphabetical Sort
490
+ groups = list.each_slice((list.size / columns.to_f).round).to_a
491
+
492
+ table = TTY::Table.new do |t|
493
+ loop do
494
+ break if groups.all?(&:empty?)
495
+
496
+ t << groups.map(&:shift)
497
+ end
498
+ end
499
+
500
+ table.render(:unicode, padding: [0, 1, 0, 1])
501
+ end
502
+
503
+ # Unified Files Interface
504
+ def self.files(file_list, base_list = nil, flags = {})
505
+ base_list ||= Thing.all
506
+
507
+ # Prepare Log List
508
+ file_list = prepare_list(file_list, base_list)
509
+
510
+ # Convert to Things
511
+ find_things(file_list, flags)
512
+ end
513
+
514
+ # Total Log List Manipulator
515
+ def self.prepare_list(log_list, base_list = nil, _flags = {})
516
+ base_list ||= GreenHat::ShellHelper::Log.list
517
+
518
+ # Assume all
519
+ log_list.push '*' if log_list.empty?
520
+
521
+ # Map for All
522
+ log_list = base_list.map(&:name) if log_list == ['*']
523
+
524
+ log_list
525
+ end
526
+
527
+ # Fuzzy match for things
528
+ def self.thing_list
529
+ @thing_list ||= Thing.all.map(&:name)
530
+
531
+ @thing_list
532
+ end
533
+
534
+ # Shortcut find things
535
+ def self.find_things(files, flags = {})
536
+ things = files.uniq.flat_map do |file|
537
+ # If Thing, Return Thing
538
+ return file if file.instance_of?(Thing)
539
+
540
+ if flags.fuzzy_file_match
541
+ Thing.all.select { |x| x.name.include? file }
542
+ else
543
+ Thing.where name: file
544
+ end
545
+ end.uniq
546
+
547
+ # Host / Archive
548
+ things.select! { |x| x.archive? flags.archive } if flags.key?(:archive)
549
+
550
+ things
551
+ end
552
+
553
+ # Main Entry Point for Searching
554
+ # def self.search_start(log_list, filter_type, args, opts)
555
+ def self.search_start(files, flags, args)
556
+ # Convert to Things
557
+ logs = ShellHelper.find_things(files, flags)
558
+
559
+ logs.each_with_object({}) do |log, obj|
560
+ # Ignore Empty Results / No Thing
561
+ next if log&.data.blank?
562
+
563
+ obj[log.friendly_name] = ShellHelper.search(log.data, flags, args)
564
+
565
+ obj
566
+ end
567
+ end
568
+
569
+ # Generic Search Helper / String/Regex
570
+ def self.search(data, flags = {}, args = {})
571
+ results = data.clone.flatten.compact
572
+ results.select! do |row|
573
+ args.send(flags.logic) do |arg|
574
+ search_row(row, arg, flags)
575
+ end
576
+ end
577
+
578
+ # Strip Results if Slice is defined
579
+ results.map! { |row| row.slice(*flags[:slice]) } if flags[:slice]
580
+
581
+ # Strip Results if Except is defined
582
+ results.map! { |row| row.except(*flags[:except]) } if flags[:except]
583
+
584
+ # Remove Blank from either slice or except
585
+ results.reject!(&:empty?)
586
+
587
+ results
588
+ end
589
+
590
+ # Break out filter row logic into separate method
591
+ def self.search_row(row, arg, flags)
592
+ # Sensitivity Check / Check for Match
593
+ included = filter_row_entry(row.to_s, arg, flags)
594
+
595
+ # Pivot of off include vs exclude
596
+ if arg.bang
597
+ !included
598
+ else
599
+ included
600
+ end
601
+ end
602
+
603
+ # TODO: Remove?
604
+ # Color Reader Helper
605
+ # def self.pastel
606
+ # @pastel ||= Pastel.new
607
+ # end
608
+
609
+ # Number Helper
610
+ # https://gitlab.com/zedtux/human_size_to_number/-/blob/master/lib/human_size_to_number/helper.rb
611
+ def self.human_size_to_number(string)
612
+ size, unit = string.scan(/(\d*\.?\d+)\s?(Bytes?|KB|MB|GB|TB)/i).first
613
+ number = size.to_f
614
+
615
+ number = case unit.downcase
616
+ when 'byte', 'bytes'
617
+ number
618
+ when 'kb'
619
+ number * 1024
620
+ when 'mb'
621
+ number * 1024 * 1024
622
+ when 'gb'
623
+ number * 1024 * 1024 * 1024
624
+ when 'tb'
625
+ number * 1024 * 1024 * 1024 * 1024
626
+ end
627
+ number.round
628
+ end
629
+
630
+ # TODO: Needed?
631
+ def self.filter_and(data, params = {})
632
+ result = data.clone.flatten.compact
633
+ params.each do |k, v|
634
+ result.select! do |row|
635
+ if row.key? k.to_sym
636
+ row[k.to_sym].include? v
637
+ else
638
+ false
639
+ end
640
+ end
641
+ next
642
+ end
643
+
644
+ result
645
+ end
646
+
647
+ # General Helper for `show`
648
+ def self.common_opts
649
+ puts 'Common Options'.pastel(:blue)
650
+ puts ' --raw'.pastel(:green)
651
+ puts ' Do not use less/paging'
652
+ puts
653
+
654
+ puts ' --archive'.pastel(:green)
655
+ puts ' Limit to specific archive name (inclusive). Matching SOS tar.gz name'
656
+ puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
657
+ puts
658
+ end
659
+ end
660
+ # rubocop:enable Metrics/ModuleLength
661
+ end