greenhat 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +64 -0
- data/bin/greenhat +12 -0
- data/lib/greenhat.rb +80 -0
- data/lib/greenhat/accessors/disk.rb +27 -0
- data/lib/greenhat/accessors/logs/production.rb +41 -0
- data/lib/greenhat/accessors/logs/sidekiq.rb +41 -0
- data/lib/greenhat/accessors/memory.rb +46 -0
- data/lib/greenhat/accessors/network.rb +8 -0
- data/lib/greenhat/accessors/process.rb +8 -0
- data/lib/greenhat/archive.rb +108 -0
- data/lib/greenhat/cli.rb +448 -0
- data/lib/greenhat/host.rb +182 -0
- data/lib/greenhat/logbot.rb +86 -0
- data/lib/greenhat/pry_helpers.rb +51 -0
- data/lib/greenhat/settings.rb +51 -0
- data/lib/greenhat/shell.rb +92 -0
- data/lib/greenhat/shell/cat.rb +125 -0
- data/lib/greenhat/shell/disk.rb +68 -0
- data/lib/greenhat/shell/faststats.rb +195 -0
- data/lib/greenhat/shell/gitlab.rb +45 -0
- data/lib/greenhat/shell/help.rb +15 -0
- data/lib/greenhat/shell/helper.rb +514 -0
- data/lib/greenhat/shell/log.rb +344 -0
- data/lib/greenhat/shell/memory.rb +31 -0
- data/lib/greenhat/shell/network.rb +12 -0
- data/lib/greenhat/shell/process.rb +12 -0
- data/lib/greenhat/shell/report.rb +319 -0
- data/lib/greenhat/thing.rb +121 -0
- data/lib/greenhat/thing/file_types.rb +705 -0
- data/lib/greenhat/thing/formatters/api_json.rb +34 -0
- data/lib/greenhat/thing/formatters/bracket_log.rb +44 -0
- data/lib/greenhat/thing/formatters/clean_raw.rb +23 -0
- data/lib/greenhat/thing/formatters/colon_split_strip.rb +12 -0
- data/lib/greenhat/thing/formatters/dotenv.rb +10 -0
- data/lib/greenhat/thing/formatters/format.rb +12 -0
- data/lib/greenhat/thing/formatters/free_m.rb +29 -0
- data/lib/greenhat/thing/formatters/gitlab_ctl_tail.rb +51 -0
- data/lib/greenhat/thing/formatters/gitlab_status.rb +26 -0
- data/lib/greenhat/thing/formatters/json.rb +63 -0
- data/lib/greenhat/thing/formatters/json_shellwords.rb +44 -0
- data/lib/greenhat/thing/formatters/multiline_json.rb +10 -0
- data/lib/greenhat/thing/formatters/raw.rb +18 -0
- data/lib/greenhat/thing/formatters/shellwords.rb +23 -0
- data/lib/greenhat/thing/formatters/table.rb +26 -0
- data/lib/greenhat/thing/formatters/time_json.rb +21 -0
- data/lib/greenhat/thing/formatters/time_shellwords.rb +28 -0
- data/lib/greenhat/thing/formatters/time_space.rb +36 -0
- data/lib/greenhat/thing/helpers.rb +71 -0
- data/lib/greenhat/thing/history.rb +51 -0
- data/lib/greenhat/thing/info_format.rb +193 -0
- data/lib/greenhat/thing/kind.rb +97 -0
- data/lib/greenhat/thing/spinner.rb +52 -0
- data/lib/greenhat/thing/super_log.rb +102 -0
- data/lib/greenhat/tty/custom_line.rb +29 -0
- data/lib/greenhat/tty/line.rb +326 -0
- data/lib/greenhat/tty/reader.rb +110 -0
- data/lib/greenhat/version.rb +3 -0
- data/lib/greenhat/views/css.slim +126 -0
- data/lib/greenhat/views/disk_free.slim +18 -0
- data/lib/greenhat/views/index.slim +51 -0
- data/lib/greenhat/views/info.slim +39 -0
- data/lib/greenhat/views/manifest.slim +22 -0
- data/lib/greenhat/views/memory.slim +18 -0
- data/lib/greenhat/views/netstat.slim +20 -0
- data/lib/greenhat/views/process.slim +21 -0
- data/lib/greenhat/views/systemctl.slim +40 -0
- data/lib/greenhat/views/ulimit.slim +15 -0
- data/lib/greenhat/web.rb +46 -0
- metadata +476 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
# Top
|
2
|
+
module GreenHat
|
3
|
+
# Log
|
4
|
+
module Formatters
|
5
|
+
# ==========================================================================
|
6
|
+
# Time Space JSON
|
7
|
+
# 2021-05-04_18:29:28.66542 {"timestamp":"2021-05-04T18:29:28.665Z","pid":3435539,"message":"Use Ctrl-C to stop"}
|
8
|
+
# ==========================================================================
|
9
|
+
def format_time_json
|
10
|
+
self.result = raw.map do |row|
|
11
|
+
time, msg = row.split(' ', 2)
|
12
|
+
|
13
|
+
result = Oj.load msg
|
14
|
+
result.time = time
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
# ==========================================================================
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Top
|
2
|
+
module GreenHat
|
3
|
+
# Log
|
4
|
+
module Formatters
|
5
|
+
# ==========================================================================
|
6
|
+
# Formatters Not Handled
|
7
|
+
# ==========================================================================
|
8
|
+
def format_time_shellwords
|
9
|
+
self.result = raw.map do |row|
|
10
|
+
time, msg = row.split(' ', 2)
|
11
|
+
|
12
|
+
result = Shellwords.split(msg).each_with_object({}) do |x, h|
|
13
|
+
key, value = x.split('=')
|
14
|
+
next if value.nil?
|
15
|
+
|
16
|
+
h[key] = value.numeric? ? value.to_f : value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Timestamp Parsing
|
20
|
+
result.ts = Time.parse result.ts if result.key? 'ts'
|
21
|
+
result.time = Time.parse time
|
22
|
+
|
23
|
+
result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
# ==========================================================================
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Top
|
2
|
+
module GreenHat
|
3
|
+
# Log
|
4
|
+
module Formatters
|
5
|
+
# ==========================================================================
|
6
|
+
# Formatters Not Handled
|
7
|
+
# ==========================================================================
|
8
|
+
def format_time_space
|
9
|
+
self.result = raw.map do |row|
|
10
|
+
time, msg = row.split(' ', 2)
|
11
|
+
|
12
|
+
{
|
13
|
+
time: Time.parse(time),
|
14
|
+
msg: msg
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
# ==========================================================================
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# def time_space
|
23
|
+
# IO.foreach(file) do |row|
|
24
|
+
# time, msg = row.split(' ', 2)
|
25
|
+
# result = {
|
26
|
+
# time: parse_datetime(time), msg: msg, host: thing.host,
|
27
|
+
# source: path_format
|
28
|
+
# }
|
29
|
+
|
30
|
+
# result[:time] ||= datetime
|
31
|
+
# post result
|
32
|
+
# rescue StandardError => e
|
33
|
+
# puts "Unable to Parse, #{name}:#{e.message}"
|
34
|
+
# post(time: datetime, msg: row, source: path_format, host: thing.host)
|
35
|
+
# end
|
36
|
+
# end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# Global Namespace
|
2
|
+
module GreenHat
|
3
|
+
# Accessors / Readers
|
4
|
+
module ThingHelpers
|
5
|
+
# Console Helper
|
6
|
+
def inspect
|
7
|
+
[
|
8
|
+
'Thing'.colorize(:light_black),
|
9
|
+
kind&.to_s&.colorize(:blue),
|
10
|
+
type&.colorize(:light_yellow),
|
11
|
+
name&.colorize(:cyan)
|
12
|
+
].compact.join(' ')
|
13
|
+
end
|
14
|
+
|
15
|
+
# Format the name of this thing
|
16
|
+
def build_path(divider = '_')
|
17
|
+
tmp_path = file.gsub("#{archive.path}/", '')
|
18
|
+
|
19
|
+
case tmp_path.count('/')
|
20
|
+
when 0
|
21
|
+
tmp_path
|
22
|
+
when 1
|
23
|
+
tmp_path.split('/').last
|
24
|
+
else
|
25
|
+
tmp_path.split('/').last(2).join(divider)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Check what kind of file we have
|
30
|
+
def type_check
|
31
|
+
if info?
|
32
|
+
:info
|
33
|
+
elsif SuperLog.type?(path)
|
34
|
+
:log
|
35
|
+
else
|
36
|
+
:raw
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Pretty sure this is a bad idea to patch string.
|
43
|
+
# https://mentalized.net/journal/2011/04/14/ruby-how-to-check-if-a-string-is-numeric/
|
44
|
+
class String
|
45
|
+
def numeric?
|
46
|
+
!Float(self).nil?
|
47
|
+
rescue StandardError
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Shims
|
53
|
+
class Float
|
54
|
+
def numeric?
|
55
|
+
true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Shims
|
60
|
+
class TrueClass
|
61
|
+
def to_i
|
62
|
+
1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Shims
|
67
|
+
class FalseClass
|
68
|
+
def to_i
|
69
|
+
0
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module GreenHat
|
2
|
+
# Helper for Remembering what files were
|
3
|
+
module ThingHistory
|
4
|
+
def self.files
|
5
|
+
@files ||= read
|
6
|
+
|
7
|
+
@files
|
8
|
+
end
|
9
|
+
|
10
|
+
# Read File / Remove Old Entries
|
11
|
+
def self.read
|
12
|
+
if File.exist? file
|
13
|
+
results = Oj.load File.read(file)
|
14
|
+
|
15
|
+
results.reject { |_k, v| Time.at(v.time) < Time.now }
|
16
|
+
else
|
17
|
+
{}
|
18
|
+
end
|
19
|
+
ensure
|
20
|
+
{}
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.file
|
24
|
+
Settings.history_file
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initial Entry Point
|
28
|
+
def self.match?(name)
|
29
|
+
files.key? name.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.match(name)
|
33
|
+
files.dig(name.to_sym, :type)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.add(name, type)
|
37
|
+
files[name] = { type: type, time: expire }
|
38
|
+
|
39
|
+
write
|
40
|
+
end
|
41
|
+
|
42
|
+
# Date to Expire
|
43
|
+
def self.expire
|
44
|
+
2.weeks.from_now.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.write
|
48
|
+
File.write(file, Oj.dump(files))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
module GreenHat
|
2
|
+
# Info Formatter
|
3
|
+
module InfoFormat
|
4
|
+
# Is this something that can be formatted by info?
|
5
|
+
def info?
|
6
|
+
methods.include? "format_#{path}".to_sym
|
7
|
+
end
|
8
|
+
|
9
|
+
# Handle Info Formatting
|
10
|
+
def info_format
|
11
|
+
self.result = send("format_#{path}")
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_headers_n_lines
|
15
|
+
# Headers to Readable Symbol
|
16
|
+
headers = raw.first.split(' ', 6).map(&:downcase).map do |x|
|
17
|
+
x.gsub(/\s+/, '_').gsub(/[^0-9A-Za-z_]/, '')
|
18
|
+
end.map(&:to_sym)
|
19
|
+
|
20
|
+
output = []
|
21
|
+
|
22
|
+
# Put fields into a Hash based on Location/Key
|
23
|
+
raw[1..].map(&:split).each do |row|
|
24
|
+
result = {}
|
25
|
+
row.each_with_index do |detail, i|
|
26
|
+
result[headers[i]] = detail
|
27
|
+
end
|
28
|
+
output.push result
|
29
|
+
end
|
30
|
+
|
31
|
+
self.result = output
|
32
|
+
end
|
33
|
+
|
34
|
+
# All Similar to Header and Lines
|
35
|
+
alias format_df_inodes format_headers_n_lines
|
36
|
+
alias format_df_h format_headers_n_lines
|
37
|
+
|
38
|
+
def format_mount(data)
|
39
|
+
data.select { |x| x.include? ':' }
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_free_m
|
43
|
+
split_white_space raw
|
44
|
+
end
|
45
|
+
|
46
|
+
def format_meminfo
|
47
|
+
raw.map { |x| x.split(' ', 2) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def format_netstat_conn_headers
|
51
|
+
[
|
52
|
+
'Proto', 'Recv-Q', 'Send-Q', 'Local Address', 'Foreign Address', 'State', 'PID/Program name'
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
def format_netstat_socket_headers
|
57
|
+
[
|
58
|
+
'Proto', 'RefCnt', 'Flags', 'Type', 'State', 'I-Node', 'PID/Program name', 'Path'
|
59
|
+
]
|
60
|
+
end
|
61
|
+
|
62
|
+
def format_netstat
|
63
|
+
# Data can return blank or single item array
|
64
|
+
conns, sockets = raw.split { |x| x.include? 'Active' }.reject(&:empty?)
|
65
|
+
|
66
|
+
formatted_conns = conns[1..].map do |entry|
|
67
|
+
entry.split(' ', 7).map(&:strip).each_with_index.map do |field, idx|
|
68
|
+
[format_netstat_conn_headers[idx], field]
|
69
|
+
end.to_h
|
70
|
+
end
|
71
|
+
|
72
|
+
formatted_sockets = sockets[1..].map do |entry|
|
73
|
+
entry.split(' ').map(&:strip).reject(&:blank?).each_with_index.map do |field, idx|
|
74
|
+
[format_netstat_socket_headers[idx], field]
|
75
|
+
end.to_h
|
76
|
+
end
|
77
|
+
|
78
|
+
{
|
79
|
+
connections: formatted_conns,
|
80
|
+
sockets: formatted_sockets
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def format_netstat_i
|
85
|
+
headers = raw[1].split
|
86
|
+
result = raw[2..].map do |row|
|
87
|
+
row.split.each_with_index.map do |x, idx|
|
88
|
+
[headers[idx], x]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
result.map(&:to_h)
|
93
|
+
end
|
94
|
+
|
95
|
+
def split_white_space(data)
|
96
|
+
data.map(&:split)
|
97
|
+
end
|
98
|
+
|
99
|
+
def ps_format
|
100
|
+
headers = info.ps.first.split(' ', 11)
|
101
|
+
list = info.ps[1..].each.map do |row|
|
102
|
+
row.split(' ', 11).each_with_index.each_with_object({}) do |(v, i), obj|
|
103
|
+
obj[headers[i]] = v
|
104
|
+
end
|
105
|
+
end
|
106
|
+
{ headers: headers, list: list }
|
107
|
+
end
|
108
|
+
|
109
|
+
def manifest_json_format
|
110
|
+
Oj.load info[:gitlab_version_manifest_json].join
|
111
|
+
end
|
112
|
+
|
113
|
+
def cpuinfo_format
|
114
|
+
info.cpuinfo.join("\n").split("\n\n").map do |cpu|
|
115
|
+
all = cpu.split("\n").map do |row|
|
116
|
+
row.delete("\t").split(': ')
|
117
|
+
end
|
118
|
+
{ details: all[1..], order: all[0].last }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def cpu_speed
|
123
|
+
return nil unless data? :lscpu
|
124
|
+
|
125
|
+
info.lscpu.find { |x| x.include? 'MHz' }.split(' ')
|
126
|
+
end
|
127
|
+
|
128
|
+
def total_memory
|
129
|
+
return nil unless data? :free_m
|
130
|
+
|
131
|
+
value = info.free_m.dig(1, 1).to_i
|
132
|
+
number_to_human_size(value * 1024 * 1024)
|
133
|
+
end
|
134
|
+
|
135
|
+
def ulimit
|
136
|
+
return nil unless data? :ulimit
|
137
|
+
|
138
|
+
results = info.ulimit.map do |entry|
|
139
|
+
{
|
140
|
+
value: entry.split[-1],
|
141
|
+
details: entry.split(' ').first
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
results.sort_by { |x| x[:details].downcase }
|
146
|
+
end
|
147
|
+
|
148
|
+
def systemctl_format
|
149
|
+
return nil unless data? :systemctl_unit_files
|
150
|
+
|
151
|
+
all = info.systemctl_unit_files[1..-2].map do |x|
|
152
|
+
unit, status = x.split
|
153
|
+
{ unit: unit, status: status }
|
154
|
+
end
|
155
|
+
all.reject! { |x| x[:unit].nil? }
|
156
|
+
all.sort_by(&:unit)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Helper to color the status files
|
160
|
+
def systemctl_color(entry)
|
161
|
+
case entry.status
|
162
|
+
when 'enabled' then :green
|
163
|
+
when 'static' then :orange
|
164
|
+
when 'disabled' then :red
|
165
|
+
else
|
166
|
+
:grey
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def vmstat_format
|
171
|
+
return nil unless data? :vmstat
|
172
|
+
|
173
|
+
info.vmstat[2..].map(&:split)
|
174
|
+
end
|
175
|
+
|
176
|
+
def uptime
|
177
|
+
info.uptime.join.split(',', 4).map(&:lstrip) if info.key? :uptime
|
178
|
+
end
|
179
|
+
|
180
|
+
def mount
|
181
|
+
return nil unless info.key? :mount
|
182
|
+
return nil if info.mount.empty?
|
183
|
+
|
184
|
+
MountFormat.parse(info.mount)
|
185
|
+
end
|
186
|
+
|
187
|
+
def format_single_json
|
188
|
+
Oj.load raw.join("\n")
|
189
|
+
end
|
190
|
+
|
191
|
+
alias format_gitlab_version_manifest_json format_single_json
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module GreenHat
|
2
|
+
# Overall Type Parsing
|
3
|
+
module Kind
|
4
|
+
# rubobop:disable *
|
5
|
+
def kind_collect
|
6
|
+
# If Direct Match
|
7
|
+
if types.key? name
|
8
|
+
self.type = name
|
9
|
+
|
10
|
+
return true
|
11
|
+
end
|
12
|
+
|
13
|
+
# Check Pattern Matches
|
14
|
+
matches = types.select do |_k, v|
|
15
|
+
v.pattern.any? { |x| x =~ name }
|
16
|
+
end
|
17
|
+
|
18
|
+
# If there is only one match
|
19
|
+
if matches.keys.count == 1
|
20
|
+
self.type = matches.keys.first
|
21
|
+
|
22
|
+
true
|
23
|
+
|
24
|
+
# TODO: Prompt for smaller selection
|
25
|
+
elsif matches.keys.count > 1
|
26
|
+
puts 'Multiple!'
|
27
|
+
# History Match
|
28
|
+
elsif ThingHistory.match? name
|
29
|
+
self.type = ThingHistory.match(name)
|
30
|
+
else
|
31
|
+
prompt_for_kind
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# File Identifier
|
36
|
+
def kind_setup
|
37
|
+
self.kind = types.dig(type, :format)
|
38
|
+
self.log = types.dig(type, :log)
|
39
|
+
end
|
40
|
+
|
41
|
+
def prompt
|
42
|
+
# Quiet Exit
|
43
|
+
@prompt ||= TTY::Prompt.new(interrupt: :exit)
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_oj_parse?(first_line)
|
47
|
+
Oj.load(first_line)
|
48
|
+
true
|
49
|
+
rescue StandardError
|
50
|
+
false
|
51
|
+
end
|
52
|
+
|
53
|
+
# rubocop:disable Style/SymbolProc
|
54
|
+
def prompt_for_kind
|
55
|
+
binding.pry if ENV['DEBUG']
|
56
|
+
|
57
|
+
# Default to everything
|
58
|
+
prompt_list = types.clone
|
59
|
+
|
60
|
+
first_line = File.open(file) { |f| f.readline }
|
61
|
+
json = check_oj_parse?(first_line)
|
62
|
+
|
63
|
+
if json
|
64
|
+
prompt_list.select! do |_k, v|
|
65
|
+
v.to_s.include? 'json'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
puts "Unable to determine file type for #{name.colorize(:yellow)}"
|
70
|
+
puts "Use '#{'json'.colorize(:cyan)}' and '#{'raw'.colorize(:cyan)}' if you cannot find any matches"
|
71
|
+
|
72
|
+
option = prompt.select('Wat is this?', prompt_list.keys.sort_by(&:length), filter: true)
|
73
|
+
|
74
|
+
# Store for later
|
75
|
+
ThingHistory.add(name, option)
|
76
|
+
|
77
|
+
self.type = option
|
78
|
+
end
|
79
|
+
# rubocop:enable Style/SymbolProc
|
80
|
+
|
81
|
+
# Pattern Match / Look for `match_` patterns and then strip name for kind type
|
82
|
+
def kind_pattern_match(name)
|
83
|
+
pattern_match = methods.grep(/^match_/).find do |pattern|
|
84
|
+
send(pattern).any? { |x| name =~ x }
|
85
|
+
end
|
86
|
+
|
87
|
+
if pattern_match
|
88
|
+
pattern_match.to_s.split('match_', 2).last.to_sym
|
89
|
+
else
|
90
|
+
false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Load All Formatters
|
97
|
+
require_all "#{File.dirname(__FILE__)}/formatters"
|