greenhat 0.4.0 → 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.
- checksums.yaml +4 -4
- data/bin/greenhat +1 -2
- data/lib/greenhat/accessors/disk.rb +1 -1
- data/lib/greenhat/accessors/gitlab.rb +4 -2
- data/lib/greenhat/archive.rb +7 -1
- data/lib/greenhat/cli.rb +24 -11
- data/lib/greenhat/entrypoint.rb +37 -34
- 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 +49 -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 +35 -8
- 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 +65 -185
- data/lib/greenhat/shell/gitlab.rb +1 -0
- data/lib/greenhat/shell/log.rb +24 -30
- data/lib/greenhat/shell/markdown.rb +355 -352
- data/lib/greenhat/shell/process.rb +11 -5
- data/lib/greenhat/shell/query.rb +534 -0
- data/lib/greenhat/shell/report.rb +415 -410
- data/lib/greenhat/shell/reports.rb +41 -0
- data/lib/greenhat/shell/shell_helper.rb +95 -387
- data/lib/greenhat/shell.rb +22 -3
- data/lib/greenhat/thing/file_types.rb +30 -1
- data/lib/greenhat/thing/formatters/api_json.rb +4 -2
- data/lib/greenhat/thing/formatters/bracket_log.rb +1 -1
- data/lib/greenhat/thing/formatters/clean_raw.rb +1 -1
- data/lib/greenhat/thing/formatters/colon_split_strip.rb +2 -2
- data/lib/greenhat/thing/formatters/dotenv.rb +1 -1
- data/lib/greenhat/thing/formatters/format.rb +0 -11
- data/lib/greenhat/thing/formatters/free_m.rb +2 -2
- data/lib/greenhat/thing/formatters/json.rb +41 -17
- data/lib/greenhat/thing/formatters/json_shellwords.rb +3 -2
- data/lib/greenhat/thing/formatters/kube_json.rb +3 -2
- data/lib/greenhat/thing/formatters/multiline_json.rb +1 -1
- data/lib/greenhat/thing/formatters/nginx.rb +5 -1
- data/lib/greenhat/thing/formatters/runner_log.rb +70 -0
- data/lib/greenhat/thing/formatters/table.rb +17 -3
- data/lib/greenhat/thing/formatters/time_json.rb +12 -1
- data/lib/greenhat/thing/helpers.rb +0 -11
- data/lib/greenhat/thing/info_format.rb +4 -4
- data/lib/greenhat/thing/kind.rb +1 -1
- data/lib/greenhat/thing.rb +21 -25
- data/lib/greenhat/version.rb +1 -1
- data/lib/greenhat.rb +6 -8
- metadata +32 -4
- data/lib/greenhat/pry_helpers.rb +0 -51
- data/lib/greenhat/thing/super_log.rb +0 -102
@@ -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
|
@@ -0,0 +1,534 @@
|
|
1
|
+
# NameSpace
|
2
|
+
module GreenHat
|
3
|
+
# Query Handlers
|
4
|
+
# rubocop:disable Metrics/ModuleLength
|
5
|
+
module Query
|
6
|
+
# Main Entry Point for Filtering
|
7
|
+
def self.start(files, flags = {}, args = {})
|
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
|
13
|
+
|
14
|
+
# Ignore Archive/Host Dividers
|
15
|
+
if flags[:combine]
|
16
|
+
# Add SRC Field to Combined Entries
|
17
|
+
results = combine_src_add(files)
|
18
|
+
Query.filter(results.flatten.compact, flags, args)
|
19
|
+
else
|
20
|
+
file_filter(files, flags, args)
|
21
|
+
end
|
22
|
+
end
|
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
|
34
|
+
|
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?
|
41
|
+
|
42
|
+
# Include Total Count in Name
|
43
|
+
results = Query.filter(file.data, flags, args)
|
44
|
+
|
45
|
+
next if results.count.zero? # Skip if there are no results
|
46
|
+
|
47
|
+
duration = calculate_duration(results)
|
48
|
+
|
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
|
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)
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.calculate_duration(results)
|
133
|
+
# Skip for Pluck
|
134
|
+
only_with_time = results.select { |x| x.instance_of?(Hash) && x.key?(:time) && x[:time].instance_of?(Time) }
|
135
|
+
|
136
|
+
# If slice is used ignore
|
137
|
+
return nil if only_with_time.empty?
|
138
|
+
|
139
|
+
sorted = only_with_time.map(&:time).sort
|
140
|
+
humanize_time(sorted.first, sorted.last)
|
141
|
+
end
|
142
|
+
|
143
|
+
# Replace TimeDifference with https://stackoverflow.com/a/4136485/1678507
|
144
|
+
def self.humanize_time(time_start, time_end, increments = 2)
|
145
|
+
miliseconds = (time_end - time_start) * 1000
|
146
|
+
|
147
|
+
list = [[1000, :ms], [60, :s], [60, :m], [24, :h]].map do |count, name|
|
148
|
+
next unless miliseconds.positive?
|
149
|
+
|
150
|
+
miliseconds, n = miliseconds.divmod(count)
|
151
|
+
|
152
|
+
"#{n.to_i}#{name}" unless n.to_i.zero?
|
153
|
+
end
|
154
|
+
|
155
|
+
list.compact.reverse[0..increments - 1].join(' ')
|
156
|
+
end
|
157
|
+
|
158
|
+
# Filter Logic
|
159
|
+
# TODO: Simplify
|
160
|
+
def self.filter(data, flags = {}, args = {})
|
161
|
+
results = data.clone
|
162
|
+
|
163
|
+
# Handle Transform
|
164
|
+
process_transform(flags[:transform], results) if flags.key? :transform
|
165
|
+
|
166
|
+
results.select! do |row|
|
167
|
+
args.send(flags.logic || :all?) do |arg|
|
168
|
+
filter_row_key(row, arg, flags)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Ensure presecense of a specific field
|
173
|
+
results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
|
174
|
+
|
175
|
+
# Time Zone
|
176
|
+
results = filter_modify_timezone(results, flags[:time_zone]) if flags.key?(:time_zone)
|
177
|
+
|
178
|
+
# Time Filtering
|
179
|
+
results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
|
180
|
+
|
181
|
+
# Strip Results if Slice is defined
|
182
|
+
results = filter_slice(results, flags[:slice]) if flags.key?(:slice)
|
183
|
+
|
184
|
+
# Strip Results if Except is defined
|
185
|
+
results = filter_except(results, flags[:except]) if flags.key?(:except)
|
186
|
+
|
187
|
+
# Remove Blank from either slice or except
|
188
|
+
results.reject!(&:empty?)
|
189
|
+
|
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
|
195
|
+
|
196
|
+
# JSON Formatting
|
197
|
+
results = results.map { |x| Oj.dump(x) } if flags.key?(:json)
|
198
|
+
|
199
|
+
# Show Unique Only
|
200
|
+
results = filter_uniq(results, flags[:uniq]) if flags.key?(:uniq)
|
201
|
+
|
202
|
+
# Reverse
|
203
|
+
results.reverse! if flags[:reverse]
|
204
|
+
|
205
|
+
# Count occurrences / Skip Results
|
206
|
+
return filter_stats(results, flags) if flags.key?(:stats)
|
207
|
+
|
208
|
+
# Percentile Breakdown
|
209
|
+
return filter_percentile(results, flags) if flags.key?(:percentile)
|
210
|
+
|
211
|
+
# Limit before Pluck / Flattening
|
212
|
+
results = filter_limit(results, flags[:limit]) if flags.key?(:limit)
|
213
|
+
|
214
|
+
# Pluck
|
215
|
+
results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
|
216
|
+
|
217
|
+
# Total Counter
|
218
|
+
return [ShellHelper.total_count(results, flags)] if flags.key?(:total)
|
219
|
+
|
220
|
+
results
|
221
|
+
end
|
222
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
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
|
+
|
243
|
+
# Limit / Ensure Exists and Valid Number
|
244
|
+
def self.filter_limit(results, limit)
|
245
|
+
return results unless limit.integer? && limit.positive?
|
246
|
+
|
247
|
+
results.shift limit
|
248
|
+
end
|
249
|
+
|
250
|
+
def self.filter_modify_timezone(results, time_zone)
|
251
|
+
results.map do |x|
|
252
|
+
next unless x.key? :time
|
253
|
+
|
254
|
+
x = x.clone # Prevent Top Level Modification
|
255
|
+
x[:time] = x[:time].in_time_zone time_zone
|
256
|
+
|
257
|
+
x
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
# Filter Start and End Times
|
262
|
+
# TODO: This is a bit icky, simplify/dry
|
263
|
+
def self.filter_time(results, flags)
|
264
|
+
if flags.key?(:start)
|
265
|
+
begin
|
266
|
+
time_start = Time.parse(flags[:start])
|
267
|
+
|
268
|
+
results.select! do |x|
|
269
|
+
if x.time
|
270
|
+
time_start < x.time
|
271
|
+
else
|
272
|
+
true
|
273
|
+
end
|
274
|
+
end
|
275
|
+
rescue StandardError
|
276
|
+
puts 'Unable to Process Start Time Filter'.pastel(:red)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
if flags.key?(:end)
|
281
|
+
begin
|
282
|
+
time_start = Time.parse(flags[:end])
|
283
|
+
|
284
|
+
results.select! do |x|
|
285
|
+
if x.time
|
286
|
+
time_start > x.time
|
287
|
+
else
|
288
|
+
true
|
289
|
+
end
|
290
|
+
end
|
291
|
+
rescue StandardError
|
292
|
+
puts 'Unable to Process End Time Filter'.pastel(:red)
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
results
|
297
|
+
end
|
298
|
+
|
299
|
+
def self.filter_except(results, except)
|
300
|
+
# Avoid Empty Results
|
301
|
+
if except.empty?
|
302
|
+
filter_empty_arg('except')
|
303
|
+
return results
|
304
|
+
end
|
305
|
+
|
306
|
+
results.map { |row| row.except(*except) }
|
307
|
+
end
|
308
|
+
|
309
|
+
def self.filter_exists(results, exists)
|
310
|
+
# Avoid Empty Results
|
311
|
+
if exists.empty?
|
312
|
+
filter_empty_arg('exists')
|
313
|
+
return results
|
314
|
+
end
|
315
|
+
|
316
|
+
results.select { |row| (exists - row.keys).empty? }
|
317
|
+
end
|
318
|
+
|
319
|
+
def self.filter_slice(results, slice)
|
320
|
+
# Avoid Empty Results
|
321
|
+
if slice.empty?
|
322
|
+
filter_empty_arg('slice')
|
323
|
+
return results
|
324
|
+
end
|
325
|
+
|
326
|
+
results.compact.map { |row| row.slice(*slice) }
|
327
|
+
end
|
328
|
+
|
329
|
+
def self.filter_pluck(results, pluck)
|
330
|
+
# Avoid Empty Results
|
331
|
+
if pluck.empty?
|
332
|
+
filter_empty_arg('pluck')
|
333
|
+
return results
|
334
|
+
end
|
335
|
+
|
336
|
+
results.map { |x| x.slice(*pluck).values }.flatten
|
337
|
+
end
|
338
|
+
|
339
|
+
def self.filter_uniq(results, unique)
|
340
|
+
# Avoid Empty Results
|
341
|
+
if unique.empty?
|
342
|
+
filter_empty_arg('uniq')
|
343
|
+
return results
|
344
|
+
end
|
345
|
+
|
346
|
+
unique.map do |field|
|
347
|
+
results.uniq { |x| x[field] }
|
348
|
+
end.inject(:&)
|
349
|
+
end
|
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
|
+
|
390
|
+
def self.filter_stats(results, flags)
|
391
|
+
stats = flags[:stats]
|
392
|
+
|
393
|
+
# Avoid Empty Results
|
394
|
+
if stats.empty?
|
395
|
+
filter_empty_arg('stats')
|
396
|
+
return results
|
397
|
+
end
|
398
|
+
|
399
|
+
# Loop through Stats, Separate Hash/Tables
|
400
|
+
stats.map do |field|
|
401
|
+
occurrences = filter_count_occurrences(results, field, flags)
|
402
|
+
|
403
|
+
# Use Truncate For Long Keys
|
404
|
+
occurrences.transform_keys! { |key| key.to_s[0..flags[:truncate]] } if flags[:truncate]
|
405
|
+
|
406
|
+
# Total Occurences
|
407
|
+
total = occurrences.values.sum
|
408
|
+
|
409
|
+
# Percs
|
410
|
+
occurrences.transform_values! do |count|
|
411
|
+
[
|
412
|
+
count,
|
413
|
+
" #{percent(count, total)}%".pastel(:bright_black)
|
414
|
+
]
|
415
|
+
end
|
416
|
+
|
417
|
+
# Sort by total occurances / New Variable for Total
|
418
|
+
output = occurrences.sort_by(&:last).to_h.transform_values!(&:join).to_a
|
419
|
+
|
420
|
+
# Append Header / Total with field name
|
421
|
+
output.unshift([field.to_s.pastel(:bright_black), total])
|
422
|
+
|
423
|
+
# Format
|
424
|
+
output.to_h
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
# Percent Helper
|
429
|
+
def self.percent(value, total)
|
430
|
+
((value / total.to_f) * 100).round
|
431
|
+
end
|
432
|
+
|
433
|
+
# Helper to Count occurrences
|
434
|
+
def self.filter_count_occurrences(results, field, flags = {})
|
435
|
+
results.each_with_object(Hash.new(0)) do |entry, counts|
|
436
|
+
if entry.key? field
|
437
|
+
# Rounding in pagination breaks stats
|
438
|
+
key = if flags.key?(:round) && entry[field].numeric?
|
439
|
+
entry[field].to_f.round(flags.round)
|
440
|
+
else
|
441
|
+
entry[field]
|
442
|
+
end
|
443
|
+
|
444
|
+
counts[key] += 1
|
445
|
+
else
|
446
|
+
counts['None'.pastel(:bright_black)] += 1
|
447
|
+
end
|
448
|
+
|
449
|
+
counts
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def self.filter_empty_arg(arg)
|
454
|
+
puts [
|
455
|
+
'Ignoring'.pastel(:bright_yellow),
|
456
|
+
"--#{arg}".pastel(:cyan),
|
457
|
+
'it requires an argument'.pastel(:red)
|
458
|
+
].join(' ')
|
459
|
+
end
|
460
|
+
|
461
|
+
# Break out filter row logic into separate method
|
462
|
+
def self.filter_row_key(row, arg, flags)
|
463
|
+
return false if row.nil? # Nothing to filter if row empty
|
464
|
+
|
465
|
+
# Ignore Other Logic if Field isn't even included / Full Text Searching
|
466
|
+
return false unless row.key?(arg[:field]) || arg[:field] == :text
|
467
|
+
|
468
|
+
# Sensitivity Check / Check for Match / Full Text Searching
|
469
|
+
search_data = arg[:field] == :text ? row : row[arg.field]
|
470
|
+
match = filter_row_entry(search_data.to_s, arg, flags)
|
471
|
+
|
472
|
+
# Pivot of off include vs exclude
|
473
|
+
if arg.bang
|
474
|
+
!match
|
475
|
+
else
|
476
|
+
match
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Field Partial / Case / Exact search
|
481
|
+
def self.filter_row_entry(entry, arg, flags)
|
482
|
+
# Exact Matching / Unless doing full text search
|
483
|
+
return entry == arg.value.to_s if flags.key?(:exact) && arg.field != :text
|
484
|
+
|
485
|
+
# Cast to String/Integer Helper
|
486
|
+
entry, value = filter_entry_cast(entry, arg, flags)
|
487
|
+
|
488
|
+
entry.send(arg.logic, value)
|
489
|
+
end
|
490
|
+
|
491
|
+
# Handle casting to strings or integers
|
492
|
+
def self.filter_entry_cast(entry, arg, flags)
|
493
|
+
# Cast to String
|
494
|
+
value = arg.value.to_s
|
495
|
+
|
496
|
+
# No Logic on Empty Entries
|
497
|
+
return [entry, value] if entry.empty?
|
498
|
+
|
499
|
+
case arg.logic
|
500
|
+
when :include?
|
501
|
+
|
502
|
+
# Exact Case argument
|
503
|
+
unless flags.key?(:case)
|
504
|
+
entry = entry.downcase
|
505
|
+
value = value.downcase
|
506
|
+
end
|
507
|
+
when :>=, :<=
|
508
|
+
entry = entry.to_i if entry.numeric?
|
509
|
+
value = value.to_i if value&.numeric?
|
510
|
+
end
|
511
|
+
|
512
|
+
[entry, value]
|
513
|
+
end
|
514
|
+
|
515
|
+
def self.filter_entry_logic(entry, arg)
|
516
|
+
entry.send(arg.logic, arg.value)
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
# Internal Query Helper
|
521
|
+
# query = 'gitlab-rails/application_json.log --message!="Cannot obtain an exclusive lease" --severity=error'
|
522
|
+
# Query.filter_internal(query)
|
523
|
+
def self.filter_internal(search = '')
|
524
|
+
files, flags, args = Args.parse(Shellwords.split(search))
|
525
|
+
flags[:combine] = true
|
526
|
+
|
527
|
+
# Default to everything
|
528
|
+
files = Thing.all.map(&:name) if files.empty?
|
529
|
+
|
530
|
+
Query.start(files, flags, args)
|
531
|
+
end
|
532
|
+
|
533
|
+
# rubocop:enable Metrics/ModuleLength
|
534
|
+
end
|