greenhat 0.1.5 → 0.3.2

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 +58 -2
  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 +10 -1
  6. data/lib/greenhat/cli.rb +148 -63
  7. data/lib/greenhat/color.rb +27 -0
  8. data/lib/greenhat/logbot.rb +9 -9
  9. data/lib/greenhat/settings.rb +51 -3
  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 +43 -0
  13. data/lib/greenhat/shell/disk.rb +30 -42
  14. data/lib/greenhat/shell/faststats.rb +104 -58
  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 +115 -209
  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 +654 -0
  25. data/lib/greenhat/shell.rb +27 -13
  26. data/lib/greenhat/thing/file_types.rb +54 -7
  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 +25 -3
  34. data/lib/greenhat/tty/columns.rb +44 -0
  35. data/lib/greenhat/version.rb +1 -1
  36. data/lib/greenhat.rb +16 -14
  37. metadata +42 -17
  38. data/lib/greenhat/shell/helper.rb +0 -541
@@ -0,0 +1,654 @@
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)
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)
409
+ results.each_with_object(Hash.new(0)) do |entry, counts|
410
+ if entry.key? field
411
+ counts[entry[field]] += 1
412
+ else
413
+ counts['None'.pastel(:bright_black)] += 1
414
+ end
415
+
416
+ counts
417
+ end
418
+ end
419
+
420
+ def self.filter_empty_arg(arg)
421
+ puts [
422
+ 'Ignoring'.pastel(:bright_yellow),
423
+ "--#{arg}".pastel(:cyan),
424
+ 'it requires an argument'.pastel(:red)
425
+ ].join(' ')
426
+ end
427
+
428
+ # Break out filter row logic into separate method
429
+
430
+ def self.filter_row_key(row, arg, flags)
431
+ # Ignore Other Logic if Field isn't even included / Full Text Searching
432
+ return false unless row.key?(arg[:field]) || arg[:field] == :text
433
+
434
+ # Sensitivity Check / Check for Match / Full Text Searching
435
+ included = if arg[:field] == :text
436
+ filter_row_entry(row.to_s, arg, flags)
437
+ else
438
+ filter_row_entry(row[arg.field].to_s, arg, flags)
439
+ end
440
+
441
+ # Pivot of off include vs exclude
442
+ if arg.bang
443
+ !included
444
+ else
445
+ included
446
+ end
447
+ end
448
+
449
+ # Field Partial / Case / Exact search
450
+ def self.filter_row_entry(entry, arg, flags)
451
+ # Exact Matching / Unless doing full text search
452
+ return entry.to_s == arg.value.to_s if flags.key?(:exact) && arg.field != :text
453
+
454
+ if flags.key?(:case)
455
+ entry.include? arg.value.to_s
456
+ else
457
+ entry.downcase.include? arg.value.to_s.downcase
458
+ end
459
+ end
460
+
461
+ # Total Count Helper
462
+ def self.total_count(results)
463
+ results.each do |k, v|
464
+ puts k
465
+ puts "Total: #{v.count.to_s.pastel(:blue)}"
466
+ puts
467
+ end
468
+ end
469
+
470
+ # Total Count Helper
471
+ def self.fields_print(results)
472
+ results.each do |k, v|
473
+ puts k
474
+ puts field_table(v.map(&:keys).flatten.uniq.sort)
475
+ puts
476
+ end
477
+ end
478
+
479
+ def self.field_table(list, columns = 4)
480
+ return nil if list.size.zero?
481
+
482
+ # Keep Alphabetical Sort
483
+ groups = list.each_slice((list.size / columns.to_f).round).to_a
484
+
485
+ table = TTY::Table.new do |t|
486
+ loop do
487
+ break if groups.all?(&:empty?)
488
+
489
+ t << groups.map(&:shift)
490
+ end
491
+ end
492
+
493
+ table.render(:unicode, padding: [0, 1, 0, 1])
494
+ end
495
+
496
+ # Unified Files Interface
497
+ def self.files(file_list, base_list = nil, flags = {})
498
+ base_list ||= Thing.all
499
+
500
+ # Prepare Log List
501
+ file_list = prepare_list(file_list, base_list)
502
+
503
+ # Convert to Things
504
+ find_things(file_list, flags)
505
+ end
506
+
507
+ # Total Log List Manipulator
508
+ def self.prepare_list(log_list, base_list = nil, _flags = {})
509
+ base_list ||= GreenHat::ShellHelper::Log.list
510
+
511
+ # Assume all
512
+ log_list.push '*' if log_list.empty?
513
+
514
+ # Map for All
515
+ log_list = base_list.map(&:name) if log_list == ['*']
516
+
517
+ log_list
518
+ end
519
+
520
+ # Fuzzy match for things
521
+ def self.thing_list
522
+ @thing_list ||= Thing.all.map(&:name)
523
+
524
+ @thing_list
525
+ end
526
+
527
+ # Shortcut find things
528
+ def self.find_things(files, flags = {})
529
+ things = files.uniq.flat_map do |file|
530
+ # If Thing, Return Thing
531
+ return file if file.instance_of?(Thing)
532
+
533
+ if flags.fuzzy_file_match
534
+ Thing.all.select { |x| x.name.include? file }
535
+ else
536
+ Thing.where name: file
537
+ end
538
+ end.uniq
539
+
540
+ # Host / Archive
541
+ things.select! { |x| x.archive? flags.archive } if flags.key?(:archive)
542
+
543
+ things
544
+ end
545
+
546
+ # Main Entry Point for Searching
547
+ # def self.search_start(log_list, filter_type, args, opts)
548
+ def self.search_start(files, flags, args)
549
+ # Convert to Things
550
+ logs = ShellHelper.find_things(files, flags)
551
+
552
+ logs.each_with_object({}) do |log, obj|
553
+ # Ignore Empty Results / No Thing
554
+ next if log&.data.blank?
555
+
556
+ obj[log.friendly_name] = ShellHelper.search(log.data, flags, args)
557
+
558
+ obj
559
+ end
560
+ end
561
+
562
+ # Generic Search Helper / String/Regex
563
+ def self.search(data, flags = {}, args = {})
564
+ results = data.clone.flatten.compact
565
+ results.select! do |row|
566
+ args.send(flags.logic) do |arg|
567
+ search_row(row, arg, flags)
568
+ end
569
+ end
570
+
571
+ # Strip Results if Slice is defined
572
+ results.map! { |row| row.slice(*flags[:slice]) } if flags[:slice]
573
+
574
+ # Strip Results if Except is defined
575
+ results.map! { |row| row.except(*flags[:except]) } if flags[:except]
576
+
577
+ # Remove Blank from either slice or except
578
+ results.reject!(&:empty?)
579
+
580
+ results
581
+ end
582
+
583
+ # Break out filter row logic into separate method
584
+ def self.search_row(row, arg, flags)
585
+ # Sensitivity Check / Check for Match
586
+ included = filter_row_entry(row.to_s, arg, flags)
587
+
588
+ # Pivot of off include vs exclude
589
+ if arg.bang
590
+ !included
591
+ else
592
+ included
593
+ end
594
+ end
595
+
596
+ # TODO: Remove?
597
+ # Color Reader Helper
598
+ # def self.pastel
599
+ # @pastel ||= Pastel.new
600
+ # end
601
+
602
+ # Number Helper
603
+ # https://gitlab.com/zedtux/human_size_to_number/-/blob/master/lib/human_size_to_number/helper.rb
604
+ def self.human_size_to_number(string)
605
+ size, unit = string.scan(/(\d*\.?\d+)\s?(Bytes?|KB|MB|GB|TB)/i).first
606
+ number = size.to_f
607
+
608
+ number = case unit.downcase
609
+ when 'byte', 'bytes'
610
+ number
611
+ when 'kb'
612
+ number * 1024
613
+ when 'mb'
614
+ number * 1024 * 1024
615
+ when 'gb'
616
+ number * 1024 * 1024 * 1024
617
+ when 'tb'
618
+ number * 1024 * 1024 * 1024 * 1024
619
+ end
620
+ number.round
621
+ end
622
+
623
+ # TODO: Needed?
624
+ def self.filter_and(data, params = {})
625
+ result = data.clone.flatten.compact
626
+ params.each do |k, v|
627
+ result.select! do |row|
628
+ if row.key? k.to_sym
629
+ row[k.to_sym].include? v
630
+ else
631
+ false
632
+ end
633
+ end
634
+ next
635
+ end
636
+
637
+ result
638
+ end
639
+
640
+ # General Helper for `show`
641
+ def self.common_opts
642
+ puts 'Common Options'.pastel(:blue)
643
+ puts ' --raw'.pastel(:green)
644
+ puts ' Do not use less/paging'
645
+ puts
646
+
647
+ puts ' --archive'.pastel(:green)
648
+ puts ' Limit to specific archive name (inclusive). Matching SOS tar.gz name'
649
+ puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
650
+ puts
651
+ end
652
+ end
653
+ # rubocop:enable Metrics/ModuleLength
654
+ end