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.
@@ -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
- # TODO: Fix Icky Double Nesting
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(args = {})
80
- if args.empty?
81
- ShellHelper::Filter.help
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, ShellHelper::Log.list)
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, ShellHelper::Log.list, flags)
208
+ files = ShellHelper.prepare_list(files)
215
209
 
216
- results = ShellHelper.filter_start(files, flags, args)
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
@@ -234,6 +234,8 @@ module GreenHat
234
234
  end
235
235
 
236
236
  def selinux
237
+ return nil if selinux_status.data.nil?
238
+
237
239
  status = selinux_status.data['SELinux status']
238
240
  status_color = status == 'enabled' ? :green : :red
239
241