greenhat 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/lib/greenhat/accessors/disk.rb +1 -1
- data/lib/greenhat/archive.rb +2 -0
- data/lib/greenhat/cli.rb +12 -2
- data/lib/greenhat/entrypoint.rb +36 -33
- data/lib/greenhat/host.rb +1 -1
- data/lib/greenhat/logbot.rb +1 -1
- data/lib/greenhat/paper/flag_helper.rb +18 -0
- data/lib/greenhat/paper/paper_helper.rb +118 -0
- data/lib/greenhat/paper.rb +34 -0
- data/lib/greenhat/reports/builder.rb +98 -0
- data/lib/greenhat/reports/helpers.rb +101 -0
- data/lib/greenhat/reports/internal_methods.rb +156 -0
- data/lib/greenhat/reports/reports/errors.rb +51 -0
- data/lib/greenhat/reports/reports/faststats.rb +42 -0
- data/lib/greenhat/reports/reports/full.rb +143 -0
- data/lib/greenhat/reports/runner.rb +58 -0
- data/lib/greenhat/reports/shared.rb +37 -0
- data/lib/greenhat/reports/shell_helper.rb +34 -0
- data/lib/greenhat/reports.rb +79 -0
- data/lib/greenhat/settings.rb +6 -1
- data/lib/greenhat/shell/args.rb +9 -9
- data/lib/greenhat/shell/color_string.rb +1 -1
- data/lib/greenhat/shell/faststats.rb +24 -5
- data/lib/greenhat/shell/field_helper.rb +1 -1
- data/lib/greenhat/shell/filter_help.rb +36 -189
- data/lib/greenhat/shell/log.rb +28 -16
- data/lib/greenhat/shell/markdown.rb +355 -352
- data/lib/greenhat/shell/process.rb +11 -5
- data/lib/greenhat/shell/query.rb +184 -28
- data/lib/greenhat/shell/report.rb +415 -412
- data/lib/greenhat/shell/reports.rb +41 -0
- data/lib/greenhat/shell/shell_helper.rb +172 -117
- data/lib/greenhat/shell.rb +13 -2
- data/lib/greenhat/thing/file_types.rb +38 -2
- data/lib/greenhat/thing/formatters/clean_raw.rb +1 -1
- data/lib/greenhat/thing/formatters/exporters.rb +48 -0
- data/lib/greenhat/thing/formatters/identify_db.rb +32 -0
- data/lib/greenhat/thing/formatters/runner_log.rb +70 -0
- data/lib/greenhat/thing/formatters/table.rb +15 -1
- data/lib/greenhat/thing/formatters/time_json.rb +12 -1
- data/lib/greenhat/thing/kind.rb +1 -1
- data/lib/greenhat/thing.rb +1 -0
- data/lib/greenhat/version.rb +1 -1
- data/lib/greenhat.rb +6 -8
- metadata +33 -4
- data/lib/greenhat/pry_helpers.rb +0 -51
- 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
|
-
|
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 =
|
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
|
-
|
62
|
-
ShellHelper.show(results
|
67
|
+
|
68
|
+
ShellHelper.show(results, flags)
|
63
69
|
end
|
64
70
|
end
|
65
71
|
end
|
data/lib/greenhat/shell/query.rb
CHANGED
@@ -5,43 +5,133 @@ 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
|
-
|
14
|
-
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
next if file&.blank?
|
20
|
+
file_filter(files, flags, args)
|
21
|
+
end
|
22
|
+
end
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
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
|
-
|
35
|
-
obj[title.join] = results unless results.count.zero?
|
47
|
+
duration = calculate_duration(results)
|
36
48
|
|
37
|
-
|
38
|
-
|
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)
|
43
133
|
# Skip for Pluck
|
44
|
-
only_with_time = results.select { |x| x.instance_of?(Hash) && x.key?(:time) }
|
134
|
+
only_with_time = results.select { |x| x.instance_of?(Hash) && x.key?(:time) && x[:time].instance_of?(Time) }
|
45
135
|
|
46
136
|
# If slice is used ignore
|
47
137
|
return nil if only_with_time.empty?
|
@@ -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
|
-
|
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
|
-
|
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
|
|