overwatch-collection 0.1.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 +4 -0
- data/.watchr +18 -0
- data/Gemfile +33 -0
- data/Gemfile.lock +104 -0
- data/README.md +26 -0
- data/Rakefile +57 -0
- data/bin/overwatch-collection +38 -0
- data/config.ru +9 -0
- data/config/overwatch.yml +6 -0
- data/lib/..rb +5 -0
- data/lib/overwatch/collection.rb +42 -0
- data/lib/overwatch/collection/application.rb +46 -0
- data/lib/overwatch/collection/attributes.rb +136 -0
- data/lib/overwatch/collection/models/resource.rb +50 -0
- data/lib/overwatch/collection/models/snapshot.rb +88 -0
- data/lib/overwatch/collection/routes/resource.rb +107 -0
- data/lib/overwatch/collection/routes/snapshot.rb +66 -0
- data/lib/overwatch/collection/version.rb +5 -0
- data/log/.gitkeep +0 -0
- data/overwatch-collection.gemspec +43 -0
- data/spec/overwatch/collection/application_spec.rb +0 -0
- data/spec/overwatch/collection/attributes_spec.rb +64 -0
- data/spec/overwatch/collection/helpers_spec.rb +0 -0
- data/spec/overwatch/collection/models/resource_spec.rb +60 -0
- data/spec/overwatch/collection/models/snapshot_spec.rb +32 -0
- data/spec/overwatch/collection/routes/resource_spec.rb +146 -0
- data/spec/overwatch/collection/routes/snapshot_spec.rb +161 -0
- data/spec/overwatch/collection_spec.rb +0 -0
- data/spec/overwatch_spec.rb +2 -0
- data/spec/spec_helper.rb +51 -0
- data/spec/support/json.rb +3 -0
- data/spec/support/respond_with_matchers.rb +33 -0
- data/spec/support/snapshot_data.rb +7 -0
- data/spec/support/time_travel.rb +3 -0
- metadata +304 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module Overwatch
|
2
|
+
class Resource
|
3
|
+
include DataMapper::Resource
|
4
|
+
include Overwatch::Collection::Attributes
|
5
|
+
|
6
|
+
property :id, Serial, :index => true
|
7
|
+
property :name, String, :index => true
|
8
|
+
property :api_key, String, :index => true
|
9
|
+
property :created_at, DateTime
|
10
|
+
property :updated_at, DateTime
|
11
|
+
|
12
|
+
has n, :snapshots
|
13
|
+
|
14
|
+
before :create, :generate_api_key
|
15
|
+
|
16
|
+
validates_presence_of :name
|
17
|
+
validates_uniqueness_of :name
|
18
|
+
validates_uniqueness_of :api_key
|
19
|
+
|
20
|
+
# after :create, :schedule_no_data_check
|
21
|
+
|
22
|
+
def previous_update
|
23
|
+
snapshots[-2]
|
24
|
+
end
|
25
|
+
|
26
|
+
def last_update
|
27
|
+
snapshots[-1]
|
28
|
+
end
|
29
|
+
#
|
30
|
+
# def run_checks
|
31
|
+
# Resque.enqueue(CheckRun, self.id.to_s)
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# def schedule_no_data_check
|
35
|
+
# Resque.enqueue_in(5.minutes, NoDataCheck, self)
|
36
|
+
# end
|
37
|
+
|
38
|
+
def regenerate_api_key
|
39
|
+
generate_api_key
|
40
|
+
self.save
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def generate_api_key
|
46
|
+
api_key = Digest::SHA1.hexdigest(Time.now.to_s + rand(12341234).to_s)[1..30]
|
47
|
+
self.api_key = api_key
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Overwatch
|
2
|
+
class Snapshot
|
3
|
+
include DataMapper::Resource
|
4
|
+
|
5
|
+
property :id, Serial, :index => true
|
6
|
+
property :data, Json
|
7
|
+
property :created_at, DateTime
|
8
|
+
property :updated_at, DateTime
|
9
|
+
property :created_on, Date
|
10
|
+
property :updated_on, Date
|
11
|
+
property :created_at_hour, Integer
|
12
|
+
property :created_at_min, Integer
|
13
|
+
property :created_at_int, Integer
|
14
|
+
|
15
|
+
belongs_to :resource
|
16
|
+
|
17
|
+
# attr_accessor :data
|
18
|
+
attr_accessor :raw_data
|
19
|
+
|
20
|
+
after :create, :parse_data
|
21
|
+
# after :create, :run_checks
|
22
|
+
after :create, :update_attribute_keys
|
23
|
+
after :create, :generate_timestamps
|
24
|
+
# after :create, :schedule_reaper
|
25
|
+
|
26
|
+
def data
|
27
|
+
begin
|
28
|
+
Hashie::Mash.new(
|
29
|
+
Yajl.load($redis.hget("overwatch::snapshot:#{self.id}", "data"))
|
30
|
+
)
|
31
|
+
rescue
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_checks
|
36
|
+
self.resource.run_checks
|
37
|
+
end
|
38
|
+
|
39
|
+
def schedule_reaper
|
40
|
+
if self.created_at.min % 5 != 0
|
41
|
+
Resque.enqueue_in(60.minutes, SnapshotReaper, self)
|
42
|
+
# Resque.enqueue_in(60.minutes, AttributeReaper, "resource:#{self.resource_id}:#{key}")
|
43
|
+
elsif self.created_at.hour != 0 && self.created_at.min != 0
|
44
|
+
Resque.enqueue_in(1.day, SnapshotReaper, self)
|
45
|
+
else
|
46
|
+
Resque.enqueue_in(30.days, SnapshotReaper, self)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_dotted_hash(source=self.data,target = {}, namespace = nil)
|
51
|
+
prefix = "#{namespace}." if namespace
|
52
|
+
case source
|
53
|
+
when Hash
|
54
|
+
source.each do |key, value|
|
55
|
+
to_dotted_hash(value, target, "#{prefix}#{key}")
|
56
|
+
end
|
57
|
+
else
|
58
|
+
target[namespace] = source
|
59
|
+
end
|
60
|
+
target
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def parse_data
|
66
|
+
self.to_dotted_hash.each_pair do |key, value|
|
67
|
+
timestamp = self.created_at.to_i * 1000
|
68
|
+
$redis.zadd "overwatch::resource:#{resource.id}:#{key}", timestamp, "#{timestamp}:#{value}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def update_attribute_keys
|
73
|
+
self.to_dotted_hash.keys.each do |key|
|
74
|
+
$redis.sadd "overwatch::resource:#{self.resource.id}:attribute_keys", key
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def generate_timestamps
|
79
|
+
self.created_at_int = self.created_at.to_i
|
80
|
+
self.save
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class AttributeError < Exception; end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Overwatch
|
2
|
+
module Collection
|
3
|
+
class Application < Sinatra::Base
|
4
|
+
|
5
|
+
get '/resources/?' do
|
6
|
+
resources = Resource.all
|
7
|
+
if resources.size < 1
|
8
|
+
[].to_json
|
9
|
+
else
|
10
|
+
resources.to_json
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/resources/:id/?' do |id|
|
15
|
+
resource = Resource.get(id)
|
16
|
+
if resource
|
17
|
+
status 200
|
18
|
+
resource.to_json
|
19
|
+
else
|
20
|
+
halt 404
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
post '/resources/?' do
|
26
|
+
data = Yajl.load(request.body.read)
|
27
|
+
resource = Resource.new(:name => data['name'])
|
28
|
+
if resource.save
|
29
|
+
status 201
|
30
|
+
resource.to_json
|
31
|
+
else
|
32
|
+
status 422
|
33
|
+
resource.errors.to_json
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
put '/resources/:id/?' do |id|
|
38
|
+
data = Yajl.load(request.body.read)
|
39
|
+
resource = Resource.get(id)
|
40
|
+
if resource
|
41
|
+
if resource.update(data)
|
42
|
+
status 200
|
43
|
+
resource.to_json
|
44
|
+
else
|
45
|
+
status 409
|
46
|
+
resource.errors.to_json
|
47
|
+
end
|
48
|
+
else
|
49
|
+
halt 404
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
delete '/resources/:id/?' do |id|
|
55
|
+
resource = Resource.get(id)
|
56
|
+
if resource
|
57
|
+
if resource.destroy
|
58
|
+
status 200
|
59
|
+
else
|
60
|
+
status 409
|
61
|
+
resource.errors.to_json
|
62
|
+
end
|
63
|
+
else
|
64
|
+
halt 404
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
get '/resources/:id/attributes/?' do |id|
|
70
|
+
resource = Resource.get(id)
|
71
|
+
attributes = resource.attribute_keys
|
72
|
+
if attributes.length < 1
|
73
|
+
[].to_json
|
74
|
+
else
|
75
|
+
attributes.sort.to_json
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
get '/resources/:id/attributes/:attribute/?' do |id, attribute|
|
80
|
+
resource = Resource.get(id)
|
81
|
+
|
82
|
+
options = {
|
83
|
+
:start_at => (params[:start_at].to_i || nil),
|
84
|
+
:end_at => (params[:end_at].to_i || nil),
|
85
|
+
:interval => (params[:interval].to_s || 'hour')
|
86
|
+
}
|
87
|
+
|
88
|
+
values = resource.values_with_dates_for(attribute, options)
|
89
|
+
values.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
get '/resources/:id/attributes/:attribute/:function/?' do |id, attribute, function|
|
93
|
+
resource = Resource.get(id)
|
94
|
+
|
95
|
+
options = {
|
96
|
+
:start_at => (params[:start_at].to_i || nil),
|
97
|
+
:end_at => (params[:end_at].to_i || nil),
|
98
|
+
:interval => (params[:interval].to_s || 'hour')
|
99
|
+
}
|
100
|
+
|
101
|
+
attribute = resource.function(params[:function].to_sym, attribute, options)
|
102
|
+
attribute.to_json
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Overwatch
|
2
|
+
module Collection
|
3
|
+
class Application < Sinatra::Base
|
4
|
+
|
5
|
+
get '/resources/:resource_id/snapshots/?' do |resource_id|
|
6
|
+
resource = Resource.get(resource_id)
|
7
|
+
snapshots = resource.snapshots
|
8
|
+
|
9
|
+
if snapshots.size < 1
|
10
|
+
[].to_json
|
11
|
+
else
|
12
|
+
snapshots.to_json
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
get '/resources/:resource_id/snapshots/:id/?' do |resource_id, id|
|
17
|
+
resource = Resource.get(resource_id)
|
18
|
+
snapshot = resource.snapshots.first(:id => id)
|
19
|
+
|
20
|
+
if resource
|
21
|
+
if snapshot
|
22
|
+
status 200
|
23
|
+
snapshot.to_json
|
24
|
+
else
|
25
|
+
halt 404
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
post '/snapshots/?' do
|
31
|
+
resource = Resource.first(:api_key => params['key'])
|
32
|
+
data = Yajl.load(request.body.read)
|
33
|
+
snapshot = resource.snapshots.create(:data => data)
|
34
|
+
|
35
|
+
if resource
|
36
|
+
if snapshot
|
37
|
+
status 201
|
38
|
+
snapshot.to_json
|
39
|
+
else
|
40
|
+
status 409
|
41
|
+
snapshot.errors.to_json
|
42
|
+
end
|
43
|
+
else
|
44
|
+
halt 404
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
get '/resources/:resource_id/snapshots/:id/data/?' do |resource_id, id|
|
49
|
+
resource = Resource.get(resource_id)
|
50
|
+
snapshot = resource.snapshots.first(:id => id)
|
51
|
+
|
52
|
+
if resource
|
53
|
+
if snapshot
|
54
|
+
status 200
|
55
|
+
snapshot.data.to_json
|
56
|
+
else
|
57
|
+
halt 404
|
58
|
+
end
|
59
|
+
else
|
60
|
+
halt 404
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/log/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
require 'overwatch/collection/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = 'overwatch-collection'
|
8
|
+
s.version = Overwatch::Collection::VERSION
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.authors = [ "Dan Ryan" ]
|
11
|
+
s.email = [ "dan@appliedawesome.com" ]
|
12
|
+
s.homepage = "https://github.com/danryan/overwatch-collection"
|
13
|
+
s.summary = "Overwatch statistical time series collection app"
|
14
|
+
s.description = "overwatch-collection is a Redis-backed statistical time series collection application designed to be fast, extensible, and easy to use."
|
15
|
+
|
16
|
+
s.rubyforge_project = "overwatch-collection"
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_dependency 'dm-core', '>= 1.1.0'
|
23
|
+
s.add_dependency 'dm-active_model', '>= 1.1.0'
|
24
|
+
s.add_dependency 'dm-redis-adapter', '>= 0.4.0'
|
25
|
+
s.add_dependency 'dm-serializer', '>= 1.1.0'
|
26
|
+
s.add_dependency 'dm-timestamps', '>= 1.1.0'
|
27
|
+
s.add_dependency 'dm-validations', '>= 1.1.0'
|
28
|
+
s.add_dependency 'dm-types', '>= 1.1.0'
|
29
|
+
s.add_dependency 'yajl-ruby', '>= 0.8.2'
|
30
|
+
s.add_dependency 'hashie', '>= 1.0.0'
|
31
|
+
s.add_dependency 'rest-client', '>= 1.6.3'
|
32
|
+
s.add_dependency 'sinatra', '>= 1.2.6'
|
33
|
+
s.add_dependency 'sinatra-logger', '>= 0.1.1'
|
34
|
+
s.add_dependency 'activesupport', '>= 3.0.9'
|
35
|
+
|
36
|
+
s.add_development_dependency 'rspec', '>= 2.6.0'
|
37
|
+
s.add_development_dependency 'rack-test', '>= 0.6.0'
|
38
|
+
s.add_development_dependency 'spork', '>= 0.9.0.rc8'
|
39
|
+
s.add_development_dependency 'watchr', '>= 0.7'
|
40
|
+
s.add_development_dependency 'factory_girl', '>= 1.3.3'
|
41
|
+
s.add_development_dependency 'json_spec', '>= 0.5.0'
|
42
|
+
s.add_development_dependency 'timecop', '>= 0.3.5'
|
43
|
+
end
|
File without changes
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Overwatch
|
4
|
+
module Collection
|
5
|
+
describe Attributes do
|
6
|
+
let(:resource) { Overwatch::Resource.create(:name => 'foo') }
|
7
|
+
let(:start_at) { (Time.now - 1.hour).to_i * 1000 }
|
8
|
+
let(:end_at) { Time.now.to_i * 1000 }
|
9
|
+
|
10
|
+
before do
|
11
|
+
resource.snapshots.create(:data => snapshot_data)
|
12
|
+
time_travel!
|
13
|
+
resource.snapshots.create(:data => snapshot_data)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#attribute_keys" do
|
17
|
+
it "should return all available attributes" do
|
18
|
+
resource.attribute_keys.should have(2).items
|
19
|
+
resource.attribute_keys.should include("load_average.one_minute")
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#values_for" do
|
25
|
+
before do
|
26
|
+
time_travel!(Time.now + 1.hour)
|
27
|
+
resource.snapshots.create(:data => snapshot_data)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should return all values by default" do
|
31
|
+
values = resource.values_for("load_average.one_minute")
|
32
|
+
values[:data].should have(3).items
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return values within a given date range" do
|
36
|
+
values = resource.values_for("load_average.one_minute",
|
37
|
+
:start_at => start_at, :end_at => end_at
|
38
|
+
)
|
39
|
+
values[:data].should have(2).items
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#values_with_dates_for" do
|
44
|
+
before do
|
45
|
+
time_travel!(Time.now + 1.hour)
|
46
|
+
resource.snapshots.create(:data => snapshot_data)
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return all values by default" do
|
50
|
+
values = resource.values_with_dates_for("load_average.one_minute")
|
51
|
+
values[:data].should have(3).items
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should return values within a given date range" do
|
55
|
+
values = resource.values_for("load_average.one_minute",
|
56
|
+
:start_at => start_at, :end_at => end_at
|
57
|
+
)
|
58
|
+
values[:data].should have(2).items
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
File without changes
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Overwatch
|
4
|
+
describe Resource do
|
5
|
+
|
6
|
+
describe "new record" do
|
7
|
+
|
8
|
+
it "generates an api key" do
|
9
|
+
resource = Overwatch::Resource.create(:name => "foo")
|
10
|
+
resource.api_key.should_not be_nil
|
11
|
+
end
|
12
|
+
|
13
|
+
it "creates a new resource" do
|
14
|
+
resource = Overwatch::Resource.create(:name => "foo")
|
15
|
+
resource.should be_valid
|
16
|
+
resource.name.should == "foo"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "disallows duplicate names" do
|
20
|
+
resource = Overwatch::Resource.create(:name => "foo")
|
21
|
+
resource2 = Overwatch::Resource.create(:name => "foo")
|
22
|
+
resource2.should_not be_valid
|
23
|
+
resource2.errors[:name].should == ["Name is already taken"]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "requires a name" do
|
27
|
+
resource = Overwatch::Resource.create
|
28
|
+
resource.should_not be_valid
|
29
|
+
resource.errors[:name].should == ["Name must not be blank"]
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe '#generate_api_key' do
|
35
|
+
it "generates an api key" do
|
36
|
+
resource = Overwatch::Resource.create(:name => "foo")
|
37
|
+
resource.api_key.should_not be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'disallows duplicate api keys' do
|
41
|
+
resource = Overwatch::Resource.create(:name => "foo")
|
42
|
+
resource2 = Overwatch::Resource.create(:name => "bar")
|
43
|
+
resource.api_key = '1234asdf' && resource2.api_key = '1234asdf'
|
44
|
+
resource.save && resource2.save
|
45
|
+
resource2.should_not be_valid
|
46
|
+
resource2.errors[:api_key].should == ["Api key is already taken"]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe '#regenerate_api_key' do
|
51
|
+
it "regenerates an api key" do
|
52
|
+
resource = Overwatch::Resource.create(:name => 'foo')
|
53
|
+
old_api_key = resource.api_key
|
54
|
+
resource.regenerate_api_key
|
55
|
+
resource.api_key.should_not == old_api_key
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|