pulse-meter 0.2.7 → 0.2.8

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.
@@ -0,0 +1,24 @@
1
+ module Enumerable
2
+ require 'csv'
3
+ require 'terminal-table'
4
+
5
+ def convert_time
6
+ map {|el| el.is_a?(Time) ? el.to_i : el}
7
+ end
8
+
9
+ def to_table(format = nil)
10
+ if "csv" == format.to_s
11
+ CSV.generate(:col_sep => ';') do |csv|
12
+ self.each {|row| csv << row.convert_time}
13
+ end
14
+ else
15
+ self.each_with_object(Terminal::Table.new) do |row, table|
16
+ table << if row.respond_to?(:map)
17
+ row.map(&:to_s)
18
+ else
19
+ row
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,30 +1,4 @@
1
- module Enumerable
2
- def convert_time
3
- map do |el|
4
- if el.is_a?(Time)
5
- el.to_i
6
- else
7
- el
8
- end
9
- end
10
- end
11
-
12
- def to_table(format = nil)
13
- if "csv" == format
14
- CSV.generate(:col_sep => ';') do |csv|
15
- self.each {|row| csv << row.convert_time}
16
- end
17
- else
18
- self.each_with_object(Terminal::Table.new) do |row, table|
19
- table << if row.respond_to?(:map)
20
- row.map(&:to_s)
21
- else
22
- row
23
- end
24
- end
25
- end
26
- end
27
- end
1
+ require 'pulse-meter/extensions/enumerable'
28
2
 
29
3
  module PulseMeter
30
4
  module Mixins
@@ -52,7 +26,7 @@ module PulseMeter
52
26
  data = [
53
27
  ["Name", "Class", "ttl", "raw data ttl", "interval", "reduce delay"],
54
28
  ]
55
- data << :separator unless format == 'csv'
29
+ data << :separator unless 'csv' == format.to_s
56
30
  all_sensors.each do |s|
57
31
  if s.kind_of? PulseMeter::Sensor::Timeline
58
32
  data << [s.name, s.class, s.ttl, s.raw_data_ttl, s.interval, s.reduce_delay]
@@ -1,11 +1,16 @@
1
1
  module PulseMeter
2
2
  module Sensor
3
+ # Constructs multiple sensors from configuration passed
3
4
  class Configuration
4
5
  include PulseMeter::Mixins::Utils
5
6
  include Enumerable
6
7
 
8
+ # @!attribute [r] sensors
9
+ # @return [Hash] hash of sensors with names as keys and sensors as values
7
10
  attr_reader :sensors
8
11
 
12
+ # Initializes sensors
13
+ # @param opts [Hash] sensors' configuration
9
14
  def initialize(opts = {})
10
15
  @sensors = {}
11
16
  opts.each do |name, opts|
@@ -13,6 +18,9 @@ module PulseMeter
13
18
  end
14
19
  end
15
20
 
21
+ # Adds sensor
22
+ # @param name [Symbol] sensor name
23
+ # @param opts [Hash] sensor options
16
24
  def add_sensor(name, opts)
17
25
  sensor_type = opts.respond_to?(:sensor_type) ? opts.sensor_type : opts[:sensor_type]
18
26
  klass_s = sensor_class(sensor_type)
@@ -22,16 +30,22 @@ module PulseMeter
22
30
  @sensors[name.to_s] = klass.new(name, symbolize_keys(args.to_hash))
23
31
  end
24
32
 
33
+ # Returns previously initialized sensor by name
34
+ # @param name [Symbol] sensor name
35
+ # @return [Sensor] sensor
25
36
  def sensor(name)
26
37
  @sensors[name.to_s]
27
38
  end
28
39
 
40
+ # Iterates over each sensor
29
41
  def each
30
42
  @sensors.each_value do |sensor|
31
43
  yield(sensor)
32
44
  end
33
45
  end
34
46
 
47
+ # Invokes event for any sensor
48
+ # @raise [ArgumentError] unless sensor exists
35
49
  def method_missing(name, *args)
36
50
  name = name.to_s
37
51
  if @sensors.has_key?(name)
@@ -25,7 +25,10 @@ module PulseMeter
25
25
  # @param data [Hash] hash where keys represent counter keys
26
26
  # and values are increments for their keys
27
27
  def process_event(data)
28
- data.each_pair {|k, v| redis.hincrby(value_key, k, v.to_i)}
28
+ data.each_pair do |k, v|
29
+ redis.hincrby(value_key, k, v.to_i)
30
+ redis.hincrby(value_key, :total, v.to_i)
31
+ end
29
32
  end
30
33
 
31
34
  end
@@ -108,12 +108,17 @@ module PulseMeter
108
108
  # Returts sensor data within given time
109
109
  # @param from [Time] lower bound
110
110
  # @param till [Time] upper bound
111
+ # @param skip_optimization [Boolean] must be set to true to skip interval optimization
111
112
  # @return [Array<SensorData>]
112
113
  # @raise ArgumentError if argumets are not valid time objects
113
- def timeline_within(from, till)
114
+ def timeline_within(from, till, skip_optimization = false)
114
115
  raise ArgumentError unless from.kind_of?(Time) && till.kind_of?(Time)
115
116
  start_time, end_time = from.to_i, till.to_i
116
- actual_interval = optimized_inteval(start_time, end_time)
117
+ actual_interval = if skip_optimization
118
+ interval
119
+ else
120
+ optimized_interval(start_time, end_time)
121
+ end
117
122
  current_interval_id = get_interval_id(start_time) + actual_interval
118
123
  keys = []
119
124
  ids = []
@@ -134,19 +139,6 @@ module PulseMeter
134
139
  res
135
140
  end
136
141
 
137
- # Makes interval optimization so that the requested timespan contains less than MAX_TIMESPAN_POINTS values
138
- # @param start_time [Fixnum] unix timestamp of timespan start
139
- # @param end_time [Fixnum] unix timestamp of timespan start
140
- # @return [Fixnum] optimized interval in seconds.
141
- def optimized_inteval(start_time, end_time)
142
- res_interval = interval
143
- timespan = end_time - start_time
144
- while timespan / res_interval > MAX_TIMESPAN_POINTS - 1
145
- res_interval *= 2
146
- end
147
- res_interval
148
- end
149
-
150
142
  # Returns sensor data for given interval making in-memory summarization
151
143
  # and returns calculated value
152
144
  # @param interval_id [Fixnum]
@@ -174,11 +166,7 @@ module PulseMeter
174
166
  keys << raw_data_key(current_interval_id)
175
167
  current_interval_id += interval
176
168
  end
177
- if keys.empty?
178
- 0
179
- else
180
- redis.del(*keys)
181
- end
169
+ keys.empty? ? 0 : redis.del(*keys)
182
170
  end
183
171
 
184
172
  # Returns Redis key by which raw data for current interval is stored
@@ -255,6 +243,20 @@ module PulseMeter
255
243
  end
256
244
  end
257
245
 
246
+ # Makes interval optimization so that the requested timespan contains less than MAX_TIMESPAN_POINTS values
247
+ # @param start_time [Fixnum] unix timestamp of timespan start
248
+ # @param end_time [Fixnum] unix timestamp of timespan start
249
+ # @return [Fixnum] optimized interval in seconds.
250
+ def optimized_interval(start_time, end_time)
251
+ res_interval = interval
252
+ timespan = end_time - start_time
253
+ while timespan / res_interval > MAX_TIMESPAN_POINTS - 1
254
+ res_interval *= 2
255
+ end
256
+ res_interval
257
+ end
258
+
259
+
258
260
  end
259
261
  end
260
262
  end
@@ -7,7 +7,10 @@ module PulseMeter
7
7
  # Good replacement for multiple counters to be visualized together
8
8
  class HashedCounter < Timeline
9
9
  def aggregate_event(key, data)
10
- data.each_pair {|k, v| redis.hincrby(key, k, v)}
10
+ data.each_pair do |k, v|
11
+ redis.hincrby(key, k, v)
12
+ redis.hincrby(key, :total, v)
13
+ end
11
14
  end
12
15
 
13
16
  def summarize(key)
@@ -1,3 +1,3 @@
1
1
  module PulseMeter
2
- VERSION = "0.2.7"
2
+ VERSION = "0.2.8"
3
3
  end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'pulse-meter/extensions/enumerable'
3
+
4
+ describe Enumerable do
5
+ let!(:time) {Time.new}
6
+ describe "#convert_time" do
7
+ it "converts Time objects to unixtime" do
8
+ [time].convert_time.should == [time.to_i]
9
+ end
10
+
11
+ it "does not change other members" do
12
+ [1, 2, 3].convert_time.should == [1, 2 ,3]
13
+ end
14
+ end
15
+
16
+ describe "#to_table" do
17
+ context "when format is csv" do
18
+ it "returns csv as string" do
19
+ [].to_table(:csv).should be_instance_of(String)
20
+ end
21
+
22
+ it "returns csv containing each subarray as a row" do
23
+ [[:a, :b], [:c, :d]].to_table(:csv).should == "a;b\nc;d\n"
24
+ end
25
+
26
+ it "converts Time objects to unixtime" do
27
+ [[time]].to_table(:csv).should == "#{time.to_i}\n"
28
+ end
29
+
30
+ it "takes format argument both as string and as symbol" do
31
+ [[:foo]].to_table("csv").should == "foo\n"
32
+ [[:foo]].to_table(:csv).should == "foo\n"
33
+ end
34
+ end
35
+
36
+ context "when format is table" do
37
+ it "return Terminal::Table instance" do
38
+ [].to_table.should be_instance_of(Terminal::Table)
39
+ end
40
+
41
+ it "returns table containing each subarray as a row" do
42
+ data = [[:a, :b], [:c, :d]]
43
+ table = [[:a, :b], [:c, :d]].to_table
44
+ table.rows.map do |row|
45
+ row.cells.map(&:to_s).map(&:strip).map(&:to_sym)
46
+ end.should == data
47
+ end
48
+ end
49
+
50
+ it "uses table format as default" do
51
+ [].to_table.should be_instance_of(Terminal::Table)
52
+ end
53
+
54
+ it "uses table format unless it is :csv or 'csv'" do
55
+ [].to_table(:unknown_format).should be_instance_of(Terminal::Table)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,112 @@
1
+ require 'spec_helper'
2
+
3
+ describe PulseMeter::Mixins::Cmd do
4
+ class Dummy
5
+ extend PulseMeter::Mixins::Cmd
6
+
7
+ def self.options
8
+ {host: :localhost, port: 6379, db: 0}
9
+ end
10
+ end
11
+
12
+ let(:dummy){ Dummy }
13
+ before {PulseMeter.redis = Redis.new}
14
+
15
+ describe "#fail!" do
16
+ it "prints given message and exits" do
17
+ STDOUT.should_receive(:puts).with(:msg)
18
+ lambda {dummy.fail!(:msg)}.should raise_error(SystemExit)
19
+ end
20
+ end
21
+
22
+ describe '#with_redis' do
23
+ it "initializes redies and yields a block" do
24
+ PulseMeter.redis = nil
25
+ dummy.with_redis do
26
+ PulseMeter.redis.should_not be_nil
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#with_safe_restore_of" do
32
+ it "restores sensor by name and passes it to block" do
33
+ sensor = PulseMeter::Sensor::Counter.new(:foo)
34
+ dummy.with_safe_restore_of(:foo) do |s|
35
+ s.should be_instance_of(sensor.class)
36
+ end
37
+ end
38
+
39
+ it "prints error and exits if sensor cannot be restored" do
40
+ STDOUT.should_receive(:puts).with("Sensor nonexistant is unknown or cannot be restored")
41
+ lambda {dummy.with_safe_restore_of(:nonexistant) {|s| s}}.should raise_error(SystemExit)
42
+ end
43
+ end
44
+
45
+ describe "#all_sensors" do
46
+ it "is just an alias to PulseMeter::Sensor::Timeline.list_objects" do
47
+ PulseMeter::Sensor::Timeline.should_receive(:list_objects)
48
+ dummy.all_sensors
49
+ end
50
+ end
51
+
52
+ describe "#all_sensors_table" do
53
+ before {PulseMeter.redis.flushall}
54
+ let(:init_values){ {:ttl => 1, :raw_data_ttl => 2, :interval => 3, :reduce_delay => 4} }
55
+ let!(:s1) {PulseMeter::Sensor::Counter.new(:s1)}
56
+ let!(:s2) {PulseMeter::Sensor::Timelined::Counter.new(:s2, init_values)}
57
+ let!(:table) {dummy.all_sensors_table}
58
+ let!(:csv) {dummy.all_sensors_table(:csv)}
59
+ let!(:parsed_csv) {CSV.parse(csv, col_sep: ";")}
60
+
61
+ def rows(format)
62
+ if "csv" == format.to_s
63
+ parsed_csv
64
+ else
65
+ table.rows.map do |row|
66
+ row.cells.map(&:to_s).map(&:strip)
67
+ end
68
+ end
69
+ end
70
+
71
+ def sensor_row(name, format)
72
+ rows(format).select {|row| row[0] == name}.first
73
+ end
74
+
75
+ [:csv, :table].each do |format|
76
+ context "when format is #{format}" do
77
+
78
+ if "csv" == format.to_s
79
+ it "returns csv as string" do
80
+ csv.should be_instance_of(String)
81
+ end
82
+ else
83
+ it "returns Terminal::Table instance" do
84
+ table.should be_instance_of(Terminal::Table)
85
+ end
86
+ end
87
+
88
+ it "has title row" do
89
+ rows(format)[0].should == ["Name", "Class", "ttl", "raw data ttl", "interval", "reduce delay"]
90
+ end
91
+
92
+ it "has one row for each sensor (and a title)" do
93
+ rows(format).count.should == 3
94
+ end
95
+
96
+ it "can display timelined sensors" do
97
+ sensor_row("s2", format).should == [
98
+ s2.name, s2.class, s2.ttl, s2.raw_data_ttl, s2.interval, s2.reduce_delay
99
+ ].map(&:to_s)
100
+ end
101
+
102
+ it "can display static sensors" do
103
+ sensor_row("s1", format).should == [
104
+ s1.name, s1.class, "", "", "", ""
105
+ ].map(&:to_s)
106
+ end
107
+
108
+ end
109
+ end
110
+ end
111
+
112
+ end
@@ -15,6 +15,10 @@ describe PulseMeter::Sensor::HashedCounter do
15
15
  expect{ sensor.event({"foo" => 10.4}) }.to change{ sensor.value["foo"] }.from(0).to(10)
16
16
  expect{ sensor.event({"foo" => 15.1}) }.to change{ sensor.value["foo"] }.from(10).to(25)
17
17
  end
18
+
19
+ it "should increment total value" do
20
+ expect{ sensor.event({"foo" => 1, "bar" => 2}) }.to change{sensor.value["total"]}.from(0).to(3)
21
+ end
18
22
  end
19
23
 
20
24
  describe "#value" do
@@ -24,8 +28,8 @@ describe PulseMeter::Sensor::HashedCounter do
24
28
 
25
29
  it "should store redis hash by value_key" do
26
30
  sensor.event({"foo" => 1})
27
- sensor.value.should == {"foo" => 1}
28
- redis.hgetall(sensor.value_key).should == {"foo" => "1"}
31
+ sensor.value.should == {"foo" => 1, "total" => 1}
32
+ redis.hgetall(sensor.value_key).should == {"foo" => "1", "total" => "1"}
29
33
  end
30
34
  end
31
35
 
@@ -2,7 +2,7 @@ require 'spec_helper'
2
2
 
3
3
  describe PulseMeter::Sensor::Timelined::HashedCounter do
4
4
  it_should_behave_like "timeline sensor", {}, {:foo => 1}
5
- it_should_behave_like "timelined subclass", [{:foo => 1}, {:foo => 2}], {:foo => 3}.to_json
6
- it_should_behave_like "timelined subclass", [{:foo => 1}, {:foo => :bad_value}], {:foo => 1}.to_json
7
- it_should_behave_like "timelined subclass", [{:foo => 1}, {:boo => 2}], {:foo => 1, :boo => 2}.to_json
5
+ it_should_behave_like "timelined subclass", [{:foo => 1}, {:foo => 2}], {:foo => 3, :total => 3}.to_json
6
+ it_should_behave_like "timelined subclass", [{:foo => 1}, {:foo => :bad_value}], {:foo => 1, :total => 1}.to_json
7
+ it_should_behave_like "timelined subclass", [{:foo => 1}, {:boo => 2}], {:foo => 1, :boo => 2, :total => 3}.to_json
8
8
  end
@@ -242,12 +242,32 @@ shared_examples_for "timeline sensor" do |extra_init_values, default_event|
242
242
  end
243
243
  end
244
244
 
245
- it "should not return more than #{PulseMeter::Sensor::Timeline::MAX_TIMESPAN_POINTS} points" do
246
- max = PulseMeter::Sensor::Timeline::MAX_TIMESPAN_POINTS
247
- (1..10).each do |i|
248
- timespan = sensor.interval * max * (2**i)
249
- sensor.timeline_within(Time.now, Time.now - timespan).size.should < max
245
+ context "to avoid getting to much data" do
246
+ let(:max) {PulseMeter::Sensor::Timeline::MAX_TIMESPAN_POINTS}
250
247
 
248
+ it "should skip some points not to exceed MAX_TIMESPAN_POINTS" do
249
+ count = max * 2
250
+ sensor.timeline_within(
251
+ Time.at(@start_of_interval - 1),
252
+ Time.at(@start_of_interval + count * interval)
253
+ ).size.should < max
254
+ end
255
+
256
+ it "should not skip any points when timeline orginal size is less then MAX_TIMESPAN_POINTS" do
257
+ count = max - 1
258
+ sensor.timeline_within(
259
+ Time.at(@start_of_interval - 1),
260
+ Time.at(@start_of_interval + count * interval)
261
+ ).size.should == count
262
+ end
263
+
264
+ it "should give full data in case skip_optimization parameter set to true" do
265
+ count = max * 2
266
+ sensor.timeline_within(
267
+ Time.at(@start_of_interval - 1),
268
+ Time.at(@start_of_interval + count * interval),
269
+ true
270
+ ).size.should == count
251
271
  end
252
272
  end
253
273
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pulse-meter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.7
4
+ version: 0.2.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-07-23 00:00:00.000000000 Z
13
+ date: 2012-08-02 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: gon-sinatra
@@ -333,6 +333,7 @@ files:
333
333
  - examples/server_config.yml
334
334
  - lib/cmd.rb
335
335
  - lib/pulse-meter.rb
336
+ - lib/pulse-meter/extensions/enumerable.rb
336
337
  - lib/pulse-meter/mixins/cmd.rb
337
338
  - lib/pulse-meter/mixins/dumper.rb
338
339
  - lib/pulse-meter/mixins/utils.rb
@@ -405,6 +406,8 @@ files:
405
406
  - lib/pulse-meter/visualize/widgets/timeline.rb
406
407
  - lib/pulse-meter/visualizer.rb
407
408
  - pulse-meter.gemspec
409
+ - spec/pulse_meter/extensions/enumerable_spec.rb
410
+ - spec/pulse_meter/mixins/cmd_spec.rb
408
411
  - spec/pulse_meter/mixins/dumper_spec.rb
409
412
  - spec/pulse_meter/mixins/utils_spec.rb
410
413
  - spec/pulse_meter/sensor/base_spec.rb
@@ -472,7 +475,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
472
475
  version: '0'
473
476
  segments:
474
477
  - 0
475
- hash: -450227772948230282
478
+ hash: -2299028715226414900
476
479
  requirements: []
477
480
  rubyforge_project:
478
481
  rubygems_version: 1.8.24
@@ -481,6 +484,8 @@ specification_version: 3
481
484
  summary: Lightweight Redis-based metrics aggregator and processor with CLI and simple
482
485
  and customizable WEB interfaces
483
486
  test_files:
487
+ - spec/pulse_meter/extensions/enumerable_spec.rb
488
+ - spec/pulse_meter/mixins/cmd_spec.rb
484
489
  - spec/pulse_meter/mixins/dumper_spec.rb
485
490
  - spec/pulse_meter/mixins/utils_spec.rb
486
491
  - spec/pulse_meter/sensor/base_spec.rb