event-counter 0.0.1 → 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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +19 -0
- data/Appraisals +11 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +4 -0
- data/README.md +65 -28
- data/event-counter.gemspec +2 -7
- data/gemfiles/pg_ar_30.gemfile +12 -0
- data/gemfiles/pg_ar_30.gemfile.lock +60 -0
- data/gemfiles/pg_ar_31.gemfile +12 -0
- data/gemfiles/pg_ar_31.gemfile.lock +60 -0
- data/gemfiles/pg_ar_32.gemfile +12 -0
- data/gemfiles/pg_ar_32.gemfile.lock +60 -0
- data/gemfiles/pg_ar_40.gemfile +12 -0
- data/gemfiles/pg_ar_40.gemfile.lock +65 -0
- data/gemfiles/pg_ar_41.gemfile +12 -0
- data/gemfiles/pg_ar_41.gemfile.lock +65 -0
- data/lib/event_counter.rb +18 -6
- data/lib/event_counter/active_record_extension.rb +101 -85
- data/lib/event_counter/version.rb +1 -1
- data/log/.gitkeep +0 -0
- data/spec/fixtures/.gitkeep +0 -0
- data/spec/lib/event_counter_spec.rb +61 -21
- data/spec/lib/performance_spec.rb +82 -31
- data/spec/spec_helper.rb +10 -13
- data/spec/support/matchers.rb +6 -4
- metadata +26 -68
- data/spec/support/fabrications.rb +0 -2
@@ -0,0 +1,65 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../
|
3
|
+
specs:
|
4
|
+
event-counter (0.1.0)
|
5
|
+
activerecord (>= 3)
|
6
|
+
activesupport (>= 3)
|
7
|
+
pg (~> 0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.1.6)
|
13
|
+
activesupport (= 4.1.6)
|
14
|
+
builder (~> 3.1)
|
15
|
+
activerecord (4.1.6)
|
16
|
+
activemodel (= 4.1.6)
|
17
|
+
activesupport (= 4.1.6)
|
18
|
+
arel (~> 5.0.0)
|
19
|
+
activesupport (4.1.6)
|
20
|
+
i18n (~> 0.6, >= 0.6.9)
|
21
|
+
json (~> 1.7, >= 1.7.7)
|
22
|
+
minitest (~> 5.1)
|
23
|
+
thread_safe (~> 0.1)
|
24
|
+
tzinfo (~> 1.1)
|
25
|
+
appraisal (1.0.2)
|
26
|
+
bundler
|
27
|
+
rake
|
28
|
+
thor (>= 0.14.0)
|
29
|
+
arel (5.0.1.20140414130214)
|
30
|
+
builder (3.2.2)
|
31
|
+
database_cleaner (1.3.0)
|
32
|
+
diff-lcs (1.2.5)
|
33
|
+
i18n (0.6.11)
|
34
|
+
json (1.8.1)
|
35
|
+
minitest (5.4.1)
|
36
|
+
pg (0.17.1)
|
37
|
+
rake (10.3.2)
|
38
|
+
rspec (3.1.0)
|
39
|
+
rspec-core (~> 3.1.0)
|
40
|
+
rspec-expectations (~> 3.1.0)
|
41
|
+
rspec-mocks (~> 3.1.0)
|
42
|
+
rspec-core (3.1.7)
|
43
|
+
rspec-support (~> 3.1.0)
|
44
|
+
rspec-expectations (3.1.2)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.1.0)
|
47
|
+
rspec-mocks (3.1.3)
|
48
|
+
rspec-support (~> 3.1.0)
|
49
|
+
rspec-support (3.1.2)
|
50
|
+
thor (0.19.1)
|
51
|
+
thread_safe (0.3.4)
|
52
|
+
tzinfo (1.2.2)
|
53
|
+
thread_safe (~> 0.1)
|
54
|
+
|
55
|
+
PLATFORMS
|
56
|
+
ruby
|
57
|
+
|
58
|
+
DEPENDENCIES
|
59
|
+
activerecord (~> 4.0)
|
60
|
+
activesupport (~> 4.0)
|
61
|
+
appraisal
|
62
|
+
database_cleaner
|
63
|
+
event-counter!
|
64
|
+
pg
|
65
|
+
rspec
|
@@ -0,0 +1,65 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ../
|
3
|
+
specs:
|
4
|
+
event-counter (0.1.0)
|
5
|
+
activerecord (>= 3)
|
6
|
+
activesupport (>= 3)
|
7
|
+
pg (~> 0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
activemodel (4.1.6)
|
13
|
+
activesupport (= 4.1.6)
|
14
|
+
builder (~> 3.1)
|
15
|
+
activerecord (4.1.6)
|
16
|
+
activemodel (= 4.1.6)
|
17
|
+
activesupport (= 4.1.6)
|
18
|
+
arel (~> 5.0.0)
|
19
|
+
activesupport (4.1.6)
|
20
|
+
i18n (~> 0.6, >= 0.6.9)
|
21
|
+
json (~> 1.7, >= 1.7.7)
|
22
|
+
minitest (~> 5.1)
|
23
|
+
thread_safe (~> 0.1)
|
24
|
+
tzinfo (~> 1.1)
|
25
|
+
appraisal (1.0.2)
|
26
|
+
bundler
|
27
|
+
rake
|
28
|
+
thor (>= 0.14.0)
|
29
|
+
arel (5.0.1.20140414130214)
|
30
|
+
builder (3.2.2)
|
31
|
+
database_cleaner (1.3.0)
|
32
|
+
diff-lcs (1.2.5)
|
33
|
+
i18n (0.6.11)
|
34
|
+
json (1.8.1)
|
35
|
+
minitest (5.4.1)
|
36
|
+
pg (0.17.1)
|
37
|
+
rake (10.3.2)
|
38
|
+
rspec (3.1.0)
|
39
|
+
rspec-core (~> 3.1.0)
|
40
|
+
rspec-expectations (~> 3.1.0)
|
41
|
+
rspec-mocks (~> 3.1.0)
|
42
|
+
rspec-core (3.1.7)
|
43
|
+
rspec-support (~> 3.1.0)
|
44
|
+
rspec-expectations (3.1.2)
|
45
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
46
|
+
rspec-support (~> 3.1.0)
|
47
|
+
rspec-mocks (3.1.3)
|
48
|
+
rspec-support (~> 3.1.0)
|
49
|
+
rspec-support (3.1.2)
|
50
|
+
thor (0.19.1)
|
51
|
+
thread_safe (0.3.4)
|
52
|
+
tzinfo (1.2.2)
|
53
|
+
thread_safe (~> 0.1)
|
54
|
+
|
55
|
+
PLATFORMS
|
56
|
+
ruby
|
57
|
+
|
58
|
+
DEPENDENCIES
|
59
|
+
activerecord (~> 4.1)
|
60
|
+
activesupport (~> 4.1)
|
61
|
+
appraisal
|
62
|
+
database_cleaner
|
63
|
+
event-counter!
|
64
|
+
pg
|
65
|
+
rspec
|
data/lib/event_counter.rb
CHANGED
@@ -6,6 +6,8 @@ class EventCounter < ActiveRecord::Base
|
|
6
6
|
|
7
7
|
belongs_to :countable, polymorphic: true
|
8
8
|
|
9
|
+
scope :within, ->(range) { range && where(created_at: range) }
|
10
|
+
|
9
11
|
def increase_by(val)
|
10
12
|
self.class.where(id: id).update_all(['value = value + ?', val])
|
11
13
|
increment(:value, val)
|
@@ -86,13 +88,13 @@ class EventCounter < ActiveRecord::Base
|
|
86
88
|
end
|
87
89
|
|
88
90
|
def self.normalize_on_time(on_time)
|
89
|
-
on_time ||= Time.now
|
91
|
+
on_time ||= Time.zone.now
|
90
92
|
on_time =
|
91
93
|
case current_interval
|
92
94
|
when Symbol
|
93
|
-
on_time.send(:"beginning_of_#{current_interval}")
|
95
|
+
on_time.in_time_zone.send(:"beginning_of_#{current_interval}")
|
94
96
|
else
|
95
|
-
on_time.floor(current_interval)
|
97
|
+
on_time.in_time_zone.floor(current_interval)
|
96
98
|
end
|
97
99
|
on_time
|
98
100
|
end
|
@@ -123,16 +125,26 @@ class EventCounter < ActiveRecord::Base
|
|
123
125
|
end
|
124
126
|
|
125
127
|
require 'event_counter/active_record_extension'
|
126
|
-
|
127
128
|
ActiveRecord::Base.send(:include, EventCounter::ActiveRecordExtension)
|
128
129
|
|
130
|
+
if ActiveSupport::VERSION::MAJOR > 3
|
131
|
+
require 'active_support/core_ext/time'
|
132
|
+
else
|
133
|
+
require 'active_support/time'
|
134
|
+
end
|
135
|
+
|
129
136
|
# :nodoc:
|
130
|
-
class
|
137
|
+
class ActiveSupport::TimeWithZone
|
138
|
+
def round_off(seconds = 60)
|
139
|
+
Time.zone.at((to_f / seconds).round * seconds)
|
140
|
+
end
|
141
|
+
|
131
142
|
def floor(seconds = 60)
|
132
|
-
Time.at((to_f / seconds).floor * seconds)
|
143
|
+
Time.zone.at((to_f / seconds).floor * seconds)
|
133
144
|
end
|
134
145
|
end
|
135
146
|
|
147
|
+
# :nodoc:
|
136
148
|
class String
|
137
149
|
|
138
150
|
unless method_defined?(:squish!)
|
@@ -42,8 +42,8 @@ class EventCounter < ActiveRecord::Base
|
|
42
42
|
rotate_counter(*args, opts)
|
43
43
|
end
|
44
44
|
|
45
|
-
def data_for(name,
|
46
|
-
self.class.data_for(name, id,
|
45
|
+
def data_for(name, opts = {})
|
46
|
+
self.class.data_for(name, id, opts)
|
47
47
|
end
|
48
48
|
|
49
49
|
private
|
@@ -68,99 +68,113 @@ class EventCounter < ActiveRecord::Base
|
|
68
68
|
day: 1.day
|
69
69
|
}.freeze
|
70
70
|
|
71
|
-
def data_for(
|
72
|
-
interval = normalize_interval!(
|
71
|
+
def data_for(name, id = nil, interval: nil, range: nil, raw: nil, tz: nil)
|
72
|
+
interval = normalize_interval!(name, interval)
|
73
73
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
74
|
+
range = normalize_range!(range, interval) if range
|
75
|
+
|
76
|
+
tz ||= (Time.zone || 'UTC')
|
77
|
+
tz_abbr = tz.now.zone
|
78
|
+
|
79
|
+
subq = EventCounter
|
80
|
+
.select(subq_select(interval, tz_abbr))
|
81
|
+
.where(name: name, countable_type: self)
|
82
|
+
.where(id && { countable_id: id })
|
83
|
+
.within(range)
|
84
|
+
.group("1")
|
85
|
+
.order("1")
|
86
|
+
.to_sql
|
87
|
+
|
88
|
+
sql = <<-SQL.squish!
|
89
|
+
SELECT created_at, value
|
90
|
+
FROM (#{series(interval, range, tz_abbr)}) intervals
|
91
|
+
LEFT OUTER JOIN (#{subq}) counters USING (created_at)
|
92
|
+
ORDER BY 1
|
93
|
+
SQL
|
83
94
|
|
84
95
|
result = connection.execute(sql).to_a
|
85
96
|
|
86
|
-
raw ? result : normalize_counters_data(result)
|
97
|
+
raw ? result : normalize_counters_data(result, tz)
|
87
98
|
end
|
88
99
|
|
89
|
-
def
|
90
|
-
|
91
|
-
WITH CTE AS (
|
92
|
-
SELECT #{cte_extract(interval)} as created_at, sum(value) AS value
|
93
|
-
FROM event_counters
|
94
|
-
WHERE
|
95
|
-
countable_type = #{sanitize(name)} AND
|
96
|
-
#{ "countable_id = #{sanitize(id)} AND" if id.present? }
|
97
|
-
name = #{sanitize(counter_name)}
|
98
|
-
GROUP BY 1
|
99
|
-
)
|
100
|
-
SQL
|
100
|
+
def subq_select(interval, tz)
|
101
|
+
"#{subq_extract(interval, tz)} as created_at, sum(value) AS value"
|
101
102
|
end
|
102
103
|
|
103
|
-
def
|
104
|
+
def subq_extract(interval, tz)
|
104
105
|
case interval
|
105
106
|
when Symbol
|
106
|
-
"date_trunc(#{sanitize(interval)}, created_at)"
|
107
|
+
"date_trunc(#{sanitize(interval)}, #{tstamp_tz('created_at', tz)})"
|
107
108
|
else
|
108
|
-
|
109
|
-
floor(EXTRACT(EPOCH FROM created_at
|
110
|
-
#{sanitize(interval)})::int * #{sanitize(interval)}
|
111
|
-
SQL
|
109
|
+
time = <<-SQL
|
110
|
+
floor(EXTRACT(EPOCH FROM created_at) /
|
111
|
+
#{sanitize(interval)})::int * #{sanitize(interval)}
|
112
|
+
SQL
|
113
|
+
tstamp_tz("to_timestamp(#{time})", tz)
|
112
114
|
end
|
113
115
|
end
|
114
116
|
|
115
|
-
def
|
116
|
-
|
117
|
-
|
118
|
-
args =
|
117
|
+
def series(interval, range, tz)
|
118
|
+
a =
|
119
119
|
case interval
|
120
120
|
when Symbol
|
121
|
-
|
122
|
-
if range
|
123
|
-
[
|
124
|
-
"date_trunc(#{sanitize(interval)}, #{tstamp(range.min.to_i)} )",
|
125
|
-
"date_trunc(#{sanitize(interval)}, #{tstamp(range.max.to_i)} )",
|
126
|
-
interval_sql
|
127
|
-
]
|
128
|
-
else
|
129
|
-
[
|
130
|
-
"date_trunc(#{sanitize(interval)}, min(created_at))",
|
131
|
-
"date_trunc(#{sanitize(interval)}, max(created_at))",
|
132
|
-
interval_sql
|
133
|
-
]
|
134
|
-
end
|
121
|
+
series_for_symbol(interval, range, tz)
|
135
122
|
else
|
136
|
-
|
137
|
-
if range
|
138
|
-
[
|
139
|
-
tstamp(sanitize(range_min)),
|
140
|
-
tstamp(sanitize(range_max)),
|
141
|
-
interval_sql
|
142
|
-
]
|
143
|
-
else
|
144
|
-
[ 'min(created_at)', 'max(created_at)', interval_sql ]
|
145
|
-
end
|
123
|
+
series_for_integer(interval, range, tz)
|
146
124
|
end
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
125
|
+
EventCounter.within(range).select(<<-SQL).to_sql
|
126
|
+
count(*), generate_series(#{a[0]}, #{a[1] }, #{a[2]}) AS created_at
|
127
|
+
SQL
|
128
|
+
end
|
129
|
+
|
130
|
+
def series_for_symbol(interval, range, tz)
|
131
|
+
interval_sql = "interval '1 #{interval}'"
|
132
|
+
if range
|
133
|
+
a = [
|
134
|
+
dtrunc(interval, sanitize(range.min).to_s, tz),
|
135
|
+
dtrunc(interval, sanitize(range.max).to_s, tz),
|
136
|
+
interval_sql
|
137
|
+
]
|
138
|
+
else
|
139
|
+
a = [
|
140
|
+
dtrunc(interval, 'min(created_at)', tz),
|
141
|
+
dtrunc(interval, 'max(created_at)', tz),
|
142
|
+
interval_sql
|
143
|
+
]
|
144
|
+
end
|
152
145
|
end
|
153
146
|
|
154
|
-
def
|
155
|
-
|
147
|
+
def series_for_integer(interval, range, tz)
|
148
|
+
interval_sql = %Q(#{sanitize(interval)} * interval '1 seconds')
|
149
|
+
if range
|
150
|
+
a = [
|
151
|
+
tstamp_tz("to_timestamp(#{sanitize(range.min.to_i)})", tz),
|
152
|
+
tstamp_tz("to_timestamp(#{sanitize(range.max.to_i)})", tz),
|
153
|
+
interval_sql
|
154
|
+
]
|
155
|
+
else
|
156
|
+
a = [
|
157
|
+
tstamp_tz('min(created_at)', tz),
|
158
|
+
tstamp_tz('max(created_at)', tz),
|
159
|
+
interval_sql
|
160
|
+
]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def dtrunc(interval, value, tz)
|
165
|
+
"date_trunc(#{sanitize(interval)}, #{tstamp_tz(value, tz)})"
|
166
|
+
end
|
167
|
+
|
168
|
+
def tstamp_tz(str, tz)
|
169
|
+
"#{str}::timestamptz AT TIME ZONE #{sanitize(tz)}"
|
156
170
|
end
|
157
171
|
|
158
172
|
def counter_error!(*args)
|
159
173
|
fail EventCounter::CounterError, args
|
160
174
|
end
|
161
175
|
|
162
|
-
def normalize_interval!(
|
163
|
-
default_interval = default_interval_for(
|
176
|
+
def normalize_interval!(name, interval)
|
177
|
+
default_interval = default_interval_for(name)
|
164
178
|
|
165
179
|
h = {
|
166
180
|
default_interval: default_interval,
|
@@ -195,28 +209,30 @@ SQL
|
|
195
209
|
interval.is_a?(Symbol) ? INTERVALS[interval] : interval
|
196
210
|
end
|
197
211
|
|
198
|
-
def normalize_counters_data(
|
199
|
-
|
200
|
-
[ Time.parse(i['created_at']), i['value'].to_i ]
|
212
|
+
def normalize_counters_data(data, tz)
|
213
|
+
Time.use_zone(tz) do
|
214
|
+
data.map { |i| [ Time.zone.parse(i['created_at']), i['value'].to_i ] }
|
201
215
|
end
|
202
216
|
end
|
203
217
|
|
204
|
-
def default_interval_for(
|
205
|
-
event_counters[
|
218
|
+
def default_interval_for(name)
|
219
|
+
event_counters[name.to_sym]
|
206
220
|
end
|
207
221
|
|
208
|
-
def
|
209
|
-
|
222
|
+
def normalize_range!(range, interval)
|
223
|
+
range_min, range_max =
|
224
|
+
case interval
|
225
|
+
when Symbol
|
226
|
+
[
|
227
|
+
range.min.send(:"beginning_of_#{interval}"),
|
228
|
+
range.max.send(:"end_of_#{interval}")
|
229
|
+
]
|
230
|
+
else
|
231
|
+
[ range.min.floor(interval), range.max.floor(interval) ]
|
232
|
+
end
|
210
233
|
|
211
|
-
|
212
|
-
|
213
|
-
[
|
214
|
-
range.min.send(:"beginning_of_#{interval}").to_i,
|
215
|
-
range.max.send(:"end_of_#{interval}").to_i
|
216
|
-
]
|
217
|
-
else
|
218
|
-
[ range.min.floor(interval).to_i, range.max.floor(interval).to_i ]
|
219
|
-
end
|
234
|
+
# TODO: ensure that range in time zone
|
235
|
+
range_min..range_max
|
220
236
|
end
|
221
237
|
end
|
222
238
|
end
|