greenhat 0.3.0 → 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.
@@ -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
@@ -2,11 +2,17 @@ module GreenHat
2
2
  # Root Level Shell / Report Helper
3
3
  module Shell
4
4
  def self.report(raw)
5
- args = {
6
- raw: raw.include?('--raw')
7
- }
5
+ _files, flags, _args = Args.parse(raw)
8
6
 
9
- ShellHelper.show(Archive.all.map(&:report).map(&:show).flatten, args)
7
+ archives = if flags.archive
8
+ Archive.all.select do |archive|
9
+ flags.archive.any? { |x| archive.name.include? x.to_s }
10
+ end
11
+ else
12
+ Archive.all
13
+ end
14
+
15
+ ShellHelper.show(archives.map(&:report).map(&:show).flatten, flags)
10
16
  end
11
17
  end
12
18
  end
@@ -19,7 +25,8 @@ module GreenHat
19
25
 
20
26
  attr_accessor :archive, :host, :os_release, :selinux_status, :cpu, :uname,
21
27
  :timedatectl, :uptime, :meminfo, :gitlab_manifest, :gitlab_status,
22
- :production_log, :api_log, :application_log, :sidekiq_log, :free_m, :disk_free
28
+ :production_log, :api_log, :application_log, :sidekiq_log,
29
+ :exceptions_log, :gitaly_log, :free_m, :disk_free
23
30
 
24
31
  # Find Needed Files for Report
25
32
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
@@ -39,6 +46,8 @@ module GreenHat
39
46
  self.production_log = archive.things.find { |x| x.name == 'gitlab-rails/production_json.log' }
40
47
  self.api_log = archive.things.find { |x| x.name == 'gitlab-rails/api_json.log' }
41
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' }
42
51
  self.sidekiq_log = archive.things.find { |x| x.name == 'sidekiq/current' }
43
52
  self.disk_free = archive.things.find { |x| x.name == 'df_h' }
44
53
  end
@@ -80,9 +89,11 @@ module GreenHat
80
89
  output << gitlab_services if gitlab_status
81
90
  output << title('Errors') if production_log || api_log || application_log || sidekiq_log
82
91
  output << production_errors if production_log
83
- output << api_errors if api_log
84
92
  output << application_errors if application_log
85
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
86
97
 
87
98
  # Final Space / Return
88
99
  output << ''
@@ -90,6 +101,26 @@ module GreenHat
90
101
  end
91
102
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/MethodLength
92
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
+
93
124
  def production_errors
94
125
  count = production_log.data.count { |x| x.status == 500 }
95
126
  color = count.zero? ? :green : :red
@@ -111,7 +142,13 @@ module GreenHat
111
142
  end
112
143
 
113
144
  def application_errors
114
- 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' }
115
152
  color = count.zero? ? :green : :red
116
153
 
117
154
  [
@@ -120,10 +120,12 @@ module GreenHat
120
120
  entry = entry.map { |k, v| [k, format_table_entry(flags, v, k)] }.to_h
121
121
  # Pre-format Entry
122
122
 
123
+ table_style = flags[:table_style]&.to_sym || :unicode
124
+
123
125
  table = TTY::Table.new(header: entry.keys, rows: [entry], orientation: :vertical)
124
126
 
125
127
  LogBot.debug('Rendering Entries') if ENV['DEBUG']
126
- table.render(:unicode, padding: [0, 1, 0, 1], multiline: true) do |renderer|
128
+ table.render(table_style, padding: [0, 1, 0, 1], multiline: true) do |renderer|
127
129
  renderer.border.style = :cyan
128
130
  end
129
131
 
@@ -148,8 +150,18 @@ module GreenHat
148
150
  format_table_entry(flags, val)
149
151
  end
150
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
+
151
163
  # Main Entry Point for Filtering
152
- def self.filter_start(files, flags, args)
164
+ def self.filter_start(files, flags = {}, args = {})
153
165
  # Convert to Things
154
166
  logs = ShellHelper.find_things(files, flags).select(&:processed?)
155
167
 
@@ -182,9 +194,12 @@ module GreenHat
182
194
  # TODO: Simplify
183
195
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
184
196
  def self.filter(data, flags = {}, args = {})
185
- results = data.clone.flatten.compact
197
+ # results = data.clone.flatten.compact
198
+
199
+ # Experimenting with deep clone
200
+ results = Marshal.load(Marshal.dump(data))
186
201
  results.select! do |row|
187
- args.send(flags.logic) do |arg|
202
+ args.send(flags.logic || :all?) do |arg|
188
203
  filter_row_key(row, arg, flags)
189
204
  end
190
205
  end
@@ -192,6 +207,9 @@ module GreenHat
192
207
  # Ensure presecense of a specific field
193
208
  results = filter_exists(results, flags[:exists]) if flags.key?(:exists)
194
209
 
210
+ # Time Zone
211
+ results = filter_modify_timezone(results, flags[:time_zone]) if flags.key?(:time_zone)
212
+
195
213
  # Time Filtering
196
214
  results = filter_time(results, flags) if flags.key?(:start) || flags.key?(:end)
197
215
 
@@ -217,24 +235,34 @@ module GreenHat
217
235
  results.reverse! if flags[:reverse]
218
236
 
219
237
  # Count occurrences / Skip Results
220
- return filter_stats(results, flags[:stats]) if flags.key?(:stats)
238
+ return filter_stats(results, flags) if flags.key?(:stats)
239
+
240
+ # Limit before Pluck / Flattening
241
+ results = filter_limit(results, flags[:limit]) if flags.key?(:limit)
221
242
 
222
243
  # Pluck
223
244
  results = filter_pluck(results, flags[:pluck]) if flags.key?(:pluck)
224
245
 
225
- # Limit / Ensure Exists and Valid Number
226
- if flags.key?(:limit) && flags[:limit]
227
- # Old
228
- # results[0..flags[:limit].map(&:to_s).join.to_i - 1]
246
+ results
247
+ end
248
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
229
249
 
230
- # New
231
- results.shift flags[:limit]
250
+ # Limit / Ensure Exists and Valid Number
251
+ def self.filter_limit(results, limit)
252
+ return results unless limit.integer? && limit.positive?
232
253
 
233
- else
234
- results
254
+ results.shift limit
255
+ end
256
+
257
+ def self.filter_modify_timezone(results, time_zone)
258
+ results.each do |x|
259
+ next unless x.key? :time
260
+
261
+ x[:time] = x[:time].in_time_zone time_zone
235
262
  end
263
+
264
+ results
236
265
  end
237
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
238
266
 
239
267
  # Filter Start and End Times
240
268
  # rubocop:disable Metrics/MethodLength
@@ -339,7 +367,9 @@ module GreenHat
339
367
  end.inject(:&)
340
368
  end
341
369
 
342
- def self.filter_stats(results, stats)
370
+ def self.filter_stats(results, flags)
371
+ stats = flags[:stats]
372
+
343
373
  # Avoid Empty Results
344
374
  if stats.empty?
345
375
  filter_empty_arg('stats')
@@ -348,7 +378,7 @@ module GreenHat
348
378
 
349
379
  # Loop through Stats, Separate Hash/Tables
350
380
  stats.map do |field|
351
- occurrences = filter_count_occurrences(results, field)
381
+ occurrences = filter_count_occurrences(results, field, flags)
352
382
 
353
383
  # Total Occurences
354
384
  total = occurrences.values.sum
@@ -367,6 +397,13 @@ module GreenHat
367
397
  # Append Header / Total with field name
368
398
  output.unshift([field.to_s.pastel(:bright_black), total])
369
399
 
400
+ # Use Truncate For Long Keys
401
+ if flags[:truncate]
402
+ output.map! do |key, value|
403
+ [key.to_s[0..flags[:truncate]], value]
404
+ end
405
+ end
406
+
370
407
  # Format
371
408
  output.to_h
372
409
  end
@@ -378,10 +415,17 @@ module GreenHat
378
415
  end
379
416
 
380
417
  # Helper to Count occurrences
381
- def self.filter_count_occurrences(results, field)
418
+ def self.filter_count_occurrences(results, field, flags = {})
382
419
  results.each_with_object(Hash.new(0)) do |entry, counts|
383
420
  if entry.key? field
384
- counts[entry[field]] += 1
421
+ # Rounding in pagination breaks stats
422
+ key = if flags.key?(:round) && entry[field].numeric?
423
+ entry[field].to_f.round(flags.round)
424
+ else
425
+ entry[field]
426
+ end
427
+
428
+ counts[key] += 1
385
429
  else
386
430
  counts['None'.pastel(:bright_black)] += 1
387
431
  end
@@ -440,6 +484,32 @@ module GreenHat
440
484
  end
441
485
  end
442
486
 
487
+ # Total Count Helper
488
+ def self.fields_print(results)
489
+ results.each do |k, v|
490
+ puts k
491
+ puts field_table(v.map(&:keys).flatten.uniq.sort)
492
+ puts
493
+ end
494
+ end
495
+
496
+ def self.field_table(list, columns = 4)
497
+ return nil if list.size.zero?
498
+
499
+ # Keep Alphabetical Sort
500
+ groups = list.each_slice((list.size / columns.to_f).round).to_a
501
+
502
+ table = TTY::Table.new do |t|
503
+ loop do
504
+ break if groups.all?(&:empty?)
505
+
506
+ t << groups.map(&:shift)
507
+ end
508
+ end
509
+
510
+ table.render(:unicode, padding: [0, 1, 0, 1])
511
+ end
512
+
443
513
  # Unified Files Interface
444
514
  def self.files(file_list, base_list = nil, flags = {})
445
515
  base_list ||= Thing.all