greenhat 0.3.3 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac7b0e70e5a13dc6f8f88a4f12fec812e38e7d838537abe83b7d4dc2e8b43663
4
- data.tar.gz: 84246bc31bd38af74e9f4f8d95f2f7ffa1ca33978dc0d0862ec0b50eaff20196
3
+ metadata.gz: 4c055abc7397135be9ca07e7965fc88d67f1d7d16099a5cc5db1977986490361
4
+ data.tar.gz: 2e2f66a3bbc5bdc84e8b5bef9abfa28706f48841f25c7e642fe352c063745e22
5
5
  SHA512:
6
- metadata.gz: ba104ea8d7b4e52babf8bdf27ea4f3dbdb9e92820589881bab639a01e8558c355389a0f10b3c17dbe5c7ac881f10364ad06ec0c973934f06d735f948bb57fbd3
7
- data.tar.gz: a2013229826758ca61335f5cf8d58c3a4a4206e656dce084f90781c264e4930c5c3fd09b7475a43dd31819bc2562e85e5799de775cc5db9dfaa3f73c0228ac3a
6
+ metadata.gz: d9fcd1274e4887fa332a053185ea1049e5db2f3d449714542a31084234326d48623e95e0fb8678b6cb67b20b636ec04c82db1555ff43aac09f158d6df4fc7dc0
7
+ data.tar.gz: c6fe018479b7cc90f2e70b411761fe5e3b8da5d4237064e3bb450399f966cf00a0302adc433ebf39fad57565ca11efbd95fbd0601a15f2989bc1c6455029a4e8
@@ -79,5 +79,46 @@ module GreenHat
79
79
  output
80
80
  end
81
81
  # rubocop:enable Metrics/MethodLength
82
+
83
+ # Unified Output Handler
84
+ def self.markdown_format(file, name = false, limit = nil, filter = %w[tmpfs loop])
85
+ output = []
86
+
87
+ output << file.friendly_name if name
88
+
89
+ # Reject TMPFS
90
+ disks = file.data.sort_by { |x| x.use.to_i }.reverse
91
+
92
+ # Filter
93
+ disks.reject! { |x| filter.any? { |y| x.filesystem.include? y } }
94
+
95
+ disks = disks[0..limit - 1] if limit
96
+
97
+ pad_mount, pad_size, pad_used, pad_avail = GreenHat::Disk.padding(disks)
98
+
99
+ # Headers
100
+ output << [
101
+ 'Mount'.ljust(pad_mount),
102
+ 'Size'.ljust(pad_size),
103
+ 'Used'.ljust(pad_used),
104
+ 'Avail'.ljust(pad_avail),
105
+ '% Use'.ljust(pad_avail)
106
+ ].join
107
+
108
+ # Table Summary
109
+ disks.map do |disk|
110
+ # Whole Thing
111
+ output << [
112
+ disk.mounted_on.ljust(pad_mount),
113
+ disk[:size].to_s.ljust(pad_size),
114
+ disk.used.to_s.ljust(pad_used),
115
+ disk.avail.to_s.ljust(pad_avail),
116
+ disk.use.rjust(5).ljust(5)
117
+ ].join
118
+ end
119
+
120
+ output
121
+ end
122
+ # =
82
123
  end
83
124
  end
@@ -71,5 +71,31 @@ module GreenHat
71
71
 
72
72
  table.render(:unicode, padding: [0, 1, 0, 1], indent: indent)
73
73
  end
74
+
75
+ def self.services_markdown(archive)
76
+ # manifest = archive.things.find { |x| x.type == 'gitlab/version-manifest.json' }
77
+ gitlab_status = archive.things.find { |x| x.name == 'gitlab_status' }
78
+
79
+ list = gitlab_status.data.keys.sort.map do |service|
80
+ status = gitlab_status.data.dig(service, 0, :status) == 'run' ? '↑' : '!'
81
+
82
+ "#{service}#{status}"
83
+ end
84
+
85
+ # Keep Alphabetical Sort
86
+ groups = list.each_slice((list.size / 3.to_f).round).to_a
87
+
88
+ table = TTY::Table.new do |t|
89
+ loop do
90
+ break if groups.all?(&:empty?)
91
+
92
+ t << groups.map(&:shift)
93
+ end
94
+ end
95
+
96
+ "```\n#{table.render(:basic)}\n```"
97
+ end
98
+
99
+ # ==
74
100
  end
75
101
  end
@@ -105,4 +105,8 @@ class Archive < Teron
105
105
  def report
106
106
  GreenHat::Report.new(self)
107
107
  end
108
+
109
+ def report_markdown
110
+ GreenHat::ReportMarkdown.new(self)
111
+ end
108
112
  end
data/lib/greenhat/cli.rb CHANGED
@@ -420,6 +420,7 @@ module GreenHat
420
420
 
421
421
  # CTL Tails need to be parsed for new 'things'
422
422
  Thing.where(kind: :gitlab_tail)&.map(&:process)
423
+ Thing.where(kind: :kube_webservice)&.map(&:process)
423
424
 
424
425
  Thing.all.each(&:process) if flags?(%i[load l], flags)
425
426
  end
@@ -2,8 +2,8 @@ module GreenHat
2
2
  # CLI Helper
3
3
  module ShellHelper
4
4
  # Unify Filter / Filter Help
5
+ # rubocop:disable Metrics/MethodLength,Metrics/ModuleLength
5
6
  module Filter
6
- # rubocop:disable Metrics/MethodLength
7
7
  def self.help
8
8
  puts "\u2500".pastel(:cyan) * 20
9
9
  puts 'Filter'.pastel(:yellow)
@@ -156,7 +156,208 @@ module GreenHat
156
156
 
157
157
  puts
158
158
  end
159
- # rubocop:enable Metrics/MethodLength
159
+
160
+ def self.help_index
161
+ {
162
+ title: [
163
+ "\u2500".pastel(:cyan) * 20,
164
+ 'Filter'.pastel(:yellow),
165
+ "\u2500".pastel(:cyan) * 20
166
+ ],
167
+ options: [
168
+ 'Options'.pastel(:blue)
169
+ ],
170
+
171
+ raw: [
172
+ '--raw'.pastel(:green),
173
+ ' Disable formatting and page/less'
174
+ ],
175
+
176
+ page: [
177
+ '--page'.pastel(:green),
178
+ ' Specifically enable or disable paging',
179
+ ' E.g. --page (default to true), --page=true, --page=false'
180
+ ],
181
+
182
+ round: ['--round'.pastel(:green),
183
+ ' Attempt to round all integers. Default: 2.',
184
+ ' E.g. --round, --round=3, --round=0'],
185
+
186
+ limit: [
187
+
188
+ '--limit'.pastel(:green),
189
+ ' Limit total output lines. Disabled by default. Default without value is based on screen height',
190
+ ' E.g. --limit, --limit=5'
191
+ ],
192
+
193
+ json: [
194
+ '--json'.pastel(:green),
195
+ ' Print output back into JSON'
196
+ ],
197
+
198
+ or: [
199
+ '--or'.pastel(:green),
200
+ ' Filters will use OR instead of AND (all match vs any match)'
201
+ ],
202
+
203
+ total: [
204
+ '--total'.pastel(:green),
205
+ ' Print only total count of matching entries'
206
+ ],
207
+
208
+ fields: [
209
+ '--fields'.pastel(:green),
210
+ ' Print only Available fields for selected files'
211
+ ],
212
+
213
+ slice: [
214
+ '--slice'.pastel(:green),
215
+ ' Extract specific fields from entries (slice multiple with comma)',
216
+ ' Ex: --slice=path or --slice=path,params'
217
+
218
+ ],
219
+
220
+ except: [
221
+ '--except'.pastel(:green),
222
+ ' Exclude specific fields (except multiple with comma)',
223
+ ' Ex: --except=params --except=params,path'
224
+
225
+ ],
226
+
227
+ exists: [
228
+ '--exists'.pastel(:green),
229
+ ' Ensure field exists regardless of contents',
230
+ ' Ex: --exists=params --exists=params,path'
231
+
232
+ ],
233
+
234
+ stats: [
235
+ '--stats'.pastel(:green),
236
+ ' Order/Count occurrances by field. Combine with `truncate` for key names',
237
+ ' Ex: --stats=params --except=params,path'
238
+
239
+ ],
240
+
241
+ uniq: [
242
+ '--uniq'.pastel(:green),
243
+ ' Show unique values only',
244
+ ' Ex: --uniq=params --uniq=params,path'
245
+
246
+ ],
247
+
248
+ pluck: [
249
+ '--pluck'.pastel(:green),
250
+ ' Extract values from entries',
251
+ ' Ex: --pluck=params --pluck=params,path'
252
+
253
+ ],
254
+
255
+ archive: [
256
+ '--archive'.pastel(:green),
257
+ ' Limit to specific archvie name (partial matching /inclusive). Matching SOS tar.gz name',
258
+ ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
259
+
260
+ ],
261
+
262
+ sort: [
263
+ '--sort'.pastel(:green),
264
+ ' Sort by multiple fields',
265
+ ' Ex: --sort=duration_s,db_duration_s'
266
+
267
+ ],
268
+
269
+ reverse: [
270
+ '--reverse'.pastel(:green),
271
+ ' Reverse all results',
272
+ ' Ex: --reverse'
273
+
274
+ ],
275
+
276
+ combine: [
277
+ '--combine'.pastel(:green),
278
+ ' Omit archive identifier dividers. Useful with sort or time filters',
279
+ ' Ex: --combine'
280
+
281
+ ],
282
+
283
+ case: [
284
+ '--case'.pastel(:green),
285
+ ' Exact case match. Defaults to case insensitive',
286
+ ' Ex: --case; --name=Jon, --name=jane --case'
287
+
288
+ ],
289
+
290
+ exact: [
291
+ '--exact'.pastel(:green),
292
+ ' Exact parameter/value match. Defaults to partial match',
293
+ ' Ex: --field=CommonPartial --exact'
294
+
295
+ ],
296
+
297
+ start: [
298
+ '--start'.pastel(:green),
299
+ ' Show events after specified time. Filtered by the `time` field',
300
+ ' Use with `--end` for between selections',
301
+ ' Ex: log filter --start="2021-06-22 14:44 UTC" --end="2021-06-22 14:45 UTC"'
302
+
303
+ ],
304
+
305
+ end: [
306
+ '--end'.pastel(:green),
307
+ ' Show events before specified time. Filtered by the `time` field',
308
+ ' Use with `--start` for between selections',
309
+ ' Ex: log filter --end="2021-06-22"'
310
+ ],
311
+
312
+ time_zone: [
313
+ '--time_zone'.pastel(:green),
314
+ ' Manipulate the `time` field into a specific timezone',
315
+ ' Ex: log filter --time_zone=EDT'
316
+
317
+ ],
318
+
319
+ text: [
320
+ '--text'.pastel(:green),
321
+ ' Full entry text searching (slow)',
322
+ ' --text="anything here"'
323
+ ],
324
+
325
+ table_style: [
326
+ '--table_style'.pastel(:green),
327
+ ' Renderer used for formatted output. basic, ascii, or unicode(default)',
328
+ ' Ex: log filter --table_style=base'
329
+ ],
330
+
331
+ truncate: [
332
+ '--truncate'.pastel(:green),
333
+ ' Truncate field length. On by default (4 rows). Performance issues!',
334
+ ' Disable with --truncate=0'.pastel(:bright_red),
335
+ ' Ex: --truncate=200, --truncate=2048"'
336
+ ],
337
+
338
+ field: [
339
+ 'Field Searching'.pastel(:blue),
340
+ ' --[key]=[value]',
341
+ ' Search in key for value',
342
+ ' Example: --path=mirror/pull'
343
+ ],
344
+
345
+ ls: [
346
+ 'Search specific logs'.pastel(:blue),
347
+ ' Any non dash parameters will be the log list to search from',
348
+ " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.pastel(:yellow)}` for log names"
349
+ ],
350
+
351
+ examples: [
352
+ 'Example Queries'.pastel(:blue),
353
+ " Also see #{'examples'.pastel(:bright_blue)} for even more examples",
354
+ ' log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message',
355
+ ' log filter gitlab-rails/api_json.log --slice=ua --uniq=ua --ua=gitlab-runner'
356
+ ]
357
+
358
+ }
359
+ end
360
+ # rubocop:enable Metrics/MethodLength,Metrics/ModuleLength
160
361
  end
161
362
  end
162
363
  end
@@ -47,6 +47,7 @@ module GreenHat
47
47
  puts ' filter'.pastel(:green)
48
48
  puts " Primary way for log searching within greenhat. See #{'filter_help'.pastel(:blue)}"
49
49
  puts ' Time, round, slice/except, and/or, stats, uniq, sort'
50
+ puts " #{'filter_help'.pastel(:blue)} supports filtering Ex: #{'filter_help stats'.pastel(:blue)}"
50
51
  puts
51
52
 
52
53
  puts ' show'.pastel(:green)
@@ -62,8 +63,16 @@ module GreenHat
62
63
  puts "See #{'examples'.pastel(:bright_blue)} for query examples"
63
64
  end
64
65
 
65
- def self.filter_help
66
- ShellHelper::Filter.help
66
+ def self.filter_help(args = {})
67
+ if args.empty?
68
+ ShellHelper::Filter.help
69
+ else
70
+ list = ShellHelper::Filter.help_index.select do |k, _v|
71
+ k.to_s.include? args.first
72
+ end
73
+
74
+ puts list.values.map { |x| x.join("\n") }.join("\n\n")
75
+ end
67
76
  end
68
77
 
69
78
  def self.ls(args = [])
@@ -0,0 +1,356 @@
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
131
+ groups = output.each_slice((output.size / 3.to_f).round).to_a
132
+
133
+ table = TTY::Table.new do |t|
134
+ loop do
135
+ break if groups.all?(&:empty?)
136
+
137
+ t << groups.map(&:shift)
138
+ end
139
+ end
140
+
141
+ "```\n#{table.render(:basic, padding: [0, 2, 0, 0])}\n```"
142
+ end
143
+
144
+ def exception_errors
145
+ count = exceptions_log.data.count
146
+
147
+ "Exception: #{count}"
148
+ end
149
+
150
+ def gitaly_errors
151
+ count = gitaly_log.data.count { |x| x.level == 'error' }
152
+
153
+ "Gitaly: #{count}"
154
+ end
155
+
156
+ def production_errors
157
+ count = production_log.data.count { |x| x.status == 500 }
158
+
159
+ "Production: #{count}"
160
+ end
161
+
162
+ def api_errors
163
+ count = api_log.data.count { |x| x.status == 500 }
164
+
165
+ "API: #{count}"
166
+ end
167
+
168
+ def application_errors
169
+ results = ShellHelper.filter_internal([
170
+ 'gitlab-rails/application_json.log',
171
+ '--message!="Cannot obtain an exclusive lease"',
172
+ '--severity=error'
173
+ ].join(' '))
174
+
175
+ "Application: #{results.count}"
176
+ end
177
+
178
+ def sidekiq_errors
179
+ count = sidekiq_log.data.count { |x| x&.severity == 'ERROR' }
180
+
181
+ "Sidekiq: #{count}"
182
+ end
183
+
184
+ def gitlab_services
185
+ [
186
+ "**Services**\n",
187
+ "\n",
188
+ GreenHat::GitLab.services_markdown(archive)
189
+ ].join
190
+ rescue StandardError => e
191
+ LogBot.fatal('GitLab Services', message: e.message, backtrace: e.backtrace.first)
192
+ end
193
+
194
+ def gitlab_version
195
+ "Version: #{gitlab_manifest.data.build_version}\n"
196
+ end
197
+
198
+ def hostname
199
+ "Hostname: #{host.data.first}"
200
+ end
201
+
202
+ def distro
203
+ [
204
+ "Distro: [#{os_release.data.ID}] ",
205
+ os_release.data.PRETTY_NAME
206
+ ].join
207
+ end
208
+
209
+ def selinux
210
+ status = selinux_status.data['SELinux status']
211
+
212
+ [
213
+ 'SeLinux: ',
214
+ status,
215
+ ' (',
216
+ selinux_status.data['Current mode'],
217
+ ')'
218
+ ].join
219
+ end
220
+
221
+ def arch
222
+ [
223
+ 'Arch: ',
224
+ cpu.data.Architecture
225
+ ].join
226
+ end
227
+
228
+ def kernel
229
+ # TODO: Better way to consistently get uname info?
230
+ value, build = uname.data.first.split[2].split('-')
231
+ [
232
+ 'Kernel: ',
233
+ value,
234
+ " (#{build})"
235
+ ].join
236
+ end
237
+
238
+ # Helper for finding if NTP is enabled
239
+ def ntp_keys
240
+ [
241
+ 'Network time on', 'NTP enabled', 'NTP service', 'System clock synchronized'
242
+ ]
243
+ end
244
+
245
+ def sys_time
246
+ # Ignore if Empty
247
+ return false if timedatectl.data.nil?
248
+
249
+ ntp_statuses = timedatectl.data.slice(*ntp_keys).values.compact
250
+
251
+ ntp_status = ntp_statuses.first
252
+
253
+ # Fall Back
254
+ ntp_status ||= 'unknown'
255
+
256
+ [
257
+ 'Sys Time: ',
258
+ timedatectl.data['Local time'],
259
+ "(ntp: #{ntp_status})"
260
+ ].join
261
+ end
262
+
263
+ # Strip/Simplify Uptime
264
+ def sys_uptime
265
+ init = uptime.data.first.split(', load average').first.strip
266
+
267
+ "Uptime: #{init.split('up ', 2).last}"
268
+ end
269
+
270
+ def load_average
271
+ cpu_count = cpu.data['CPU(s)'].to_i
272
+ intervals = uptime.data.first.split('load average: ', 2).last.split(', ').map(&:to_f)
273
+
274
+ # Generate Colorized Text for Output
275
+ intervals_text = intervals.map do |interval|
276
+ value = percent(interval, cpu_count)
277
+
278
+ "#{interval} (#{value}%)"
279
+ end
280
+
281
+ [
282
+ 'LoadAvg: ',
283
+ "[CPU #{cpu_count}] ",
284
+ intervals_text.join(', ')
285
+ ].join
286
+ end
287
+
288
+ # def memory_perc
289
+ # total = ShellHelper.human_size_to_number(meminfo.data['MemTotal'])
290
+ # free = ShellHelper.human_size_to_number(meminfo.data['MemFree'])
291
+ # used = percent((total - free), total)
292
+
293
+ # [
294
+ # title('Usage'),
295
+ # ' ['.pastel(:bright_black),
296
+ # '='.pastel(:green) * (used / 2),
297
+ # ' ' * (50 - used / 2),
298
+ # ']'.pastel(:bright_black),
299
+ # " #{100 - percent(free, total)}%".pastel(:green) # Inverse
300
+ # ].join
301
+ # end
302
+
303
+ def memory_free
304
+ free = free_m.data.find { |x| x.kind == 'Mem' }
305
+
306
+ return unless free
307
+
308
+ pad = 6
309
+ list = [
310
+ "#{title('Total', pad)} #{number_to_human_size(free.total.to_i * 1024**2)}",
311
+ "#{title('Used', pad)} #{number_to_human_size(free.used.to_i * 1024**2)}",
312
+ "#{title('Free', pad)} #{number_to_human_size(free.free.to_i * 1024**2)}",
313
+ "#{title('Avail', pad)} #{number_to_human_size(free.available.to_i * 1024**2)}"
314
+ ]
315
+
316
+ # Keep Alphabetical Sort
317
+ groups = list.each_slice((list.size / 2.to_f).round).to_a
318
+
319
+ table = TTY::Table.new do |t|
320
+ loop do
321
+ break if groups.all?(&:empty?)
322
+
323
+ t << groups.map(&:shift)
324
+ end
325
+ end
326
+
327
+ "```\n#{table.render(:basic, padding: [0, 2, 0, 0])}\n```"
328
+ end
329
+
330
+ def disks
331
+ # GreenHat::Disk.df({archive: []})
332
+ file = GreenHat::Disk.df({ archive: [archive.name] })
333
+
334
+ disk_list = GreenHat::Disk.markdown_format(file.first, false, 3)
335
+
336
+ # Preapre / Indent List
337
+ [
338
+ '**Disks**',
339
+ "\n\n```\n#{disk_list.join("\n")}\n```"
340
+ ].join
341
+ end
342
+
343
+ # ----------------------------
344
+ # Helpers
345
+ # ----------------------------
346
+ def percent(value, total)
347
+ ((value / total.to_f) * 100).round
348
+ end
349
+
350
+ # Helper to Make Cyan Titles
351
+ def title(name, ljust = 16)
352
+ "#{name}:".ljust(ljust)
353
+ end
354
+ end
355
+ # rubocop:enable Metrics/ClassLength
356
+ 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, :free_m, :disk_free
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
- count = application_log.data.count { |x| x&.severity == 'ERROR' }
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
  [
@@ -150,8 +150,18 @@ module GreenHat
150
150
  format_table_entry(flags, val)
151
151
  end
152
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
+
153
163
  # Main Entry Point for Filtering
154
- def self.filter_start(files, flags, args)
164
+ def self.filter_start(files, flags = {}, args = {})
155
165
  # Convert to Things
156
166
  logs = ShellHelper.find_things(files, flags).select(&:processed?)
157
167
 
@@ -189,7 +199,7 @@ module GreenHat
189
199
  # Experimenting with deep clone
190
200
  results = Marshal.load(Marshal.dump(data))
191
201
  results.select! do |row|
192
- args.send(flags.logic) do |arg|
202
+ args.send(flags.logic || :all?) do |arg|
193
203
  filter_row_key(row, arg, flags)
194
204
  end
195
205
  end
@@ -43,6 +43,13 @@ module GreenHat
43
43
  /dmesg/
44
44
  ]
45
45
  },
46
+ 'lets-encrypt/renewal' => {
47
+ format: :bracket_log,
48
+ log: true,
49
+ pattern: [
50
+ %r{lets-encrypt/renewal}
51
+ ]
52
+ },
46
53
  'repmgrd/current' => {
47
54
  format: :bracket_log,
48
55
  log: true,
@@ -225,7 +232,8 @@ module GreenHat
225
232
  format: :json,
226
233
  log: true,
227
234
  pattern: [
228
- %r{gitlab-rails/audit_json.log}
235
+ %r{gitlab-rails/audit_json.log},
236
+ %r{webservice.log/audit_json.log}
229
237
  ]
230
238
  },
231
239
  'gitlab-rails/auth.log' => {
@@ -336,6 +344,13 @@ module GreenHat
336
344
  %r{nginx/current}
337
345
  ]
338
346
  },
347
+ 'nginx-ingress.log' => {
348
+ format: :kube_nginx,
349
+ log: true,
350
+ pattern: [
351
+ /nginx-ingress.log/
352
+ ]
353
+ },
339
354
  'nginx/gitlab_pages_access.log' => {
340
355
  format: :nginx,
341
356
  log: true,
@@ -422,7 +437,7 @@ module GreenHat
422
437
  ]
423
438
  },
424
439
  'registry/current' => {
425
- format: :time_shellwords,
440
+ format: :time_registry,
426
441
  log: true,
427
442
  pattern: [
428
443
  %r{registry/current},
@@ -444,16 +459,18 @@ module GreenHat
444
459
  },
445
460
  'puma/puma_stderr.log' => {
446
461
  format: :raw,
447
- log: true,
462
+ log: false,
448
463
  pattern: [
449
- %r{puma/puma_stderr.log}
464
+ %r{puma/puma_stderr.log},
465
+ %r{webservice.log/puma.stderr.log}
450
466
  ]
451
467
  },
452
468
  'puma/puma_stdout.log' => {
453
469
  format: :json,
454
470
  log: true,
455
471
  pattern: [
456
- %r{puma/puma_stdout.log}
472
+ %r{puma/puma_stdout.log},
473
+ %r{webservice.log/puma.stdout.log}
457
474
  ]
458
475
  },
459
476
  'gitlab-pages/current' => {
@@ -745,13 +762,15 @@ module GreenHat
745
762
  'gitlab-rails/application.log' => {
746
763
  format: :raw,
747
764
  pattern: [
748
- %r{gitlab-rails/application.log}
765
+ %r{gitlab-rails/application.log},
766
+ %r{webservice.log/application.log}
749
767
  ]
750
768
  },
751
769
  'gitlab-rails/production.log' => {
752
770
  format: :raw,
753
771
  pattern: [
754
- %r{gitlab-rails/production.log}
772
+ %r{gitlab-rails/production.log},
773
+ %r{webservice.log/production.log}
755
774
  ]
756
775
  },
757
776
  'gitlab/version-manifest.txt' => {
@@ -765,6 +784,72 @@ module GreenHat
765
784
  pattern: [
766
785
  %r{sidekiq/perf.data}
767
786
  ]
787
+ },
788
+
789
+ # ======================================================================
790
+ # KubeSoS TODO Section
791
+ # Things I am going to shortcut and set to raw for now
792
+ # ======================================================================
793
+ # Attempted Parsing
794
+ 'kubesos_json' => {
795
+ log: true,
796
+ format: :kube_json,
797
+ pattern: [
798
+ /gitaly.log/
799
+ ]
800
+ },
801
+
802
+ 'kube_webservice' => {
803
+ log: false,
804
+ format: :kube_webservice,
805
+ pattern: [
806
+ /^webservice\.log$/
807
+ ]
808
+ },
809
+
810
+ 'kubesos' => {
811
+ format: :raw,
812
+ pattern: [
813
+ /all_values.yaml/,
814
+ %r{webservice.log/sidekiq_client.log},
815
+ /chart-version/,
816
+ /configmaps/,
817
+ /describe_deployments/,
818
+ /describe_ingress/,
819
+ /describe_nodes/,
820
+ /describe_pods/,
821
+ /describe_pv/,
822
+ /describe_pvc/,
823
+ /events/,
824
+ /get_deployments/,
825
+ /get_endpoints/,
826
+ /get_jobs/,
827
+ /get_pods/,
828
+ /get_pv/,
829
+ /get_pvc/,
830
+ /get_services/,
831
+ /gitaly.log/,
832
+ /gitlab-exporter.log/,
833
+ /gitlab-pages.log/,
834
+ /gitlab-shell.log/,
835
+ /grafana.log/,
836
+ /helm-version/,
837
+ /kubectl-check/,
838
+ /migrations.log/,
839
+ /minio.log/,
840
+ /nfs-client-provisioner.log/,
841
+ /operator.log/,
842
+ /postgresql.log/,
843
+ /prometheus.log/,
844
+ /redis.log/,
845
+ /registry.log/,
846
+ /secrets/,
847
+ /sidekiq.log/,
848
+ /task-runner.log/,
849
+ /top_nodes/,
850
+ /top_pods/,
851
+ /user_supplied_values.yaml/
852
+ ]
768
853
  }
769
854
  }
770
855
  end
@@ -0,0 +1,36 @@
1
+ # Top
2
+ module GreenHat
3
+ # Log
4
+ module Formatters
5
+ # ==========================================================================
6
+ # K8s logs seem to have a lot of non-json entries
7
+ # Extract all json, ignore everything else
8
+ # ==========================================================================
9
+ def format_kube_json
10
+ self.result = raw.map do |row|
11
+ # Skip all non-json
12
+ next unless row.first == '{'
13
+
14
+ result = begin
15
+ Oj.load row
16
+ rescue EncodingError
17
+ puts
18
+ next
19
+ end
20
+
21
+ # Parsing Time
22
+ format_json_traverse result
23
+
24
+ result.sort.to_h
25
+ rescue StandardError => e
26
+ # TODO: Background Logger?
27
+ e.message
28
+ LogBot.warn('JSON Parse', e.message)
29
+ next
30
+ end.compact
31
+
32
+ :ok
33
+ end
34
+ # =========================================================================
35
+ end
36
+ end
@@ -0,0 +1,48 @@
1
+ # Top
2
+ module GreenHat
3
+ # Log
4
+ module Formatters
5
+ # ==========================================================================
6
+ # Formatters
7
+ # ==========================================================================
8
+ def format_kube_nginx
9
+ self.result = raw.map do |row|
10
+ ip, _sym, remote_user, rest = row.split(' ', 4)
11
+
12
+ time, rest = rest.split(']', 2)
13
+ time = Time.strptime(time, '[%d/%b/%Y:%H:%M:%S %z')
14
+
15
+ verb, status, bytes, http_referer, http_user_agent, gzip_ratio = Shellwords.split(rest)
16
+
17
+ method, path, http_version = verb.split
18
+
19
+ {
20
+ remote_addr: ip,
21
+ remote_user: remote_user,
22
+ method: method,
23
+ path: path,
24
+ status: status,
25
+ bytes: bytes,
26
+ http_version: http_version,
27
+ http_referer: http_referer,
28
+ http_user_agent: http_user_agent,
29
+ gzip_ratio: gzip_ratio,
30
+ time: time
31
+ }
32
+
33
+ # Fall back for malformed logs
34
+ rescue StandardError
35
+ { message: row }
36
+ end
37
+
38
+ :ok
39
+ end
40
+
41
+ # rubocop:disable Layout/LineLength
42
+ # NGINX Conf: /var/opt/gitlab/nginx/conf/nginx.conf
43
+ # log_format gitlab_access '$remote_addr - $remote_user [$time_local] "$request_method $filtered_request_uri $server_protocol" $status $body_bytes_sent "$filtered_http_referer" "$http_user_agent" $gzip_ratio';
44
+ # rubocop:enable Layout/LineLength
45
+
46
+ # ==========================================================================
47
+ end
48
+ end
@@ -0,0 +1,51 @@
1
+ # Top
2
+ module GreenHat
3
+ # Log
4
+ module Formatters
5
+ # ==========================================================================
6
+ # Gitlab Tail Formatter
7
+ # ==========================================================================
8
+ def format_kube_webservice
9
+ # Revert to raw for cats
10
+ self.kind = :raw
11
+
12
+ output = {}
13
+ current_log = nil
14
+
15
+ raw.each do |line|
16
+ next if line.blank?
17
+
18
+ if line.include? '*** '
19
+ current_log = /\*\*\* (.+?) \*\*\*/.match(line).captures.first
20
+ else
21
+ output[current_log] ||= [] unless current_log.nil?
22
+ output[current_log].push line unless current_log.nil?
23
+ end
24
+ end
25
+
26
+ # Remove Empty Entries
27
+ output.reject { |_k, v| v.empty? }
28
+
29
+ # Root Dir
30
+ root_dir = "#{$TMP}/#{name}"
31
+ Dir.mkdir(root_dir)
32
+
33
+ # Write Files / Create Things
34
+ output.each do |k, v|
35
+ file_name = k.gsub('/var/log/gitlab/', '')
36
+
37
+ dir = "#{root_dir}/gitlab-rails"
38
+ Dir.mkdir(dir) unless File.exist?(dir)
39
+
40
+ File.write("#{root_dir}/#{file_name}", v.join("\n"))
41
+
42
+ # Thing Setup
43
+ archive.things_create(file: "#{root_dir}/#{file_name}").setup
44
+ end
45
+
46
+ # Link
47
+ self.result = raw
48
+ end
49
+ # ==========================================================================
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ # Top
2
+ module GreenHat
3
+ # Log
4
+ module Formatters
5
+ # ==========================================================================
6
+ # Registry Split
7
+ # ==========================================================================
8
+ # https://docs.docker.com/registry/configuration/#log
9
+ # Formatters text, json
10
+ # registry['log_formatter'] = "json"
11
+ # TODO: Logstash (Not working in 14.3)
12
+
13
+ def format_time_registry
14
+ self.result = raw.map do |row|
15
+ result = row[0] == '{' ? registry_json(row) : registry_shell_words(row)
16
+
17
+ # Timestamp Parsing
18
+ result.ts = Time.parse result.ts if result.key? 'ts'
19
+ result.time = Time.parse(result.time)
20
+
21
+ result
22
+ end
23
+ end
24
+
25
+ def registry_shell_words(row)
26
+ time, msg = row.split(' ', 2)
27
+
28
+ output = Shellwords.split(msg).each_with_object({}) do |x, h|
29
+ key, value = x.split('=')
30
+ next if value.nil?
31
+
32
+ h[key.to_sym] = value.numeric? ? value.to_f : value
33
+ end
34
+
35
+ output[:time] = time
36
+
37
+ output
38
+ end
39
+
40
+ def registry_json(msg)
41
+ Oj.load(msg)
42
+ rescue EncodingError
43
+ { message: msg }
44
+ end
45
+ # ==========================================================================
46
+ end
47
+ end
@@ -1,3 +1,3 @@
1
1
  module GreenHat
2
- VERSION = '0.3.3'.freeze
2
+ VERSION = '0.3.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: greenhat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Davin Walker
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-24 00:00:00.000000000 Z
11
+ date: 2021-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: amazing_print
@@ -425,6 +425,7 @@ files:
425
425
  - lib/greenhat/shell/help.rb
426
426
  - lib/greenhat/shell/list.rb
427
427
  - lib/greenhat/shell/log.rb
428
+ - lib/greenhat/shell/markdown.rb
428
429
  - lib/greenhat/shell/memory.rb
429
430
  - lib/greenhat/shell/network.rb
430
431
  - lib/greenhat/shell/page.rb
@@ -444,9 +445,13 @@ files:
444
445
  - lib/greenhat/thing/formatters/gitlab_status.rb
445
446
  - lib/greenhat/thing/formatters/json.rb
446
447
  - lib/greenhat/thing/formatters/json_shellwords.rb
448
+ - lib/greenhat/thing/formatters/kube_json.rb
449
+ - lib/greenhat/thing/formatters/kube_nginx.rb
450
+ - lib/greenhat/thing/formatters/kube_webservice.rb
447
451
  - lib/greenhat/thing/formatters/multiline_json.rb
448
452
  - lib/greenhat/thing/formatters/nginx.rb
449
453
  - lib/greenhat/thing/formatters/raw.rb
454
+ - lib/greenhat/thing/formatters/registry.rb
450
455
  - lib/greenhat/thing/formatters/shellwords.rb
451
456
  - lib/greenhat/thing/formatters/syslog.rb
452
457
  - lib/greenhat/thing/formatters/table.rb