status_page_ruby 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +25 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +10 -0
  5. data/Gemfile +14 -0
  6. data/Gemfile.lock +61 -0
  7. data/README.md +53 -0
  8. data/bin/status-page +6 -0
  9. data/lib/status_page_ruby.rb +20 -0
  10. data/lib/status_page_ruby/pages/base.rb +52 -0
  11. data/lib/status_page_ruby/pages/bitbucket.rb +14 -0
  12. data/lib/status_page_ruby/pages/cloudflare.rb +29 -0
  13. data/lib/status_page_ruby/pages/github.rb +19 -0
  14. data/lib/status_page_ruby/pages/rubygems.rb +14 -0
  15. data/lib/status_page_ruby/repositories/status.rb +40 -0
  16. data/lib/status_page_ruby/services/backup_data.rb +15 -0
  17. data/lib/status_page_ruby/services/build_history_table.rb +32 -0
  18. data/lib/status_page_ruby/services/build_log_table.rb +14 -0
  19. data/lib/status_page_ruby/services/build_stats_table.rb +79 -0
  20. data/lib/status_page_ruby/services/pull_statuses.rb +44 -0
  21. data/lib/status_page_ruby/services/restore_data.rb +15 -0
  22. data/lib/status_page_ruby/status.rb +36 -0
  23. data/lib/status_page_ruby/storage.rb +55 -0
  24. data/lib/status_page_ruby/version.rb +3 -0
  25. data/lib/status_page_ruby_cli.rb +76 -0
  26. data/spec/lib/status_page_ruby/pages/bitbucket_spec.rb +39 -0
  27. data/spec/lib/status_page_ruby/pages/cloudflare_spec.rb +64 -0
  28. data/spec/lib/status_page_ruby/pages/github_spec.rb +57 -0
  29. data/spec/lib/status_page_ruby/pages/rubygems_spec.rb +39 -0
  30. data/spec/lib/status_page_ruby/repositories/status_spec.rb +124 -0
  31. data/spec/lib/status_page_ruby/services/backup_data_spec.rb +13 -0
  32. data/spec/lib/status_page_ruby/services/build_history_table_spec.rb +71 -0
  33. data/spec/lib/status_page_ruby/services/build_log_table_spec.rb +43 -0
  34. data/spec/lib/status_page_ruby/services/build_stats_table_spec.rb +72 -0
  35. data/spec/lib/status_page_ruby/services/pull_statuses_spec.rb +30 -0
  36. data/spec/lib/status_page_ruby/services/restore_data_spec.rb +13 -0
  37. data/spec/lib/status_page_ruby/status_spec.rb +65 -0
  38. data/spec/lib/status_page_ruby/storage_spec.rb +134 -0
  39. data/spec/spec_helper.rb +15 -0
  40. data/status_page_ruby.gemspec +22 -0
  41. metadata +139 -0
@@ -0,0 +1,14 @@
1
+ module StatusPageRuby
2
+ module Services
3
+ class BuildLogTable
4
+ HEADINGS = %w[Service Status Time].freeze
5
+
6
+ def call(records)
7
+ Terminal::Table.new(
8
+ headings: HEADINGS,
9
+ rows: records.map(&:history_record)
10
+ ).to_s
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ module StatusPageRuby
2
+ module Services
3
+ class BuildStatsTable
4
+ HEADINGS = ['Service', 'Up since', 'Down time'].freeze
5
+ SECONDS_IN_MINUTE = 60
6
+ SECONDS_IN_HOUR = 3600
7
+ SECONDS_IN_DAY = 86_400
8
+
9
+ attr_reader :status_repository
10
+
11
+ def initialize(status_repository)
12
+ @status_repository = status_repository
13
+ end
14
+
15
+ def call(service = nil)
16
+ Terminal::Table.new(
17
+ headings: HEADINGS,
18
+ rows: build_rows(service)
19
+ ).to_s
20
+ end
21
+
22
+ private
23
+
24
+ def build_rows(service)
25
+ find_records(service)
26
+ .group_by(&:service)
27
+ .each_with_object([]) do |(key, records), results|
28
+
29
+ results << [
30
+ key,
31
+ readable_time_amount(calculate_up_since(records)),
32
+ readable_time_amount(calculate_down_time(records))
33
+ ]
34
+ end
35
+ end
36
+
37
+ def calculate_down_time(records)
38
+ records
39
+ .sort_by { |record| record.time.to_i }
40
+ .each_cons(2)
41
+ .inject(0) { |result, (first, second)| result + duration_between(first, second) }
42
+ end
43
+
44
+ def duration_between(first, second)
45
+ return 0 if first.up?
46
+
47
+ (second.nil? ? Time.now.to_i : second.time.to_i) - first.time.to_i
48
+ end
49
+
50
+ def calculate_up_since(records)
51
+ record = take_up_since(records)
52
+ return if record.nil?
53
+
54
+ Time.now.to_i - record.time.to_i
55
+ end
56
+
57
+ def readable_time_amount(duration_sec)
58
+ return 'N/A' if duration_sec.nil?
59
+ return "#{duration_sec / SECONDS_IN_MINUTE} minutes" if duration_sec < SECONDS_IN_HOUR
60
+ return "#{duration_sec / SECONDS_IN_HOUR} hours" if duration_sec < SECONDS_IN_DAY
61
+
62
+ "#{duration_sec / SECONDS_IN_DAY} days"
63
+ end
64
+
65
+ def take_up_since(records)
66
+ records
67
+ .sort_by { |record| -record.time.to_i }
68
+ .take_while(&:up?)
69
+ .first
70
+ end
71
+
72
+ def find_records(service)
73
+ return status_repository.all if service.nil?
74
+
75
+ status_repository.where(service: service)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,44 @@
1
+ module StatusPageRuby
2
+ module Services
3
+ class PullStatuses
4
+ attr_reader :status_repository
5
+
6
+ def initialize(status_repository)
7
+ @status_repository = status_repository
8
+ end
9
+
10
+ def call
11
+ fetch_statuses.tap do |statuses|
12
+ status_repository.create_batch(statuses) unless statuses.empty?
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def fetch_statuses
19
+ page_classes
20
+ .map(&:open)
21
+ .map(&method(:build_status))
22
+ end
23
+
24
+ def page_classes
25
+ ObjectSpace
26
+ .each_object(Class)
27
+ .select(&method(:page_class?))
28
+ end
29
+
30
+ def page_class?(klass)
31
+ klass < ::StatusPageRuby::Pages::Base
32
+ end
33
+
34
+ def build_status(page)
35
+ StatusPageRuby::Status.new(
36
+ page.class.name.split('::').last,
37
+ page.success? ? 'up' : 'down',
38
+ page.status,
39
+ page.time
40
+ )
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ module StatusPageRuby
2
+ module Services
3
+ class RestoreData
4
+ attr_reader :storage
5
+
6
+ def initialize(storage)
7
+ @storage = storage
8
+ end
9
+
10
+ def call(path)
11
+ storage.restore(path)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,36 @@
1
+ module StatusPageRuby
2
+ class Status
3
+ HISTORY_RECORD_TIME_FORMAT = '%d.%m.%Y %H:%M:%S'.freeze
4
+ UP_STATE = 'up'.freeze
5
+ DOWN_STATE = 'down'.freeze
6
+
7
+ attr_reader :service, :state, :status, :time
8
+
9
+ def initialize(service, state, status, time)
10
+ @service = service.to_s
11
+ @state = state.to_s
12
+ @status = status.to_s
13
+ @time = time.to_s
14
+ end
15
+
16
+ def up?
17
+ state == UP_STATE
18
+ end
19
+
20
+ def down?
21
+ state == DOWN_STATE
22
+ end
23
+
24
+ def history_record
25
+ [service, state, Time.at(time.to_i).utc.strftime(HISTORY_RECORD_TIME_FORMAT)]
26
+ end
27
+
28
+ def record
29
+ [service, state, status, time]
30
+ end
31
+
32
+ def to_csv
33
+ record.to_csv
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ module StatusPageRuby
2
+ class Storage
3
+ attr_reader :data_file_path
4
+
5
+ def initialize(data_file_path)
6
+ validate_file(data_file_path)
7
+ @data_file_path = data_file_path
8
+ end
9
+
10
+ def include?(record)
11
+ read.lazy.include?(record)
12
+ end
13
+
14
+ def read
15
+ CSV.foreach(data_file_path)
16
+ end
17
+
18
+ def write(record)
19
+ CSV.open(data_file_path, 'a') do |csv|
20
+ csv << record
21
+ end
22
+ end
23
+
24
+ def merge(records)
25
+ updated_records = merge_records(records)
26
+ CSV.open(data_file_path, 'w') do |csv|
27
+ updated_records.each do |record|
28
+ csv << record
29
+ end
30
+ end
31
+ end
32
+
33
+ def copy(target_file_path)
34
+ FileUtils.mkpath(File.dirname(target_file_path))
35
+ FileUtils.cp(data_file_path, target_file_path)
36
+ end
37
+
38
+ def restore(new_file_path)
39
+ validate_file(new_file_path)
40
+ merge(CSV.foreach(new_file_path).to_a)
41
+ end
42
+
43
+ private
44
+
45
+ def validate_file(path)
46
+ return if File.file?(path.to_s) && File.readable?(path.to_s)
47
+
48
+ raise ArgumentError, 'Invalid file given.'
49
+ end
50
+
51
+ def merge_records(records)
52
+ (read.to_a | records).sort_by(&:last)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,3 @@
1
+ module StatusPageRuby
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -0,0 +1,76 @@
1
+ class StatusPageRubyCli < Thor
2
+ PULL_SECONDS = 10
3
+
4
+ def initialize(*args)
5
+ super(*args)
6
+ data_file_path = File.join(ENV['HOME'], '.status_page_ruby_data.csv')
7
+ create_data_file_if_needed(data_file_path)
8
+ storage = StatusPageRuby::Storage.new(data_file_path)
9
+ repository = StatusPageRuby::Repositories::Status.new(storage)
10
+ setup_services(repository)
11
+ end
12
+
13
+ desc :pull, 'Pulls, saves and optionally log statuses.'
14
+ option :log, type: :boolean
15
+ def pull
16
+ records = pull_statuses.call
17
+ puts build_log_table.call(records) if options[:log]
18
+ end
19
+
20
+ desc :live, "Pulls, saves and log statuses every #{PULL_SECONDS} seconds."
21
+ def live
22
+ loop do
23
+ puts build_log_table.call(pull_statuses.call)
24
+ sleep PULL_SECONDS
25
+ end
26
+ end
27
+
28
+ desc :history, 'Display all the data which was gathered.'
29
+ option :service, type: :string, required: false
30
+ def history
31
+ puts build_history_table.call(options[:service])
32
+ end
33
+
34
+ desc :stats, 'Summarizes the data and displays it.'
35
+ option :service, type: :string, required: false
36
+ def stats
37
+ puts build_stats_table.call(options[:service])
38
+ end
39
+
40
+ desc :backup, 'Backups data.'
41
+ option :path, type: :string, required: true
42
+ def backup
43
+ backup_data.call(options[:path])
44
+ end
45
+
46
+ desc :restore, 'Restores data.'
47
+ option :path, type: :string, required: true
48
+ def restore
49
+ restore_data.call(options[:path])
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :build_history_table,
55
+ :build_log_table,
56
+ :build_stats_table,
57
+ :pull_statuses,
58
+ :backup_data,
59
+ :restore_data
60
+
61
+ def setup_services(status_repository)
62
+ @build_history_table = StatusPageRuby::Services::BuildHistoryTable.new(status_repository)
63
+ @build_log_table = StatusPageRuby::Services::BuildLogTable.new
64
+ @build_stats_table = StatusPageRuby::Services::BuildStatsTable.new(status_repository)
65
+ @pull_statuses = StatusPageRuby::Services::PullStatuses.new(status_repository)
66
+ @backup_data = StatusPageRuby::Services::BackupData.new(status_repository.storage)
67
+ @restore_data = StatusPageRuby::Services::RestoreData.new(status_repository.storage)
68
+ end
69
+
70
+ def create_data_file_if_needed(path)
71
+ return if File.exist?(path)
72
+
73
+ FileUtils.mkpath(File.dirname(path))
74
+ FileUtils.touch(path)
75
+ end
76
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe StatusPageRuby::Pages::Bitbucket do
4
+ let(:html) { "<html><body><div class='page-status'><div class='status'>#{status}</div></div></body></html>" }
5
+
6
+ before { allow(OpenURI).to receive(:open_uri).with('https://status.bitbucket.org/') { html } }
7
+
8
+ describe '#status' do
9
+ subject { described_class.open.status }
10
+
11
+ context 'when success status' do
12
+ let(:status) { 'All Systems Operational' }
13
+
14
+ it { is_expected.to eq('All Systems Operational') }
15
+ end
16
+
17
+ context 'when not success status' do
18
+ let(:status) { 'All is Failed' }
19
+
20
+ it { is_expected.to eq('All is Failed') }
21
+ end
22
+ end
23
+
24
+ describe '#success?' do
25
+ subject { described_class.open.success? }
26
+
27
+ context 'when success status' do
28
+ let(:status) { 'All Systems Operational' }
29
+
30
+ it { is_expected.to be_truthy }
31
+ end
32
+
33
+ context 'when not success status' do
34
+ let(:status) { 'All is Failed' }
35
+
36
+ it { is_expected.to be_falsey }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe StatusPageRuby::Pages::Cloudflare do
4
+ let(:html) do
5
+ <<-HTML
6
+ <html>
7
+ <body>
8
+ <div class='page-status'>
9
+ <div class='status'>All is Failed</div>
10
+ </div>
11
+ <div data-component-id>
12
+ <div class="component-status">#{component_status}</div>
13
+ </div>
14
+ </body>
15
+ </html>
16
+ HTML
17
+ end
18
+
19
+ before { allow(OpenURI).to receive(:open_uri).with('https://www.cloudflarestatus.com/') { html } }
20
+
21
+ describe '#status' do
22
+ subject { described_class.open.status }
23
+
24
+ context 'when component_status is Failed' do
25
+ let(:component_status) { 'Failed' }
26
+
27
+ it { is_expected.to eq('All is Failed') }
28
+ end
29
+
30
+ context 'when component_status is Re-routed' do
31
+ let(:component_status) { 'Re-routed' }
32
+
33
+ it { is_expected.to eq('All Systems Operational') }
34
+ end
35
+
36
+ context 'when component_status is Operational' do
37
+ let(:component_status) { 'Operational' }
38
+
39
+ it { is_expected.to eq('All Systems Operational') }
40
+ end
41
+ end
42
+
43
+ describe '#success?' do
44
+ subject { described_class.open.success? }
45
+
46
+ context 'when component_status is Failed' do
47
+ let(:component_status) { 'Failed' }
48
+
49
+ it { is_expected.to be_falsey }
50
+ end
51
+
52
+ context 'when component_status is Re-routed' do
53
+ let(:component_status) { 'Re-routed' }
54
+
55
+ it { is_expected.to be_truthy }
56
+ end
57
+
58
+ context 'when component_status is Operational' do
59
+ let(:component_status) { 'Operational' }
60
+
61
+ it { is_expected.to be_truthy }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe StatusPageRuby::Pages::Github do
4
+ let(:html) do
5
+ <<-HTML
6
+ <html>
7
+ <body>
8
+ <div class='message'>
9
+ <div class='title'>#{status}</div>
10
+ <div class='time' datetime='2018-10-12T22:00:00.000Z'></div>
11
+ </div>
12
+ </body>
13
+ </html>
14
+ HTML
15
+ end
16
+
17
+ before { allow(OpenURI).to receive(:open_uri).with('https://status.github.com/messages') { html } }
18
+
19
+ describe '#status' do
20
+ subject { described_class.open.status }
21
+
22
+ context 'when success status' do
23
+ let(:status) { 'All systems reporting at 100%' }
24
+
25
+ it { is_expected.to eq('All systems reporting at 100%') }
26
+ end
27
+
28
+ context 'when not success status' do
29
+ let(:status) { 'All is Failed' }
30
+
31
+ it { is_expected.to eq('All is Failed') }
32
+ end
33
+ end
34
+
35
+ describe '#time' do
36
+ subject { described_class.open.time }
37
+ let(:status) { 'All systems reporting at 100%' }
38
+
39
+ it { is_expected.to eq(1_539_381_600) }
40
+ end
41
+
42
+ describe '#success?' do
43
+ subject { described_class.open.success? }
44
+
45
+ context 'when success status' do
46
+ let(:status) { 'All systems reporting at 100%' }
47
+
48
+ it { is_expected.to be_truthy }
49
+ end
50
+
51
+ context 'when not success status' do
52
+ let(:status) { 'All is Failed' }
53
+
54
+ it { is_expected.to be_falsey }
55
+ end
56
+ end
57
+ end