greenhat 0.1.4
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 +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"
|