health_status 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.
data/.gitignore ADDED
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .*.swp
19
+ vendor/bundle
20
+ *.db
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in health_status.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 riywo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # HealthStatus
2
+
3
+ API server to store and visualize applications' health status.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'health_status'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install health_status
18
+
19
+ ## Usage
20
+
21
+ $ health_status_server -d /path/to/sqlite.db
22
+
23
+ $ curl -v -d "status=1" localhost:5678/service1/application1/metric1
24
+ $ curl -v -d "status=2" localhost:5678/service1/application1/metric2
25
+
26
+ ## Contributing
27
+
28
+ 1. Fork it
29
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
30
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
31
+ 4. Push to the branch (`git push origin my-new-feature`)
32
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'sinatra/activerecord/rake'
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.expand_path("../../lib", __FILE__)
3
+ begin
4
+ require 'vegas'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'vegas'
8
+ end
9
+ require "health_status/app"
10
+ require "health_status/model"
11
+
12
+ Vegas::Runner.new(HealthStatus::App, 'health_status_server') do |runner, opts, app|
13
+ opts.on("-d", "--database DATABASE_FILE", "path/to/sqlite.db") do |database|
14
+ runner.logger.info "Using database #{database}"
15
+ HealthStatus::App.database_path = database
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ class HealthStatusMigration < ActiveRecord::Migration
2
+ def up
3
+ create_table :applications do |t|
4
+ t.string :name, :null => false
5
+ t.integer :status, :null => false
6
+ t.datetime :datetime, :null => false
7
+ t.datetime :saved_at, :null => false
8
+ end
9
+ add_index :applications, [ :name ], :unique => true
10
+
11
+ create_table :half_hour_statuses do |t|
12
+ t.integer :application_id, :null => false
13
+ t.integer :status, :null => false
14
+ t.datetime :datetime, :null => false
15
+ t.datetime :saved_at, :null => false
16
+ end
17
+ add_index :half_hour_statuses, [:application_id]
18
+ end
19
+
20
+ def down
21
+ end
22
+ end
@@ -0,0 +1,9 @@
1
+ class RemoveDatetime < ActiveRecord::Migration
2
+ def up
3
+ remove_column(:applications, :datetime)
4
+ end
5
+
6
+ def down
7
+ add_column(:applications, :datetime, :datetime, { :null => false })
8
+ end
9
+ end
@@ -0,0 +1,64 @@
1
+ class AddServiceMetric < ActiveRecord::Migration
2
+ def up
3
+ rename_table :applications, :v1_applications
4
+ rename_table :half_hour_statuses, :v1_half_hour_statuses
5
+
6
+ create_table :services do |t|
7
+ t.string :name, :null => false
8
+ t.integer :status, :null => false
9
+ t.datetime :saved_at, :null => false
10
+ end
11
+ add_index :services, [ :name ], :unique => true
12
+
13
+ create_table :service_half_hour_statuses do |t|
14
+ t.integer :service_id, :null => false
15
+ t.integer :status, :null => false
16
+ t.datetime :datetime, :null => false
17
+ t.datetime :saved_at, :null => false
18
+ end
19
+ add_index :service_half_hour_statuses, [ :service_id ]
20
+
21
+ create_table :applications do |t|
22
+ t.integer :service_id, :null => false
23
+ t.string :name, :null => false
24
+ t.integer :status, :null => false
25
+ t.datetime :saved_at, :null => false
26
+ end
27
+ add_index :applications, [ :service_id, :name ], :unique => true
28
+
29
+ create_table :application_half_hour_statuses do |t|
30
+ t.integer :application_id, :null => false
31
+ t.integer :status, :null => false
32
+ t.datetime :datetime, :null => false
33
+ t.datetime :saved_at, :null => false
34
+ end
35
+ add_index :application_half_hour_statuses, [ :application_id ]
36
+
37
+ create_table :metrics do |t|
38
+ t.integer :application_id, :null => false
39
+ t.string :name, :null => false
40
+ t.integer :status, :null => false
41
+ t.datetime :saved_at, :null => false
42
+ end
43
+ add_index :metrics, [ :application_id, :name ], :unique => true
44
+
45
+ create_table :metric_half_hour_statuses do |t|
46
+ t.integer :metric_id, :null => false
47
+ t.integer :status, :null => false
48
+ t.datetime :datetime, :null => false
49
+ t.datetime :saved_at, :null => false
50
+ end
51
+ add_index :metric_half_hour_statuses, [ :metric_id ]
52
+ end
53
+
54
+ def down
55
+ drop_table :services
56
+ drop_table :service_half_hour_statuses
57
+ drop_table :applications
58
+ drop_table :application_half_hour_statuses
59
+ drop_table :metrics
60
+ drop_table :metric_half_hour_statuses
61
+ rename_table :v1_applications, :applications
62
+ rename_table :v1_half_hour_statuses, :half_hour_statuses
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'health_status/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "health_status"
8
+ spec.version = HealthStatus::VERSION
9
+ spec.authors = ["Ryosuke IWANAGA"]
10
+ spec.email = ["riywo.jp@gmail.com"]
11
+ spec.description = %q{Health status API server}
12
+ spec.summary = %q{API server to store and visualize applications' health status}
13
+ spec.homepage = "https://github.com/riywo/health_status"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'sinatra'
22
+ spec.add_dependency 'sinatra-contrib'
23
+ spec.add_dependency 'activerecord'
24
+ spec.add_dependency 'sinatra-activerecord'
25
+ spec.add_dependency 'sqlite3'
26
+ spec.add_dependency 'vegas'
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.3"
29
+ spec.add_development_dependency 'tapp'
30
+ spec.add_development_dependency 'rake'
31
+ end
@@ -0,0 +1,5 @@
1
+ require "health_status/version"
2
+
3
+ module HealthStatus
4
+ # Your code goes here...
5
+ end
@@ -0,0 +1,83 @@
1
+ require "health_status"
2
+ require "health_status/model"
3
+ require "health_status/web"
4
+ require "sinatra/base"
5
+ require "sinatra/activerecord"
6
+ require "sinatra/cookies"
7
+
8
+ class HealthStatus::App < Sinatra::Base
9
+
10
+ class << self
11
+ attr_accessor :database_path
12
+ end
13
+
14
+ helpers Sinatra::Cookies, ERB::Util
15
+ register Sinatra::ActiveRecordExtension
16
+
17
+ set :public_folder, File.expand_path("../../../public", __FILE__)
18
+ set :views, File.expand_path("../../../views", __FILE__)
19
+
20
+ before do
21
+ settings.database = "sqlite3:///#{HealthStatus::App.database_path}"
22
+ ActiveRecord::Base.logger = nil
23
+ HealthStatus::Model::Migrate.migrate
24
+ end
25
+
26
+ get '/' do
27
+ @timezone = params["timezone"] || cookies[:timezone] || HealthStatus::Web.system_timezone
28
+ @zones = HealthStatus::Web.timezones
29
+ erb :index
30
+ end
31
+
32
+ get '/api/v2/' do
33
+ HealthStatus::Model::Service.readonly.fetch_all_info().to_json
34
+ end
35
+
36
+ get '/api/v2/:service' do |service_name|
37
+ args = {}
38
+ args[:end_time] = Time.now.in_time_zone(params["timezone"])
39
+ args[:with_status] = true
40
+ service = HealthStatus::Model::Service.find_by_name(service_name)
41
+ service.fetch_info(args).to_json
42
+ end
43
+
44
+ get '/api/v2/:service/:application' do |service_name, application_name|
45
+ Time.zone = params["timezone"]
46
+ args = {}
47
+ args[:end_time] = Time.now.in_time_zone(params["timezone"])
48
+ args[:with_status] = true
49
+ application = HealthStatus::Model::Service.find_by_name(service_name).applications.find_by_name(application_name)
50
+ application.fetch_info(args).to_json
51
+ end
52
+
53
+ get '/api/v2/:service/:application/:metric' do |service_name, application_name, metric_name|
54
+ Time.zone = params["timezone"]
55
+ args = {}
56
+ args[:end_time] = Time.now.in_time_zone(params["timezone"])
57
+ args[:with_status] = true
58
+ metric = HealthStatus::Model::Service.find_by_name(service_name).applications.find_by_name(application_name).metrics.find_by_name(metric_name)
59
+ metric.fetch_info(args).to_json
60
+ end
61
+
62
+ post '/api/v2/:service/:application/:metric' do |service_name, application_name, metric_name|
63
+ raise unless params.has_key? "status"
64
+ HealthStatus::Model::Service.save_metric(service_name, application_name, metric_name, params["status"])
65
+ "OK"
66
+ end
67
+
68
+ delete '/api/v2/:service' do |service_name|
69
+ HealthStatus::Model::Service.find_by_name(service_name).destroy
70
+ "OK"
71
+ end
72
+
73
+ delete '/api/v2/:service/:application' do |service_name, application_name|
74
+ HealthStatus::Model::Service.find_by_name(service_name).applications.find_by_name(application_name).destroy
75
+ "OK"
76
+ end
77
+
78
+ delete '/api/v2/:service/:application/:metric' do |service_name, application_name, metric_name|
79
+ HealthStatus::Model::Service.find_by_name(service_name).applications.find_by_name(application_name).metrics.find_by_name(metric_name).destroy
80
+ "OK"
81
+ end
82
+
83
+ end
@@ -0,0 +1,284 @@
1
+ require "health_status"
2
+ require 'sinatra/activerecord'
3
+ require "sinatra/activerecord/rake"
4
+
5
+ class HealthStatus::Model
6
+
7
+ module AggregateStatus
8
+ @@default_timezone = :utc
9
+ @@half_hour = 30 * 60
10
+ @@hour = 60 * 60
11
+ @@day = 24 * @@hour
12
+
13
+ def fetch_current_status
14
+ if saved_at < floor_half_hour(Time.now.utc)
15
+ nil
16
+ else
17
+ status
18
+ end
19
+ end
20
+
21
+ def fetch_saved_at(args)
22
+ end_time = args[:end_time]
23
+ end_time ||= Time.now
24
+ Time.at(saved_at.to_i).localtime(end_time.utc_offset)
25
+ end
26
+
27
+ def fetch_status_with_interval(interval, args = {})
28
+ validate_args = validate_fetch_status(interval, args)
29
+ interval_status = []
30
+ time = validate_args[:start_time]
31
+ while time <= validate_args[:end_time]
32
+ data = {
33
+ :datetime => time,
34
+ :status => self_half_hour_statuses.where(:datetime => time..(time + interval - 1)).maximum(:status),
35
+ }
36
+ interval_status << data
37
+ time += interval
38
+ end
39
+ interval_status
40
+ end
41
+
42
+ def fetch_hourly_status(args = {})
43
+ fetch_status_with_interval(@@hour, args)
44
+ end
45
+
46
+ def fetch_daily_status(args = {})
47
+ fetch_status_with_interval(@@day, args)
48
+ end
49
+
50
+ def fetch_info(args = {})
51
+ depth = args[:depth].nil? ? 0 : args[:depth]
52
+ data = {
53
+ :id => id,
54
+ :name => name,
55
+ :current_status => fetch_current_status,
56
+ :saved_at => fetch_saved_at(args),
57
+ }
58
+
59
+ if args[:with_status]
60
+ data[:hourly_status] = fetch_hourly_status(args)
61
+ data[:daily_status] = fetch_daily_status(args)
62
+ end
63
+
64
+ if args[:with_id]
65
+ data[:service_id] = service.id if respond_to? :service
66
+ if respond_to? :application
67
+ data[:service_id] = application.service.id
68
+ data[:application_id] = application.id
69
+ end
70
+ end
71
+
72
+ if children.respond_to? :map and depth > 0
73
+ data[children_name] = children.map do |child|
74
+ args[:depth] = depth - 1
75
+ child.fetch_info(args)
76
+ end
77
+ end
78
+ data
79
+ end
80
+
81
+ def save_status(new_status)
82
+ children_status = children.maximum(:status)
83
+ if !children_status.nil? and children_status <= new_status.to_i
84
+ self.status = new_status.to_i
85
+ end
86
+ save!
87
+ end
88
+
89
+ private
90
+
91
+ def validate_fetch_status(interval, args)
92
+ end_time = args[:end_time]
93
+ end_time ||= Time.now
94
+ start_time = args[:start_time]
95
+ start_time ||= end_time - (interval == @@day ? 7 * @@day : @@day)
96
+
97
+ offset = start_time.utc_offset
98
+ raise if offset != end_time.utc_offset
99
+ raise if offset % @@half_hour != 0
100
+
101
+ start_time = floor(start_time, interval)
102
+ end_time = floor(end_time, interval)
103
+ { :start_time => start_time, :end_time => end_time }
104
+ end
105
+
106
+ def floor_half_hour(datetime)
107
+ floor(datetime, @@half_hour)
108
+ end
109
+
110
+ def floor_hour(datetime)
111
+ floor(datetime, @@hour)
112
+ end
113
+
114
+ def floor_day(datetime)
115
+ floor(datetime, @@day)
116
+ end
117
+
118
+ def floor(datetime, seconds)
119
+ offset = datetime.utc_offset
120
+ Time.at(datetime.to_i - ((datetime.to_i + offset) % seconds).floor).localtime(offset)
121
+ end
122
+
123
+ def update_half_hour_status
124
+ half_hour = floor_half_hour(self.saved_at)
125
+ current = self.self_half_hour_statuses.find(:all, :conditions => { :datetime => half_hour } ).first
126
+ if current
127
+ current.status = status if current.status < status
128
+ current.saved_at = self.saved_at
129
+ current.save
130
+ else
131
+ self_half_hour_statuses.create(
132
+ :status => self.status,
133
+ :datetime => half_hour,
134
+ :saved_at => self.saved_at,
135
+ )
136
+ end
137
+ end
138
+ end
139
+
140
+ class Service < ActiveRecord::Base
141
+ include AggregateStatus
142
+
143
+ has_many :applications, :dependent => :destroy
144
+ has_many :service_half_hour_statuses, :order => "datetime ASC", :dependent => :destroy
145
+
146
+ before_validation do self.saved_at = Time.now.utc end
147
+ after_save :update_half_hour_status
148
+ validates_inclusion_of :status, :in => 1..3
149
+
150
+ def self.fetch_all_info(args = {})
151
+ all.map do |service|
152
+ args[:depth] = 2
153
+ service.fetch_info(args)
154
+ end
155
+ end
156
+
157
+ def self.save_metric(service_name, application_name, metric_name, new_status)
158
+ ActiveRecord::Base.transaction do
159
+ service = find_or_initialize_by_name(service_name)
160
+ application = service.applications.find_or_initialize_by_name(application_name)
161
+ metric = application.metrics.find_or_initialize_by_name(metric_name)
162
+
163
+ if service.new_record?
164
+ service.status = new_status
165
+ application.status = new_status
166
+ elsif application.new_record?
167
+ application.status = new_status
168
+ end
169
+ metric.status = new_status
170
+ service.save!
171
+ application.save!
172
+ metric.save!
173
+
174
+ application.save_status(new_status)
175
+ service.save_status(new_status)
176
+ end
177
+ end
178
+
179
+ def self_half_hour_statuses
180
+ service_half_hour_statuses
181
+ end
182
+
183
+ def children_name
184
+ :applications
185
+ end
186
+
187
+ def children
188
+ applications
189
+ end
190
+ end
191
+
192
+ class Application < ActiveRecord::Base
193
+ include AggregateStatus
194
+
195
+ belongs_to :service
196
+ has_many :metrics, :dependent => :destroy
197
+ has_many :application_half_hour_statuses, :order => "datetime ASC", :dependent => :destroy
198
+
199
+ before_validation do self.saved_at = Time.now.utc end
200
+ after_save :update_half_hour_status
201
+ validates_inclusion_of :status, :in => 1..3
202
+
203
+ def self_half_hour_statuses
204
+ application_half_hour_statuses
205
+ end
206
+
207
+ def children_name
208
+ :metrics
209
+ end
210
+
211
+ def children
212
+ metrics
213
+ end
214
+ end
215
+
216
+ class Metric < ActiveRecord::Base
217
+ include AggregateStatus
218
+
219
+ belongs_to :application
220
+ has_many :metric_half_hour_statuses, :order => "datetime ASC", :dependent => :destroy
221
+
222
+ before_validation do self.saved_at = Time.now.utc end
223
+ after_save :update_half_hour_status
224
+ validates_inclusion_of :status, :in => 1..3
225
+
226
+ def self_half_hour_statuses
227
+ metric_half_hour_statuses
228
+ end
229
+
230
+ def children
231
+ self
232
+ end
233
+
234
+ end
235
+
236
+ class ServiceHalfHourStatus < ActiveRecord::Base
237
+ @@default_timezone = :utc
238
+ belongs_to :service
239
+ validates_inclusion_of :status, :in => 1..3
240
+ end
241
+
242
+ class ApplicationHalfHourStatus < ActiveRecord::Base
243
+ @@default_timezone = :utc
244
+ belongs_to :application
245
+ validates_inclusion_of :status, :in => 1..3
246
+ end
247
+
248
+ class MetricHalfHourStatus < ActiveRecord::Base
249
+ @@default_timezone = :utc
250
+ belongs_to :metric
251
+ validates_inclusion_of :status, :in => 1..3
252
+ end
253
+
254
+ module Sort
255
+ def self.names_sort_by_status
256
+ all.sort do |a, b|
257
+ a_status = a.fetch_current_status
258
+ b_status = b.fetch_current_status
259
+ if !a_status and !b_status
260
+ a.name <=> b.name
261
+ elsif !a_status
262
+ 1
263
+ elsif !b_status
264
+ -1
265
+ else
266
+ (b.fetch_current_status <=> a.fetch_current_status).nonzero? or
267
+ a.name <=> b.name
268
+ end
269
+ end.map { |app| app.name }
270
+ end
271
+ end
272
+
273
+ module Migrate
274
+ extend Sinatra::ActiveRecordTasks
275
+ extend self
276
+
277
+ private
278
+
279
+ def migrations_dir
280
+ File.expand_path("../../../db/migrate", __FILE__)
281
+ end
282
+ end
283
+
284
+ end