status_page_ruby 0.0.1

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 (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