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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +25 -0
- data/.gitignore +3 -0
- data/.rubocop.yml +10 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +61 -0
- data/README.md +53 -0
- data/bin/status-page +6 -0
- data/lib/status_page_ruby.rb +20 -0
- data/lib/status_page_ruby/pages/base.rb +52 -0
- data/lib/status_page_ruby/pages/bitbucket.rb +14 -0
- data/lib/status_page_ruby/pages/cloudflare.rb +29 -0
- data/lib/status_page_ruby/pages/github.rb +19 -0
- data/lib/status_page_ruby/pages/rubygems.rb +14 -0
- data/lib/status_page_ruby/repositories/status.rb +40 -0
- data/lib/status_page_ruby/services/backup_data.rb +15 -0
- data/lib/status_page_ruby/services/build_history_table.rb +32 -0
- data/lib/status_page_ruby/services/build_log_table.rb +14 -0
- data/lib/status_page_ruby/services/build_stats_table.rb +79 -0
- data/lib/status_page_ruby/services/pull_statuses.rb +44 -0
- data/lib/status_page_ruby/services/restore_data.rb +15 -0
- data/lib/status_page_ruby/status.rb +36 -0
- data/lib/status_page_ruby/storage.rb +55 -0
- data/lib/status_page_ruby/version.rb +3 -0
- data/lib/status_page_ruby_cli.rb +76 -0
- data/spec/lib/status_page_ruby/pages/bitbucket_spec.rb +39 -0
- data/spec/lib/status_page_ruby/pages/cloudflare_spec.rb +64 -0
- data/spec/lib/status_page_ruby/pages/github_spec.rb +57 -0
- data/spec/lib/status_page_ruby/pages/rubygems_spec.rb +39 -0
- data/spec/lib/status_page_ruby/repositories/status_spec.rb +124 -0
- data/spec/lib/status_page_ruby/services/backup_data_spec.rb +13 -0
- data/spec/lib/status_page_ruby/services/build_history_table_spec.rb +71 -0
- data/spec/lib/status_page_ruby/services/build_log_table_spec.rb +43 -0
- data/spec/lib/status_page_ruby/services/build_stats_table_spec.rb +72 -0
- data/spec/lib/status_page_ruby/services/pull_statuses_spec.rb +30 -0
- data/spec/lib/status_page_ruby/services/restore_data_spec.rb +13 -0
- data/spec/lib/status_page_ruby/status_spec.rb +65 -0
- data/spec/lib/status_page_ruby/storage_spec.rb +134 -0
- data/spec/spec_helper.rb +15 -0
- data/status_page_ruby.gemspec +22 -0
- metadata +139 -0
@@ -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,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,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
|