pulse-meter-client-backport 0.1.0

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 (50) hide show
  1. data/.gitignore +19 -0
  2. data/.rbenv-version +1 -0
  3. data/.rspec +1 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +3 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +22 -0
  8. data/Procfile +3 -0
  9. data/README.md +98 -0
  10. data/Rakefile +53 -0
  11. data/bin/pulse +6 -0
  12. data/examples/readme_client_example.rb +52 -0
  13. data/lib/pulse-meter.rb +17 -0
  14. data/lib/pulse-meter/mixins/dumper.rb +76 -0
  15. data/lib/pulse-meter/mixins/utils.rb +93 -0
  16. data/lib/pulse-meter/sensor.rb +44 -0
  17. data/lib/pulse-meter/sensor/base.rb +75 -0
  18. data/lib/pulse-meter/sensor/counter.rb +36 -0
  19. data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
  20. data/lib/pulse-meter/sensor/indicator.rb +33 -0
  21. data/lib/pulse-meter/sensor/timeline.rb +180 -0
  22. data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
  23. data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
  24. data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
  25. data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
  26. data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
  27. data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
  28. data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
  29. data/lib/pulse-meter/version.rb +3 -0
  30. data/pulse-meter-client-backport.gemspec +33 -0
  31. data/spec/pulse_meter/mixins/dumper_spec.rb +141 -0
  32. data/spec/pulse_meter/mixins/utils_spec.rb +125 -0
  33. data/spec/pulse_meter/sensor/base_spec.rb +97 -0
  34. data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
  35. data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
  36. data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
  37. data/spec/pulse_meter/sensor/timeline_spec.rb +45 -0
  38. data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
  39. data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
  40. data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
  41. data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
  42. data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
  43. data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
  44. data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
  45. data/spec/pulse_meter_spec.rb +16 -0
  46. data/spec/shared_examples/timeline_sensor.rb +274 -0
  47. data/spec/shared_examples/timelined_subclass.rb +23 -0
  48. data/spec/spec_helper.rb +21 -0
  49. data/spec/support/matchers.rb +45 -0
  50. metadata +276 -0
@@ -0,0 +1,22 @@
1
+ require 'json'
2
+
3
+ module PulseMeter
4
+ module Sensor
5
+ module Timelined
6
+ # Counts multiple types of events per interval.
7
+ # Good replacement for multiple counters to be visualized together
8
+ class HashedCounter < Timeline
9
+ def aggregate_event(key, data)
10
+ data.each_pair {|k, v| redis.hincrby(key, k, v)}
11
+ end
12
+
13
+ def summarize(key)
14
+ redis.
15
+ hgetall(key).
16
+ inject({}) {|h, (k, v)| h[k] = v.to_i; h}.
17
+ to_json
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates max value in interval
5
+ class Max < Timeline
6
+
7
+ def aggregate_event(key, value)
8
+ redis.zadd(key, value, "#{value}::#{uniqid}")
9
+ redis.zremrangebyrank(key, 0, -2)
10
+ end
11
+
12
+ def summarize(key)
13
+ count = redis.zcard(key)
14
+ if count > 0
15
+ max_el = redis.zrange(key, -1, -1)[0]
16
+ redis.zscore(key, max_el).to_f
17
+ else
18
+ nil
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates median of iterval values
5
+ class Median < Percentile
6
+
7
+ def initialize(name, options)
8
+ super(name, options.merge({:p => 0.5}))
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates min value in interval
5
+ class Min < Timeline
6
+
7
+ def aggregate_event(key, value)
8
+ redis.zadd(key, value, "#{value}::#{uniqid}")
9
+ redis.zremrangebyrank(key, 1, -1)
10
+ end
11
+
12
+ def summarize(key)
13
+ count = redis.zcard(key)
14
+ if count > 0
15
+ min_el = redis.zrange(key, 0, 0)[0]
16
+ redis.zscore(key, min_el).to_f
17
+ else
18
+ nil
19
+ end
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ module PulseMeter
2
+ module Sensor
3
+ module Timelined
4
+ # Calculates n'th percentile in interval
5
+ class Percentile < Timeline
6
+ attr_reader :p_value
7
+
8
+ def initialize(name, options)
9
+ @p_value = assert_ranged_float!(options, :p, 0, 1)
10
+ super(name, options)
11
+ end
12
+
13
+ def aggregate_event(key, value)
14
+ redis.zadd(key, value, "#{value}::#{uniqid}")
15
+ end
16
+
17
+ def summarize(key)
18
+ count = redis.zcard(key)
19
+ if count > 0
20
+ position = @p_value > 0 ? (@p_value * count).round - 1 : 0
21
+ el = redis.zrange(key, position, position)[0]
22
+ redis.zscore(key, el).to_f
23
+ else
24
+ nil
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ module PulseMeter
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/pulse-meter/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Ilya Averyanov", "Sergey Averyanov"]
6
+ gem.email = ["av@fun-box.ru", "averyanov@gmail.com"]
7
+ gem.description = %q{Ruby 1.8 compatible PulseMeter client}
8
+ gem.summary = %q{
9
+ Ruby 1.8 compatible PulseMeter client
10
+ }
11
+ gem.homepage = ""
12
+
13
+ gem.required_ruby_version = '~> 1.8.0'
14
+ gem.files = `git ls-files`.split($\)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.name = "pulse-meter-client-backport"
18
+ gem.require_paths = ["lib"]
19
+ gem.version = PulseMeter::VERSION
20
+
21
+ gem.add_runtime_dependency('json')
22
+ gem.add_runtime_dependency('redis')
23
+
24
+ gem.add_development_dependency('foreman')
25
+ gem.add_development_dependency('mock_redis')
26
+ gem.add_development_dependency('rack-test')
27
+ gem.add_development_dependency('rake')
28
+ gem.add_development_dependency('redcarpet')
29
+ gem.add_development_dependency('rspec')
30
+ gem.add_development_dependency('timecop')
31
+ gem.add_development_dependency('yard')
32
+
33
+ end
@@ -0,0 +1,141 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Mixins::Dumper do
4
+ class Base
5
+ include PulseMeter::Mixins::Dumper
6
+ end
7
+
8
+ class Bad < Base; end
9
+
10
+ class Good < Base
11
+ attr_accessor :foo
12
+ def name; foo.to_s; end
13
+
14
+ def redis; PulseMeter.redis; end
15
+
16
+ def initialize(foo)
17
+ @foo = foo
18
+ end
19
+ end
20
+
21
+ let(:bad_obj){ Bad.new }
22
+ let(:good_obj){ Good.new(:foo) }
23
+ let(:another_good_obj){ Good.new(:bar) }
24
+ let(:redis){ PulseMeter.redis }
25
+
26
+ describe '#dump' do
27
+ context "when class violates dump contract" do
28
+ context "when it has no name attribute" do
29
+ it "should raise exception" do
30
+ def bad_obj.redis; PulseMeter.redis; end
31
+ expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
32
+ end
33
+ end
34
+
35
+ context "when it has no redis attribute" do
36
+ it "should raise exception" do
37
+ def bad_obj.name; :foo; end
38
+ expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
39
+ end
40
+ end
41
+
42
+ context "when redis is not avalable" do
43
+ it "should raise exception" do
44
+ def bad_obj.name; :foo; end
45
+ def bad_obj.redis; nil; end
46
+ expect{ bad_obj.dump! }.to raise_exception(PulseMeter::DumpError)
47
+ end
48
+ end
49
+ end
50
+
51
+ context "when class follows dump contract" do
52
+ it "should not raise dump exception" do
53
+ expect {good_obj.dump!}.not_to raise_exception(PulseMeter::DumpError)
54
+ end
55
+
56
+ it "should save dump to redis" do
57
+ expect {good_obj.dump!}.to change {redis.hlen(Good::DUMP_REDIS_KEY)}.by(1)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe ".restore" do
63
+ context "when object has never been dumped" do
64
+ it "should raise exception" do
65
+ expect{ Base.restore(:nonexistant) }.to raise_exception(PulseMeter::RestoreError)
66
+ end
67
+ end
68
+
69
+ context "when object was dumped" do
70
+ before do
71
+ good_obj.dump!
72
+ end
73
+
74
+ it "should keep object class" do
75
+ Base.restore(good_obj.name).should be_instance_of(good_obj.class)
76
+ end
77
+
78
+ it "should restore object data" do
79
+ restored = Base.restore(good_obj.name)
80
+ restored.foo.should == good_obj.foo
81
+ end
82
+
83
+ it "should restore last dumped object" do
84
+ good_obj.foo = :bar
85
+ good_obj.dump!
86
+ restored = Base.restore(good_obj.name)
87
+ restored.foo.should == :bar
88
+ end
89
+ end
90
+ end
91
+
92
+ describe ".list_names" do
93
+ context "when redis is not available" do
94
+ before do
95
+ PulseMeter.stub(:redis).and_return(nil)
96
+ end
97
+
98
+ it "should raise exception" do
99
+ expect {Base.list_names}.to raise_exception(PulseMeter::RestoreError)
100
+ end
101
+ end
102
+
103
+ context "when redis if fine" do
104
+ it "should return empty list if nothing is registered" do
105
+ Base.list_names.should == []
106
+ end
107
+
108
+ it "should return list of registered objects" do
109
+ good_obj.dump!
110
+ another_good_obj.dump!
111
+ Base.list_names.should =~ [good_obj.name, another_good_obj.name]
112
+ end
113
+ end
114
+ end
115
+
116
+ describe ".list_objects" do
117
+ before do
118
+ good_obj.dump!
119
+ another_good_obj.dump!
120
+ end
121
+
122
+ it "should return restored objects" do
123
+ objects = Base.list_objects
124
+ objects.map(&:name).should =~ [good_obj.name, another_good_obj.name]
125
+ end
126
+
127
+ it "should skip unrestorable objects" do
128
+ Base.stub(:list_names).and_return([good_obj.name, "scoundrel", another_good_obj.name])
129
+ objects = Base.list_objects
130
+ objects.map(&:name).should =~ [good_obj.name, another_good_obj.name]
131
+ end
132
+ end
133
+
134
+ describe "#cleanup_dump" do
135
+ it "should remove data from redis" do
136
+ good_obj.dump!
137
+ another_good_obj.dump!
138
+ expect {good_obj.cleanup_dump}.to change{good_obj.class.list_names.count}.by(-1)
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Mixins::Utils do
4
+ class Dummy
5
+ include PulseMeter::Mixins::Utils
6
+ end
7
+
8
+ let(:dummy){ Dummy.new }
9
+
10
+ describe '#constantize' do
11
+ context "when argument is a string with a valid class name" do
12
+ it "should return class" do
13
+ dummy.constantize("PulseMeter::Mixins::Utils").should == PulseMeter::Mixins::Utils
14
+ end
15
+ end
16
+ context "when argument is a string with invalid class name" do
17
+ it "should return nil" do
18
+ dummy.constantize("Pumpkin::Eater").should be_nil
19
+ end
20
+ end
21
+ end
22
+
23
+ describe "#assert_positive_integer!" do
24
+ it "should extract integer value from hash by passed key" do
25
+ dummy.assert_positive_integer!({:val => 4}, :val).should == 4
26
+ end
27
+
28
+ context "when no default value given" do
29
+ context "when the value by the passed key is not integer" do
30
+ it "should convert non-integers to integers" do
31
+ dummy.assert_positive_integer!({:val => 4.4}, :val).should == 4
32
+ end
33
+
34
+ it "should change the original value to the obtained integer" do
35
+ h = {:val => 4.4}
36
+ dummy.assert_positive_integer!(h, :val).should == 4
37
+ h[:val].should == 4
38
+ end
39
+
40
+ end
41
+
42
+ it "should raise exception if the value is not positive" do
43
+ expect{ dummy.assert_positive_integer!({:val => -1}, :val) }.to raise_exception(ArgumentError)
44
+ end
45
+
46
+ it "should raise exception if the value is not defined" do
47
+ expect{ dummy.assert_positive_integer!({}, :val) }.to raise_exception(ArgumentError)
48
+ end
49
+ end
50
+
51
+ context "when default value given" do
52
+ it "should prefer value from options to default" do
53
+ dummy.assert_positive_integer!({:val => 4}, :val, 22).should == 4
54
+ end
55
+
56
+ it "should use default value when there is no one in options" do
57
+ dummy.assert_positive_integer!({}, :val, 22).should == 22
58
+ end
59
+
60
+ it "should check default value if it is to be used" do
61
+ expect{dummy.assert_positive_integer!({}, :val, -1)}.to raise_exception(ArgumentError)
62
+ end
63
+ end
64
+ end
65
+
66
+ describe "#assert_ranged_float!" do
67
+
68
+ it "should extract float value from hash by passed key" do
69
+ dummy.assert_ranged_float!({:val => 4}, :val, 0, 100).should be_generally_equal(4)
70
+ end
71
+
72
+ context "when the value by the passed key is not float" do
73
+ it "should convert non-floats to floats" do
74
+ dummy.assert_ranged_float!({:val => "4.0000"}, :val, 0, 100).should be_generally_equal(4)
75
+ end
76
+
77
+ it "should change the original value to the obtained float" do
78
+ h = {:val => "4.000"}
79
+ dummy.assert_ranged_float!(h, :val, 0, 100).should be_generally_equal(4)
80
+ h[:val].should be_generally_equal(4)
81
+ end
82
+
83
+ it "should raise exception if the original value cannot be converted to float" do
84
+ expect{ dummy.assert_ranged_float!({:val => :bad_float}, :val, 0, 100) }.to raise_exception(ArgumentError)
85
+ end
86
+ end
87
+
88
+ it "should raise exception if the value is not within range" do
89
+ expect{ dummy.assert_ranged_float!({:val => -0.1}, :val, 0, 100) }.to raise_exception(ArgumentError)
90
+ expect{ dummy.assert_ranged_float!({:val => 100.1}, :val, 0, 100) }.to raise_exception(ArgumentError)
91
+ end
92
+
93
+ it "should raise exception if the value is not defined" do
94
+ expect{ dummy.assert_ranged_float!({}, :val) }.to raise_exception(ArgumentError)
95
+ end
96
+ end
97
+
98
+ describe "#uniqid" do
99
+ it "should return uniq strings" do
100
+ uniq_values = (1..1000).map{|_| dummy.uniqid}
101
+ uniq_values.uniq.count.should == uniq_values.count
102
+ end
103
+ end
104
+
105
+ describe "#titleize" do
106
+ it "should convert identificator to title" do
107
+ dummy.titleize("aaa_bbb").should == 'Aaa Bbb'
108
+ dummy.titleize(:aaa_bbb).should == 'Aaa Bbb'
109
+ dummy.titleize("aaa bbb").should == 'Aaa Bbb'
110
+ end
111
+ end
112
+
113
+ describe "#camelize" do
114
+ it "should camelize string" do
115
+ dummy.camelize("aa_bb_cc").should == "aaBbCc"
116
+ dummy.camelize("aa_bb_cc", true).should == "AaBbCc"
117
+ end
118
+ end
119
+
120
+ describe "#camelize_keys" do
121
+ it "should deeply camelize keys in hashes" do
122
+ dummy.camelize_keys({ :aa_bb_cc => [ { :dd_ee => 123 }, 456 ] }).should =={ 'aaBbCc' => [ { 'ddEe' => 123 }, 456 ] }
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Sensor::Base do
4
+ let(:name){ :some_sensor }
5
+ let(:description) {"Le awesome description"}
6
+ let!(:sensor){ described_class.new(name) }
7
+ let(:redis){ PulseMeter.redis }
8
+
9
+ describe '#initialize' do
10
+ context 'when PulseMeter.redis is not initialized' do
11
+ it "should raise RedisNotInitialized exception" do
12
+ PulseMeter.redis = nil
13
+ expect{ described_class.new(:foo) }.to raise_exception(PulseMeter::RedisNotInitialized)
14
+ end
15
+ end
16
+
17
+ context 'when PulseMeter.redis is initialized' do
18
+
19
+ context 'when passed sensor name is bad' do
20
+ it "should raise BadSensorName exception" do
21
+ ['name with whitespace', 'name|with|bad|characters'].each do |bad_name|
22
+ expect{ described_class.new(bad_name) }.to raise_exception(PulseMeter::BadSensorName)
23
+ end
24
+ end
25
+ end
26
+
27
+ context 'when passed sensor name is valid' do
28
+ it "should successfully create object" do
29
+ described_class.new(:foo).should_not be_nil
30
+ end
31
+
32
+ it "should initialize attributes #redis and #name" do
33
+ sensor = described_class.new(:foo)
34
+ sensor.name.should == 'foo'
35
+ sensor.redis.should == PulseMeter.redis
36
+ end
37
+
38
+ it "should save dump to redis automatically to let the object be restored by name" do
39
+ described_class.restore(name).should be_instance_of(described_class)
40
+ end
41
+
42
+ it "should annotate object if annotation given" do
43
+ described_class.new(:foo, :annotation => "annotation")
44
+ sensor = described_class.restore(:foo)
45
+ sensor.annotation.should == "annotation"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#annotate' do
52
+
53
+ it "should store sensor annotation in redis" do
54
+ expect {sensor.annotate(description)}.to change{redis.keys('*').count}.by(1)
55
+ end
56
+
57
+ end
58
+
59
+ describe '#annotation' do
60
+ context "when sensor was annotated" do
61
+ it "should return stored annotation" do
62
+ sensor.annotate(description)
63
+ sensor.annotation.should == description
64
+ end
65
+ end
66
+
67
+ context "when sensor was not annotated" do
68
+ it "should return nil" do
69
+ sensor.annotation.should be_nil
70
+ end
71
+ end
72
+
73
+ context "after sensor data was cleaned" do
74
+ it "should return nil" do
75
+ sensor.annotate(description)
76
+ sensor.cleanup
77
+ sensor.annotation.should be_nil
78
+ end
79
+ end
80
+ end
81
+
82
+ describe "#cleanup" do
83
+ it "should remove from redis all sensor data" do
84
+ sensor.event(123)
85
+ sensor.annotate(description)
86
+ sensor.cleanup
87
+ redis.keys('*').should be_empty
88
+ end
89
+ end
90
+
91
+ describe "#event" do
92
+ it "should actually do nothing for base sensor" do
93
+ sensor.event(nil)
94
+ end
95
+ end
96
+
97
+ end