duly_noted 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.travis.yml +14 -0
- data/DOCS.md +90 -0
- data/Gemfile +4 -0
- data/Guardfile +13 -0
- data/README.md +52 -0
- data/Rakefile +19 -0
- data/duly_noted.gemspec +25 -0
- data/lib/duly_noted.rb +270 -0
- data/lib/duly_noted/helpers.rb +14 -0
- data/lib/duly_noted/version.rb +3 -0
- data/spec/duly_noted_spec.rb +96 -0
- data/spec/spec_helper.rb +10 -0
- metadata +149 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.8.7
|
4
|
+
- 1.9.2
|
5
|
+
- 1.9.3
|
6
|
+
# - jruby-18mode # JRuby in 1.8 mode
|
7
|
+
# - jruby-19mode # JRuby in 1.9 mode
|
8
|
+
- rbx-18mode
|
9
|
+
- rbx-19mode
|
10
|
+
matrix:
|
11
|
+
allow_failures:
|
12
|
+
- rvm: 1.8.7
|
13
|
+
# uncomment this line if your project needs to run something other than `rake`:
|
14
|
+
script: bundle exec rspec spec
|
data/DOCS.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
##Dependency
|
2
|
+
* Redis
|
3
|
+
|
4
|
+
The **DulyNoted** module contains four main methods:
|
5
|
+
|
6
|
+
* `track`
|
7
|
+
* `update`
|
8
|
+
* `query`
|
9
|
+
* `count`
|
10
|
+
|
11
|
+
##Track
|
12
|
+
|
13
|
+
_parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional)_
|
14
|
+
|
15
|
+
`metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
|
16
|
+
|
17
|
+
`for`_(optional)_: A name space for your metric, ex: `home_page`
|
18
|
+
|
19
|
+
`generated_at`_(optional)_: If the metric was generated in the past but is just now being logged, you can set the time it was generated at
|
20
|
+
|
21
|
+
`meta`_(optional)_: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
|
22
|
+
|
23
|
+
`ref_id`_(optional)_: If you need to reference the metric later, perhaps to add more metadata later on, you can set a reference id that you can use to update the metric
|
24
|
+
|
25
|
+
##Update
|
26
|
+
|
27
|
+
_parameters: `metric_name`, `ref_id`, `for`(required if set when created), `meta`(optional)_
|
28
|
+
|
29
|
+
The meta hash will not overwrite the old meta hash but be merged with it, with the new one overwriting conflicts.
|
30
|
+
|
31
|
+
`metric_name`: The name of the metric to update, ex: `page_views`, `downloads`
|
32
|
+
|
33
|
+
`ref_id`: The reference ID that you set when you called `track`
|
34
|
+
|
35
|
+
`for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
|
36
|
+
|
37
|
+
`meta`_(optional)_: A hash with whatever meta data fields you might want to store, or update ex: `ip_address`, `file_type`, `time_left`
|
38
|
+
|
39
|
+
###Usage
|
40
|
+
|
41
|
+
DulyNoted.update("page_views", "a_unique_id", for: "home_page", meta: { time_on_page: 30 })
|
42
|
+
|
43
|
+
##Query
|
44
|
+
|
45
|
+
_parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
|
46
|
+
|
47
|
+
Query will return an array of all the metadata in chronological order from a time range, or for the whole data set.
|
48
|
+
|
49
|
+
`metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
|
50
|
+
|
51
|
+
`for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
|
52
|
+
|
53
|
+
`time_start`_(optional)_: The start of the time range to grab the data from.
|
54
|
+
|
55
|
+
`time_end`_(optional)_: The end of the time range to grab the data from.
|
56
|
+
|
57
|
+
###Usage
|
58
|
+
|
59
|
+
DulyNoted.query("page_views",
|
60
|
+
for: "home_page",
|
61
|
+
time_start: 1.day.ago,
|
62
|
+
time_end: Time.now)
|
63
|
+
|
64
|
+
##Count
|
65
|
+
|
66
|
+
_parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
|
67
|
+
|
68
|
+
Count will return the number of events logged in a given time range, or if no time range is given, the total count.
|
69
|
+
|
70
|
+
`metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
|
71
|
+
|
72
|
+
`for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
|
73
|
+
|
74
|
+
`time_start`_(optional)_: The start of the time range to grab the data from.
|
75
|
+
|
76
|
+
`time_end`_(optional)_: The end of the time range to grab the data from.
|
77
|
+
|
78
|
+
###Usage
|
79
|
+
|
80
|
+
DulyNoted.count("page_views",
|
81
|
+
for: "home_page",
|
82
|
+
time_start: 1.day.ago,
|
83
|
+
time_end: Time.now)
|
84
|
+
|
85
|
+
##Redis
|
86
|
+
|
87
|
+
DulyNoted will try to connect to Redis's default url and port if you don't specify a Redis connection URL. You can set the url with the method
|
88
|
+
|
89
|
+
DulyNoted.redis = REDIS_URL
|
90
|
+
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'rspec', :version => 2 do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { "spec" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
9
|
+
|
10
|
+
guard :bundler do
|
11
|
+
watch('Gemfile')
|
12
|
+
end
|
13
|
+
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
#Duly Noted
|
2
|
+
[![Build Status](https://secure.travis-ci.org/willcosgrove/duly_noted.png?branch=master)](http://travis-ci.org/willcosgrove/duly_noted)
|
3
|
+
|
4
|
+
Duly noted is a redis backed stats and metrics tracker. It works as follows:
|
5
|
+
|
6
|
+
DulyNoted.track("page_views", for: "homepage")
|
7
|
+
|
8
|
+
This would log one page view for the home page. Then to see how many page views the home page has gotten, you would simply call:
|
9
|
+
|
10
|
+
DulyNoted.count("page_views", for: "homepage")
|
11
|
+
|
12
|
+
You can also store meta data with your metrics by passing your data in a hash to the `meta` key like so:
|
13
|
+
|
14
|
+
DulyNoted.track("page_views", for: "homepage", meta: {user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.121 Safari/535.2", ip_address: "128.194.3.138"})
|
15
|
+
|
16
|
+
If the metric was generated in the past but is just now being logged, you can alter it's time stamp with the `generated_at` key:
|
17
|
+
|
18
|
+
DulyNoted.track("page_views", for: "homepage", generated_at: 10.minutes.ago)
|
19
|
+
|
20
|
+
To get the count for a particular time range, you can use the `time_start` and `time_end` keys in the count call like so:
|
21
|
+
|
22
|
+
DulyNoted.count("page_views", for: "homepage", time_start: 1.day.ago, time_end: Time.now)
|
23
|
+
|
24
|
+
You can also just specify a `time_range` like so:
|
25
|
+
|
26
|
+
DulyNoted.count("page_views", for: "homepage", time_range: 1.day.ago..Time.now)
|
27
|
+
|
28
|
+
This will return the page view count for the home page for the past day.
|
29
|
+
|
30
|
+
##What's New
|
31
|
+
|
32
|
+
### 0.1.0
|
33
|
+
|
34
|
+
* Added the `time_range` option to `count`, and `query`
|
35
|
+
|
36
|
+
* Added the `meta_fields` option to `query`, which takes an array of fields to pull out from the meta hash
|
37
|
+
|
38
|
+
* Added the `ref_id` option to `query`, which takes a reference id and will return an array with one meta hash. I was going back and forth on whether or not it should wrap the hash in an array. It doesn't need to be, but I thought, just to make it consistant with it's usual output, I should make it return an array.
|
39
|
+
|
40
|
+
* Enough bug fixes to make it production ready! Yay!
|
41
|
+
|
42
|
+
|
43
|
+
##To Do
|
44
|
+
|
45
|
+
* Count by meta fields: How many page views from each browser?
|
46
|
+
|
47
|
+
* A `chart` method which would take a `time_start`, `time_end`, and a `granularity` which would allow you to easily get the per hour for each hour for the past day, for example.
|
48
|
+
|
49
|
+
* Maybe some Rails view helpers to generate some code for a Javascript charting library, or Google Charts API.
|
50
|
+
|
51
|
+
##Contributing
|
52
|
+
If you want to help, you should do it. Fork it, fix it, and send me a pull request. I will be delighted.
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
|
5
|
+
desc 'Default: run specs.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc "Run specs"
|
9
|
+
RSpec::Core::RakeTask.new do |t|
|
10
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
11
|
+
# Put spec opts in a file named .rspec in root
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Generate code coverage"
|
15
|
+
RSpec::Core::RakeTask.new(:coverage) do |t|
|
16
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
17
|
+
t.rcov = true
|
18
|
+
t.rcov_opts = ['--exclude', 'spec']
|
19
|
+
end
|
data/duly_noted.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/duly_noted/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Will Cosgrove"]
|
6
|
+
gem.email = ["will@willcosgrove.com"]
|
7
|
+
gem.description = %q{keep detailed metrics on your project with a speedy, powerful redis backend.}
|
8
|
+
gem.summary = %q{a simple redis based stat-tracker}
|
9
|
+
gem.homepage = "http://github.com/willcosgrove/duly_noted"
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "duly_noted"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = DulyNoted::VERSION
|
17
|
+
gem.add_dependency("redis")
|
18
|
+
gem.add_development_dependency("rspec")
|
19
|
+
gem.add_development_dependency("rake")
|
20
|
+
gem.add_development_dependency("rb-fsevent")
|
21
|
+
gem.add_development_dependency("guard-rspec")
|
22
|
+
gem.add_development_dependency("growl")
|
23
|
+
gem.add_development_dependency("guard-bundler")
|
24
|
+
gem.add_development_dependency("chronic")
|
25
|
+
end
|
data/lib/duly_noted.rb
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
# Duly noted is a redis backed stats and metrics tracker. It works as follows:
|
2
|
+
#
|
3
|
+
# DulyNoted.track("page_views",
|
4
|
+
# for: "home_page")
|
5
|
+
#
|
6
|
+
# This would log one page view for the home page. Then to see how many page views the home page has gotten, you would simply call:
|
7
|
+
#
|
8
|
+
# DulyNoted.count("page_views",
|
9
|
+
# for: "home_page")
|
10
|
+
#
|
11
|
+
# You can also store meta data with your metrics by passing your data in a hash to the `meta` key like so:
|
12
|
+
#
|
13
|
+
# DulyNoted.track("page_views",
|
14
|
+
# for: "home_page",
|
15
|
+
# meta: {
|
16
|
+
# user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2)...",
|
17
|
+
# ip_address: "128.194.3.138"
|
18
|
+
# })
|
19
|
+
#
|
20
|
+
# If the metric was generated in the past but is just now being logged, you can alter it's time stamp with the `generated_at` key:
|
21
|
+
#
|
22
|
+
# DulyNoted.track("page_views",
|
23
|
+
# for: "home_page",
|
24
|
+
# generated_at: 10.minutes.ago)
|
25
|
+
#
|
26
|
+
# To get the count for a particular time range, you can use the `time_start` and `time_end` keys in the count call like so:
|
27
|
+
#
|
28
|
+
# DulyNoted.count("page_views",
|
29
|
+
# for: "home_page",
|
30
|
+
# time_start: 1.day.ago,
|
31
|
+
# time_end: Time.now)
|
32
|
+
#
|
33
|
+
# This will return the page view count for the home page for the past day.
|
34
|
+
#
|
35
|
+
# You can also just specify a `time_range` like so:
|
36
|
+
#
|
37
|
+
# DulyNoted.count("page_views",
|
38
|
+
# for: "homepage",
|
39
|
+
# time_range: 1.day.ago..Time.now)
|
40
|
+
|
41
|
+
# ##Dependency
|
42
|
+
# * Redis
|
43
|
+
|
44
|
+
require "redis"
|
45
|
+
require "uri"
|
46
|
+
require "duly_noted/helpers"
|
47
|
+
require "duly_noted/version"
|
48
|
+
|
49
|
+
# The **DulyNoted** module contains four main methods:
|
50
|
+
#
|
51
|
+
# * `track`
|
52
|
+
# * `update`
|
53
|
+
# * `query`
|
54
|
+
# * `count`
|
55
|
+
|
56
|
+
module DulyNoted
|
57
|
+
include Helpers
|
58
|
+
extend self # the following are class methods
|
59
|
+
|
60
|
+
# ##Track
|
61
|
+
#
|
62
|
+
# _parameters: `metric_name`, `for`(optional), `generated_at`(optional), `meta`(optional), `ref_id`(optional)_
|
63
|
+
#
|
64
|
+
# `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
|
65
|
+
#
|
66
|
+
# `for`_(optional)_: A name space for your metric, ex: `home_page`
|
67
|
+
#
|
68
|
+
# `generated_at`_(optional)_: If the metric was generated in the past but is just now being logged, you can set the time it was generated at
|
69
|
+
#
|
70
|
+
# `meta`_(optional)_: A hash with whatever meta data fields you might want to store, ex: `ip_address`, `file_type`
|
71
|
+
#
|
72
|
+
# `ref_id`_(optional)_: If you need to reference the metric later, perhaps to add more metadata later on, you can set a reference id that you can use to update the metric
|
73
|
+
|
74
|
+
def track(metric_name, options={})
|
75
|
+
options = {:generated_at => Time.now}.merge(options)
|
76
|
+
key = normalize(metric_name)
|
77
|
+
key << ":#{options[:for]}" if options[:for]
|
78
|
+
DulyNoted.redis.zadd key, options[:generated_at].to_f, "#{key}:#{options[:generated_at].to_f}:meta"
|
79
|
+
DulyNoted.redis.set "#{key}:#{options[:ref_id]}", "#{key}:#{options[:generated_at].to_f}:meta" if options[:ref_id] # set alias key
|
80
|
+
DulyNoted.redis.mapped_hmset "#{key}:#{options[:generated_at].to_f}:meta", options[:meta] if options[:meta] # set meta data
|
81
|
+
end
|
82
|
+
|
83
|
+
# ##Update
|
84
|
+
#
|
85
|
+
# _parameters: `metric_name`, `ref_id`, `for`(required if set when created), `meta`(optional)_
|
86
|
+
#
|
87
|
+
# The meta hash will not overwrite the old meta hash but be merged with it, with the new one overwriting conflicts.
|
88
|
+
#
|
89
|
+
# `metric_name`: The name of the metric to track, ex: `page_views`, `downloads`
|
90
|
+
#
|
91
|
+
# `ref_id`: The reference ID that you set when you called `track`
|
92
|
+
#
|
93
|
+
# `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
|
94
|
+
#
|
95
|
+
# `meta`_(optional)_: A hash with whatever meta data fields you might want to store, or update ex: `ip_address`, `file_type`, `time_left`
|
96
|
+
#
|
97
|
+
# ###Usage
|
98
|
+
#
|
99
|
+
# DulyNoted.update("page_views",
|
100
|
+
# "a_unique_id",
|
101
|
+
# for: "home_page",
|
102
|
+
# meta: { time_on_page: 30 })
|
103
|
+
|
104
|
+
def update(metric_name, ref_id, options={})
|
105
|
+
key = normalize(metric_name)
|
106
|
+
key << ":#{options[:for]}" if options[:for]
|
107
|
+
key << ":#{ref_id}"
|
108
|
+
real_key = DulyNoted.redis.get key
|
109
|
+
DulyNoted.redis.mapped_hmset real_key, options[:meta] if options[:meta]
|
110
|
+
end
|
111
|
+
|
112
|
+
# ##Query
|
113
|
+
#
|
114
|
+
# _parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
|
115
|
+
#
|
116
|
+
# Query will return an array of all the metadata in chronological order from a time range, or for the whole data set.
|
117
|
+
#
|
118
|
+
# `metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
|
119
|
+
#
|
120
|
+
# `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
|
121
|
+
#
|
122
|
+
# `ref_id`: _(optional)_: The reference ID that you set when you called `track` (if you set this, the time restraints is ignored)
|
123
|
+
#
|
124
|
+
# `meta_fields` _(optional)_: An array of fields to retrieve from the meta hash. If not specified, the entire hash will be grabbed. Fields will be converted to strings, because redis converts all hash keys and values to strings.
|
125
|
+
#
|
126
|
+
# `time_start`_(optional)_: The start of the time range to grab the data from.
|
127
|
+
#
|
128
|
+
# `time_end`_(optional)_: The end of the time range to grab the data from.
|
129
|
+
#
|
130
|
+
# `time_range _(optional)_: Alternatively you can specify a time range, instead of `time_start` and `time_end`.
|
131
|
+
#
|
132
|
+
# ###Usage
|
133
|
+
#
|
134
|
+
# DulyNoted.query("page_views",
|
135
|
+
# for: "home_page",
|
136
|
+
# time_start: 1.day.ago,
|
137
|
+
# time_end: Time.now)
|
138
|
+
#
|
139
|
+
#
|
140
|
+
# DulyNoted.query("page_views",
|
141
|
+
# for: "home_page",
|
142
|
+
# time_range: 1.day.ago..Time.now)
|
143
|
+
|
144
|
+
def query(metric_name, options={})
|
145
|
+
key = normalize(metric_name)
|
146
|
+
parse_time_range(options)
|
147
|
+
key << ":#{options[:for]}" if options[:for]
|
148
|
+
if options[:ref_id]
|
149
|
+
key << ":#{options[:ref_id]}"
|
150
|
+
real_key = DulyNoted.redis.get key
|
151
|
+
if options[:meta_fields]
|
152
|
+
options[:meta_fields].collect! { |x| x.to_s }
|
153
|
+
result = {}
|
154
|
+
options[:meta_fields].each do |field|
|
155
|
+
result[field] = DulyNoted.redis.hget real_key, field
|
156
|
+
end
|
157
|
+
results = [result]
|
158
|
+
else
|
159
|
+
results = [DulyNoted.redis.hgetall(real_key)]
|
160
|
+
end
|
161
|
+
else
|
162
|
+
grab_results = Proc.new do |metric|
|
163
|
+
if options[:meta_fields]
|
164
|
+
options[:meta_fields].collect! { |x| x.to_s }
|
165
|
+
result = {}
|
166
|
+
options[:meta_fields].each do |field|
|
167
|
+
result[field] = DulyNoted.redis.hget metric, field
|
168
|
+
end
|
169
|
+
result
|
170
|
+
else
|
171
|
+
DulyNoted.redis.hgetall metric
|
172
|
+
end
|
173
|
+
end
|
174
|
+
if options[:time_start] && options[:time_end]
|
175
|
+
results = DulyNoted.redis.zrangebyscore(key, options[:time_start].to_f, options[:time_end].to_f).collect(&grab_results)
|
176
|
+
else
|
177
|
+
results = DulyNoted.redis.zrange(key, 0, -1).collect(&grab_results)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
return results
|
181
|
+
end
|
182
|
+
|
183
|
+
# ##Count
|
184
|
+
#
|
185
|
+
# _parameters: `metric_name`, `for`(required if set when created), `time_start`(optional), `time_end`(optional)_
|
186
|
+
#
|
187
|
+
# Count will return the number of events logged in a given time range, or if no time range is given, the total count.
|
188
|
+
#
|
189
|
+
# `metric_name`: The name of the metric to query, ex: `page_views`, `downloads`
|
190
|
+
#
|
191
|
+
# `for`_(required if you set `for` when you generated the metric)_: A name space for your metric, ex: `home_page`
|
192
|
+
#
|
193
|
+
# `time_start`_(optional)_: The start of the time range to grab the data from.
|
194
|
+
#
|
195
|
+
# `time_end`_(optional)_: The end of the time range to grab the data from.
|
196
|
+
#
|
197
|
+
# `time_range _(optional)_: Alternatively you can specify a time range, instead of `time_start` and `time_end`.
|
198
|
+
#
|
199
|
+
# ###Usage
|
200
|
+
#
|
201
|
+
# DulyNoted.count("page_views",
|
202
|
+
# for: "home_page",
|
203
|
+
# time_start: Time.now,
|
204
|
+
# time_end: 1.day.ago)
|
205
|
+
#
|
206
|
+
#
|
207
|
+
# DulyNoted.count("page_views",
|
208
|
+
# for: "home_page",
|
209
|
+
# time_range: Time.now..1.day.ago)
|
210
|
+
|
211
|
+
def count(metric_name, options={})
|
212
|
+
parse_time_range(options)
|
213
|
+
key = normalize(metric_name)
|
214
|
+
keys = []
|
215
|
+
if options[:for]
|
216
|
+
key << ":#{options[:for]}"
|
217
|
+
else
|
218
|
+
keys << DulyNoted.redis.keys("#{key}*")
|
219
|
+
keys - DulyNoted.redis.keys("#{key}*:meta")
|
220
|
+
keys - DulyNoted.redis.keys("#{key}:*:")
|
221
|
+
keys.flatten!
|
222
|
+
end
|
223
|
+
if keys.empty?
|
224
|
+
if options[:time_start] && options[:time_end]
|
225
|
+
return DulyNoted.redis.zcount(key, options[:time_start].to_f, options[:time_end].to_f)
|
226
|
+
else
|
227
|
+
return DulyNoted.redis.zcard(key)
|
228
|
+
end
|
229
|
+
else
|
230
|
+
sum = 0
|
231
|
+
if options[:time_start] && options[:time_end]
|
232
|
+
keys.each do |key|
|
233
|
+
sum += DulyNoted.redis.zcount(key, options[:time_start].to_f, options[:time_end].to_f)
|
234
|
+
end
|
235
|
+
return sum
|
236
|
+
else
|
237
|
+
keys.each do |key|
|
238
|
+
sum += DulyNoted.redis.zcard(key)
|
239
|
+
end
|
240
|
+
return sum
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# ##Redis
|
246
|
+
#
|
247
|
+
# DulyNoted will try to connect to Redis's default url and port if you don't specify a Redis connection URL. You can set the url with the method
|
248
|
+
#
|
249
|
+
# DulyNoted.redis = REDIS_URL
|
250
|
+
|
251
|
+
def redis=(url)
|
252
|
+
@redis = nil
|
253
|
+
@redis_url = url
|
254
|
+
redis
|
255
|
+
end
|
256
|
+
|
257
|
+
def redis
|
258
|
+
@redis ||= (
|
259
|
+
url = URI(@redis_url || "redis://127.0.0.1:6379/0")
|
260
|
+
|
261
|
+
Redis.new({
|
262
|
+
:host => url.host,
|
263
|
+
:port => url.port,
|
264
|
+
:db => url.path[1..-1],
|
265
|
+
:password => url.password
|
266
|
+
})
|
267
|
+
)
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module DulyNoted
|
2
|
+
module Helpers
|
3
|
+
def normalize(str)
|
4
|
+
str.downcase.gsub(/[^a-z0-9 ]/i, '').strip
|
5
|
+
end
|
6
|
+
|
7
|
+
def parse_time_range(options)
|
8
|
+
if options[:time_range]
|
9
|
+
options[:time_start] = options[:time_range].first
|
10
|
+
options[:time_end] = options[:time_range].last
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DulyNoted do
|
4
|
+
before :each do
|
5
|
+
DulyNoted.redis.flushall
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#track" do
|
9
|
+
it "keeps an accurate count of events" do
|
10
|
+
2.times { DulyNoted.track "page_views" }
|
11
|
+
DulyNoted.count("page_views").should eq(2)
|
12
|
+
end
|
13
|
+
it "separates by context" do
|
14
|
+
2.times { DulyNoted.track "page_views", :for => "home" }
|
15
|
+
5.times { DulyNoted.track "page_views", :for => "contact_us" }
|
16
|
+
DulyNoted.count("page_views", :for => "home").should eq(2)
|
17
|
+
DulyNoted.count("page_views", :for => "contact_us").should eq(5)
|
18
|
+
end
|
19
|
+
it "stores metadata" do
|
20
|
+
DulyNoted.track "page_views", :meta => {:open => true}
|
21
|
+
DulyNoted.query("page_views").should include({"open" => "true"})
|
22
|
+
end
|
23
|
+
it "can track past events" do
|
24
|
+
DulyNoted.track "page_views", :generated_at => Time.now-10
|
25
|
+
DulyNoted.count "page_views", :time_range => Time.now-11..Time.now-9
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#update" do
|
30
|
+
it "overwrites duplicate keys" do
|
31
|
+
DulyNoted.track "page_views", :meta => {:seconds_open => 0}, :ref_id => "unique"
|
32
|
+
DulyNoted.update "page_views", "unique", :meta => {:seconds_open => 5}
|
33
|
+
DulyNoted.query("page_views").should include({"seconds_open" => "5"})
|
34
|
+
DulyNoted.query("page_views").should_not include({"seconds_open" => "0"})
|
35
|
+
end
|
36
|
+
it "doesn't replace old hash" do
|
37
|
+
DulyNoted.track "page_views", :meta => {:seconds_open => 0}, :ref_id => "unique"
|
38
|
+
DulyNoted.update "page_views", "unique", :meta => {:ip_address => "19.27.182.32"}
|
39
|
+
DulyNoted.query("page_views").should include({"seconds_open" => "0", "ip_address" => "19.27.182.32"})
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#query" do
|
44
|
+
it "should grab entire meta hash" do
|
45
|
+
DulyNoted.track "page_views", :meta => {:seconds_open => 0, :browser => "chrome"}
|
46
|
+
DulyNoted.query("page_views").should include({"seconds_open" => "0", "browser" => "chrome"})
|
47
|
+
end
|
48
|
+
it "can query a certain ref_id" do
|
49
|
+
DulyNoted.track "page_views", :meta => {:seconds_open => 0, :browser => "chrome"}, :ref_id => "unique"
|
50
|
+
DulyNoted.track "page_views", :meta => {:seconds_open => 10, :browser => "firefox"}
|
51
|
+
DulyNoted.query("page_views", :ref_id => "unique").should include({"seconds_open" => "0", "browser" => "chrome"})
|
52
|
+
DulyNoted.query("page_views", :ref_id => "unique").should_not include({"seconds_open" => "10", "browser" => "firefox"})
|
53
|
+
end
|
54
|
+
it "can grab only specific fields from the hash" do
|
55
|
+
DulyNoted.track "page_views", :meta => {:seconds_open => 0, :browser => "chrome"}
|
56
|
+
DulyNoted.query("page_views", :meta_fields => [:browser]).should include({"browser" => "chrome"})
|
57
|
+
DulyNoted.query("page_views", :meta_fields => [:browser]).should_not include({"seconds_open" => "0"})
|
58
|
+
DulyNoted.track "downloads", :meta => {:file_name => "rules.pdf", :browser => "chrome"}, :ref_id => "unique"
|
59
|
+
DulyNoted.query("downloads", :ref_id => "unique", :meta_fields => [:browser]).should include({"browser" => "chrome"})
|
60
|
+
DulyNoted.query("downloads", :ref_id => "unique", :meta_fields => [:browser]).should_not include({"file_name" => "rules.pdf"})
|
61
|
+
end
|
62
|
+
it "can get only meta hashes from a certain time range" do
|
63
|
+
5.times { DulyNoted.track "page_views", :meta => {:seconds_open => 5, :browser => "chrome"} }
|
64
|
+
sleep 0.5
|
65
|
+
5.times { DulyNoted.track "page_views", :meta => {:seconds_open => 0, :browser => "firefox"} }
|
66
|
+
DulyNoted.query("page_views", :time_start => Time.now-0.5, :time_end => Time.now).should include({"seconds_open" => "0", "browser" => "firefox"})
|
67
|
+
DulyNoted.query("page_views", :time_range => Time.now-0.5..Time.now).should include({"seconds_open" => "0", "browser" => "firefox"})
|
68
|
+
DulyNoted.query("page_views", :time_start => Time.now-0.5, :time_end => Time.now).should_not include({"seconds_open" => "5", "browser" => "chrome"})
|
69
|
+
DulyNoted.query("page_views", :time_range => Time.now-0.5..Time.now).should_not include({"seconds_open" => "5", "browser" => "chrome"})
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#count" do
|
74
|
+
it "can count events in between a time range" do
|
75
|
+
5.times { DulyNoted.track "page_views" }
|
76
|
+
sleep 0.2
|
77
|
+
5.times { DulyNoted.track "page_views" }
|
78
|
+
DulyNoted.count("page_views", :time_start => Time.now-0.2, :time_end => Time.now).should eq(5)
|
79
|
+
DulyNoted.count("page_views", :time_range => Time.now-0.2..Time.now).should eq(5)
|
80
|
+
end
|
81
|
+
it "can count all of one type of metric" do
|
82
|
+
5.times { DulyNoted.track "page_views", :for => "home" }
|
83
|
+
5.times { DulyNoted.track "page_views", :for => "contact_us" }
|
84
|
+
DulyNoted.count("page_views").should eq(10)
|
85
|
+
end
|
86
|
+
it "can count all of one type between a time range" do
|
87
|
+
5.times { DulyNoted.track "page_views", :for => "home" }
|
88
|
+
5.times { DulyNoted.track "page_views", :for => "contact_us" }
|
89
|
+
sleep 0.2
|
90
|
+
5.times { DulyNoted.track "page_views", :for => "home" }
|
91
|
+
5.times { DulyNoted.track "page_views", :for => "contact_us" }
|
92
|
+
DulyNoted.count("page_views", :time_start => Time.now-0.2, :time_end => Time.now).should eq(10)
|
93
|
+
DulyNoted.count("page_views", :time_range => Time.now-0.2..Time.now).should eq(10)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: duly_noted
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Will Cosgrove
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-03-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: redis
|
16
|
+
requirement: &70245889112380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70245889112380
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
requirement: &70245889111680 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70245889111680
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rake
|
38
|
+
requirement: &70245889110680 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70245889110680
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rb-fsevent
|
49
|
+
requirement: &70245889109440 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70245889109440
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: guard-rspec
|
60
|
+
requirement: &70245889108340 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70245889108340
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: growl
|
71
|
+
requirement: &70245889107640 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70245889107640
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: guard-bundler
|
82
|
+
requirement: &70245889106880 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70245889106880
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: chronic
|
93
|
+
requirement: &70245889106080 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *70245889106080
|
102
|
+
description: keep detailed metrics on your project with a speedy, powerful redis backend.
|
103
|
+
email:
|
104
|
+
- will@willcosgrove.com
|
105
|
+
executables: []
|
106
|
+
extensions: []
|
107
|
+
extra_rdoc_files: []
|
108
|
+
files:
|
109
|
+
- .DS_Store
|
110
|
+
- .gitignore
|
111
|
+
- .travis.yml
|
112
|
+
- DOCS.md
|
113
|
+
- Gemfile
|
114
|
+
- Guardfile
|
115
|
+
- README.md
|
116
|
+
- Rakefile
|
117
|
+
- duly_noted.gemspec
|
118
|
+
- lib/duly_noted.rb
|
119
|
+
- lib/duly_noted/helpers.rb
|
120
|
+
- lib/duly_noted/version.rb
|
121
|
+
- spec/duly_noted_spec.rb
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
homepage: http://github.com/willcosgrove/duly_noted
|
124
|
+
licenses: []
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ! '>='
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '0'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
none: false
|
137
|
+
requirements:
|
138
|
+
- - ! '>='
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
requirements: []
|
142
|
+
rubyforge_project:
|
143
|
+
rubygems_version: 1.8.16
|
144
|
+
signing_key:
|
145
|
+
specification_version: 3
|
146
|
+
summary: a simple redis based stat-tracker
|
147
|
+
test_files:
|
148
|
+
- spec/duly_noted_spec.rb
|
149
|
+
- spec/spec_helper.rb
|