greenhat 0.5.1 → 0.6.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/greenhat/archive.rb +2 -0
  3. data/lib/greenhat/cli.rb +12 -2
  4. data/lib/greenhat/entrypoint.rb +36 -33
  5. data/lib/greenhat/logbot.rb +1 -1
  6. data/lib/greenhat/paper/flag_helper.rb +18 -0
  7. data/lib/greenhat/paper/paper_helper.rb +118 -0
  8. data/lib/greenhat/paper.rb +34 -0
  9. data/lib/greenhat/reports/builder.rb +98 -0
  10. data/lib/greenhat/reports/helpers.rb +101 -0
  11. data/lib/greenhat/reports/internal_methods.rb +156 -0
  12. data/lib/greenhat/reports/reports/errors.rb +49 -0
  13. data/lib/greenhat/reports/reports/faststats.rb +42 -0
  14. data/lib/greenhat/reports/reports/full.rb +143 -0
  15. data/lib/greenhat/reports/runner.rb +58 -0
  16. data/lib/greenhat/reports/shared.rb +37 -0
  17. data/lib/greenhat/reports/shell_helper.rb +34 -0
  18. data/lib/greenhat/reports.rb +79 -0
  19. data/lib/greenhat/settings.rb +6 -1
  20. data/lib/greenhat/shell/args.rb +9 -9
  21. data/lib/greenhat/shell/color_string.rb +1 -1
  22. data/lib/greenhat/shell/faststats.rb +24 -5
  23. data/lib/greenhat/shell/field_helper.rb +1 -1
  24. data/lib/greenhat/shell/filter_help.rb +36 -189
  25. data/lib/greenhat/shell/log.rb +18 -14
  26. data/lib/greenhat/shell/markdown.rb +355 -352
  27. data/lib/greenhat/shell/process.rb +11 -5
  28. data/lib/greenhat/shell/query.rb +183 -27
  29. data/lib/greenhat/shell/report.rb +415 -412
  30. data/lib/greenhat/shell/reports.rb +41 -0
  31. data/lib/greenhat/shell/shell_helper.rb +92 -34
  32. data/lib/greenhat/shell.rb +13 -2
  33. data/lib/greenhat/thing/file_types.rb +14 -0
  34. data/lib/greenhat/thing/formatters/clean_raw.rb +1 -1
  35. data/lib/greenhat/thing/formatters/runner_log.rb +70 -0
  36. data/lib/greenhat/thing/formatters/time_json.rb +12 -1
  37. data/lib/greenhat/thing/kind.rb +1 -1
  38. data/lib/greenhat/version.rb +1 -1
  39. data/lib/greenhat.rb +6 -8
  40. metadata +31 -4
  41. data/lib/greenhat/pry_helpers.rb +0 -51
  42. data/lib/greenhat/thing/super_log.rb +0 -1
@@ -30,8 +30,10 @@ module GreenHat
30
30
  puts
31
31
  end
32
32
 
33
- def self.filter_help
34
- ShellHelper::Filter.help
33
+ def self.filter_help(raw = {})
34
+ args, flags, _args = Args.parse(raw)
35
+
36
+ ShellHelper.show(ShellHelper::Filter.help(args.first), flags)
35
37
  end
36
38
 
37
39
  def self.ps(raw = {})
@@ -45,6 +47,10 @@ module GreenHat
45
47
  ShellHelper.file_output(files, flags)
46
48
  end
47
49
 
50
+ def self.default(raw_list)
51
+ filter(raw_list)
52
+ end
53
+
48
54
  def self.filter(raw = {})
49
55
  # Argument Parsing
50
56
  files, flags, args = Args.parse(raw)
@@ -52,14 +58,14 @@ module GreenHat
52
58
  # Prepare Log List
53
59
  file_list = ShellHelper.prepare_list(files, GreenHat::Ps.things)
54
60
 
55
- results = ShellHelper.filter_start(file_list, flags, args)
61
+ results = Query.start(file_list, flags, args)
56
62
 
57
63
  # Check Search Results
58
64
  if results.instance_of?(Hash) && results.values.flatten.empty?
59
65
  puts 'No results'.pastel(:red)
60
66
  else
61
- # This causes the key 'colorized' output to also be included
62
- ShellHelper.show(results.to_a.compact.flatten, flags)
67
+
68
+ ShellHelper.show(results, flags)
63
69
  end
64
70
  end
65
71
  end
@@ -5,38 +5,128 @@ module GreenHat
5
5
  module Query
6
6
  # Main Entry Point for Filtering
7
7
  def self.start(files, flags = {}, args = {})
8
- # Convert to Things
9
- files = ShellHelper.find_things(files, flags).select(&:query?)
8
+ # Convert to Things / Thing.list == processed files
9
+ files = ShellHelper.find_things(files, flags, Thing.list).select(&:query?)
10
+
11
+ # Breakdown by Interval
12
+ return process_interval(files, flags, args) if flags.key? :interval
10
13
 
11
14
  # Ignore Archive/Host Dividers
12
15
  if flags[:combine]
13
- results = files.reject(&:blank?).map(&:data).flatten.compact
14
- Query.filter(results, flags, args)
16
+ # Add SRC Field to Combined Entries
17
+ results = combine_src_add(files)
18
+ Query.filter(results.flatten.compact, flags, args)
15
19
  else
16
- # Iterate and Preserve Archive/Host Index
17
- files.each_with_object({}) do |file, obj|
18
- # Ignore Empty Results / No Thing
19
- next if file&.blank?
20
+ file_filter(files, flags, args)
21
+ end
22
+ end
20
23
 
21
- # Include Total Count in Name
22
- results = Query.filter(file.data, flags, args)
23
- duration = calculate_duration(results)
24
+ def self.combine_src_add(files)
25
+ files.reject(&:blank?).map do |file|
26
+ src = file.archive&.friendly_name || file&.name
27
+ file.data.map do |entry|
28
+ bit = entry.clone
29
+ bit[:src] = src
30
+ bit
31
+ end
32
+ end
33
+ end
24
34
 
25
- title = [
26
- file.friendly_name,
27
- " #{results.count}".pastel(:bright_black)
35
+ # Simplify Loop for Query.start
36
+ def self.file_filter(files, flags = {}, args = {})
37
+ # Iterate and Preserve Archive/Host Index
38
+ files.each_with_object([]) do |file, obj|
39
+ # Ignore Empty Results / No Thing
40
+ next if file&.blank?
28
41
 
29
- ]
42
+ # Include Total Count in Name
43
+ results = Query.filter(file.data, flags, args)
30
44
 
31
- # Append Duration
32
- title.push(" #{duration.pastel(:cyan, :dim)}") unless duration.blank?
45
+ next if results.count.zero? # Skip if there are no results
33
46
 
34
- # Save unless empty
35
- obj[title.join] = results unless results.count.zero?
47
+ duration = calculate_duration(results)
36
48
 
37
- obj
38
- end
49
+ # Create Title
50
+ title = create_title(file, flags, results)
51
+
52
+ # Append Duration
53
+ title += " #{duration.pastel(:cyan, :dim)}" unless duration.blank?
54
+
55
+ # Add Title
56
+ obj.push(title)
57
+
58
+ # Add Results
59
+ obj.concat(results)
60
+
61
+ obj
62
+ end
63
+ end
64
+
65
+ # Helper to simplify Title Creation
66
+ def self.create_title(file, flags, results)
67
+ title = file.friendly_name
68
+ # Ignore for Total Results
69
+ title += " #{results.count}".pastel(:bright_black) unless flags.key?(:total)
70
+ title
71
+ end
72
+
73
+ # Helper to transform fields from one to another (timestamp => time)
74
+ def self.process_transform(transform, results)
75
+ # TODO: Eventually provide more splat options
76
+ from, to, *splat = transform
77
+ results.each do |entry|
78
+ entry[to.to_sym] = case splat
79
+ when [:to_i]
80
+ entry[from.to_sym].to_i
81
+ else
82
+ entry[from.to_sym]
83
+ end
84
+ end
85
+ end
86
+
87
+ # Organize Results by Interval
88
+ # TODO Simplify
89
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
90
+ def self.process_interval(files, flags, args)
91
+ data = files.reject(&:blank?).map(&:data).flatten.compact
92
+
93
+ # Handle Transform
94
+ process_transform(flags[:transform], data) if flags.key? :transform
95
+
96
+ interval = ChronicDuration.parse(flags[:interval]) || 5.minutes
97
+ index = build_time_index(data, interval)
98
+ index.transform_values! { |_y| [] }
99
+
100
+ data.each do |r|
101
+ index[r.time.floor(interval)] << r
102
+ end
103
+
104
+ output = index.each_with_object({}) do |(time, entries), obj|
105
+ results = Query.filter(entries, flags, args)
106
+
107
+ next if results.count.zero? # Skip if No Results
108
+
109
+ duration = calculate_duration(results)
110
+
111
+ # Timzone Manipulation
112
+ time_title = flags.key?(:time_zone) ? time.in_time_zone(flags[:time_zone]) : time
113
+
114
+ title = time_title.to_s.pastel(:bright_blue, :bold)
115
+ title += " #{results.count}".pastel(:bright_black) unless flags.key?(:total)
116
+
117
+ # Append Duration
118
+ title += " #{duration.pastel(:cyan, :dim)}" unless duration.blank?
119
+
120
+ # Add Results
121
+ obj[title] = results
122
+
123
+ obj
39
124
  end
125
+
126
+ # Remove Empty Intervals / There may be empty here due to results
127
+ output.reject! { |_k, v| v.flatten.empty? }
128
+
129
+ output.to_a.flatten(2)
40
130
  end
41
131
 
42
132
  def self.calculate_duration(results)
@@ -67,12 +157,11 @@ module GreenHat
67
157
 
68
158
  # Filter Logic
69
159
  # TODO: Simplify
70
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
71
160
  def self.filter(data, flags = {}, args = {})
72
- # Experimenting with deep clone
73
- # results = Marshal.load(Marshal.dump(data))
74
161
  results = data.clone
75
- # results = data
162
+
163
+ # Handle Transform
164
+ process_transform(flags[:transform], results) if flags.key? :transform
76
165
 
77
166
  results.select! do |row|
78
167
  args.send(flags.logic || :all?) do |arg|
@@ -98,8 +187,11 @@ module GreenHat
98
187
  # Remove Blank from either slice or except
99
188
  results.reject!(&:empty?)
100
189
 
101
- # Sort
102
- results.sort_by! { |x| x.slice(*flags[:sort]).values } if flags.key?(:sort)
190
+ # Sort / Reverse by default
191
+ if flags.key?(:sort)
192
+ results.sort_by! { |x| x.slice(*flags[:sort]).values }
193
+ results.reverse!
194
+ end
103
195
 
104
196
  # JSON Formatting
105
197
  results = results.map { |x| Oj.dump(x) } if flags.key?(:json)
@@ -113,16 +205,41 @@ module GreenHat
113
205
  # Count occurrences / Skip Results
114
206
  return filter_stats(results, flags) if flags.key?(:stats)
115
207
 
208
+ # Percentile Breakdown
209
+ return filter_percentile(results, flags) if flags.key?(:percentile)
210
+
116
211
  # Limit before Pluck / Flattening
117
212
  results = filter_limit(results, flags[:limit]) if flags.key?(:limit)
118
213
 
119
214
  # Pluck
120
215
  results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
121
216
 
217
+ # Total Counter
218
+ return [ShellHelper.total_count(results, flags)] if flags.key?(:total)
219
+
122
220
  results
123
221
  end
124
222
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
125
223
 
224
+ # Create list of Times
225
+ def self.build_time_index(results, interval = 5.minutes)
226
+ index = {}
227
+ start = results.min_by(&:time).time.floor(interval)
228
+ finish = results.max_by(&:time).time.floor(interval)
229
+
230
+ loop do
231
+ index[start] = 0
232
+ start += interval
233
+ break if start > finish
234
+ end
235
+
236
+ index
237
+ rescue StandardError => e
238
+ LogBot.fatal('Index Error', e.message)
239
+ ensure
240
+ {}
241
+ end
242
+
126
243
  # Limit / Ensure Exists and Valid Number
127
244
  def self.filter_limit(results, limit)
128
245
  return results unless limit.integer? && limit.positive?
@@ -231,6 +348,45 @@ module GreenHat
231
348
  end.inject(:&)
232
349
  end
233
350
 
351
+ # TODO: Make better
352
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
353
+ def self.filter_percentile(results, flags)
354
+ output = {}
355
+
356
+ results.each do |entry|
357
+ entry.each do |key, value|
358
+ # Numbers only
359
+ next unless value.instance_of?(Integer) || value.instance_of?(Float)
360
+
361
+ output[key] ||= []
362
+ output[key].push value
363
+ end
364
+ end
365
+
366
+ data = output.map do |key, values|
367
+ # Support Rounding
368
+ l99 = values.percentile(0.99)
369
+ l99 = l99.round(flags.round) if flags.round
370
+
371
+ l95 = values.percentile(0.99)
372
+ l95 = l95.round(flags.round) if flags.round
373
+
374
+ {
375
+ key: key,
376
+ '99' => l99,
377
+ '95' => l95,
378
+ mean: flags.round ? values.mean.round(flags.round) : values.mean,
379
+ min: flags.round ? values.min.round(flags.round) : values.min,
380
+ max: flags.round ? values.max.round(flags.round) : values.max,
381
+ count: values.count
382
+ }
383
+ end
384
+
385
+ headers = data.flat_map(&:keys).uniq
386
+ [[headers, data.map(&:values)]] # Multiple Arrays for Results Flatten
387
+ end
388
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
389
+
234
390
  def self.filter_stats(results, flags)
235
391
  stats = flags[:stats]
236
392