status_page_ruby 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|