greenhat 0.1.4 → 0.3.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/README.md +2 -7
- data/lib/greenhat/accessors/disk.rb +58 -2
- data/lib/greenhat/accessors/gitlab.rb +75 -0
- data/lib/greenhat/accessors/memory.rb +10 -10
- data/lib/greenhat/accessors/process.rb +10 -1
- data/lib/greenhat/cli.rb +128 -57
- data/lib/greenhat/color.rb +27 -0
- data/lib/greenhat/logbot.rb +9 -9
- data/lib/greenhat/settings.rb +51 -3
- data/lib/greenhat/shell/args.rb +146 -0
- data/lib/greenhat/shell/cat.rb +25 -73
- data/lib/greenhat/shell/color_string.rb +43 -0
- data/lib/greenhat/shell/disk.rb +30 -42
- data/lib/greenhat/shell/faststats.rb +80 -61
- data/lib/greenhat/shell/filter_help.rb +143 -0
- data/lib/greenhat/shell/gitlab.rb +61 -2
- data/lib/greenhat/shell/help.rb +98 -15
- data/lib/greenhat/shell/list.rb +46 -0
- data/lib/greenhat/shell/log.rb +78 -203
- data/lib/greenhat/shell/page.rb +39 -0
- data/lib/greenhat/shell/process.rb +57 -2
- data/lib/greenhat/shell/report.rb +70 -60
- data/lib/greenhat/shell/shell_helper.rb +601 -0
- data/lib/greenhat/shell.rb +27 -13
- data/lib/greenhat/thing/file_types.rb +76 -8
- data/lib/greenhat/thing/formatters/json_shellwords.rb +0 -3
- data/lib/greenhat/thing/formatters/nginx.rb +44 -0
- data/lib/greenhat/thing/formatters/syslog.rb +39 -0
- data/lib/greenhat/thing/helpers.rb +4 -4
- data/lib/greenhat/thing/kind.rb +9 -2
- data/lib/greenhat/thing/spinner.rb +3 -3
- data/lib/greenhat/thing.rb +3 -3
- data/lib/greenhat/tty/columns.rb +44 -0
- data/lib/greenhat/version.rb +1 -1
- data/lib/greenhat.rb +15 -14
- metadata +30 -20
- data/lib/greenhat/shell/helper.rb +0 -514
@@ -0,0 +1,39 @@
|
|
1
|
+
module GreenHat
|
2
|
+
module ShellHelper
|
3
|
+
# Helper to organize page check / methods
|
4
|
+
module Page
|
5
|
+
# Check if paging should be skipped
|
6
|
+
# True / False / Not Set
|
7
|
+
def self.skip?(flags, data)
|
8
|
+
# Pass if Explicitly Set / Inverse for skip
|
9
|
+
return !flags[:page] if flags.key? :page
|
10
|
+
|
11
|
+
LogBot.debug('Page', count_rows(data, flags)) if ENV['DEBUG']
|
12
|
+
|
13
|
+
count_rows(data, flags)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Array/Hash and String pagination check. Don't unncessarily loop through everything
|
17
|
+
def self.count_rows(data, flags)
|
18
|
+
height = TTY::Screen.height
|
19
|
+
size = 0
|
20
|
+
|
21
|
+
data.each do |entry|
|
22
|
+
size += case entry
|
23
|
+
when Hash then entry.keys.count
|
24
|
+
when Array then entry.count
|
25
|
+
else
|
26
|
+
# Each Boxed Entry is 3 Lines
|
27
|
+
flags.key?(:row_size) ? flags[:row_size] : 1
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
break if size > height
|
32
|
+
end
|
33
|
+
|
34
|
+
size < height
|
35
|
+
end
|
36
|
+
end
|
37
|
+
# ----
|
38
|
+
end
|
39
|
+
end
|
@@ -4,8 +4,63 @@ module GreenHat
|
|
4
4
|
# Logs
|
5
5
|
module Process
|
6
6
|
# Easy Show All
|
7
|
-
|
8
|
-
|
7
|
+
# Filter --archive
|
8
|
+
|
9
|
+
def self.ls
|
10
|
+
help
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.help
|
14
|
+
puts "\u2500".pastel(:cyan) * 22
|
15
|
+
puts "#{'Process'.pastel(:yellow)} - ps helper"
|
16
|
+
puts "\u2500".pastel(:cyan) * 22
|
17
|
+
|
18
|
+
ShellHelper.common_opts
|
19
|
+
|
20
|
+
puts 'Command Summary'.pastel(:blue)
|
21
|
+
puts ' ps'.pastel(:green)
|
22
|
+
puts ' Raw `ps`'
|
23
|
+
puts
|
24
|
+
puts ' filter'.pastel(:green)
|
25
|
+
puts " Key/Field Filtering. See #{'filter_help'.pastel(:blue)}"
|
26
|
+
puts ' Examples'
|
27
|
+
puts ' filter --sort=mem --reverse'.pastel(:green)
|
28
|
+
puts ' filter --user=gitlab'.pastel(:green)
|
29
|
+
|
30
|
+
puts
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.filter_help
|
34
|
+
ShellHelper::Filter.help
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.ps(raw = {})
|
38
|
+
# Extract Args
|
39
|
+
files_list, flags, _args = Args.parse(raw)
|
40
|
+
|
41
|
+
# Collect Files
|
42
|
+
files = ShellHelper.files(files_list, GreenHat::Ps.things, flags)
|
43
|
+
|
44
|
+
# Output
|
45
|
+
ShellHelper.file_output(files, flags)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.filter(raw = {})
|
49
|
+
# Argument Parsing
|
50
|
+
files, flags, args = Args.parse(raw)
|
51
|
+
|
52
|
+
# Prepare Log List
|
53
|
+
file_list = ShellHelper.prepare_list(files, GreenHat::Ps.things)
|
54
|
+
|
55
|
+
results = ShellHelper.filter_start(file_list, flags, args)
|
56
|
+
|
57
|
+
# Check Search Results
|
58
|
+
if results.instance_of?(Hash) && results.values.flatten.empty?
|
59
|
+
puts 'No results'.pastel(:red)
|
60
|
+
else
|
61
|
+
# This causes the key 'colorized' output to also be included
|
62
|
+
ShellHelper.show(results.to_a.compact.flatten, flags)
|
63
|
+
end
|
9
64
|
end
|
10
65
|
end
|
11
66
|
end
|
@@ -2,11 +2,17 @@ module GreenHat
|
|
2
2
|
# Root Level Shell / Report Helper
|
3
3
|
module Shell
|
4
4
|
def self.report(raw)
|
5
|
-
|
6
|
-
raw: raw.include?('--raw')
|
7
|
-
}
|
5
|
+
_files, flags, _args = Args.parse(raw)
|
8
6
|
|
9
|
-
|
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).map(&:show).flatten, flags)
|
10
16
|
end
|
11
17
|
end
|
12
18
|
end
|
@@ -19,7 +25,7 @@ module GreenHat
|
|
19
25
|
|
20
26
|
attr_accessor :archive, :host, :os_release, :selinux_status, :cpu, :uname,
|
21
27
|
:timedatectl, :uptime, :meminfo, :gitlab_manifest, :gitlab_status,
|
22
|
-
:production_log, :api_log, :application_log, :sidekiq_log, :free_m
|
28
|
+
:production_log, :api_log, :application_log, :sidekiq_log, :free_m, :disk_free
|
23
29
|
|
24
30
|
# Find Needed Files for Report
|
25
31
|
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
|
@@ -40,15 +46,16 @@ module GreenHat
|
|
40
46
|
self.api_log = archive.things.find { |x| x.name == 'gitlab-rails/api_json.log' }
|
41
47
|
self.application_log = archive.things.find { |x| x.name == 'gitlab-rails/application_json.log' }
|
42
48
|
self.sidekiq_log = archive.things.find { |x| x.name == 'sidekiq/current' }
|
49
|
+
self.disk_free = archive.things.find { |x| x.name == 'df_h' }
|
43
50
|
end
|
44
51
|
|
45
52
|
def show
|
46
53
|
output = [
|
47
|
-
archive.friendly_name.
|
54
|
+
archive.friendly_name.pastel(:blue)
|
48
55
|
]
|
49
56
|
|
50
57
|
# OS
|
51
|
-
output << 'OS'.
|
58
|
+
output << 'OS'.pastel(:bright_yellow)
|
52
59
|
output << hostname if host
|
53
60
|
output << distro if os_release
|
54
61
|
output << selinux if selinux_status
|
@@ -61,14 +68,20 @@ module GreenHat
|
|
61
68
|
|
62
69
|
# Memory
|
63
70
|
if meminfo || free_m
|
64
|
-
output << 'Memory'.
|
65
|
-
output <<
|
71
|
+
output << 'Memory'.pastel(:bright_yellow)
|
72
|
+
output << memory_perc if meminfo
|
66
73
|
output << memory_free if free_m
|
67
74
|
output << ''
|
68
75
|
end
|
69
76
|
|
77
|
+
# Disk
|
78
|
+
if disk_free
|
79
|
+
output << disks
|
80
|
+
output << ''
|
81
|
+
end
|
82
|
+
|
70
83
|
# Gitlab
|
71
|
-
output << 'GitLab'.
|
84
|
+
output << 'GitLab'.pastel(:bright_yellow) if gitlab_manifest
|
72
85
|
output << gitlab_version if gitlab_manifest
|
73
86
|
output << gitlab_services if gitlab_status
|
74
87
|
output << title('Errors') if production_log || api_log || application_log || sidekiq_log
|
@@ -88,8 +101,8 @@ module GreenHat
|
|
88
101
|
color = count.zero? ? :green : :red
|
89
102
|
|
90
103
|
[
|
91
|
-
title(' Production', :
|
92
|
-
count.to_s.
|
104
|
+
title(' Production', :bright_red, 18),
|
105
|
+
count.to_s.pastel(color)
|
93
106
|
].join
|
94
107
|
end
|
95
108
|
|
@@ -98,8 +111,8 @@ module GreenHat
|
|
98
111
|
color = count.zero? ? :green : :red
|
99
112
|
|
100
113
|
[
|
101
|
-
title(' API', :
|
102
|
-
count.to_s.
|
114
|
+
title(' API', :bright_red, 18),
|
115
|
+
count.to_s.pastel(color)
|
103
116
|
].join
|
104
117
|
end
|
105
118
|
|
@@ -108,8 +121,8 @@ module GreenHat
|
|
108
121
|
color = count.zero? ? :green : :red
|
109
122
|
|
110
123
|
[
|
111
|
-
title(' Application', :
|
112
|
-
count.to_s.
|
124
|
+
title(' Application', :bright_red, 18),
|
125
|
+
count.to_s.pastel(color)
|
113
126
|
].join
|
114
127
|
end
|
115
128
|
|
@@ -118,32 +131,16 @@ module GreenHat
|
|
118
131
|
color = count.zero? ? :green : :red
|
119
132
|
|
120
133
|
[
|
121
|
-
title(' Sidekiq', :
|
122
|
-
count.to_s.
|
134
|
+
title(' Sidekiq', :bright_red, 18),
|
135
|
+
count.to_s.pastel(color)
|
123
136
|
].join
|
124
137
|
end
|
125
138
|
|
126
139
|
def gitlab_services
|
127
|
-
list = gitlab_status.data.keys.sort.map do |service|
|
128
|
-
color = gitlab_status.data.dig(service, 0, :status) == 'run' ? :green : :red
|
129
|
-
service.colorize(color)
|
130
|
-
end
|
131
|
-
|
132
|
-
# Keep Alphabetical Sort
|
133
|
-
groups = list.each_slice((list.size / 3.to_f).round).to_a
|
134
|
-
|
135
|
-
table = TTY::Table.new do |t|
|
136
|
-
loop do
|
137
|
-
break if groups.all?(&:empty?)
|
138
|
-
|
139
|
-
t << groups.map(&:shift)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
140
|
[
|
144
141
|
title('Services'),
|
145
142
|
"\n ",
|
146
|
-
|
143
|
+
GitLab.services(archive, 3)
|
147
144
|
].join
|
148
145
|
rescue StandardError => e
|
149
146
|
LogBot.fatal('GitLab Services', message: e.message, backtrace: e.backtrace.first)
|
@@ -166,7 +163,7 @@ module GreenHat
|
|
166
163
|
def distro
|
167
164
|
[
|
168
165
|
title('Distro'),
|
169
|
-
"[#{os_release.data.ID}] ".
|
166
|
+
"[#{os_release.data.ID}] ".pastel(:bright_black),
|
170
167
|
os_release.data.PRETTY_NAME
|
171
168
|
].join
|
172
169
|
end
|
@@ -177,7 +174,7 @@ module GreenHat
|
|
177
174
|
|
178
175
|
[
|
179
176
|
title('SeLinux'),
|
180
|
-
status.
|
177
|
+
status.pastel(status_color),
|
181
178
|
' (',
|
182
179
|
selinux_status.data['Current mode'],
|
183
180
|
')'
|
@@ -197,7 +194,7 @@ module GreenHat
|
|
197
194
|
[
|
198
195
|
title('Kernel'),
|
199
196
|
value,
|
200
|
-
" (#{build})".
|
197
|
+
" (#{build})".pastel(:bright_black)
|
201
198
|
].join
|
202
199
|
end
|
203
200
|
|
@@ -209,6 +206,9 @@ module GreenHat
|
|
209
206
|
end
|
210
207
|
|
211
208
|
def sys_time
|
209
|
+
# Ignore if Empty
|
210
|
+
return false if timedatectl.data.nil?
|
211
|
+
|
212
212
|
ntp_statuses = timedatectl.data.slice(*ntp_keys).values.compact
|
213
213
|
|
214
214
|
enabled = %w[active yes] & ntp_statuses
|
@@ -221,9 +221,9 @@ module GreenHat
|
|
221
221
|
[
|
222
222
|
title('Sys Time'),
|
223
223
|
timedatectl.data['Local time'],
|
224
|
-
' (ntp: '.
|
225
|
-
ntp_status.
|
226
|
-
')'.
|
224
|
+
' (ntp: '.pastel(:bright_black),
|
225
|
+
ntp_status.pastel(ntp_color),
|
226
|
+
')'.pastel(:bright_black)
|
227
227
|
].join
|
228
228
|
end
|
229
229
|
|
@@ -248,38 +248,30 @@ module GreenHat
|
|
248
248
|
[
|
249
249
|
interval,
|
250
250
|
' (',
|
251
|
-
"#{value}%".
|
251
|
+
"#{value}%".pastel(color),
|
252
252
|
')'
|
253
253
|
].join
|
254
254
|
end
|
255
255
|
|
256
256
|
[
|
257
257
|
title('LoadAvg'),
|
258
|
-
"[CPU #{cpu_count}] ".
|
258
|
+
"[CPU #{cpu_count}] ".pastel(:bright_white),
|
259
259
|
intervals_text.join(', ')
|
260
260
|
].join
|
261
261
|
end
|
262
262
|
|
263
|
-
def
|
263
|
+
def memory_perc
|
264
264
|
total = ShellHelper.human_size_to_number(meminfo.data['MemTotal'])
|
265
265
|
free = ShellHelper.human_size_to_number(meminfo.data['MemFree'])
|
266
266
|
used = percent((total - free), total)
|
267
|
+
|
267
268
|
[
|
268
|
-
title('
|
269
|
-
'['.
|
270
|
-
'='.
|
269
|
+
title('Usage'),
|
270
|
+
' ['.pastel(:bright_black),
|
271
|
+
'='.pastel(:green) * (used / 2),
|
271
272
|
' ' * (50 - used / 2),
|
272
|
-
']'.
|
273
|
-
" #{100 - percent(free, total)}%".
|
274
|
-
"\n",
|
275
|
-
title('Total'),
|
276
|
-
number_to_human_size(total).colorize(:light_white),
|
277
|
-
"\n",
|
278
|
-
title('Used'),
|
279
|
-
number_to_human_size(total - free),
|
280
|
-
"\n",
|
281
|
-
title('Free'),
|
282
|
-
number_to_human_size(free)
|
273
|
+
']'.pastel(:bright_black),
|
274
|
+
" #{100 - percent(free, total)}%".pastel(:green) # Inverse
|
283
275
|
].join
|
284
276
|
end
|
285
277
|
|
@@ -288,6 +280,8 @@ module GreenHat
|
|
288
280
|
|
289
281
|
return unless free
|
290
282
|
|
283
|
+
formatted_mem = free_m.data.map { |x| GreenHat::Memory.memory_row x }
|
284
|
+
|
291
285
|
[
|
292
286
|
title('Total', :cyan, 14),
|
293
287
|
number_to_human_size(free.total.to_i * 1024**2),
|
@@ -299,7 +293,23 @@ module GreenHat
|
|
299
293
|
number_to_human_size(free.free.to_i * 1024**2),
|
300
294
|
"\n",
|
301
295
|
title('Available', :green, 14),
|
302
|
-
number_to_human_size(free.available.to_i * 1024**2)
|
296
|
+
number_to_human_size(free.available.to_i * 1024**2),
|
297
|
+
"\n\n",
|
298
|
+
formatted_mem.map { |x| x.prepend ' ' * 2 }.join("\n")
|
299
|
+
].join
|
300
|
+
end
|
301
|
+
|
302
|
+
def disks
|
303
|
+
# GreenHat::Disk.df({archive: []})
|
304
|
+
file = GreenHat::Disk.df({ archive: [archive.name] })
|
305
|
+
|
306
|
+
disk_list = GreenHat::Disk.format_output(file.first, false, 3)
|
307
|
+
|
308
|
+
# Preapre / Indent List
|
309
|
+
[
|
310
|
+
'Disks'.pastel(:bright_yellow) + ' (Top % Usage)'.pastel(:bright_black),
|
311
|
+
"\n",
|
312
|
+
disk_list.each { |x| x.prepend(' ' * 4) }.join("\n")
|
303
313
|
].join
|
304
314
|
end
|
305
315
|
|
@@ -312,7 +322,7 @@ module GreenHat
|
|
312
322
|
|
313
323
|
# Helper to Make Cyan Titles
|
314
324
|
def title(name, color = :cyan, ljust = 12)
|
315
|
-
" #{name}:".ljust(ljust).
|
325
|
+
" #{name}:".ljust(ljust).pastel(color)
|
316
326
|
end
|
317
327
|
end
|
318
328
|
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,601 @@
|
|
1
|
+
module GreenHat
|
2
|
+
# Common Helpers
|
3
|
+
# rubocop:disable Metrics/ModuleLength
|
4
|
+
module ShellHelper
|
5
|
+
# Use File Process for Output
|
6
|
+
def self.file_output(files, flags = {})
|
7
|
+
results = file_process(files) do |file|
|
8
|
+
[
|
9
|
+
file.friendly_name,
|
10
|
+
file.output(false),
|
11
|
+
"\n"
|
12
|
+
]
|
13
|
+
end
|
14
|
+
|
15
|
+
ShellHelper.show(results.flatten, flags)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.file_process(files, &block)
|
19
|
+
files.map do |file|
|
20
|
+
next if file.output(false).empty?
|
21
|
+
|
22
|
+
block.call(file)
|
23
|
+
end.flatten
|
24
|
+
end
|
25
|
+
|
26
|
+
# Pagination Helper
|
27
|
+
def self.page(data)
|
28
|
+
TTY::Pager.page do |pager|
|
29
|
+
data.flatten.each do |output|
|
30
|
+
pager.write("\n#{output}") # write line to the pager
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Show Data / Auto Paginate Helper
|
36
|
+
def self.show(data, flags = {})
|
37
|
+
# If Block of String
|
38
|
+
if data.instance_of?(String)
|
39
|
+
TTY::Pager.page data
|
40
|
+
return true
|
41
|
+
end
|
42
|
+
|
43
|
+
# If raw just print out
|
44
|
+
if flags[:raw]
|
45
|
+
puts data.join("\n")
|
46
|
+
return true
|
47
|
+
end
|
48
|
+
|
49
|
+
# Check if content needs to paged, or if auto_height is off
|
50
|
+
if Page.skip?(flags, data)
|
51
|
+
puts data.map { |entry| entry_show(flags, entry) }.compact.join("\n")
|
52
|
+
return true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Default Pager
|
56
|
+
TTY::Pager.page do |pager|
|
57
|
+
data.each do |entry|
|
58
|
+
output = entry_show(flags, entry)
|
59
|
+
|
60
|
+
# Breaks any intentional spaces
|
61
|
+
# next if output.blank?
|
62
|
+
|
63
|
+
pager.write("\n#{output}") # write line to the pager
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Entry Shower / Top Level
|
69
|
+
def self.entry_show(flags, entry, key = nil)
|
70
|
+
LogBot.debug('Entry Show', entry.class) if ENV['DEBUG']
|
71
|
+
case entry
|
72
|
+
when Hash then render_table(entry, flags)
|
73
|
+
when Float, Integer, Array
|
74
|
+
format_table_entry(flags, entry, key)
|
75
|
+
# Ignore Special Formatting for Strings / Usually already formatted
|
76
|
+
when String
|
77
|
+
entry
|
78
|
+
else
|
79
|
+
LogBot.warn('Shell Show', "Unknown #{entry.class}")
|
80
|
+
nil
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Format Table Entries
|
85
|
+
def self.format_table_entry(flags, entry, key = nil)
|
86
|
+
formatted_entry = case entry
|
87
|
+
# Rounding
|
88
|
+
when Float, Integer || entry.numeric?
|
89
|
+
flags.key?(:round) ? entry.to_f.round(flags.round).ai : entry.ai
|
90
|
+
|
91
|
+
# General Inspecting
|
92
|
+
when Hash then entry.ai(ruby19_syntax: true)
|
93
|
+
|
94
|
+
# Arrays often contain Hashes. Dangerous Recursive?
|
95
|
+
when Array
|
96
|
+
entry.map { |x| format_table_entry(flags, x) }.join("\n")
|
97
|
+
|
98
|
+
when Time
|
99
|
+
entry.to_s.pastel(:bright_white)
|
100
|
+
|
101
|
+
# Default String Formatting
|
102
|
+
else
|
103
|
+
StringColor.do(key, entry)
|
104
|
+
end
|
105
|
+
|
106
|
+
if flags[:truncate]
|
107
|
+
entry_truncate(formatted_entry, flags[:truncate])
|
108
|
+
else
|
109
|
+
formatted_entry
|
110
|
+
end
|
111
|
+
rescue StandardError => e
|
112
|
+
if ENV['DEBUG']
|
113
|
+
LogBot.warn('Table Format Entry', message: e.message)
|
114
|
+
ap e.backtrace
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# Print the Table in a Nice way
|
119
|
+
def self.render_table(entry, flags)
|
120
|
+
entry = entry.map { |k, v| [k, format_table_entry(flags, v, k)] }.to_h
|
121
|
+
# Pre-format Entry
|
122
|
+
|
123
|
+
table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
|
124
|
+
|
125
|
+
LogBot.debug('Rendering Entries') if ENV['DEBUG']
|
126
|
+
table.render(:unicode, padding: [0, 1, 0, 1], multiline: true) do |renderer|
|
127
|
+
renderer.border.style = :cyan
|
128
|
+
end
|
129
|
+
|
130
|
+
# LogBot.debug('Finish Render Table') if ENV['DEBUG']
|
131
|
+
# Fall Back to Amazing Inspect
|
132
|
+
rescue StandardError => e
|
133
|
+
if ENV['DEBUG']
|
134
|
+
LogBot.warn('Table', message: e.message)
|
135
|
+
ap e.backtrace
|
136
|
+
end
|
137
|
+
|
138
|
+
[
|
139
|
+
entry.ai,
|
140
|
+
('_' * (TTY::Screen.width / 3)).pastel(:cyan),
|
141
|
+
"\n"
|
142
|
+
].join("\n")
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.render_table_entry(val, col_index, flags)
|
146
|
+
return val.to_s unless col_index == 1
|
147
|
+
|
148
|
+
format_table_entry(flags, val)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Main Entry Point for Filtering
|
152
|
+
def self.filter_start(files, flags, args)
|
153
|
+
# Convert to Things
|
154
|
+
logs = ShellHelper.find_things(files, flags).select(&:processed?)
|
155
|
+
|
156
|
+
# Ignore Archive/Host Dividers
|
157
|
+
if flags[:combine]
|
158
|
+
results = logs.reject(&:blank?).map(&:data).flatten.compact
|
159
|
+
ShellHelper.filter(results, flags, args)
|
160
|
+
else
|
161
|
+
# Iterate and Preserve Archive/Host Index
|
162
|
+
logs.each_with_object({}) do |log, obj|
|
163
|
+
# Ignore Empty Results / No Thing
|
164
|
+
next if log&.blank?
|
165
|
+
|
166
|
+
# Include Total Count in Name
|
167
|
+
results = ShellHelper.filter(log.data, flags, args)
|
168
|
+
title = [
|
169
|
+
log.friendly_name,
|
170
|
+
" #{results.count}".pastel(:bright_black)
|
171
|
+
]
|
172
|
+
|
173
|
+
# Save unless empty
|
174
|
+
obj[title.join] = results unless results.count.zero?
|
175
|
+
|
176
|
+
obj
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Filter Logic
|
182
|
+
# TODO: Simplify
|
183
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
184
|
+
def self.filter(data, flags = {}, args = {})
|
185
|
+
results = data.clone.flatten.compact
|
186
|
+
results.select! do |row|
|
187
|
+
args.send(flags.logic) do |arg|
|
188
|
+
filter_row_key(row, arg, flags)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Ensure presecense of a specific field
|
193
|
+
results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
|
194
|
+
|
195
|
+
# Time Filtering
|
196
|
+
results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
|
197
|
+
|
198
|
+
# Strip Results if Slice is defined
|
199
|
+
results = filter_slice(results, flags[:slice]) if flags.key?(:slice)
|
200
|
+
|
201
|
+
# Strip Results if Except is defined
|
202
|
+
results = filter_except(results, flags[:except]) if flags.key?(:except)
|
203
|
+
|
204
|
+
# Remove Blank from either slice or except
|
205
|
+
results.reject!(&:empty?)
|
206
|
+
|
207
|
+
# Sort
|
208
|
+
results.sort_by! { |x| x.slice(*flags[:sort]).values } if flags.key?(:sort)
|
209
|
+
|
210
|
+
# JSON Formatting
|
211
|
+
results = results.map { |x| Oj.dump(x) } if flags.key?(:json)
|
212
|
+
|
213
|
+
# Show Unique Only
|
214
|
+
results = filter_uniq(results, flags[:uniq]) if flags.key?(:uniq)
|
215
|
+
|
216
|
+
# Reverse
|
217
|
+
results.reverse! if flags[:reverse]
|
218
|
+
|
219
|
+
# Count occurrences / Skip Results
|
220
|
+
return filter_stats(results, flags[:stats]) if flags.key?(:stats)
|
221
|
+
|
222
|
+
# Pluck
|
223
|
+
results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
|
224
|
+
|
225
|
+
# Limit / Ensure Exists and Valid Number
|
226
|
+
if flags.key?(:limit) && flags[:limit]
|
227
|
+
# Old
|
228
|
+
# results[0..flags[:limit].map(&:to_s).join.to_i - 1]
|
229
|
+
|
230
|
+
# New
|
231
|
+
results.shift flags[:limit]
|
232
|
+
|
233
|
+
else
|
234
|
+
results
|
235
|
+
end
|
236
|
+
end
|
237
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
238
|
+
|
239
|
+
# Filter Start and End Times
|
240
|
+
# rubocop:disable Metrics/MethodLength
|
241
|
+
# TODO: This is a bit icky, simplify/dry
|
242
|
+
def self.filter_time(results, flags)
|
243
|
+
if flags.key?(:start)
|
244
|
+
begin
|
245
|
+
time_start = Time.parse(flags[:start])
|
246
|
+
|
247
|
+
results.select! do |x|
|
248
|
+
if x.time
|
249
|
+
time_start < x.time
|
250
|
+
else
|
251
|
+
true
|
252
|
+
end
|
253
|
+
end
|
254
|
+
rescue StandardError
|
255
|
+
puts 'Unable to Process Start Time Filter'.pastel(:red)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
if flags.key?(:end)
|
260
|
+
begin
|
261
|
+
time_start = Time.parse(flags[:end])
|
262
|
+
|
263
|
+
results.select! do |x|
|
264
|
+
if x.time
|
265
|
+
time_start > x.time
|
266
|
+
else
|
267
|
+
true
|
268
|
+
end
|
269
|
+
end
|
270
|
+
rescue StandardError
|
271
|
+
puts 'Unable to Process End Time Filter'.pastel(:red)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
results
|
276
|
+
end
|
277
|
+
# rubocop:enable Metrics/MethodLength
|
278
|
+
|
279
|
+
def self.filter_except(results, except)
|
280
|
+
# Avoid Empty Results
|
281
|
+
if except.empty?
|
282
|
+
filter_empty_arg('except')
|
283
|
+
return results
|
284
|
+
end
|
285
|
+
|
286
|
+
results.map { |row| row.except(*except) }
|
287
|
+
end
|
288
|
+
|
289
|
+
def self.filter_exists(results, exists)
|
290
|
+
# Avoid Empty Results
|
291
|
+
if exists.empty?
|
292
|
+
filter_empty_arg('exists')
|
293
|
+
return results
|
294
|
+
end
|
295
|
+
|
296
|
+
results.select { |row| (exists - row.keys).empty? }
|
297
|
+
end
|
298
|
+
|
299
|
+
def self.entry_truncate(entry, truncate)
|
300
|
+
# Ignore if Truncation Off
|
301
|
+
return entry if truncate.zero?
|
302
|
+
|
303
|
+
# Only truncate large strings
|
304
|
+
return entry unless entry.instance_of?(String) && entry.size > truncate
|
305
|
+
|
306
|
+
# Include '...' to indicate truncation
|
307
|
+
"#{entry.to_s[0..truncate]} #{'...'.pastel(:bright_blue)}"
|
308
|
+
end
|
309
|
+
|
310
|
+
def self.filter_slice(results, slice)
|
311
|
+
# Avoid Empty Results
|
312
|
+
if slice.empty?
|
313
|
+
filter_empty_arg('slice')
|
314
|
+
return results
|
315
|
+
end
|
316
|
+
|
317
|
+
results.map { |row| row.slice(*slice) }
|
318
|
+
end
|
319
|
+
|
320
|
+
def self.filter_pluck(results, pluck)
|
321
|
+
# Avoid Empty Results
|
322
|
+
if pluck.empty?
|
323
|
+
filter_empty_arg('pluck')
|
324
|
+
return results
|
325
|
+
end
|
326
|
+
|
327
|
+
results.map { |x| x.slice(*pluck).values }.flatten
|
328
|
+
end
|
329
|
+
|
330
|
+
def self.filter_uniq(results, unique)
|
331
|
+
# Avoid Empty Results
|
332
|
+
if unique.empty?
|
333
|
+
filter_empty_arg('uniq')
|
334
|
+
return results
|
335
|
+
end
|
336
|
+
|
337
|
+
unique.map do |field|
|
338
|
+
results.uniq { |x| x[field] }
|
339
|
+
end.inject(:&)
|
340
|
+
end
|
341
|
+
|
342
|
+
def self.filter_stats(results, stats)
|
343
|
+
# Avoid Empty Results
|
344
|
+
if stats.empty?
|
345
|
+
filter_empty_arg('stats')
|
346
|
+
return results
|
347
|
+
end
|
348
|
+
|
349
|
+
# Loop through Stats, Separate Hash/Tables
|
350
|
+
stats.map do |field|
|
351
|
+
occurrences = filter_count_occurrences(results, field)
|
352
|
+
|
353
|
+
# Total Occurences
|
354
|
+
total = occurrences.values.sum
|
355
|
+
|
356
|
+
# Percs
|
357
|
+
occurrences.transform_values! do |count|
|
358
|
+
[
|
359
|
+
count,
|
360
|
+
" #{percent(count, total)}%".pastel(:bright_black)
|
361
|
+
]
|
362
|
+
end
|
363
|
+
|
364
|
+
# Sort by total occurances / New Variable for Total
|
365
|
+
output = occurrences.sort_by(&:last).to_h.transform_values!(&:join).to_a
|
366
|
+
|
367
|
+
# Append Header / Total with field name
|
368
|
+
output.unshift([field.to_s.pastel(:bright_black), total])
|
369
|
+
|
370
|
+
# Format
|
371
|
+
output.to_h
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
# Percent Helper
|
376
|
+
def self.percent(value, total)
|
377
|
+
((value / total.to_f) * 100).round
|
378
|
+
end
|
379
|
+
|
380
|
+
# Helper to Count occurrences
|
381
|
+
def self.filter_count_occurrences(results, field)
|
382
|
+
results.each_with_object(Hash.new(0)) do |entry, counts|
|
383
|
+
if entry.key? field
|
384
|
+
counts[entry[field]] += 1
|
385
|
+
else
|
386
|
+
counts['None'.pastel(:bright_black)] += 1
|
387
|
+
end
|
388
|
+
|
389
|
+
counts
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def self.filter_empty_arg(arg)
|
394
|
+
puts [
|
395
|
+
'Ignoring'.pastel(:bright_yellow),
|
396
|
+
"--#{arg}".pastel(:cyan),
|
397
|
+
'it requires an argument'.pastel(:red)
|
398
|
+
].join(' ')
|
399
|
+
end
|
400
|
+
|
401
|
+
# Break out filter row logic into separate method
|
402
|
+
|
403
|
+
def self.filter_row_key(row, arg, flags)
|
404
|
+
# Ignore Other Logic if Field isn't even included / Full Text Searching
|
405
|
+
return false unless row.key?(arg[:field]) || arg[:field] == :text
|
406
|
+
|
407
|
+
# Sensitivity Check / Check for Match / Full Text Searching
|
408
|
+
included = if arg[:field] == :text
|
409
|
+
filter_row_entry(row.to_s, arg, flags)
|
410
|
+
else
|
411
|
+
filter_row_entry(row[arg.field].to_s, arg, flags)
|
412
|
+
end
|
413
|
+
|
414
|
+
# Pivot of off include vs exclude
|
415
|
+
if arg.bang
|
416
|
+
!included
|
417
|
+
else
|
418
|
+
included
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Field Partial / Case / Exact search
|
423
|
+
def self.filter_row_entry(entry, arg, flags)
|
424
|
+
# Exact Matching / Unless doing full text search
|
425
|
+
return entry.to_s == arg.value.to_s if flags.key?(:exact) && arg.field != :text
|
426
|
+
|
427
|
+
if flags.key?(:case)
|
428
|
+
entry.include? arg.value.to_s
|
429
|
+
else
|
430
|
+
entry.downcase.include? arg.value.to_s.downcase
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# Total Count Helper
|
435
|
+
def self.total_count(results)
|
436
|
+
results.each do |k, v|
|
437
|
+
puts k
|
438
|
+
puts "Total: #{v.count.to_s.pastel(:blue)}"
|
439
|
+
puts
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
# Unified Files Interface
|
444
|
+
def self.files(file_list, base_list = nil, flags = {})
|
445
|
+
base_list ||= Thing.all
|
446
|
+
|
447
|
+
# Prepare Log List
|
448
|
+
file_list = prepare_list(file_list, base_list)
|
449
|
+
|
450
|
+
# Convert to Things
|
451
|
+
find_things(file_list, flags)
|
452
|
+
end
|
453
|
+
|
454
|
+
# Total Log List Manipulator
|
455
|
+
def self.prepare_list(log_list, base_list = nil, _flags = {})
|
456
|
+
base_list ||= GreenHat::ShellHelper::Log.list
|
457
|
+
|
458
|
+
# Assume all
|
459
|
+
log_list.push '*' if log_list.empty?
|
460
|
+
|
461
|
+
# Map for All
|
462
|
+
log_list = base_list.map(&:name) if log_list == ['*']
|
463
|
+
|
464
|
+
log_list
|
465
|
+
end
|
466
|
+
|
467
|
+
# Fuzzy match for things
|
468
|
+
def self.thing_list
|
469
|
+
@thing_list ||= Thing.all.map(&:name)
|
470
|
+
|
471
|
+
@thing_list
|
472
|
+
end
|
473
|
+
|
474
|
+
# Shortcut find things
|
475
|
+
def self.find_things(files, flags = {})
|
476
|
+
things = files.uniq.flat_map do |file|
|
477
|
+
# If Thing, Return Thing
|
478
|
+
return file if file.instance_of?(Thing)
|
479
|
+
|
480
|
+
if flags.fuzzy_file_match
|
481
|
+
Thing.all.select { |x| x.name.include? file }
|
482
|
+
else
|
483
|
+
Thing.where name: file
|
484
|
+
end
|
485
|
+
end.uniq
|
486
|
+
|
487
|
+
# Host / Archive
|
488
|
+
things.select! { |x| x.archive? flags.archive } if flags.key?(:archive)
|
489
|
+
|
490
|
+
things
|
491
|
+
end
|
492
|
+
|
493
|
+
# Main Entry Point for Searching
|
494
|
+
# def self.search_start(log_list, filter_type, args, opts)
|
495
|
+
def self.search_start(files, flags, args)
|
496
|
+
# Convert to Things
|
497
|
+
logs = ShellHelper.find_things(files, flags)
|
498
|
+
|
499
|
+
logs.each_with_object({}) do |log, obj|
|
500
|
+
# Ignore Empty Results / No Thing
|
501
|
+
next if log&.data.blank?
|
502
|
+
|
503
|
+
obj[log.friendly_name] = ShellHelper.search(log.data, flags, args)
|
504
|
+
|
505
|
+
obj
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
# Generic Search Helper / String/Regex
|
510
|
+
def self.search(data, flags = {}, args = {})
|
511
|
+
results = data.clone.flatten.compact
|
512
|
+
results.select! do |row|
|
513
|
+
args.send(flags.logic) do |arg|
|
514
|
+
search_row(row, arg, flags)
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
# Strip Results if Slice is defined
|
519
|
+
results.map! { |row| row.slice(*flags[:slice]) } if flags[:slice]
|
520
|
+
|
521
|
+
# Strip Results if Except is defined
|
522
|
+
results.map! { |row| row.except(*flags[:except]) } if flags[:except]
|
523
|
+
|
524
|
+
# Remove Blank from either slice or except
|
525
|
+
results.reject!(&:empty?)
|
526
|
+
|
527
|
+
results
|
528
|
+
end
|
529
|
+
|
530
|
+
# Break out filter row logic into separate method
|
531
|
+
def self.search_row(row, arg, flags)
|
532
|
+
# Sensitivity Check / Check for Match
|
533
|
+
included = filter_row_entry(row.to_s, arg, flags)
|
534
|
+
|
535
|
+
# Pivot of off include vs exclude
|
536
|
+
if arg.bang
|
537
|
+
!included
|
538
|
+
else
|
539
|
+
included
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
# TODO: Remove?
|
544
|
+
# Color Reader Helper
|
545
|
+
# def self.pastel
|
546
|
+
# @pastel ||= Pastel.new
|
547
|
+
# end
|
548
|
+
|
549
|
+
# Number Helper
|
550
|
+
# https://gitlab.com/zedtux/human_size_to_number/-/blob/master/lib/human_size_to_number/helper.rb
|
551
|
+
def self.human_size_to_number(string)
|
552
|
+
size, unit = string.scan(/(\d*\.?\d+)\s?(Bytes?|KB|MB|GB|TB)/i).first
|
553
|
+
number = size.to_f
|
554
|
+
|
555
|
+
number = case unit.downcase
|
556
|
+
when 'byte', 'bytes'
|
557
|
+
number
|
558
|
+
when 'kb'
|
559
|
+
number * 1024
|
560
|
+
when 'mb'
|
561
|
+
number * 1024 * 1024
|
562
|
+
when 'gb'
|
563
|
+
number * 1024 * 1024 * 1024
|
564
|
+
when 'tb'
|
565
|
+
number * 1024 * 1024 * 1024 * 1024
|
566
|
+
end
|
567
|
+
number.round
|
568
|
+
end
|
569
|
+
|
570
|
+
# TODO: Needed?
|
571
|
+
def self.filter_and(data, params = {})
|
572
|
+
result = data.clone.flatten.compact
|
573
|
+
params.each do |k, v|
|
574
|
+
result.select! do |row|
|
575
|
+
if row.key? k.to_sym
|
576
|
+
row[k.to_sym].include? v
|
577
|
+
else
|
578
|
+
false
|
579
|
+
end
|
580
|
+
end
|
581
|
+
next
|
582
|
+
end
|
583
|
+
|
584
|
+
result
|
585
|
+
end
|
586
|
+
|
587
|
+
# General Helper for `show`
|
588
|
+
def self.common_opts
|
589
|
+
puts 'Common Options'.pastel(:blue)
|
590
|
+
puts ' --raw'.pastel(:green)
|
591
|
+
puts ' Do not use less/paging'
|
592
|
+
puts
|
593
|
+
|
594
|
+
puts ' --archive'.pastel(:green)
|
595
|
+
puts ' Limit to specific archive name (inclusive). Matching SOS tar.gz name'
|
596
|
+
puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
|
597
|
+
puts
|
598
|
+
end
|
599
|
+
end
|
600
|
+
# rubocop:enable Metrics/ModuleLength
|
601
|
+
end
|