greenhat 0.3.3 → 0.3.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 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