greenhat 0.3.5 → 0.3.6

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/lib/greenhat/accessors/disk.rb +1 -3
  3. data/lib/greenhat/accessors/gitlab.rb +5 -2
  4. data/lib/greenhat/archive.rb +7 -2
  5. data/lib/greenhat/cli.rb +36 -0
  6. data/lib/greenhat/host.rb +25 -37
  7. data/lib/greenhat/shell/args.rb +22 -9
  8. data/lib/greenhat/shell/faststats.rb +23 -3
  9. data/lib/greenhat/shell/field_helper.rb +1 -1
  10. data/lib/greenhat/shell/filter_help.rb +32 -10
  11. data/lib/greenhat/shell/log.rb +142 -7
  12. data/lib/greenhat/shell/markdown.rb +16 -8
  13. data/lib/greenhat/shell/old_search_helper.rb +54 -0
  14. data/lib/greenhat/shell/page.rb +1 -1
  15. data/lib/greenhat/shell/pipe.rb +31 -0
  16. data/lib/greenhat/shell/platform.rb +28 -0
  17. data/lib/greenhat/shell/report.rb +47 -15
  18. data/lib/greenhat/shell/shell_helper.rb +92 -101
  19. data/lib/greenhat/shell.rb +7 -0
  20. data/lib/greenhat/thing/file_types.rb +28 -0
  21. data/lib/greenhat/thing/formatters/json.rb +4 -0
  22. data/lib/greenhat/thing/formatters/nginx.rb +6 -2
  23. data/lib/greenhat/thing/formatters/time_space.rb +0 -16
  24. data/lib/greenhat/thing/helpers.rb +12 -0
  25. data/lib/greenhat/thing.rb +10 -0
  26. data/lib/greenhat/version.rb +1 -1
  27. data/lib/greenhat/views/api.slim +55 -0
  28. data/lib/greenhat/views/chart.slim +42 -0
  29. data/lib/greenhat/views/chart_template.slim +31 -0
  30. data/lib/greenhat/views/chartkick.js +21 -0
  31. data/lib/greenhat/views/css.slim +47 -0
  32. data/lib/greenhat/views/gitaly.slim +53 -0
  33. data/lib/greenhat/views/headers.slim +16 -0
  34. data/lib/greenhat/views/index-old.slim +51 -0
  35. data/lib/greenhat/views/index.slim +14 -14
  36. data/lib/greenhat/views/info.slim +17 -18
  37. data/lib/greenhat/views/production.slim +55 -0
  38. data/lib/greenhat/views/sidekiq.slim +55 -0
  39. data/lib/greenhat/views/time.slim +63 -0
  40. data/lib/greenhat/views/workhorse.slim +16 -0
  41. data/lib/greenhat/web/api.rb +94 -0
  42. data/lib/greenhat/web/chartkick_shim.rb +14 -0
  43. data/lib/greenhat/web/faststats.rb +44 -0
  44. data/lib/greenhat/web/gitaly.rb +65 -0
  45. data/lib/greenhat/web/helpers.rb +198 -0
  46. data/lib/greenhat/web/production.rb +104 -0
  47. data/lib/greenhat/web/sidekiq.rb +73 -0
  48. data/lib/greenhat/web/stats_helpers.rb +74 -0
  49. data/lib/greenhat/web/time.rb +36 -0
  50. data/lib/greenhat/web/workhorse.rb +43 -0
  51. data/lib/greenhat/web.rb +63 -19
  52. data/lib/greenhat.rb +1 -0
  53. metadata +68 -2
@@ -103,7 +103,8 @@ module GreenHat
103
103
  StringColor.do(key, entry)
104
104
  end
105
105
 
106
- if flags[:truncate]
106
+ # Stats truncation handled separately
107
+ if flags[:truncate] && !flags[:stats]
107
108
  entry_truncate(formatted_entry, flags[:truncate])
108
109
  else
109
110
  formatted_entry
@@ -157,6 +158,9 @@ module GreenHat
157
158
  files, flags, args = Args.parse(Shellwords.split(search))
158
159
  flags[:combine] = true
159
160
 
161
+ # Default to everything
162
+ files = Thing.all.map(&:name) if files.empty?
163
+
160
164
  ShellHelper.filter_start(files, flags, args)
161
165
  end
162
166
 
@@ -177,11 +181,17 @@ module GreenHat
177
181
 
178
182
  # Include Total Count in Name
179
183
  results = ShellHelper.filter(log.data, flags, args)
184
+ duration = calculate_duration(results)
185
+
180
186
  title = [
181
187
  log.friendly_name,
182
188
  " #{results.count}".pastel(:bright_black)
189
+
183
190
  ]
184
191
 
192
+ # Append Duration
193
+ title.push(" #{duration.pastel(:cyan, :dim)}") unless duration.blank?
194
+
185
195
  # Save unless empty
186
196
  obj[title.join] = results unless results.count.zero?
187
197
 
@@ -190,6 +200,31 @@ module GreenHat
190
200
  end
191
201
  end
192
202
 
203
+ def self.calculate_duration(results)
204
+ only_with_time = results.select(&:time)
205
+
206
+ # If slice is used ignore
207
+ return nil if only_with_time.empty?
208
+
209
+ sorted = only_with_time.map(&:time).sort
210
+ humanize_time(sorted.first, sorted.last)
211
+ end
212
+
213
+ # Replace TimeDifference with https://stackoverflow.com/a/4136485/1678507
214
+ def self.humanize_time(time_start, time_end, increments = 2)
215
+ miliseconds = (time_end - time_start) * 1000
216
+
217
+ list = [[1000, :ms], [60, :s], [60, :m], [24, :h]].map do |count, name|
218
+ next unless miliseconds.positive?
219
+
220
+ miliseconds, n = miliseconds.divmod(count)
221
+
222
+ "#{n.to_i}#{name}" unless n.to_i.zero?
223
+ end
224
+
225
+ list.compact.reverse[0..increments - 1].join(' ')
226
+ end
227
+
193
228
  # Filter Logic
194
229
  # TODO: Simplify
195
230
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -265,7 +300,6 @@ module GreenHat
265
300
  end
266
301
 
267
302
  # Filter Start and End Times
268
- # rubocop:disable Metrics/MethodLength
269
303
  # TODO: This is a bit icky, simplify/dry
270
304
  def self.filter_time(results, flags)
271
305
  if flags.key?(:start)
@@ -302,7 +336,6 @@ module GreenHat
302
336
 
303
337
  results
304
338
  end
305
- # rubocop:enable Metrics/MethodLength
306
339
 
307
340
  def self.filter_except(results, except)
308
341
  # Avoid Empty Results
@@ -380,6 +413,9 @@ module GreenHat
380
413
  stats.map do |field|
381
414
  occurrences = filter_count_occurrences(results, field, flags)
382
415
 
416
+ # Use Truncate For Long Keys
417
+ occurrences.transform_keys! { |key| key.to_s[0..flags[:truncate]] } if flags[:truncate]
418
+
383
419
  # Total Occurences
384
420
  total = occurrences.values.sum
385
421
 
@@ -397,13 +433,6 @@ module GreenHat
397
433
  # Append Header / Total with field name
398
434
  output.unshift([field.to_s.pastel(:bright_black), total])
399
435
 
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
-
407
436
  # Format
408
437
  output.to_h
409
438
  end
@@ -443,48 +472,83 @@ module GreenHat
443
472
  end
444
473
 
445
474
  # Break out filter row logic into separate method
446
-
447
475
  def self.filter_row_key(row, arg, flags)
448
476
  # Ignore Other Logic if Field isn't even included / Full Text Searching
449
477
  return false unless row.key?(arg[:field]) || arg[:field] == :text
450
478
 
451
479
  # Sensitivity Check / Check for Match / Full Text Searching
452
- included = if arg[:field] == :text
453
- filter_row_entry(row.to_s, arg, flags)
454
- else
455
- filter_row_entry(row[arg.field].to_s, arg, flags)
456
- end
480
+ search_data = arg[:field] == :text ? row : row[arg.field]
481
+ match = filter_row_entry(search_data.to_s, arg, flags)
457
482
 
458
483
  # Pivot of off include vs exclude
459
484
  if arg.bang
460
- !included
485
+ !match
461
486
  else
462
- included
487
+ match
463
488
  end
464
489
  end
465
490
 
466
491
  # Field Partial / Case / Exact search
467
492
  def self.filter_row_entry(entry, arg, flags)
468
493
  # Exact Matching / Unless doing full text search
469
- return entry.to_s == arg.value.to_s if flags.key?(:exact) && arg.field != :text
494
+ return entry == arg.value.to_s if flags.key?(:exact) && arg.field != :text
470
495
 
471
- if flags.key?(:case)
472
- entry.include? arg.value.to_s
473
- else
474
- entry.downcase.include? arg.value.to_s.downcase
496
+ # Cast to String/Integer Helper
497
+ entry, value = filter_entry_cast(entry, arg, flags)
498
+
499
+ entry.send(arg.logic, value)
500
+ end
501
+
502
+ # Handle casting to strings or integers
503
+ def self.filter_entry_cast(entry, arg, flags)
504
+ # Cast to String
505
+ value = arg.value.to_s
506
+
507
+ case arg.logic
508
+ when :include?
509
+
510
+ # Exact Case argument
511
+ unless flags.key?(:case)
512
+ entry.downcase!
513
+ value.downcase!
514
+ end
515
+ when :>=, :<=
516
+ entry = entry.to_i if entry.numeric?
517
+ value = value.to_i if value&.numeric?
475
518
  end
519
+
520
+ [entry, value]
521
+ end
522
+
523
+ def self.filter_entry_logic(entry, arg)
524
+ entry.send(arg.logic, arg.value)
476
525
  end
477
526
 
478
527
  # Total Count Helper
479
- def self.total_count(results)
480
- results.each do |k, v|
528
+ def self.total_count(results, flags)
529
+ results.map do |k, v|
481
530
  puts k
482
- puts "Total: #{v.count.to_s.pastel(:blue)}"
531
+ # puts "Total: #{v.count.to_s.pastel(:blue)}"
532
+ # puts "Time Covered: #{calculate_duration(v).pastel(:cyan)}"
533
+
534
+ output = {}
535
+ output[:total] = v.count
536
+ output[:duration] = calculate_duration(v)
537
+
538
+ # Sort / Get first and Last
539
+ list = v.select(&:time).map(&:time)
540
+ unless list.blank?
541
+ output[:start] = list.first
542
+ output[:end] = list.last
543
+ end
544
+
545
+ puts render_table(output, flags)
546
+
483
547
  puts
484
548
  end
485
549
  end
486
550
 
487
- # Total Count Helper
551
+ # Table Printer Helper
488
552
  def self.fields_print(results)
489
553
  results.each do |k, v|
490
554
  puts k
@@ -548,7 +612,7 @@ module GreenHat
548
612
  return file if file.instance_of?(Thing)
549
613
 
550
614
  if flags.fuzzy_file_match
551
- Thing.all.select { |x| x.name.include? file }
615
+ Thing.all.select { |x| x.name.include?(file) || x.type.include?(file) }
552
616
  else
553
617
  Thing.where name: file
554
618
  end
@@ -560,62 +624,6 @@ module GreenHat
560
624
  things
561
625
  end
562
626
 
563
- # Main Entry Point for Searching
564
- # def self.search_start(log_list, filter_type, args, opts)
565
- def self.search_start(files, flags, args)
566
- # Convert to Things
567
- logs = ShellHelper.find_things(files, flags)
568
-
569
- logs.each_with_object({}) do |log, obj|
570
- # Ignore Empty Results / No Thing
571
- next if log&.data.blank?
572
-
573
- obj[log.friendly_name] = ShellHelper.search(log.data, flags, args)
574
-
575
- obj
576
- end
577
- end
578
-
579
- # Generic Search Helper / String/Regex
580
- def self.search(data, flags = {}, args = {})
581
- results = data.clone.flatten.compact
582
- results.select! do |row|
583
- args.send(flags.logic) do |arg|
584
- search_row(row, arg, flags)
585
- end
586
- end
587
-
588
- # Strip Results if Slice is defined
589
- results.map! { |row| row.slice(*flags[:slice]) } if flags[:slice]
590
-
591
- # Strip Results if Except is defined
592
- results.map! { |row| row.except(*flags[:except]) } if flags[:except]
593
-
594
- # Remove Blank from either slice or except
595
- results.reject!(&:empty?)
596
-
597
- results
598
- end
599
-
600
- # Break out filter row logic into separate method
601
- def self.search_row(row, arg, flags)
602
- # Sensitivity Check / Check for Match
603
- included = filter_row_entry(row.to_s, arg, flags)
604
-
605
- # Pivot of off include vs exclude
606
- if arg.bang
607
- !included
608
- else
609
- included
610
- end
611
- end
612
-
613
- # TODO: Remove?
614
- # Color Reader Helper
615
- # def self.pastel
616
- # @pastel ||= Pastel.new
617
- # end
618
-
619
627
  # Number Helper
620
628
  # https://gitlab.com/zedtux/human_size_to_number/-/blob/master/lib/human_size_to_number/helper.rb
621
629
  def self.human_size_to_number(string)
@@ -637,23 +645,6 @@ module GreenHat
637
645
  number.round
638
646
  end
639
647
 
640
- # TODO: Needed?
641
- def self.filter_and(data, params = {})
642
- result = data.clone.flatten.compact
643
- params.each do |k, v|
644
- result.select! do |row|
645
- if row.key? k.to_sym
646
- row[k.to_sym].include? v
647
- else
648
- false
649
- end
650
- end
651
- next
652
- end
653
-
654
- result
655
- end
656
-
657
648
  # General Helper for `show`
658
649
  def self.common_opts
659
650
  puts 'Common Options'.pastel(:blue)
@@ -7,6 +7,13 @@ module GreenHat
7
7
  # rubocop:enable Lint/Debugger
8
8
  end
9
9
 
10
+ def self.web
11
+ # Load Required Files
12
+ require 'greenhat/web'
13
+
14
+ GreenHat::Web.toggle
15
+ end
16
+
10
17
  def self.df
11
18
  Disk.df
12
19
  end
@@ -357,6 +357,13 @@ module GreenHat
357
357
  /nginx-ingress.log/
358
358
  ]
359
359
  },
360
+ 'nginx/access.log' => {
361
+ format: :nginx,
362
+ log: true,
363
+ pattern: [
364
+ %r{nginx/access.log}
365
+ ]
366
+ },
360
367
  'nginx/gitlab_pages_access.log' => {
361
368
  format: :nginx,
362
369
  log: true,
@@ -552,6 +559,20 @@ module GreenHat
552
559
  %r{unicorn/current}
553
560
  ]
554
561
  },
562
+ 'unicorn_stats' => {
563
+ format: :time_space,
564
+ log: true,
565
+ pattern: [
566
+ /unicorn_stats/
567
+ ]
568
+ },
569
+ 'unicorn/unicorn_stdout.log' => {
570
+ format: :raw,
571
+ log: true,
572
+ pattern: [
573
+ %r{unicorn/unicorn_stdout.log}
574
+ ]
575
+ },
555
576
  'running_swappiness' => {
556
577
  format: :raw,
557
578
  pattern: [
@@ -589,6 +610,7 @@ module GreenHat
589
610
  log: true,
590
611
  pattern: [
591
612
  %r{sidekiq/current},
613
+ /sidekiq_current/,
592
614
  %r{sidekiq/@.*}
593
615
  ]
594
616
  },
@@ -652,6 +674,12 @@ module GreenHat
652
674
  /uname/
653
675
  ]
654
676
  },
677
+ 'gitlab_socket_stats' => {
678
+ format: :gitlab_socket_stats,
679
+ pattern: [
680
+ /gitlab_socket_stats/
681
+ ]
682
+ },
655
683
  'uptime' => {
656
684
  format: :raw,
657
685
  pattern: [
@@ -43,6 +43,10 @@ module GreenHat
43
43
  # Check for Common Fields
44
44
  def format_json_time(result)
45
45
  result.time = format_time_parse(result.time) if result.key? :time
46
+
47
+ # If timestamp exists but time doesn't, add time
48
+ result.time = format_time_parse(result.timestamp) if result.key?(:timestamp) && !result.key?(:time)
49
+
46
50
  result.created_at = format_time_parse(result.created_at) if result.key? :created_at
47
51
  result.enqueued_at = format_time_parse(result.enqueued_at) if result.key? :enqueued_at
48
52
  rescue StandardError => e
@@ -7,10 +7,11 @@ module GreenHat
7
7
  # ==========================================================================
8
8
  def format_nginx
9
9
  self.result = raw.map do |row|
10
- ip, _sym, remote_user, rest = row.split(' ', 4)
10
+ conn_data, rest = row.split(' [', 2)
11
+ ip, _sym, remote_user = conn_data.split(' ', 3)
11
12
 
12
13
  time, rest = rest.split(']', 2)
13
- time = Time.strptime(time, '[%d/%b/%Y:%H:%M:%S %z')
14
+ time = Time.strptime(time, '%d/%b/%Y:%H:%M:%S %z')
14
15
 
15
16
  verb, status, bytes, http_referer, http_user_agent, gzip_ratio = Shellwords.split(rest)
16
17
 
@@ -29,6 +30,9 @@ module GreenHat
29
30
  gzip_ratio: gzip_ratio,
30
31
  time: time
31
32
  }
33
+ rescue StandardError => e
34
+ LogBot.warn('NGINX Parse', e.message)
35
+ { msg: row }
32
36
  end
33
37
 
34
38
  :ok
@@ -18,19 +18,3 @@ module GreenHat
18
18
  # ==========================================================================
19
19
  end
20
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
@@ -69,3 +69,15 @@ class FalseClass
69
69
  0
70
70
  end
71
71
  end
72
+
73
+ # https://stackoverflow.com/a/449322/1678507
74
+ class Time
75
+ # Time#round already exists with different meaning in Ruby 1.9
76
+ def round_off(seconds = 60)
77
+ Time.at((to_f / seconds).round * seconds).utc
78
+ end
79
+
80
+ def floor(seconds = 60)
81
+ Time.at((to_f / seconds).floor * seconds).utc
82
+ end
83
+ end
@@ -140,4 +140,14 @@ class Thing < Teron
140
140
 
141
141
  []
142
142
  end
143
+
144
+ def query_save(results, name)
145
+ self.archive = Archive.first
146
+ self.result = results
147
+ self.name = name
148
+ self.log = true
149
+ self.parsed = true
150
+ self.result_fields = field_processing
151
+ save!
152
+ end
143
153
  end
@@ -1,3 +1,3 @@
1
1
  module GreenHat
2
- VERSION = '0.3.5'.freeze
2
+ VERSION = '0.3.6'.freeze
3
3
  end
@@ -0,0 +1,55 @@
1
+ == slim :chart_template
2
+ .row
3
+ .chart-menu
4
+ ul.section.table-of-contents
5
+ li
6
+ a href="#top" Top
7
+ li
8
+ a href="#path" Path
9
+ li
10
+ a href="#time" Time
11
+
12
+ h3.white-text.center API
13
+
14
+ h4.col.s12.white-text.center id="top" Top
15
+ .col.s12.card.padding.z-depth-4
16
+ == bubble_chart(faststats_top('gitlab-rails/api_json.log', :project_stats), xtitle:'Duration', ytitle:'% Count', label: "Total", title: 'by Project')
17
+ .col.s12.padding
18
+ .legend X: Duration
19
+ .legend.red Y: % of total calls
20
+ .legend.blue Size: Total Calls
21
+
22
+
23
+ .col.s12.card.padding.z-depth-4
24
+ == bubble_chart(faststats_top('gitlab-rails/api_json.log', :user_stats), xtitle:'Duration', ytitle:'% Count', label: "Total", title: 'by User')
25
+ .col.s12.padding
26
+ .legend X: Duration
27
+ .legend.red Y: % of total calls
28
+ .legend.blue Size: Total Calls
29
+
30
+ .col.s12.card.padding.z-depth-4
31
+ == pie_chart(api_duration_pie, title: 'Duration Total')
32
+
33
+ h4.col.s12.white-text.center id="path" Path
34
+ .col.s12.card.padding.z-depth-4
35
+ == pie_chart(faststats_field('gitlab-rails/api_json.log', :count), title: 'Total by Path')
36
+
37
+ .col.s12.card.padding.z-depth-4
38
+ == column_chart(faststats_field('gitlab-rails/api_json.log',:p95), title: 'p95', ytitle: 'ms')
39
+
40
+ .col.s12.card.padding.z-depth-4
41
+ == column_chart(faststats_field('gitlab-rails/api_json.log',:median), title: 'Median', ytitle: 'ms')
42
+
43
+ h4.col.s12.white-text.center id="time" Over Time
44
+
45
+ .col.s12.card.padding.z-depth-4
46
+ == area_chart(api_avg_path_duration_series, title: 'Path Duration', ytitle: 'seconds')
47
+
48
+ .col.s12.card.padding.z-depth-4
49
+ == area_chart(api_avg_duration_series, title: 'Avg Duration', ytitle: 'ms')
50
+
51
+ .col.s12.card.padding.z-depth-4
52
+ == column_chart(api_duration_series_stacked, stacked: true, title: 'Duration Breakdown', ytitle: 'seconds')
53
+
54
+ .col.s12.card.padding.z-depth-4
55
+ == area_chart(api_duration_series_stacked, stacked: true, title: 'Duration Breakdown', ytitle: 'seconds')
@@ -0,0 +1,42 @@
1
+ doctype html
2
+ html
3
+ head
4
+ == slim :headers
5
+ body.grey.darken-4
6
+ == slim :css
7
+
8
+ nav.blue-grey.darken-4
9
+ .nav-wrapper
10
+ a.brand-logo.center href="/" Charts Home
11
+ ul.left.white-text
12
+ li
13
+ a href ='/'
14
+ .btn-flat.white-text
15
+ i.fa.fa-home.left.line-height
16
+ | Home
17
+
18
+ li
19
+ a href ='/chart'
20
+ .btn-flat.white-text
21
+ i.fa.fa-chart-pie.left.line-height
22
+ | Charts
23
+ li
24
+ a href ='/chart/time'
25
+ .btn-flat.white-text
26
+ i.fa.fa-chart-line.left.line-height
27
+ | Time Query
28
+
29
+ .container.padding
30
+ .row
31
+ h5.white-text.center Dashboards
32
+ .row
33
+ .col.s4.padding
34
+ a.blue.btn-large.chart-btn href="/charts/gitaly" Gitaly
35
+ .col.s4.padding
36
+ a.green.btn-large.chart-btn href="/charts/sidekiq" Sidekiq
37
+ .col.s4.padding
38
+ a.red.btn-large.chart-btn href="/charts/api" Api
39
+ .col.s4.padding
40
+ a.purple.btn-large.chart-btn href="/charts/production" Production
41
+ .col.s4.padding
42
+ a.teal.btn-large.chart-btn href="/charts/workhorse" Workhorse
@@ -0,0 +1,31 @@
1
+ doctype html
2
+ html
3
+ head
4
+ == slim :headers
5
+
6
+ body.grey.darken-4
7
+ == slim :css
8
+
9
+ nav.blue-grey.darken-4
10
+ .nav-wrapper
11
+ a.brand-logo.center href="/" Charts Home
12
+ ul.left.white-text
13
+ li
14
+ a href ='/'
15
+ .btn-flat.white-text
16
+ i.fa.fa-home.left.line-height
17
+ | Home
18
+
19
+ li
20
+ a href ='/chart'
21
+ .btn-flat.white-text
22
+ i.fa.fa-chart-pie.left.line-height
23
+ | Charts
24
+ li
25
+ a href ='/chart/time'
26
+ .btn-flat.white-text
27
+ i.fa.fa-chart-line.left.line-height
28
+ | Time Query
29
+
30
+ .container.padding
31
+ == yield