counters 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +1 -0
- data/counters.gemspec +5 -0
- data/lib/counters/redis.rb +8 -3
- data/lib/counters/version.rb +1 -1
- data/spec/redis_counter_spec.rb +40 -20
- data/spec/spec_helper.rb +14 -0
- metadata +31 -5
data/README.textile
CHANGED
@@ -7,6 +7,7 @@ h2. Sample Usage
|
|
7
7
|
Let's say you have a crawler. You'd like to record the number of URLs you visit, the number of URLs you skipped due to 304 Not Modified responses, and the number of bytes you consumed, and the amount of time each page takes to process. Here's how you'd do that:
|
8
8
|
|
9
9
|
<pre><code>require "counters"
|
10
|
+
require "redis"
|
10
11
|
require "rest_client"
|
11
12
|
Counter = Counters::Redis.new(Redis.new, "counters")
|
12
13
|
|
data/counters.gemspec
CHANGED
@@ -17,6 +17,11 @@ Gem::Specification.new do |s|
|
|
17
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
|
20
|
+
# Eventually, we might have something like this
|
21
|
+
# s.add_optional_dependency "redis"
|
22
|
+
s.add_development_dependency "redis"
|
23
|
+
|
20
24
|
s.add_development_dependency "rspec"
|
21
25
|
s.add_development_dependency "timecop"
|
26
|
+
s.add_development_dependency "ruby-debug19"
|
22
27
|
end
|
data/lib/counters/redis.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
require "benchmark"
|
2
|
-
|
2
|
+
|
3
|
+
# The redis gem must already be required - we don't require it.
|
4
|
+
# This allows callers / users to use any implementation that has the right API.
|
3
5
|
|
4
6
|
module Counters
|
5
7
|
class Redis < Counters::Base
|
6
|
-
def initialize(redis, base_key)
|
8
|
+
def initialize(redis=Redis.new, base_key="counters")
|
7
9
|
@redis, @base_key = redis, base_key
|
8
10
|
end
|
9
11
|
|
@@ -12,7 +14,10 @@ module Counters
|
|
12
14
|
end
|
13
15
|
|
14
16
|
def record_magnitude(key, amount)
|
15
|
-
@redis.
|
17
|
+
@redis.multi do
|
18
|
+
@redis.hincrby(@base_key, "magnitudes.#{key}.value", amount)
|
19
|
+
@redis.hincrby(@base_key, "magnitudes.#{key}.count", 1)
|
20
|
+
end
|
16
21
|
end
|
17
22
|
|
18
23
|
def ping(key)
|
data/lib/counters/version.rb
CHANGED
data/spec/redis_counter_spec.rb
CHANGED
@@ -1,57 +1,77 @@
|
|
1
1
|
require "spec_helper"
|
2
|
+
require "redis"
|
2
3
|
|
3
|
-
describe Counters::Redis do
|
4
|
+
describe Counters::Redis, "integration tests" do
|
4
5
|
let :redis do
|
5
|
-
|
6
|
+
::Redis.new
|
6
7
|
end
|
7
8
|
|
8
9
|
let :counter do
|
9
10
|
Counters::Redis.new(redis, "counters")
|
10
11
|
end
|
11
12
|
|
13
|
+
before(:each) do
|
14
|
+
redis.flushdb
|
15
|
+
end
|
16
|
+
|
12
17
|
it_should_behave_like "all counters"
|
13
18
|
|
14
|
-
it "should record
|
15
|
-
redis.should_receive(:hincrby).with("counters", "hits.pages.read", 1).twice
|
19
|
+
it "should record 2 hits on 'pages.read'" do
|
16
20
|
2.times { counter.hit "pages.read" }
|
21
|
+
redis.hkeys("counters").should == ["hits.pages.read"]
|
22
|
+
redis.hget("counters", "hits.pages.read").to_i.should == 2
|
17
23
|
end
|
18
24
|
|
19
|
-
it "should record a
|
20
|
-
|
21
|
-
redis.
|
22
|
-
|
23
|
-
|
25
|
+
it "should record magnitude using a signed 64 bit 'value' and a 'count'" do
|
26
|
+
4.times { counter.magnitude "bytes.in", 2_047 }
|
27
|
+
redis.hkeys("counters").sort.should == %w(magnitudes.bytes.in.value magnitudes.bytes.in.count).sort
|
28
|
+
|
29
|
+
redis.hget("counters", "magnitudes.bytes.in.count").to_i.should == 4
|
30
|
+
redis.hget("counters", "magnitudes.bytes.in.value").to_i.should == 4*2_047
|
31
|
+
end
|
32
|
+
|
33
|
+
it "does not detect 63 bit integer overflow" do
|
34
|
+
counter.magnitude "bytes.in", 2**63 - 20
|
35
|
+
counter.magnitude "bytes.in", 2_047
|
36
|
+
|
37
|
+
target_value = (2**63 - 20) + 2_047
|
38
|
+
|
39
|
+
redis.hget("counters", "magnitudes.bytes.in.value").to_i.should == (target_value - 2**64)
|
40
|
+
redis.hget("counters", "magnitudes.bytes.in.count").to_i.should == 2
|
24
41
|
end
|
25
42
|
|
26
43
|
it "should record a ping on 'crawler' by HSET counters/pings.crawler with today's date/time as an int" do
|
27
|
-
|
28
|
-
|
44
|
+
now = Time.now
|
45
|
+
Timecop.freeze(now) do
|
29
46
|
counter.ping "crawler"
|
30
47
|
end
|
31
48
|
|
49
|
+
redis.hget("counters", "pings.crawler").should == now.to_i.to_s
|
50
|
+
|
32
51
|
target_time = Time.now + 9
|
33
52
|
Timecop.travel(target_time) do
|
34
|
-
redis.should_receive(:hset).with("counters", "pings.crawler", target_time.to_i).once
|
35
53
|
counter.ping "crawler"
|
36
54
|
end
|
55
|
+
|
56
|
+
redis.hget("counters", "pings.crawler").should == target_time.to_i.to_s
|
37
57
|
end
|
38
58
|
|
39
59
|
it "should record latency on 'crawler.download' by HINCRBY counters/latencies.crawler.download.count by 1 and counters/latencies.crawler.download.nanoseconds by the latency" do
|
40
|
-
redis.should_receive(:hincrby).with("counters", "latencies.crawler.download.count", 1).twice
|
41
|
-
redis.should_receive(:hincrby).with("counters", "latencies.crawler.download.nanoseconds", 1.90 * 1_000_000_000).once
|
42
|
-
redis.should_receive(:hincrby).with("counters", "latencies.crawler.download.nanoseconds", 2.02 * 1_000_000_000).once
|
43
|
-
|
44
60
|
counter.latency "crawler.download", 1.9
|
45
61
|
counter.latency "crawler.download", 2.02
|
62
|
+
|
63
|
+
redis.hget("counters", "latencies.crawler.download.count").to_i.should == 2
|
64
|
+
redis.hget("counters", "latencies.crawler.download.nanoseconds").to_i.should == (2.02 + 1.90) * ONE_NANOSECOND
|
46
65
|
end
|
47
66
|
|
48
67
|
it "should record a block's latency" do
|
49
|
-
redis.should_receive(:hincrby).with("counters", "latencies.crawler.process.count", 1).once
|
50
|
-
redis.should_receive(:hincrby).once.with do |key, subkey, latency|
|
51
|
-
key == "counters" && subkey == "latencies.crawler.process.nanoseconds" && latency >= 0.2 * 1_000_000_000 && latency < 0.3 * 1_000_000_000
|
52
|
-
end
|
53
68
|
counter.latency "crawler.process" do
|
54
69
|
sleep 0.2
|
55
70
|
end
|
71
|
+
|
72
|
+
redis.hget("counters", "latencies.crawler.process.count").to_i.should == 1
|
73
|
+
redis.hget("counters", "latencies.crawler.process.nanoseconds").to_i.should be_within(0.01 * ONE_NANOSECOND ).of ( 0.2 * ONE_NANOSECOND )
|
56
74
|
end
|
75
|
+
|
76
|
+
ONE_NANOSECOND = 1_000_000_000
|
57
77
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
require "counters"
|
2
2
|
require "timecop"
|
3
3
|
|
4
|
+
begin
|
5
|
+
require "ruby-debug"
|
6
|
+
rescue LoadError
|
7
|
+
# Optional dependency - ignoring
|
8
|
+
end
|
9
|
+
|
4
10
|
shared_examples_for "all counters" do
|
5
11
|
it "should raise a ArgumentError when the key includes invalid chars" do
|
6
12
|
lambda { counter.hit "hit!" } .should raise_error(ArgumentError)
|
@@ -23,4 +29,12 @@ shared_examples_for "all counters" do
|
|
23
29
|
it "should not raise ArgumentError when the key includes an underscore" do
|
24
30
|
lambda { counter.hit "hit_" }.should_not raise_error(ArgumentError)
|
25
31
|
end
|
32
|
+
|
33
|
+
it "should return the latency block's value" do
|
34
|
+
value = counter.latency "process" do
|
35
|
+
"the returned value"
|
36
|
+
end
|
37
|
+
|
38
|
+
value.should == "the returned value"
|
39
|
+
end
|
26
40
|
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 1
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 1.0.
|
8
|
+
- 2
|
9
|
+
version: 1.0.2
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- "Fran\xC3\xA7ois Beausoleil"
|
@@ -14,11 +14,11 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-03-21 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
|
-
name:
|
21
|
+
name: redis
|
22
22
|
prerelease: false
|
23
23
|
requirement: &id001 !ruby/object:Gem::Requirement
|
24
24
|
none: false
|
@@ -31,7 +31,7 @@ dependencies:
|
|
31
31
|
type: :development
|
32
32
|
version_requirements: *id001
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
|
-
name:
|
34
|
+
name: rspec
|
35
35
|
prerelease: false
|
36
36
|
requirement: &id002 !ruby/object:Gem::Requirement
|
37
37
|
none: false
|
@@ -43,6 +43,32 @@ dependencies:
|
|
43
43
|
version: "0"
|
44
44
|
type: :development
|
45
45
|
version_requirements: *id002
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: timecop
|
48
|
+
prerelease: false
|
49
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
segments:
|
55
|
+
- 0
|
56
|
+
version: "0"
|
57
|
+
type: :development
|
58
|
+
version_requirements: *id003
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: ruby-debug19
|
61
|
+
prerelease: false
|
62
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
type: :development
|
71
|
+
version_requirements: *id004
|
46
72
|
description: Using the provided API, record metrics (such as number of hits to a particular controller, bytes in/out, compression ratio) within your system. Visualization is NOT provided within this gem.
|
47
73
|
email:
|
48
74
|
- francois@teksol.info
|