greenhat 0.3.1 → 0.3.5
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 +41 -0
- data/lib/greenhat/accessors/gitlab.rb +26 -0
- data/lib/greenhat/accessors/memory.rb +1 -1
- data/lib/greenhat/archive.rb +4 -0
- data/lib/greenhat/cli.rb +21 -6
- data/lib/greenhat/shell/args.rb +2 -2
- data/lib/greenhat/shell/faststats.rb +27 -0
- data/lib/greenhat/shell/field_helper.rb +75 -0
- data/lib/greenhat/shell/filter_help.rb +224 -4
- data/lib/greenhat/shell/log.rb +52 -2
- data/lib/greenhat/shell/markdown.rb +344 -0
- data/lib/greenhat/shell/report.rb +67 -18
- data/lib/greenhat/shell/shell_helper.rb +88 -18
- data/lib/greenhat/thing/file_types.rb +98 -7
- data/lib/greenhat/thing/formatters/kube_json.rb +36 -0
- data/lib/greenhat/thing/formatters/kube_nginx.rb +48 -0
- data/lib/greenhat/thing/formatters/kube_webservice.rb +51 -0
- data/lib/greenhat/thing/formatters/nginx.rb +2 -2
- data/lib/greenhat/thing/formatters/registry.rb +47 -0
- data/lib/greenhat/thing.rb +22 -0
- data/lib/greenhat/version.rb +1 -1
- data/lib/greenhat.rb +1 -0
- metadata +22 -2
@@ -0,0 +1,344 @@
|
|
1
|
+
module GreenHat
|
2
|
+
# Root Level Shell / Report Helper
|
3
|
+
module Shell
|
4
|
+
def self.markdown_report(raw)
|
5
|
+
_files, flags, _args = Args.parse(raw)
|
6
|
+
|
7
|
+
archives = if flags.archive
|
8
|
+
Archive.all.select do |archive|
|
9
|
+
flags.archive.any? { |x| archive.name.include? x.to_s }
|
10
|
+
end
|
11
|
+
else
|
12
|
+
Archive.all
|
13
|
+
end
|
14
|
+
|
15
|
+
ShellHelper.show(archives.map(&:report_markdown).map(&:show).flatten, flags)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module GreenHat
|
21
|
+
# Report Generator Helper
|
22
|
+
# rubocop:disable Metrics/ClassLength
|
23
|
+
class ReportMarkdown
|
24
|
+
include ActionView::Helpers::NumberHelper
|
25
|
+
|
26
|
+
attr_accessor :archive, :host, :os_release, :selinux_status, :cpu, :uname,
|
27
|
+
:timedatectl, :uptime, :meminfo, :gitlab_manifest, :gitlab_status,
|
28
|
+
:production_log, :api_log, :application_log, :sidekiq_log,
|
29
|
+
:exceptions_log, :gitaly_log, :free_m, :disk_free
|
30
|
+
|
31
|
+
# Find Needed Files for Report
|
32
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
33
|
+
def initialize(archive)
|
34
|
+
self.archive = archive
|
35
|
+
self.host = archive.things.find { |x| x.name == 'hostname' }
|
36
|
+
self.os_release = archive.things.find { |x| x.name == 'etc/os-release' }
|
37
|
+
self.selinux_status = archive.things.find { |x| x.name == 'sestatus' }
|
38
|
+
self.cpu = archive.things.find { |x| x.name == 'lscpu' }
|
39
|
+
self.uname = archive.things.find { |x| x.name == 'uname' }
|
40
|
+
self.timedatectl = archive.things.find { |x| x.name == 'timedatectl' }
|
41
|
+
self.uptime = archive.things.find { |x| x.name == 'uptime' }
|
42
|
+
self.meminfo = archive.things.find { |x| x.name == 'meminfo' }
|
43
|
+
self.free_m = archive.things.find { |x| x.name == 'free_m' }
|
44
|
+
self.gitlab_manifest = archive.things.find { |x| x.name == 'gitlab/version-manifest.json' }
|
45
|
+
self.gitlab_status = archive.things.find { |x| x.name == 'gitlab_status' }
|
46
|
+
self.production_log = archive.things.find { |x| x.name == 'gitlab-rails/production_json.log' }
|
47
|
+
self.api_log = archive.things.find { |x| x.name == 'gitlab-rails/api_json.log' }
|
48
|
+
self.application_log = archive.things.find { |x| x.name == 'gitlab-rails/application_json.log' }
|
49
|
+
self.exceptions_log = archive.things.find { |x| x.name == 'gitlab-rails/exceptions_json.log' }
|
50
|
+
self.gitaly_log = archive.things.find { |x| x.name == 'gitaly/current' }
|
51
|
+
self.sidekiq_log = archive.things.find { |x| x.name == 'sidekiq/current' }
|
52
|
+
self.disk_free = archive.things.find { |x| x.name == 'df_h' }
|
53
|
+
end
|
54
|
+
|
55
|
+
def show
|
56
|
+
output = [
|
57
|
+
archive.friendly_name,
|
58
|
+
''
|
59
|
+
]
|
60
|
+
|
61
|
+
# OS
|
62
|
+
output << "**OS**\n"
|
63
|
+
|
64
|
+
output << collect_host
|
65
|
+
output << ''
|
66
|
+
|
67
|
+
# Memory
|
68
|
+
if meminfo || free_m
|
69
|
+
output << "**Memory**\n"
|
70
|
+
# output << memory_perc if meminfo
|
71
|
+
output << memory_free if free_m
|
72
|
+
output << ''
|
73
|
+
end
|
74
|
+
|
75
|
+
# Disk
|
76
|
+
if disk_free
|
77
|
+
output << disks
|
78
|
+
output << ''
|
79
|
+
end
|
80
|
+
|
81
|
+
# Gitlab
|
82
|
+
output << "**GitLab**\n" if gitlab_manifest || gitlab_status
|
83
|
+
output << gitlab_version if gitlab_manifest
|
84
|
+
output << gitlab_services if gitlab_status
|
85
|
+
|
86
|
+
output << ''
|
87
|
+
|
88
|
+
output << "**Errors**\n" if production_log || api_log || application_log || sidekiq_log
|
89
|
+
output << collect_errors
|
90
|
+
|
91
|
+
# Final Space / Return
|
92
|
+
output << ''
|
93
|
+
output
|
94
|
+
end
|
95
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
96
|
+
|
97
|
+
def collect_host
|
98
|
+
output = []
|
99
|
+
output << hostname if host
|
100
|
+
output << distro if os_release
|
101
|
+
output << selinux if selinux_status
|
102
|
+
# output << arch if cpu
|
103
|
+
output << kernel if uname
|
104
|
+
output << sys_time if timedatectl
|
105
|
+
output << sys_uptime if uptime
|
106
|
+
output << load_average if uptime && cpu
|
107
|
+
|
108
|
+
groups = output.each_slice((output.size / 2.to_f).round).to_a
|
109
|
+
|
110
|
+
table = TTY::Table.new do |t|
|
111
|
+
loop do
|
112
|
+
break if groups.all?(&:empty?)
|
113
|
+
|
114
|
+
t << groups.map(&:shift)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
"```\n#{table.render(:basic, padding: [0, 2, 0, 0])}\n```"
|
119
|
+
end
|
120
|
+
|
121
|
+
def collect_errors
|
122
|
+
output = []
|
123
|
+
output << production_errors if production_log
|
124
|
+
output << application_errors if application_log
|
125
|
+
output << sidekiq_errors if sidekiq_log
|
126
|
+
output << api_errors if api_log
|
127
|
+
output << exception_errors if exceptions_log
|
128
|
+
output << gitaly_errors if gitaly_log
|
129
|
+
|
130
|
+
# Keep Alphabetical Sort / Allow for only one
|
131
|
+
slice_size = (output.size / 3.to_f).round
|
132
|
+
slice_size = 1 unless slice_size.positive?
|
133
|
+
|
134
|
+
groups = output.each_slice(slice_size).to_a
|
135
|
+
|
136
|
+
table = TTY::Table.new do |t|
|
137
|
+
loop do
|
138
|
+
break if groups.all?(&:empty?)
|
139
|
+
|
140
|
+
t << groups.map(&:shift)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
"```\n#{table.render(:basic, padding: [0, 2, 0, 0])}\n```"
|
145
|
+
end
|
146
|
+
|
147
|
+
def exception_errors
|
148
|
+
count = exceptions_log.data.count
|
149
|
+
|
150
|
+
"Exception: #{count}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def gitaly_errors
|
154
|
+
count = gitaly_log.data.count { |x| x.level == 'error' }
|
155
|
+
|
156
|
+
"Gitaly: #{count}"
|
157
|
+
end
|
158
|
+
|
159
|
+
def production_errors
|
160
|
+
count = production_log.data.count { |x| x.status == 500 }
|
161
|
+
|
162
|
+
"Production: #{count}"
|
163
|
+
end
|
164
|
+
|
165
|
+
def api_errors
|
166
|
+
count = api_log.data.count { |x| x.status == 500 }
|
167
|
+
|
168
|
+
"API: #{count}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def application_errors
|
172
|
+
results = ShellHelper.filter_internal([
|
173
|
+
'gitlab-rails/application_json.log',
|
174
|
+
'--message!="Cannot obtain an exclusive lease"',
|
175
|
+
'--severity=error'
|
176
|
+
].join(' '))
|
177
|
+
|
178
|
+
"Application: #{results.count}"
|
179
|
+
end
|
180
|
+
|
181
|
+
def sidekiq_errors
|
182
|
+
count = sidekiq_log.data.count { |x| x&.severity == 'ERROR' }
|
183
|
+
|
184
|
+
"Sidekiq: #{count}"
|
185
|
+
end
|
186
|
+
|
187
|
+
def gitlab_services
|
188
|
+
[
|
189
|
+
"**Services**\n",
|
190
|
+
"\n",
|
191
|
+
GreenHat::GitLab.services_markdown(archive)
|
192
|
+
].join
|
193
|
+
rescue StandardError => e
|
194
|
+
LogBot.fatal('GitLab Services', message: e.message, backtrace: e.backtrace.first)
|
195
|
+
end
|
196
|
+
|
197
|
+
def gitlab_version
|
198
|
+
"Version: #{gitlab_manifest.data.build_version}\n"
|
199
|
+
end
|
200
|
+
|
201
|
+
def hostname
|
202
|
+
"Hostname: #{host.data.first}"
|
203
|
+
end
|
204
|
+
|
205
|
+
def distro
|
206
|
+
[
|
207
|
+
"Distro: [#{os_release.data.ID}] ",
|
208
|
+
os_release.data.PRETTY_NAME
|
209
|
+
].join
|
210
|
+
end
|
211
|
+
|
212
|
+
def selinux
|
213
|
+
status = selinux_status.data['SELinux status']
|
214
|
+
|
215
|
+
[
|
216
|
+
'SeLinux: ',
|
217
|
+
status,
|
218
|
+
' (',
|
219
|
+
selinux_status.data['Current mode'],
|
220
|
+
')'
|
221
|
+
].join
|
222
|
+
end
|
223
|
+
|
224
|
+
def arch
|
225
|
+
[
|
226
|
+
'Arch: ',
|
227
|
+
cpu.data.Architecture
|
228
|
+
].join
|
229
|
+
end
|
230
|
+
|
231
|
+
def kernel
|
232
|
+
# TODO: Better way to consistently get uname info?
|
233
|
+
value, build = uname.data.first.split[2].split('-')
|
234
|
+
[
|
235
|
+
'Kernel: ',
|
236
|
+
value,
|
237
|
+
" (#{build})"
|
238
|
+
].join
|
239
|
+
end
|
240
|
+
|
241
|
+
# Helper for finding if NTP is enabled
|
242
|
+
def ntp_keys
|
243
|
+
[
|
244
|
+
'Network time on', 'NTP enabled', 'NTP service', 'System clock synchronized'
|
245
|
+
]
|
246
|
+
end
|
247
|
+
|
248
|
+
def sys_time
|
249
|
+
# Ignore if Empty
|
250
|
+
return false if timedatectl.data.nil?
|
251
|
+
|
252
|
+
ntp_statuses = timedatectl.data.slice(*ntp_keys).values.compact
|
253
|
+
|
254
|
+
ntp_status = ntp_statuses.first
|
255
|
+
|
256
|
+
# Fall Back
|
257
|
+
ntp_status ||= 'unknown'
|
258
|
+
|
259
|
+
[
|
260
|
+
'Sys Time: ',
|
261
|
+
timedatectl.data['Local time'],
|
262
|
+
"(ntp: #{ntp_status})"
|
263
|
+
].join
|
264
|
+
end
|
265
|
+
|
266
|
+
# Strip/Simplify Uptime
|
267
|
+
def sys_uptime
|
268
|
+
init = uptime.data.first.split(', load average').first.strip
|
269
|
+
|
270
|
+
"Uptime: #{init.split('up ', 2).last}"
|
271
|
+
end
|
272
|
+
|
273
|
+
def load_average
|
274
|
+
cpu_count = cpu.data['CPU(s)'].to_i
|
275
|
+
intervals = uptime.data.first.split('load average: ', 2).last.split(', ').map(&:to_f)
|
276
|
+
|
277
|
+
# Generate Colorized Text for Output
|
278
|
+
intervals_text = intervals.map do |interval|
|
279
|
+
value = percent(interval, cpu_count)
|
280
|
+
|
281
|
+
"#{interval} (#{value}%)"
|
282
|
+
end
|
283
|
+
|
284
|
+
[
|
285
|
+
'LoadAvg: ',
|
286
|
+
"[CPU #{cpu_count}] ",
|
287
|
+
intervals_text.join(', ')
|
288
|
+
].join
|
289
|
+
end
|
290
|
+
|
291
|
+
def memory_free
|
292
|
+
free = free_m.data.find { |x| x.kind == 'Mem' }
|
293
|
+
|
294
|
+
return unless free
|
295
|
+
|
296
|
+
pad = 6
|
297
|
+
list = [
|
298
|
+
"#{title('Total', pad)} #{number_to_human_size(free.total.to_i * 1024**2)}",
|
299
|
+
"#{title('Used', pad)} #{number_to_human_size(free.used.to_i * 1024**2)}",
|
300
|
+
"#{title('Free', pad)} #{number_to_human_size(free.free.to_i * 1024**2)}",
|
301
|
+
"#{title('Avail', pad)} #{number_to_human_size(free.available.to_i * 1024**2)}"
|
302
|
+
]
|
303
|
+
|
304
|
+
# Keep Alphabetical Sort
|
305
|
+
groups = list.each_slice((list.size / 2.to_f).round).to_a
|
306
|
+
|
307
|
+
table = TTY::Table.new do |t|
|
308
|
+
loop do
|
309
|
+
break if groups.all?(&:empty?)
|
310
|
+
|
311
|
+
t << groups.map(&:shift)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
"```\n#{table.render(:basic, padding: [0, 2, 0, 0])}\n```"
|
316
|
+
end
|
317
|
+
|
318
|
+
def disks
|
319
|
+
# GreenHat::Disk.df({archive: []})
|
320
|
+
file = GreenHat::Disk.df({ archive: [archive.name] })
|
321
|
+
|
322
|
+
disk_list = GreenHat::Disk.markdown_format(file.first, false, 3)
|
323
|
+
|
324
|
+
# Preapre / Indent List
|
325
|
+
[
|
326
|
+
'**Disks**',
|
327
|
+
"\n\n```\n#{disk_list.join("\n")}\n```"
|
328
|
+
].join
|
329
|
+
end
|
330
|
+
|
331
|
+
# ----------------------------
|
332
|
+
# Helpers
|
333
|
+
# ----------------------------
|
334
|
+
def percent(value, total)
|
335
|
+
((value / total.to_f) * 100).round
|
336
|
+
end
|
337
|
+
|
338
|
+
# Helper to Make Cyan Titles
|
339
|
+
def title(name, ljust = 16)
|
340
|
+
"#{name}:".ljust(ljust)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
# rubocop:enable Metrics/ClassLength
|
344
|
+
end
|
@@ -25,7 +25,8 @@ module GreenHat
|
|
25
25
|
|
26
26
|
attr_accessor :archive, :host, :os_release, :selinux_status, :cpu, :uname,
|
27
27
|
:timedatectl, :uptime, :meminfo, :gitlab_manifest, :gitlab_status,
|
28
|
-
:production_log, :api_log, :application_log, :sidekiq_log,
|
28
|
+
:production_log, :api_log, :application_log, :sidekiq_log,
|
29
|
+
:exceptions_log, :gitaly_log, :free_m, :disk_free
|
29
30
|
|
30
31
|
# Find Needed Files for Report
|
31
32
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
@@ -45,6 +46,8 @@ module GreenHat
|
|
45
46
|
self.production_log = archive.things.find { |x| x.name == 'gitlab-rails/production_json.log' }
|
46
47
|
self.api_log = archive.things.find { |x| x.name == 'gitlab-rails/api_json.log' }
|
47
48
|
self.application_log = archive.things.find { |x| x.name == 'gitlab-rails/application_json.log' }
|
49
|
+
self.exceptions_log = archive.things.find { |x| x.name == 'gitlab-rails/exceptions_json.log' }
|
50
|
+
self.gitaly_log = archive.things.find { |x| x.name == 'gitaly/current' }
|
48
51
|
self.sidekiq_log = archive.things.find { |x| x.name == 'sidekiq/current' }
|
49
52
|
self.disk_free = archive.things.find { |x| x.name == 'df_h' }
|
50
53
|
end
|
@@ -86,9 +89,11 @@ module GreenHat
|
|
86
89
|
output << gitlab_services if gitlab_status
|
87
90
|
output << title('Errors') if production_log || api_log || application_log || sidekiq_log
|
88
91
|
output << production_errors if production_log
|
89
|
-
output << api_errors if api_log
|
90
92
|
output << application_errors if application_log
|
91
93
|
output << sidekiq_errors if sidekiq_log
|
94
|
+
output << api_errors if api_log
|
95
|
+
output << exception_errors if exceptions_log
|
96
|
+
output << gitaly_errors if gitaly_log
|
92
97
|
|
93
98
|
# Final Space / Return
|
94
99
|
output << ''
|
@@ -96,6 +101,26 @@ module GreenHat
|
|
96
101
|
end
|
97
102
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
98
103
|
|
104
|
+
def exception_errors
|
105
|
+
count = exceptions_log.data.count
|
106
|
+
color = count.zero? ? :green : :red
|
107
|
+
|
108
|
+
[
|
109
|
+
title(' Exception', :bright_red, 18),
|
110
|
+
count.to_s.pastel(color)
|
111
|
+
].join
|
112
|
+
end
|
113
|
+
|
114
|
+
def gitaly_errors
|
115
|
+
count = gitaly_log.data.count { |x| x.level == 'error' }
|
116
|
+
color = count.zero? ? :green : :red
|
117
|
+
|
118
|
+
[
|
119
|
+
title(' Gitaly', :bright_red, 18),
|
120
|
+
count.to_s.pastel(color)
|
121
|
+
].join
|
122
|
+
end
|
123
|
+
|
99
124
|
def production_errors
|
100
125
|
count = production_log.data.count { |x| x.status == 500 }
|
101
126
|
color = count.zero? ? :green : :red
|
@@ -117,7 +142,13 @@ module GreenHat
|
|
117
142
|
end
|
118
143
|
|
119
144
|
def application_errors
|
120
|
-
|
145
|
+
results = ShellHelper.filter_internal([
|
146
|
+
'gitlab-rails/application_json.log',
|
147
|
+
'--message!="Cannot obtain an exclusive lease"',
|
148
|
+
'--severity=error'
|
149
|
+
].join(' '))
|
150
|
+
|
151
|
+
count = results.count { |x| x&.severity == 'ERROR' }
|
121
152
|
color = count.zero? ? :green : :red
|
122
153
|
|
123
154
|
[
|
@@ -275,6 +306,7 @@ module GreenHat
|
|
275
306
|
].join
|
276
307
|
end
|
277
308
|
|
309
|
+
# rubocop:disable Metrics/MethodLength
|
278
310
|
def memory_free
|
279
311
|
free = free_m.data.find { |x| x.kind == 'Mem' }
|
280
312
|
|
@@ -282,22 +314,39 @@ module GreenHat
|
|
282
314
|
|
283
315
|
formatted_mem = free_m.data.map { |x| GreenHat::Memory.memory_row x }
|
284
316
|
|
285
|
-
[
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
317
|
+
output = []
|
318
|
+
unless free.total.blank?
|
319
|
+
output << title('Total', :cyan, 14)
|
320
|
+
output << number_to_human_size(free.total.to_i * 1024**2)
|
321
|
+
output << "\n"
|
322
|
+
end
|
323
|
+
|
324
|
+
unless free.total.blank?
|
325
|
+
output << title('Used', :yellow, 14)
|
326
|
+
output << number_to_human_size(free.used.to_i * 1024**2)
|
327
|
+
output << "\n"
|
328
|
+
end
|
329
|
+
|
330
|
+
unless free.total.blank?
|
331
|
+
output << title('Free', :blue, 14)
|
332
|
+
output << number_to_human_size(free.free.to_i * 1024**2)
|
333
|
+
output << "\n"
|
334
|
+
end
|
335
|
+
|
336
|
+
unless free.total.blank?
|
337
|
+
output << title('Available', :green, 14)
|
338
|
+
output << number_to_human_size(free.available.to_i * 1024**2)
|
339
|
+
output << "\n"
|
340
|
+
end
|
341
|
+
|
342
|
+
output << "\n"
|
343
|
+
output << formatted_mem.map { |x| x.prepend ' ' * 2 }.join("\n")
|
344
|
+
|
345
|
+
output.join
|
346
|
+
rescue StandardError => e
|
347
|
+
LogBot.fatal('Memory', message: e.message, backtrace: e.backtrace.first)
|
300
348
|
end
|
349
|
+
# rubocop:enable Metrics/MethodLength
|
301
350
|
|
302
351
|
def disks
|
303
352
|
# GreenHat::Disk.df({archive: []})
|
@@ -120,10 +120,12 @@ module GreenHat
|
|
120
120
|
entry = entry.map { |k, v| [k, format_table_entry(flags, v, k)] }.to_h
|
121
121
|
# Pre-format Entry
|
122
122
|
|
123
|
+
table_style = flags[:table_style]&.to_sym || :unicode
|
124
|
+
|
123
125
|
table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
|
124
126
|
|
125
127
|
LogBot.debug('Rendering Entries') if ENV['DEBUG']
|
126
|
-
table.render(
|
128
|
+
table.render(table_style, padding: [0, 1, 0, 1], multiline: true) do |renderer|
|
127
129
|
renderer.border.style = :cyan
|
128
130
|
end
|
129
131
|
|
@@ -148,8 +150,18 @@ module GreenHat
|
|
148
150
|
format_table_entry(flags, val)
|
149
151
|
end
|
150
152
|
|
153
|
+
# Internal Query Helper
|
154
|
+
# query = 'gitlab-rails/application_json.log --message!="Cannot obtain an exclusive lease" --severity=error'
|
155
|
+
# ShellHelper.filter_internal(query)
|
156
|
+
def self.filter_internal(search = '')
|
157
|
+
files, flags, args = Args.parse(Shellwords.split(search))
|
158
|
+
flags[:combine] = true
|
159
|
+
|
160
|
+
ShellHelper.filter_start(files, flags, args)
|
161
|
+
end
|
162
|
+
|
151
163
|
# Main Entry Point for Filtering
|
152
|
-
def self.filter_start(files, flags, args)
|
164
|
+
def self.filter_start(files, flags = {}, args = {})
|
153
165
|
# Convert to Things
|
154
166
|
logs = ShellHelper.find_things(files, flags).select(&:processed?)
|
155
167
|
|
@@ -182,9 +194,12 @@ module GreenHat
|
|
182
194
|
# TODO: Simplify
|
183
195
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
184
196
|
def self.filter(data, flags = {}, args = {})
|
185
|
-
results = data.clone.flatten.compact
|
197
|
+
# results = data.clone.flatten.compact
|
198
|
+
|
199
|
+
# Experimenting with deep clone
|
200
|
+
results = Marshal.load(Marshal.dump(data))
|
186
201
|
results.select! do |row|
|
187
|
-
args.send(flags.logic) do |arg|
|
202
|
+
args.send(flags.logic || :all?) do |arg|
|
188
203
|
filter_row_key(row, arg, flags)
|
189
204
|
end
|
190
205
|
end
|
@@ -192,6 +207,9 @@ module GreenHat
|
|
192
207
|
# Ensure presecense of a specific field
|
193
208
|
results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
|
194
209
|
|
210
|
+
# Time Zone
|
211
|
+
results = filter_modify_timezone(results, flags[:time_zone]) if flags.key?(:time_zone)
|
212
|
+
|
195
213
|
# Time Filtering
|
196
214
|
results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
|
197
215
|
|
@@ -217,24 +235,34 @@ module GreenHat
|
|
217
235
|
results.reverse! if flags[:reverse]
|
218
236
|
|
219
237
|
# Count occurrences / Skip Results
|
220
|
-
return filter_stats(results, flags
|
238
|
+
return filter_stats(results, flags) if flags.key?(:stats)
|
239
|
+
|
240
|
+
# Limit before Pluck / Flattening
|
241
|
+
results = filter_limit(results, flags[:limit]) if flags.key?(:limit)
|
221
242
|
|
222
243
|
# Pluck
|
223
244
|
results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
|
224
245
|
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
# results[0..flags[:limit].map(&:to_s).join.to_i - 1]
|
246
|
+
results
|
247
|
+
end
|
248
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
229
249
|
|
230
|
-
|
231
|
-
|
250
|
+
# Limit / Ensure Exists and Valid Number
|
251
|
+
def self.filter_limit(results, limit)
|
252
|
+
return results unless limit.integer? && limit.positive?
|
232
253
|
|
233
|
-
|
234
|
-
|
254
|
+
results.shift limit
|
255
|
+
end
|
256
|
+
|
257
|
+
def self.filter_modify_timezone(results, time_zone)
|
258
|
+
results.each do |x|
|
259
|
+
next unless x.key? :time
|
260
|
+
|
261
|
+
x[:time] = x[:time].in_time_zone time_zone
|
235
262
|
end
|
263
|
+
|
264
|
+
results
|
236
265
|
end
|
237
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
238
266
|
|
239
267
|
# Filter Start and End Times
|
240
268
|
# rubocop:disable Metrics/MethodLength
|
@@ -339,7 +367,9 @@ module GreenHat
|
|
339
367
|
end.inject(:&)
|
340
368
|
end
|
341
369
|
|
342
|
-
def self.filter_stats(results,
|
370
|
+
def self.filter_stats(results, flags)
|
371
|
+
stats = flags[:stats]
|
372
|
+
|
343
373
|
# Avoid Empty Results
|
344
374
|
if stats.empty?
|
345
375
|
filter_empty_arg('stats')
|
@@ -348,7 +378,7 @@ module GreenHat
|
|
348
378
|
|
349
379
|
# Loop through Stats, Separate Hash/Tables
|
350
380
|
stats.map do |field|
|
351
|
-
occurrences = filter_count_occurrences(results, field)
|
381
|
+
occurrences = filter_count_occurrences(results, field, flags)
|
352
382
|
|
353
383
|
# Total Occurences
|
354
384
|
total = occurrences.values.sum
|
@@ -367,6 +397,13 @@ module GreenHat
|
|
367
397
|
# Append Header / Total with field name
|
368
398
|
output.unshift([field.to_s.pastel(:bright_black), total])
|
369
399
|
|
400
|
+
# Use Truncate For Long Keys
|
401
|
+
if flags[:truncate]
|
402
|
+
output.map! do |key, value|
|
403
|
+
[key.to_s[0..flags[:truncate]], value]
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
370
407
|
# Format
|
371
408
|
output.to_h
|
372
409
|
end
|
@@ -378,10 +415,17 @@ module GreenHat
|
|
378
415
|
end
|
379
416
|
|
380
417
|
# Helper to Count occurrences
|
381
|
-
def self.filter_count_occurrences(results, field)
|
418
|
+
def self.filter_count_occurrences(results, field, flags = {})
|
382
419
|
results.each_with_object(Hash.new(0)) do |entry, counts|
|
383
420
|
if entry.key? field
|
384
|
-
|
421
|
+
# Rounding in pagination breaks stats
|
422
|
+
key = if flags.key?(:round) && entry[field].numeric?
|
423
|
+
entry[field].to_f.round(flags.round)
|
424
|
+
else
|
425
|
+
entry[field]
|
426
|
+
end
|
427
|
+
|
428
|
+
counts[key] += 1
|
385
429
|
else
|
386
430
|
counts['None'.pastel(:bright_black)] += 1
|
387
431
|
end
|
@@ -440,6 +484,32 @@ module GreenHat
|
|
440
484
|
end
|
441
485
|
end
|
442
486
|
|
487
|
+
# Total Count Helper
|
488
|
+
def self.fields_print(results)
|
489
|
+
results.each do |k, v|
|
490
|
+
puts k
|
491
|
+
puts field_table(v.map(&:keys).flatten.uniq.sort)
|
492
|
+
puts
|
493
|
+
end
|
494
|
+
end
|
495
|
+
|
496
|
+
def self.field_table(list, columns = 4)
|
497
|
+
return nil if list.size.zero?
|
498
|
+
|
499
|
+
# Keep Alphabetical Sort
|
500
|
+
groups = list.each_slice((list.size / columns.to_f).round).to_a
|
501
|
+
|
502
|
+
table = TTY::Table.new do |t|
|
503
|
+
loop do
|
504
|
+
break if groups.all?(&:empty?)
|
505
|
+
|
506
|
+
t << groups.map(&:shift)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
table.render(:unicode, padding: [0, 1, 0, 1])
|
511
|
+
end
|
512
|
+
|
443
513
|
# Unified Files Interface
|
444
514
|
def self.files(file_list, base_list = nil, flags = {})
|
445
515
|
base_list ||= Thing.all
|