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.
- data/.gitignore +19 -0
- data/.rbenv-version +1 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/Procfile +3 -0
- data/README.md +98 -0
- data/Rakefile +53 -0
- data/bin/pulse +6 -0
- data/examples/readme_client_example.rb +52 -0
- data/lib/pulse-meter.rb +17 -0
- data/lib/pulse-meter/mixins/dumper.rb +76 -0
- data/lib/pulse-meter/mixins/utils.rb +93 -0
- data/lib/pulse-meter/sensor.rb +44 -0
- data/lib/pulse-meter/sensor/base.rb +75 -0
- data/lib/pulse-meter/sensor/counter.rb +36 -0
- data/lib/pulse-meter/sensor/hashed_counter.rb +31 -0
- data/lib/pulse-meter/sensor/indicator.rb +33 -0
- data/lib/pulse-meter/sensor/timeline.rb +180 -0
- data/lib/pulse-meter/sensor/timelined/average.rb +26 -0
- data/lib/pulse-meter/sensor/timelined/counter.rb +16 -0
- data/lib/pulse-meter/sensor/timelined/hashed_counter.rb +22 -0
- data/lib/pulse-meter/sensor/timelined/max.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/median.rb +14 -0
- data/lib/pulse-meter/sensor/timelined/min.rb +25 -0
- data/lib/pulse-meter/sensor/timelined/percentile.rb +31 -0
- data/lib/pulse-meter/version.rb +3 -0
- data/pulse-meter-client-backport.gemspec +33 -0
- data/spec/pulse_meter/mixins/dumper_spec.rb +141 -0
- data/spec/pulse_meter/mixins/utils_spec.rb +125 -0
- data/spec/pulse_meter/sensor/base_spec.rb +97 -0
- data/spec/pulse_meter/sensor/counter_spec.rb +54 -0
- data/spec/pulse_meter/sensor/hashed_counter_spec.rb +39 -0
- data/spec/pulse_meter/sensor/indicator_spec.rb +43 -0
- data/spec/pulse_meter/sensor/timeline_spec.rb +45 -0
- data/spec/pulse_meter/sensor/timelined/average_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/counter_spec.rb +6 -0
- data/spec/pulse_meter/sensor/timelined/hashed_counter_spec.rb +8 -0
- data/spec/pulse_meter/sensor/timelined/max_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/median_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/min_spec.rb +7 -0
- data/spec/pulse_meter/sensor/timelined/percentile_spec.rb +17 -0
- data/spec/pulse_meter_spec.rb +16 -0
- data/spec/shared_examples/timeline_sensor.rb +274 -0
- data/spec/shared_examples/timelined_subclass.rb +23 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/matchers.rb +45 -0
- metadata +276 -0
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.2-p290
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color -fd
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.8.7-p357
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Ilya Averyanov
|
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/Procfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
[]
|
2
|
+
(http://travis-ci.org/savonarola/pulse-meter-client-backport)
|
3
|
+
|
4
|
+
# PulseMeter
|
5
|
+
|
6
|
+
PulseMeter is a gem for fast and convenient realtime aggregating of software internal stats through Redis.
|
7
|
+
This is a client part of it backported for ruby 1.8.x. It allows to create sensors and send data to them, but
|
8
|
+
neither reduce nor visualize.
|
9
|
+
|
10
|
+
It is supposed to use a standalone newer installation of ruby to perform reducing and visualisation.
|
11
|
+
|
12
|
+
## Client usage
|
13
|
+
|
14
|
+
Just create sensor objects and write data. Some examples below.
|
15
|
+
|
16
|
+
require 'pulse-meter'
|
17
|
+
PulseMeter.redis = Redis.new
|
18
|
+
|
19
|
+
# static sensor examples
|
20
|
+
|
21
|
+
counter = PulseMeter::Sensor::Counter.new :my_counter
|
22
|
+
counter.event(1)
|
23
|
+
counter.event(2)
|
24
|
+
puts counter.value
|
25
|
+
# prints
|
26
|
+
# 3
|
27
|
+
|
28
|
+
indicator = PulseMeter::Sensor::Indicator.new :my_value
|
29
|
+
indicator.event(3.14)
|
30
|
+
indicator.event(2.71)
|
31
|
+
puts indicator.value
|
32
|
+
# prints
|
33
|
+
# 2.71
|
34
|
+
|
35
|
+
hashed_counter = PulseMeter::Sensor::HashedCounter.new :my_h_counter
|
36
|
+
hashed_counter.event(:x => 1)
|
37
|
+
hashed_counter.event(:y => 5)
|
38
|
+
hashed_counter.event(:y => 1)
|
39
|
+
p hashed_counter.value
|
40
|
+
# prints
|
41
|
+
# {"x"=>1, "y"=>6}
|
42
|
+
|
43
|
+
# timeline sensor examples
|
44
|
+
|
45
|
+
requests_per_minute = PulseMeter::Sensor::Timelined::Counter.new(:my_t_counter,
|
46
|
+
:interval => 60, # count for each minute
|
47
|
+
:ttl => 24 * 60 * 60 # keep data one day
|
48
|
+
)
|
49
|
+
requests_per_minute.event(1)
|
50
|
+
requests_per_minute.event(1)
|
51
|
+
sleep(60)
|
52
|
+
requests_per_minute.event(1)
|
53
|
+
requests_per_minute.timeline(2 * 60).each do |v|
|
54
|
+
puts "#{v.start_time}: #{v.value}"
|
55
|
+
end
|
56
|
+
# prints somewhat like
|
57
|
+
# 2012-05-24 11:06:00 +0400: 2
|
58
|
+
# 2012-05-24 11:07:00 +0400: 1
|
59
|
+
|
60
|
+
max_per_minute = PulseMeter::Sensor::Timelined::Max.new(:my_t_max,
|
61
|
+
:interval => 60, # max for each minute
|
62
|
+
:ttl => 24 * 60 * 60 # keep data one day
|
63
|
+
)
|
64
|
+
max_per_minute.event(3)
|
65
|
+
max_per_minute.event(1)
|
66
|
+
max_per_minute.event(2)
|
67
|
+
sleep(60)
|
68
|
+
max_per_minute.event(5)
|
69
|
+
max_per_minute.event(7)
|
70
|
+
max_per_minute.event(6)
|
71
|
+
max_per_minute.timeline(2 * 60).each do |v|
|
72
|
+
puts "#{v.start_time}: #{v.value}"
|
73
|
+
end
|
74
|
+
# prints somewhat like
|
75
|
+
# 2012-05-24 11:07:00 +0400: 3.0
|
76
|
+
# 2012-05-24 11:08:00 +0400: 7.0
|
77
|
+
|
78
|
+
## Installation
|
79
|
+
|
80
|
+
Add this line to your application's Gemfile:
|
81
|
+
|
82
|
+
gem 'pulse-meter-client-backport'
|
83
|
+
|
84
|
+
And then execute:
|
85
|
+
|
86
|
+
$ bundle
|
87
|
+
|
88
|
+
Or install it yourself as:
|
89
|
+
|
90
|
+
$ gem install pulse-meter-client-backport
|
91
|
+
|
92
|
+
## Contributing
|
93
|
+
|
94
|
+
1. Fork it
|
95
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
96
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
97
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
98
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require "yard"
|
5
|
+
require 'yard/rake/yardoc_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
YARD::Rake::YardocTask.new(:yard)
|
10
|
+
|
11
|
+
ROOT = File.dirname(__FILE__)
|
12
|
+
|
13
|
+
task :default => :spec
|
14
|
+
|
15
|
+
namespace :coffee do
|
16
|
+
desc "Complile coffee to js"
|
17
|
+
task :compile do
|
18
|
+
system 'coffee', '-c', "#{ROOT}/lib/pulse-meter/visualize/public/"
|
19
|
+
puts "Done"
|
20
|
+
end
|
21
|
+
|
22
|
+
desc "Watch coffee files and recomplile them immediately"
|
23
|
+
task :watch do
|
24
|
+
system 'coffee', '--watch', '-c', "#{ROOT}/lib/pulse-meter/visualize/public/"
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
namespace :yard do
|
30
|
+
desc "Open doc index in a browser"
|
31
|
+
task :open do
|
32
|
+
system 'open', "#{ROOT}/doc/index.html"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
namespace :example do
|
37
|
+
desc "Run minimal example"
|
38
|
+
task :minimal do
|
39
|
+
chdir(ROOT) do
|
40
|
+
system "bundle"
|
41
|
+
system "cd examples/minimal && bundle exec foreman start"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
desc "Run full example"
|
46
|
+
task :full do
|
47
|
+
chdir(ROOT) do
|
48
|
+
system "bundle"
|
49
|
+
system "cd examples/full && bundle exec foreman start"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
data/bin/pulse
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
$: << File.join(File.absolute_path(__FILE__), '..', 'lib')
|
2
|
+
|
3
|
+
require 'pulse-meter'
|
4
|
+
PulseMeter.redis = Redis.new
|
5
|
+
|
6
|
+
# static sensor examples
|
7
|
+
|
8
|
+
counter = PulseMeter::Sensor::Counter.new :my_counter
|
9
|
+
counter.event(1)
|
10
|
+
counter.event(2)
|
11
|
+
puts counter.value
|
12
|
+
|
13
|
+
indicator = PulseMeter::Sensor::Indicator.new :my_value
|
14
|
+
indicator.event(3.14)
|
15
|
+
indicator.event(2.71)
|
16
|
+
puts indicator.value
|
17
|
+
|
18
|
+
hashed_counter = PulseMeter::Sensor::HashedCounter.new :my_h_counter
|
19
|
+
hashed_counter.event(:x => 1)
|
20
|
+
hashed_counter.event(:y => 5)
|
21
|
+
hashed_counter.event(:y => 1)
|
22
|
+
p hashed_counter.value
|
23
|
+
|
24
|
+
|
25
|
+
# timeline sensor examples
|
26
|
+
|
27
|
+
requests_per_minute = PulseMeter::Sensor::Timelined::Counter.new(:my_t_counter,
|
28
|
+
:interval => 60, # count for each minute
|
29
|
+
:ttl => 24 * 60 * 60 # keep data one day
|
30
|
+
)
|
31
|
+
requests_per_minute.event(1)
|
32
|
+
requests_per_minute.event(1)
|
33
|
+
sleep(60)
|
34
|
+
requests_per_minute.event(1)
|
35
|
+
requests_per_minute.timeline(2 * 60).each do |v|
|
36
|
+
puts "#{v.start_time}: #{v.value}"
|
37
|
+
end
|
38
|
+
|
39
|
+
max_per_minute = PulseMeter::Sensor::Timelined::Max.new(:my_t_max,
|
40
|
+
:interval => 60, # max for each minute
|
41
|
+
:ttl => 24 * 60 * 60 # keep data one day
|
42
|
+
)
|
43
|
+
max_per_minute.event(3)
|
44
|
+
max_per_minute.event(1)
|
45
|
+
max_per_minute.event(2)
|
46
|
+
sleep(60)
|
47
|
+
max_per_minute.event(5)
|
48
|
+
max_per_minute.event(7)
|
49
|
+
max_per_minute.event(6)
|
50
|
+
max_per_minute.timeline(2 * 60).each do |v|
|
51
|
+
puts "#{v.start_time}: #{v.value}"
|
52
|
+
end
|
data/lib/pulse-meter.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "redis"
|
2
|
+
require "pulse-meter/version"
|
3
|
+
require "pulse-meter/mixins/dumper"
|
4
|
+
require "pulse-meter/mixins/utils"
|
5
|
+
require "pulse-meter/sensor"
|
6
|
+
|
7
|
+
module PulseMeter
|
8
|
+
@@redis = nil
|
9
|
+
|
10
|
+
def self.redis
|
11
|
+
@@redis
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.redis=(redis)
|
15
|
+
@@redis = redis
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module PulseMeter
|
4
|
+
module Mixins
|
5
|
+
# Mixin with dumping utilities
|
6
|
+
module Dumper
|
7
|
+
# Prefix for Redis keys with dumped sensors' metadata
|
8
|
+
DUMP_REDIS_KEY = "pulse_meter:dump"
|
9
|
+
|
10
|
+
module InstanceMethods
|
11
|
+
# Serializes object and saves it to Redis
|
12
|
+
# @raise [DumpError] if dumping fails for any reason
|
13
|
+
def dump!
|
14
|
+
ensure_storability!
|
15
|
+
serialized_obj = self.to_yaml
|
16
|
+
redis.hset(DUMP_REDIS_KEY, self.name, serialized_obj)
|
17
|
+
rescue
|
18
|
+
raise DumpError, "object cannot be dumped"
|
19
|
+
end
|
20
|
+
|
21
|
+
# Ensures that object is dumpable
|
22
|
+
# @raise [DumpError] if object cannot be dumped
|
23
|
+
def ensure_storability!
|
24
|
+
raise DumpError, "#name attribute must be readable" unless self.respond_to?(:name)
|
25
|
+
raise DumpError, "#redis attribute must be available" unless self.respond_to?(:redis) && self.redis
|
26
|
+
end
|
27
|
+
|
28
|
+
# Cleans up object dump in Redis
|
29
|
+
def cleanup_dump
|
30
|
+
redis.hdel(DUMP_REDIS_KEY, self.name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module ClassMethods
|
35
|
+
# Restores object from Redis
|
36
|
+
# @param name [String] object name
|
37
|
+
# @return [Object]
|
38
|
+
# @raise [RestoreError] if object cannot be restored for any reason
|
39
|
+
def restore(name)
|
40
|
+
serialized_obj = PulseMeter.redis.hget(DUMP_REDIS_KEY, name)
|
41
|
+
YAML::load(serialized_obj)
|
42
|
+
rescue
|
43
|
+
raise RestoreError, "cannot restore #{name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
# Lists all dumped objects' names
|
47
|
+
# @return [Array<String>]
|
48
|
+
# @raise [RestoreError] if list cannot be retrieved for any reason
|
49
|
+
def list_names
|
50
|
+
PulseMeter.redis.hkeys(DUMP_REDIS_KEY)
|
51
|
+
rescue
|
52
|
+
raise RestoreError, "cannot get data from redis"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Safely restores all dumped objects
|
56
|
+
# @return [Array<Object>]
|
57
|
+
def list_objects
|
58
|
+
objects = []
|
59
|
+
list_names.each do |name|
|
60
|
+
begin
|
61
|
+
objects << restore(name)
|
62
|
+
rescue
|
63
|
+
end
|
64
|
+
end
|
65
|
+
objects
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.included(base)
|
70
|
+
base.send :include, InstanceMethods
|
71
|
+
base.send :extend, ClassMethods
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module PulseMeter
|
4
|
+
module Mixins
|
5
|
+
# Mixin with various useful functions
|
6
|
+
module Utils
|
7
|
+
# Tries to find a class with the name specified in the argument string
|
8
|
+
# @param const_name [String] class name
|
9
|
+
# @return [Class] if given class definde
|
10
|
+
# @return [NilClass] if given class is not defined
|
11
|
+
def constantize(const_name)
|
12
|
+
return unless const_name.respond_to?(:to_s)
|
13
|
+
const_name.to_s.split('::').reduce(Module, :const_get)
|
14
|
+
rescue NameError
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Ensures that hash value specified by key can be converted to positive integer.
|
19
|
+
# In case it can makes in-place conversion and returns the value.
|
20
|
+
# @param options [Hash] hash to be looked at
|
21
|
+
# @param key [Object] hash key
|
22
|
+
# @param default [Object] default value to be returned
|
23
|
+
# @raise [ArgumentError] unless value is positive integer
|
24
|
+
# @return [Fixnum]
|
25
|
+
def assert_positive_integer!(options, key, default = nil)
|
26
|
+
value = options[key] || default
|
27
|
+
raise ArgumentError, "#{key} should be defined" unless value
|
28
|
+
raise ArgumentError, "#{key} should be integer" unless value.respond_to?(:to_i)
|
29
|
+
raise ArgumentError, "#{key} should be positive" unless value.to_i > 0
|
30
|
+
options[key] = value.to_i
|
31
|
+
end
|
32
|
+
|
33
|
+
# Ensures that hash value specified by key is can be converted to float
|
34
|
+
# and it is within given range.
|
35
|
+
# In case it can makes in-place conversion and returns the value.
|
36
|
+
# @param options [Hash] hash to be looked at
|
37
|
+
# @param key [Object] hash key
|
38
|
+
# @param from [Float] lower bound
|
39
|
+
# @param to [Float] upper bound
|
40
|
+
# @raise [ArgumentError] unless value is float within given range
|
41
|
+
# @return [Float]
|
42
|
+
def assert_ranged_float!(options, key, from, to)
|
43
|
+
f = options[key]
|
44
|
+
raise ArgumentError, "#{key} should be defined" unless f
|
45
|
+
raise ArgumentError, "#{key} should be float" unless f.respond_to?(:to_f)
|
46
|
+
f = f.to_f
|
47
|
+
raise ArgumentError, "#{key} should be between #{from} and #{to}" unless f >= from && f <= to
|
48
|
+
options[key] = f
|
49
|
+
end
|
50
|
+
|
51
|
+
# Generates uniq random string
|
52
|
+
# @return [String]
|
53
|
+
def uniqid
|
54
|
+
SecureRandom.hex(32)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Capitalizes the first letter of each word in string
|
58
|
+
# @param str [String]
|
59
|
+
# @return [String]
|
60
|
+
# @raise [ArgumentError] unless passed value responds to to_s
|
61
|
+
def titleize(str)
|
62
|
+
raise ArgumentError unless str.respond_to?(:to_s)
|
63
|
+
str.to_s.split(/[\s_]+/).map(&:capitalize).join(' ')
|
64
|
+
end
|
65
|
+
|
66
|
+
# Converts string from snake_case to CamelCase
|
67
|
+
# @param str [String] string to be camelized
|
68
|
+
# @param first_letter_upper [TrueClass, FalseClass] says if the first letter must be uppercased
|
69
|
+
# @return [String]
|
70
|
+
# @raise [ArgumentError] unless passed value responds to to_s
|
71
|
+
def camelize(str, first_letter_upper = false)
|
72
|
+
raise ArgumentError unless str.respond_to?(:to_s)
|
73
|
+
terms = str.to_s.split(/_/)
|
74
|
+
first = terms.shift
|
75
|
+
(first_letter_upper ? first.capitalize : first.downcase) + terms.map(&:capitalize).join
|
76
|
+
end
|
77
|
+
|
78
|
+
# Deeply capitalizes Array values or Hash keys
|
79
|
+
def camelize_keys(item)
|
80
|
+
case item
|
81
|
+
when Array
|
82
|
+
item.map{|i| camelize_keys(i)}
|
83
|
+
when Hash
|
84
|
+
h = {}
|
85
|
+
item.each{ |k, v| h[camelize(k)] = camelize_keys(v)}
|
86
|
+
h
|
87
|
+
else
|
88
|
+
item
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|