pulse-meter 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 +19 -0
- data/.rbenv-version +1 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/Procfile +3 -0
- data/README.md +440 -0
- data/Rakefile +53 -0
- data/bin/pulse +6 -0
- data/examples/basic.ru +109 -0
- data/examples/basic_sensor_data.rb +38 -0
- data/examples/full/Procfile +2 -0
- data/examples/full/client.rb +82 -0
- data/examples/full/server.ru +114 -0
- data/examples/minimal/Procfile +2 -0
- data/examples/minimal/client.rb +16 -0
- data/examples/minimal/server.ru +20 -0
- data/examples/readme_client_example.rb +52 -0
- data/lib/cmd.rb +150 -0
- data/lib/pulse-meter.rb +17 -0
- data/lib/pulse-meter/mixins/dumper.rb +72 -0
- data/lib/pulse-meter/mixins/utils.rb +91 -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/lib/pulse-meter/visualize/app.rb +43 -0
- data/lib/pulse-meter/visualize/dsl.rb +0 -0
- data/lib/pulse-meter/visualize/dsl/errors.rb +46 -0
- data/lib/pulse-meter/visualize/dsl/layout.rb +55 -0
- data/lib/pulse-meter/visualize/dsl/page.rb +50 -0
- data/lib/pulse-meter/visualize/dsl/sensor.rb +21 -0
- data/lib/pulse-meter/visualize/dsl/widget.rb +84 -0
- data/lib/pulse-meter/visualize/layout.rb +54 -0
- data/lib/pulse-meter/visualize/page.rb +30 -0
- data/lib/pulse-meter/visualize/public/css/application.css +19 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.css +4883 -0
- data/lib/pulse-meter/visualize/public/css/bootstrap.min.css +729 -0
- data/lib/pulse-meter/visualize/public/favicon.ico +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings-white.png +0 -0
- data/lib/pulse-meter/visualize/public/img/glyphicons-halflings.png +0 -0
- data/lib/pulse-meter/visualize/public/js/application.coffee +262 -0
- data/lib/pulse-meter/visualize/public/js/application.js +279 -0
- data/lib/pulse-meter/visualize/public/js/backbone-min.js +38 -0
- data/lib/pulse-meter/visualize/public/js/bootstrap.js +1835 -0
- data/lib/pulse-meter/visualize/public/js/highcharts.js +203 -0
- data/lib/pulse-meter/visualize/public/js/jquery-1.7.2.min.js +4 -0
- data/lib/pulse-meter/visualize/public/js/json2.js +487 -0
- data/lib/pulse-meter/visualize/public/js/underscore-min.js +32 -0
- data/lib/pulse-meter/visualize/sensor.rb +60 -0
- data/lib/pulse-meter/visualize/views/main.haml +40 -0
- data/lib/pulse-meter/visualize/widget.rb +68 -0
- data/lib/pulse-meter/visualizer.rb +30 -0
- data/lib/test_helpers/matchers.rb +36 -0
- data/pulse-meter.gemspec +39 -0
- data/spec/pulse_meter/mixins/dumper_spec.rb +158 -0
- data/spec/pulse_meter/mixins/utils_spec.rb +134 -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 +58 -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/visualize/app_spec.rb +27 -0
- data/spec/pulse_meter/visualize/dsl/layout_spec.rb +64 -0
- data/spec/pulse_meter/visualize/dsl/page_spec.rb +75 -0
- data/spec/pulse_meter/visualize/dsl/sensor_spec.rb +30 -0
- data/spec/pulse_meter/visualize/dsl/widget_spec.rb +127 -0
- data/spec/pulse_meter/visualize/layout_spec.rb +55 -0
- data/spec/pulse_meter/visualize/page_spec.rb +150 -0
- data/spec/pulse_meter/visualize/sensor_spec.rb +120 -0
- data/spec/pulse_meter/visualize/widget_spec.rb +113 -0
- data/spec/pulse_meter/visualizer_spec.rb +42 -0
- data/spec/pulse_meter_spec.rb +16 -0
- data/spec/shared_examples/timeline_sensor.rb +279 -0
- data/spec/shared_examples/timelined_subclass.rb +23 -0
- data/spec/spec_helper.rb +29 -0
- metadata +435 -0
@@ -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/cmd.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'terminal-table'
|
3
|
+
require 'time'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Cmd
|
7
|
+
class All < Thor
|
8
|
+
include PulseMeter::Mixins::Utils
|
9
|
+
no_tasks do
|
10
|
+
def with_redis
|
11
|
+
PulseMeter.redis = Redis.new :host => options[:host], :port => options[:port], :db => options[:db]
|
12
|
+
yield
|
13
|
+
end
|
14
|
+
|
15
|
+
def with_safe_restore_of(name, &block)
|
16
|
+
with_redis do
|
17
|
+
sensor = PulseMeter::Sensor::Base.restore(name)
|
18
|
+
block.call(sensor)
|
19
|
+
end
|
20
|
+
rescue PulseMeter::RestoreError
|
21
|
+
fail! "Sensor #{name} is unknown or cannot be restored"
|
22
|
+
end
|
23
|
+
|
24
|
+
def all_sensors
|
25
|
+
PulseMeter::Sensor::Timeline.list_objects
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_sensors_table(title = nil)
|
29
|
+
table = Terminal::Table.new :title => title
|
30
|
+
table << ["Name", "Class", "ttl", "raw data ttl", "interval", "reduce delay"]
|
31
|
+
table << :separator
|
32
|
+
all_sensors.each do |s|
|
33
|
+
if s.kind_of? PulseMeter::Sensor::Timeline
|
34
|
+
table << [s.name, s.class, s.ttl, s.raw_data_ttl, s.interval, s.reduce_delay]
|
35
|
+
else
|
36
|
+
table << [s.name, s.class] + ['-'] * 4
|
37
|
+
end
|
38
|
+
end
|
39
|
+
table
|
40
|
+
end
|
41
|
+
|
42
|
+
def fail!(description = nil)
|
43
|
+
puts description if description
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.common_options
|
48
|
+
method_option :host, :default => '127.0.0.1', :desc => "Redis host"
|
49
|
+
method_option :port, :default => 6379, :desc => "Redis port"
|
50
|
+
method_option :db, :default => 0, :desc => "Redis db"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "sensors", "List all sensors available"
|
55
|
+
common_options
|
56
|
+
def sensors
|
57
|
+
with_redis {puts all_sensors_table('Registered sensors')}
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "reduce", "Execute reduction for all sensors' raw data"
|
61
|
+
common_options
|
62
|
+
def reduce
|
63
|
+
with_redis do
|
64
|
+
puts all_sensors_table('Registered sensors to be reduced')
|
65
|
+
PulseMeter::Sensor::Timeline.reduce_all_raw
|
66
|
+
puts "DONE"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
desc "event NAME VALUE", "Send event VALUE to sensor NAME"
|
71
|
+
common_options
|
72
|
+
method_option :format, :default => :plain, :desc => "Event format: plain or json"
|
73
|
+
def event(name, value)
|
74
|
+
if "json" == options[:format]
|
75
|
+
value = JSON.parse(value)
|
76
|
+
end
|
77
|
+
with_safe_restore_of(name) {|sensor| sensor.event(value)}
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "timeline NAME SECONDS", "Get sensor's NAME timeline for last SECONDS"
|
81
|
+
common_options
|
82
|
+
def timeline(name, seconds)
|
83
|
+
with_safe_restore_of(name) do |sensor|
|
84
|
+
table = Terminal::Table.new
|
85
|
+
sensor.timeline(seconds).each {|data| table << [data.start_time, data.value || '-']}
|
86
|
+
puts table
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
desc "timeline_within NAME FROM TILL", "Get sensor's NAME timeline in interval. Time format: YYYY-MM-DD HH:MM:SS"
|
91
|
+
common_options
|
92
|
+
def timeline_within(name, from, till)
|
93
|
+
with_safe_restore_of(name) do |sensor|
|
94
|
+
table = Terminal::Table.new
|
95
|
+
sensor.timeline_within(
|
96
|
+
Time.parse(from),
|
97
|
+
Time.parse(till)
|
98
|
+
).each {|data| table << [data.start_time, data.value || '-']}
|
99
|
+
puts table
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
desc "delete NAME", "Delete sensor by name"
|
104
|
+
common_options
|
105
|
+
def delete(name)
|
106
|
+
with_safe_restore_of(name) {|sensor| sensor.cleanup}
|
107
|
+
puts "Sensor #{name} deleted"
|
108
|
+
end
|
109
|
+
|
110
|
+
desc "create NAME TYPE", "Create sensor of given type"
|
111
|
+
common_options
|
112
|
+
method_option :interval, :required => true, :type => :numeric, :desc => "Rotation interval"
|
113
|
+
method_option :ttl, :required => true, :type => :numeric, :desc => "How long summarized data will be stored"
|
114
|
+
method_option :raw_data_ttl, :type => :numeric, :desc => "How long unsummarized raw data will be stored"
|
115
|
+
method_option :reduce_delay, :type => :numeric, :desc => "Delay between end of interval and summarization"
|
116
|
+
method_option :annotation, :type => :string, :desc => "Sensor annotation"
|
117
|
+
def create(name, type)
|
118
|
+
with_redis do
|
119
|
+
klass = constantize("PulseMeter::Sensor::Timelined::#{type}")
|
120
|
+
puts "PulseMeter::Sensor::Timelined::#{type}"
|
121
|
+
fail! "Unknown sensor type #{type}" unless klass
|
122
|
+
sensor = klass.new(name, options.dup)
|
123
|
+
puts "Sensor created"
|
124
|
+
puts all_sensors_table
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
desc "create_simple NAME TYPE", "Create simple non-timelined sensor of given type"
|
129
|
+
common_options
|
130
|
+
method_option :annotation, :type => :string, :desc => "Sensor annotation"
|
131
|
+
def create_simple(name, type)
|
132
|
+
with_redis do
|
133
|
+
klass = constantize("PulseMeter::Sensor::#{type}")
|
134
|
+
fail! "Unknown sensor type #{type}" unless klass
|
135
|
+
sensor = klass.new(name, options.dup)
|
136
|
+
puts "Sensor created"
|
137
|
+
puts all_sensors_table
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
desc "value NAME", "Get value of non-timelined sensor"
|
142
|
+
def value(name)
|
143
|
+
with_safe_restore_of(name) do |sensor|
|
144
|
+
fail! "Sensor #{name} has no value method" unless sensor.respond_to?(:value)
|
145
|
+
puts "Value: #{sensor.value}"
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
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,72 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Mixins
|
3
|
+
# Mixin with dumping utilities
|
4
|
+
module Dumper
|
5
|
+
# Prefix for Redis keys with dumped sensors' metadata
|
6
|
+
DUMP_REDIS_KEY = "pulse_meter:dump"
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
# Serializes object and saves it to Redis
|
10
|
+
# @raise [DumpError] if dumping fails for any reason
|
11
|
+
def dump!
|
12
|
+
ensure_storability!
|
13
|
+
serialized_obj = Marshal.dump(self)
|
14
|
+
redis.hset(DUMP_REDIS_KEY, self.name, serialized_obj)
|
15
|
+
rescue
|
16
|
+
raise DumpError, "object cannot be dumped"
|
17
|
+
end
|
18
|
+
|
19
|
+
# Ensures that object is dumpable
|
20
|
+
# @raise [DumpError] if object cannot be dumped
|
21
|
+
def ensure_storability!
|
22
|
+
raise DumpError, "#name attribute must be readable" unless self.respond_to?(:name)
|
23
|
+
raise DumpError, "#redis attribute must be available" unless self.respond_to?(:redis) && self.redis
|
24
|
+
end
|
25
|
+
|
26
|
+
# Cleans up object dump in Redis
|
27
|
+
def cleanup_dump
|
28
|
+
redis.hdel(DUMP_REDIS_KEY, self.name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
# Restores object from Redis
|
34
|
+
# @param name [String] object name
|
35
|
+
# @return [Object]
|
36
|
+
# @raise [RestoreError] if object cannot be restored for any reason
|
37
|
+
def restore(name)
|
38
|
+
serialized_obj = PulseMeter.redis.hget(DUMP_REDIS_KEY, name)
|
39
|
+
Marshal.load(serialized_obj)
|
40
|
+
rescue
|
41
|
+
raise RestoreError, "cannot restore #{name}"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Lists all dumped objects' names
|
45
|
+
# @return [Array<String>]
|
46
|
+
# @raise [RestoreError] if list cannot be retrieved for any reason
|
47
|
+
def list_names
|
48
|
+
PulseMeter.redis.hkeys(DUMP_REDIS_KEY)
|
49
|
+
rescue
|
50
|
+
raise RestoreError, "cannot get data from redis"
|
51
|
+
end
|
52
|
+
|
53
|
+
# Safely restores all dumped objects
|
54
|
+
# @return [Array<Object>]
|
55
|
+
def list_objects
|
56
|
+
list_names.each_with_object([]) do |name, objects|
|
57
|
+
begin
|
58
|
+
objects << restore(name)
|
59
|
+
rescue
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.included(base)
|
66
|
+
base.send :include, InstanceMethods
|
67
|
+
base.send :extend, ClassMethods
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,91 @@
|
|
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
|
+
item.each_with_object({}) { |(k, v), h| h[camelize(k)] = camelize_keys(v)}
|
85
|
+
else
|
86
|
+
item
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'pulse-meter/sensor/base'
|
2
|
+
require 'pulse-meter/sensor/counter'
|
3
|
+
require 'pulse-meter/sensor/hashed_counter'
|
4
|
+
require 'pulse-meter/sensor/indicator'
|
5
|
+
require 'pulse-meter/sensor/timeline'
|
6
|
+
require 'pulse-meter/sensor/timelined/average'
|
7
|
+
require 'pulse-meter/sensor/timelined/counter'
|
8
|
+
require 'pulse-meter/sensor/timelined/hashed_counter'
|
9
|
+
require 'pulse-meter/sensor/timelined/min'
|
10
|
+
require 'pulse-meter/sensor/timelined/max'
|
11
|
+
require 'pulse-meter/sensor/timelined/percentile'
|
12
|
+
require 'pulse-meter/sensor/timelined/median'
|
13
|
+
|
14
|
+
# Top level sensor module
|
15
|
+
module PulseMeter
|
16
|
+
|
17
|
+
# Atomic sensor data
|
18
|
+
SensorData = Struct.new(:start_time, :value)
|
19
|
+
|
20
|
+
# General sensor exception
|
21
|
+
class SensorError < StandardError; end
|
22
|
+
|
23
|
+
# Exception to be raised when sensor name is malformed
|
24
|
+
class BadSensorName < SensorError
|
25
|
+
def initialize(name, options = {})
|
26
|
+
super("Bad sensor name: `#{name}', only a-z letters and _ are allowed")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Exception to be raised when Redis is not initialized
|
31
|
+
class RedisNotInitialized < SensorError
|
32
|
+
def initialize
|
33
|
+
super("PulseMeter.redis is not set")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Exception to be raised when sensor cannot be dumped
|
38
|
+
class DumpError < SensorError; end
|
39
|
+
|
40
|
+
# Exception to be raised when sensor cannot be restored
|
41
|
+
class RestoreError < SensorError; end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module PulseMeter
|
2
|
+
module Sensor
|
3
|
+
# @abstract Subclass and override {#event} to implement sensor
|
4
|
+
class Base
|
5
|
+
include PulseMeter::Mixins::Dumper
|
6
|
+
|
7
|
+
# @!attribute [rw] redis
|
8
|
+
# @return [Redis]
|
9
|
+
attr_accessor :redis
|
10
|
+
# @!attribute [r] name
|
11
|
+
# @return [String] sensor name
|
12
|
+
attr_reader :name
|
13
|
+
|
14
|
+
# Initializes sensor and dumps it to redis
|
15
|
+
# @param name [String] sensor name
|
16
|
+
# @option options [String] :annotation Sensor annotation
|
17
|
+
# @raise [BadSensorName] if sensor name is malformed
|
18
|
+
# @raise [RedisNotInitialized] unless Redis is initialized
|
19
|
+
def initialize(name, options={})
|
20
|
+
@name = name.to_s
|
21
|
+
if options[:annotation]
|
22
|
+
annotate(options[:annotation])
|
23
|
+
end
|
24
|
+
raise BadSensorName, @name unless @name =~ /\A\w+\z/
|
25
|
+
raise RedisNotInitialized unless PulseMeter.redis
|
26
|
+
dump!
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns Redis instance
|
30
|
+
def redis
|
31
|
+
PulseMeter.redis
|
32
|
+
end
|
33
|
+
|
34
|
+
# Saves annotation to Redis
|
35
|
+
# @param description [String] Sensor annotation
|
36
|
+
def annotate(description)
|
37
|
+
redis.set(desc_key, description)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Retrieves annotation from Redis
|
41
|
+
# @return [String] Sensor annotation
|
42
|
+
def annotation
|
43
|
+
redis.get(desc_key)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Cleans up all sensor metadata in Redis
|
47
|
+
def cleanup
|
48
|
+
redis.del(desc_key)
|
49
|
+
cleanup_dump
|
50
|
+
end
|
51
|
+
|
52
|
+
# @abstract Processes event
|
53
|
+
# @param value [Object] value produced by some kind of event
|
54
|
+
def event(value)
|
55
|
+
# do nothing here
|
56
|
+
end
|
57
|
+
|
58
|
+
protected
|
59
|
+
|
60
|
+
# Forms Redis key to store annotation
|
61
|
+
def desc_key
|
62
|
+
"pulse_meter:desc:#{name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
# For a block
|
66
|
+
# @yield Executes it within Redis multi
|
67
|
+
def multi
|
68
|
+
redis.multi
|
69
|
+
yield
|
70
|
+
redis.exec
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|