greenhat 0.4.0 → 0.5.0
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/gitlab.rb +4 -2
- data/lib/greenhat/archive.rb +5 -1
- data/lib/greenhat/cli.rb +12 -9
- data/lib/greenhat/entrypoint.rb +4 -4
- data/lib/greenhat/settings.rb +29 -7
- data/lib/greenhat/shell/filter_help.rb +216 -183
- data/lib/greenhat/shell/gitlab.rb +1 -0
- data/lib/greenhat/shell/log.rb +11 -21
- data/lib/greenhat/shell/query.rb +378 -0
- data/lib/greenhat/shell/report.rb +2 -0
- data/lib/greenhat/shell/shell_helper.rb +11 -361
- data/lib/greenhat/shell.rb +9 -1
- data/lib/greenhat/thing/file_types.rb +7 -0
- 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/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/table.rb +3 -3
- data/lib/greenhat/thing/helpers.rb +0 -11
- data/lib/greenhat/thing/info_format.rb +4 -4
- data/lib/greenhat/thing/super_log.rb +0 -101
- data/lib/greenhat/thing.rb +20 -25
- data/lib/greenhat/version.rb +1 -1
- metadata +3 -2
data/lib/greenhat/shell/log.rb
CHANGED
@@ -4,6 +4,7 @@ module GreenHat
|
|
4
4
|
# Logs
|
5
5
|
# rubocop:disable Metrics/ModuleLength
|
6
6
|
module Log
|
7
|
+
extend Spinner # TODO: DEBUGGING REMOVE
|
7
8
|
def self.auto_complete(list, word)
|
8
9
|
# Argument Parsing
|
9
10
|
files, flags, _args = Args.parse(list)
|
@@ -20,8 +21,8 @@ module GreenHat
|
|
20
21
|
elsif matches.count > 1
|
21
22
|
puts "#{'Filter Options:'.pastel(:bright_blue)} #{matches.join(' ').pastel(:bright_green)}"
|
22
23
|
"--#{Cli.common_substr(matches)}"
|
23
|
-
|
24
|
-
|
24
|
+
# -----------------------------------
|
25
|
+
# TODO: Fix Icky Double Nesting
|
25
26
|
elsif files.count.nonzero?
|
26
27
|
matches = FieldHelper.fields_find(files, word, flags)
|
27
28
|
|
@@ -76,20 +77,13 @@ module GreenHat
|
|
76
77
|
puts "See #{'examples'.pastel(:bright_blue)} for query examples"
|
77
78
|
end
|
78
79
|
|
79
|
-
def self.filter_help(
|
80
|
-
|
81
|
-
|
82
|
-
else
|
83
|
-
list = ShellHelper::Filter.help_index.select do |k, _v|
|
84
|
-
k.to_s.include? args.first
|
85
|
-
end
|
86
|
-
|
87
|
-
puts list.values.map { |x| x.join("\n") }.join("\n\n")
|
88
|
-
end
|
80
|
+
def self.filter_help(raw = {})
|
81
|
+
args, flags, _args = Args.parse(raw)
|
82
|
+
ShellHelper.show(ShellHelper::Filter.help(args.first), flags)
|
89
83
|
end
|
90
84
|
|
91
85
|
def self.ls(args = [])
|
92
|
-
ShellHelper::List.list(args,
|
86
|
+
ShellHelper::List.list(args, Thing.list)
|
93
87
|
end
|
94
88
|
|
95
89
|
def self.show(raw = {})
|
@@ -201,7 +195,7 @@ module GreenHat
|
|
201
195
|
def self.filter(raw)
|
202
196
|
# Print Helper
|
203
197
|
if raw == ['help']
|
204
|
-
filter_help
|
198
|
+
filter_help(raw)
|
205
199
|
return true
|
206
200
|
end
|
207
201
|
|
@@ -211,9 +205,9 @@ module GreenHat
|
|
211
205
|
files, flags, args = Args.parse(raw)
|
212
206
|
|
213
207
|
# Prepare Log List
|
214
|
-
files = ShellHelper.prepare_list(files
|
208
|
+
files = ShellHelper.prepare_list(files)
|
215
209
|
|
216
|
-
results =
|
210
|
+
results = Query.start(files, flags, args)
|
217
211
|
|
218
212
|
# Skip and Print Total if set
|
219
213
|
if flags[:total]
|
@@ -306,7 +300,6 @@ module GreenHat
|
|
306
300
|
if results.values.flatten.empty?
|
307
301
|
puts 'No results'.pastel(:red)
|
308
302
|
ShellHelper::Log.no_files_warning(files) if ShellHelper.find_things(files, flags).count.zero?
|
309
|
-
|
310
303
|
else
|
311
304
|
# This causes the key 'colorized' output to also be included
|
312
305
|
ShellHelper.show(results.to_a.compact.flatten, flags)
|
@@ -390,15 +383,12 @@ module GreenHat
|
|
390
383
|
@last
|
391
384
|
end
|
392
385
|
|
393
|
-
def self.list
|
394
|
-
Thing.all.select(&:log)
|
395
|
-
end
|
396
|
-
|
397
386
|
def self.no_files_warning(files)
|
398
387
|
puts "No matching files found for pattern #{files.to_s.pastel(:yellow)}"
|
399
388
|
puts "See #{'ls'.pastel(:blue)} for available files"
|
400
389
|
end
|
401
390
|
end
|
391
|
+
|
402
392
|
# --------
|
403
393
|
end
|
404
394
|
end
|
@@ -0,0 +1,378 @@
|
|
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
|
9
|
+
files = ShellHelper.find_things(files, flags).select(&:query?)
|
10
|
+
|
11
|
+
# Ignore Archive/Host Dividers
|
12
|
+
if flags[:combine]
|
13
|
+
results = files.reject(&:blank?).map(&:data).flatten.compact
|
14
|
+
Query.filter(results, flags, args)
|
15
|
+
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
|
+
|
21
|
+
# Include Total Count in Name
|
22
|
+
results = Query.filter(file.data, flags, args)
|
23
|
+
duration = calculate_duration(results)
|
24
|
+
|
25
|
+
title = [
|
26
|
+
file.friendly_name,
|
27
|
+
" #{results.count}".pastel(:bright_black)
|
28
|
+
|
29
|
+
]
|
30
|
+
|
31
|
+
# Append Duration
|
32
|
+
title.push(" #{duration.pastel(:cyan, :dim)}") unless duration.blank?
|
33
|
+
|
34
|
+
# Save unless empty
|
35
|
+
obj[title.join] = results unless results.count.zero?
|
36
|
+
|
37
|
+
obj
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.calculate_duration(results)
|
43
|
+
# Skip for Pluck
|
44
|
+
only_with_time = results.select { |x| x.instance_of?(Hash) && x.key?(:time) }
|
45
|
+
|
46
|
+
# If slice is used ignore
|
47
|
+
return nil if only_with_time.empty?
|
48
|
+
|
49
|
+
sorted = only_with_time.map(&:time).sort
|
50
|
+
humanize_time(sorted.first, sorted.last)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Replace TimeDifference with https://stackoverflow.com/a/4136485/1678507
|
54
|
+
def self.humanize_time(time_start, time_end, increments = 2)
|
55
|
+
miliseconds = (time_end - time_start) * 1000
|
56
|
+
|
57
|
+
list = [[1000, :ms], [60, :s], [60, :m], [24, :h]].map do |count, name|
|
58
|
+
next unless miliseconds.positive?
|
59
|
+
|
60
|
+
miliseconds, n = miliseconds.divmod(count)
|
61
|
+
|
62
|
+
"#{n.to_i}#{name}" unless n.to_i.zero?
|
63
|
+
end
|
64
|
+
|
65
|
+
list.compact.reverse[0..increments - 1].join(' ')
|
66
|
+
end
|
67
|
+
|
68
|
+
# Filter Logic
|
69
|
+
# TODO: Simplify
|
70
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
71
|
+
def self.filter(data, flags = {}, args = {})
|
72
|
+
# Experimenting with deep clone
|
73
|
+
# results = Marshal.load(Marshal.dump(data))
|
74
|
+
results = data.clone
|
75
|
+
# results = data
|
76
|
+
|
77
|
+
results.select! do |row|
|
78
|
+
args.send(flags.logic || :all?) do |arg|
|
79
|
+
filter_row_key(row, arg, flags)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Ensure presecense of a specific field
|
84
|
+
results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
|
85
|
+
|
86
|
+
# Time Zone
|
87
|
+
results = filter_modify_timezone(results, flags[:time_zone]) if flags.key?(:time_zone)
|
88
|
+
|
89
|
+
# Time Filtering
|
90
|
+
results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
|
91
|
+
|
92
|
+
# Strip Results if Slice is defined
|
93
|
+
results = filter_slice(results, flags[:slice]) if flags.key?(:slice)
|
94
|
+
|
95
|
+
# Strip Results if Except is defined
|
96
|
+
results = filter_except(results, flags[:except]) if flags.key?(:except)
|
97
|
+
|
98
|
+
# Remove Blank from either slice or except
|
99
|
+
results.reject!(&:empty?)
|
100
|
+
|
101
|
+
# Sort
|
102
|
+
results.sort_by! { |x| x.slice(*flags[:sort]).values } if flags.key?(:sort)
|
103
|
+
|
104
|
+
# JSON Formatting
|
105
|
+
results = results.map { |x| Oj.dump(x) } if flags.key?(:json)
|
106
|
+
|
107
|
+
# Show Unique Only
|
108
|
+
results = filter_uniq(results, flags[:uniq]) if flags.key?(:uniq)
|
109
|
+
|
110
|
+
# Reverse
|
111
|
+
results.reverse! if flags[:reverse]
|
112
|
+
|
113
|
+
# Count occurrences / Skip Results
|
114
|
+
return filter_stats(results, flags) if flags.key?(:stats)
|
115
|
+
|
116
|
+
# Limit before Pluck / Flattening
|
117
|
+
results = filter_limit(results, flags[:limit]) if flags.key?(:limit)
|
118
|
+
|
119
|
+
# Pluck
|
120
|
+
results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
|
121
|
+
|
122
|
+
results
|
123
|
+
end
|
124
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
125
|
+
|
126
|
+
# Limit / Ensure Exists and Valid Number
|
127
|
+
def self.filter_limit(results, limit)
|
128
|
+
return results unless limit.integer? && limit.positive?
|
129
|
+
|
130
|
+
results.shift limit
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.filter_modify_timezone(results, time_zone)
|
134
|
+
results.map do |x|
|
135
|
+
next unless x.key? :time
|
136
|
+
|
137
|
+
x = x.clone # Prevent Top Level Modification
|
138
|
+
x[:time] = x[:time].in_time_zone time_zone
|
139
|
+
|
140
|
+
x
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Filter Start and End Times
|
145
|
+
# TODO: This is a bit icky, simplify/dry
|
146
|
+
def self.filter_time(results, flags)
|
147
|
+
if flags.key?(:start)
|
148
|
+
begin
|
149
|
+
time_start = Time.parse(flags[:start])
|
150
|
+
|
151
|
+
results.select! do |x|
|
152
|
+
if x.time
|
153
|
+
time_start < x.time
|
154
|
+
else
|
155
|
+
true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
rescue StandardError
|
159
|
+
puts 'Unable to Process Start Time Filter'.pastel(:red)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
if flags.key?(:end)
|
164
|
+
begin
|
165
|
+
time_start = Time.parse(flags[:end])
|
166
|
+
|
167
|
+
results.select! do |x|
|
168
|
+
if x.time
|
169
|
+
time_start > x.time
|
170
|
+
else
|
171
|
+
true
|
172
|
+
end
|
173
|
+
end
|
174
|
+
rescue StandardError
|
175
|
+
puts 'Unable to Process End Time Filter'.pastel(:red)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
results
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.filter_except(results, except)
|
183
|
+
# Avoid Empty Results
|
184
|
+
if except.empty?
|
185
|
+
filter_empty_arg('except')
|
186
|
+
return results
|
187
|
+
end
|
188
|
+
|
189
|
+
results.map { |row| row.except(*except) }
|
190
|
+
end
|
191
|
+
|
192
|
+
def self.filter_exists(results, exists)
|
193
|
+
# Avoid Empty Results
|
194
|
+
if exists.empty?
|
195
|
+
filter_empty_arg('exists')
|
196
|
+
return results
|
197
|
+
end
|
198
|
+
|
199
|
+
results.select { |row| (exists - row.keys).empty? }
|
200
|
+
end
|
201
|
+
|
202
|
+
def self.filter_slice(results, slice)
|
203
|
+
# Avoid Empty Results
|
204
|
+
if slice.empty?
|
205
|
+
filter_empty_arg('slice')
|
206
|
+
return results
|
207
|
+
end
|
208
|
+
|
209
|
+
results.compact.map { |row| row.slice(*slice) }
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.filter_pluck(results, pluck)
|
213
|
+
# Avoid Empty Results
|
214
|
+
if pluck.empty?
|
215
|
+
filter_empty_arg('pluck')
|
216
|
+
return results
|
217
|
+
end
|
218
|
+
|
219
|
+
results.map { |x| x.slice(*pluck).values }.flatten
|
220
|
+
end
|
221
|
+
|
222
|
+
def self.filter_uniq(results, unique)
|
223
|
+
# Avoid Empty Results
|
224
|
+
if unique.empty?
|
225
|
+
filter_empty_arg('uniq')
|
226
|
+
return results
|
227
|
+
end
|
228
|
+
|
229
|
+
unique.map do |field|
|
230
|
+
results.uniq { |x| x[field] }
|
231
|
+
end.inject(:&)
|
232
|
+
end
|
233
|
+
|
234
|
+
def self.filter_stats(results, flags)
|
235
|
+
stats = flags[:stats]
|
236
|
+
|
237
|
+
# Avoid Empty Results
|
238
|
+
if stats.empty?
|
239
|
+
filter_empty_arg('stats')
|
240
|
+
return results
|
241
|
+
end
|
242
|
+
|
243
|
+
# Loop through Stats, Separate Hash/Tables
|
244
|
+
stats.map do |field|
|
245
|
+
occurrences = filter_count_occurrences(results, field, flags)
|
246
|
+
|
247
|
+
# Use Truncate For Long Keys
|
248
|
+
occurrences.transform_keys! { |key| key.to_s[0..flags[:truncate]] } if flags[:truncate]
|
249
|
+
|
250
|
+
# Total Occurences
|
251
|
+
total = occurrences.values.sum
|
252
|
+
|
253
|
+
# Percs
|
254
|
+
occurrences.transform_values! do |count|
|
255
|
+
[
|
256
|
+
count,
|
257
|
+
" #{percent(count, total)}%".pastel(:bright_black)
|
258
|
+
]
|
259
|
+
end
|
260
|
+
|
261
|
+
# Sort by total occurances / New Variable for Total
|
262
|
+
output = occurrences.sort_by(&:last).to_h.transform_values!(&:join).to_a
|
263
|
+
|
264
|
+
# Append Header / Total with field name
|
265
|
+
output.unshift([field.to_s.pastel(:bright_black), total])
|
266
|
+
|
267
|
+
# Format
|
268
|
+
output.to_h
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Percent Helper
|
273
|
+
def self.percent(value, total)
|
274
|
+
((value / total.to_f) * 100).round
|
275
|
+
end
|
276
|
+
|
277
|
+
# Helper to Count occurrences
|
278
|
+
def self.filter_count_occurrences(results, field, flags = {})
|
279
|
+
results.each_with_object(Hash.new(0)) do |entry, counts|
|
280
|
+
if entry.key? field
|
281
|
+
# Rounding in pagination breaks stats
|
282
|
+
key = if flags.key?(:round) && entry[field].numeric?
|
283
|
+
entry[field].to_f.round(flags.round)
|
284
|
+
else
|
285
|
+
entry[field]
|
286
|
+
end
|
287
|
+
|
288
|
+
counts[key] += 1
|
289
|
+
else
|
290
|
+
counts['None'.pastel(:bright_black)] += 1
|
291
|
+
end
|
292
|
+
|
293
|
+
counts
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def self.filter_empty_arg(arg)
|
298
|
+
puts [
|
299
|
+
'Ignoring'.pastel(:bright_yellow),
|
300
|
+
"--#{arg}".pastel(:cyan),
|
301
|
+
'it requires an argument'.pastel(:red)
|
302
|
+
].join(' ')
|
303
|
+
end
|
304
|
+
|
305
|
+
# Break out filter row logic into separate method
|
306
|
+
def self.filter_row_key(row, arg, flags)
|
307
|
+
return false if row.nil? # Nothing to filter if row empty
|
308
|
+
|
309
|
+
# Ignore Other Logic if Field isn't even included / Full Text Searching
|
310
|
+
return false unless row.key?(arg[:field]) || arg[:field] == :text
|
311
|
+
|
312
|
+
# Sensitivity Check / Check for Match / Full Text Searching
|
313
|
+
search_data = arg[:field] == :text ? row : row[arg.field]
|
314
|
+
match = filter_row_entry(search_data.to_s, arg, flags)
|
315
|
+
|
316
|
+
# Pivot of off include vs exclude
|
317
|
+
if arg.bang
|
318
|
+
!match
|
319
|
+
else
|
320
|
+
match
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Field Partial / Case / Exact search
|
325
|
+
def self.filter_row_entry(entry, arg, flags)
|
326
|
+
# Exact Matching / Unless doing full text search
|
327
|
+
return entry == arg.value.to_s if flags.key?(:exact) && arg.field != :text
|
328
|
+
|
329
|
+
# Cast to String/Integer Helper
|
330
|
+
entry, value = filter_entry_cast(entry, arg, flags)
|
331
|
+
|
332
|
+
entry.send(arg.logic, value)
|
333
|
+
end
|
334
|
+
|
335
|
+
# Handle casting to strings or integers
|
336
|
+
def self.filter_entry_cast(entry, arg, flags)
|
337
|
+
# Cast to String
|
338
|
+
value = arg.value.to_s
|
339
|
+
|
340
|
+
# No Logic on Empty Entries
|
341
|
+
return [entry, value] if entry.empty?
|
342
|
+
|
343
|
+
case arg.logic
|
344
|
+
when :include?
|
345
|
+
|
346
|
+
# Exact Case argument
|
347
|
+
unless flags.key?(:case)
|
348
|
+
entry = entry.downcase
|
349
|
+
value = value.downcase
|
350
|
+
end
|
351
|
+
when :>=, :<=
|
352
|
+
entry = entry.to_i if entry.numeric?
|
353
|
+
value = value.to_i if value&.numeric?
|
354
|
+
end
|
355
|
+
|
356
|
+
[entry, value]
|
357
|
+
end
|
358
|
+
|
359
|
+
def self.filter_entry_logic(entry, arg)
|
360
|
+
entry.send(arg.logic, arg.value)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# Internal Query Helper
|
365
|
+
# query = 'gitlab-rails/application_json.log --message!="Cannot obtain an exclusive lease" --severity=error'
|
366
|
+
# Query.filter_internal(query)
|
367
|
+
def self.filter_internal(search = '')
|
368
|
+
files, flags, args = Args.parse(Shellwords.split(search))
|
369
|
+
flags[:combine] = true
|
370
|
+
|
371
|
+
# Default to everything
|
372
|
+
files = Thing.all.map(&:name) if files.empty?
|
373
|
+
|
374
|
+
Query.start(files, flags, args)
|
375
|
+
end
|
376
|
+
|
377
|
+
# rubocop:enable Metrics/ModuleLength
|
378
|
+
end
|