tabs 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +3 -0
  5. data/README.md +85 -5
  6. data/lib/tabs/config.rb +37 -2
  7. data/lib/tabs/metrics/counter.rb +14 -5
  8. data/lib/tabs/metrics/task.rb +8 -6
  9. data/lib/tabs/metrics/task/token.rb +25 -10
  10. data/lib/tabs/metrics/value.rb +35 -27
  11. data/lib/tabs/resolution.rb +26 -10
  12. data/lib/tabs/resolutionable.rb +36 -13
  13. data/lib/tabs/resolutions/day.rb +9 -1
  14. data/lib/tabs/resolutions/hour.rb +9 -1
  15. data/lib/tabs/resolutions/minute.rb +9 -1
  16. data/lib/tabs/resolutions/month.rb +9 -1
  17. data/lib/tabs/resolutions/week.rb +13 -7
  18. data/lib/tabs/resolutions/year.rb +9 -1
  19. data/lib/tabs/storage.rb +39 -17
  20. data/lib/tabs/tabs.rb +12 -4
  21. data/lib/tabs/version.rb +1 -1
  22. data/spec/lib/tabs/config_spec.rb +60 -0
  23. data/spec/lib/tabs/metrics/counter_spec.rb +44 -1
  24. data/spec/lib/tabs/{task_spec.rb → metrics/task_spec.rb} +31 -3
  25. data/spec/lib/tabs/metrics/value_spec.rb +36 -0
  26. data/spec/lib/tabs/resolution_spec.rb +26 -3
  27. data/spec/lib/tabs/resolutionable_spec.rb +53 -0
  28. data/spec/lib/tabs/resolutions/day_spec.rb +23 -0
  29. data/spec/lib/tabs/resolutions/hour_spec.rb +23 -0
  30. data/spec/lib/tabs/resolutions/minute_spec.rb +23 -0
  31. data/spec/lib/tabs/resolutions/month_spec.rb +23 -0
  32. data/spec/lib/tabs/resolutions/week_spec.rb +24 -0
  33. data/spec/lib/tabs/resolutions/year_spec.rb +23 -0
  34. data/spec/lib/tabs/storage_spec.rb +138 -0
  35. data/spec/lib/tabs_spec.rb +28 -1
  36. data/spec/spec_helper.rb +9 -1
  37. data/spec/support/custom_resolutions.rb +10 -2
  38. data/tabs.gemspec +6 -21
  39. metadata +48 -81
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5925ee887e482c08b4ae405b4463162a90b41494
4
+ data.tar.gz: 878be73e42a4c32a58853c61385f71460bbafa60
5
+ SHA512:
6
+ metadata.gz: 531f0554860cb6143ee9d8cb29d7362d2111c63324a52ca5cc773f1767a80da58e2bae4680479abf791b024c185275334cc9b07fa7014dd459099af49e2cb4cd
7
+ data.tar.gz: 62bc646ae94b276fa7a089fe7e88824473b65e9080244be8a54a5ea0f5b064114226d06e82cbf918320c748bbedbfecc545280156c7b0542d2f3f4466f8c7be7
data/.gitignore CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  dump.rdb
19
+ tags
@@ -1 +1 @@
1
- 1.9.3
1
+ 2.0.0-p247
@@ -1,3 +1,6 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ - 2.0.0
5
+ services:
6
+ - redis-server
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Tabs
4
4
 
5
- Tabs is a redis-backed metrics tracker that supports counts, sums,
5
+ Tabs is a redis-backed metrics tracker for time-based events that supports counts, sums,
6
6
  averages, and min/max, and task based stats sliceable by the minute, hour, day, week, month, and year.
7
7
 
8
8
  ## Installation
@@ -208,11 +208,15 @@ that's necessary is a module that conforms to the following protocol:
208
208
 
209
209
  ```ruby
210
210
  module SecondResolution
211
- extend Tabs::Resolutionable
211
+ include Tabs::Resolutionable
212
212
  extend self
213
213
 
214
214
  PATTERN = "%Y-%m-%d-%H-%M-%S"
215
215
 
216
+ def name
217
+ :seconds
218
+ end
219
+
216
220
  def serialize(timestamp)
217
221
  timestamp.strftime(PATTERN)
218
222
  end
@@ -225,6 +229,10 @@ module SecondResolution
225
229
  def from_seconds(s)
226
230
  s / 1
227
231
  end
232
+
233
+ def to_seconds
234
+ 1
235
+ end
228
236
 
229
237
  def add(timestamp, num_of_seconds)
230
238
  timestamp + num_of_seconds.seconds
@@ -239,6 +247,8 @@ end
239
247
 
240
248
  A little description on each of the above methods:
241
249
 
250
+ *`name`*: unique symbol used to reference registered resolution
251
+
242
252
  *`serialize`*: converts the timestamp to a string. The return value
243
253
  here will be used as part of the Redis key storing values associated
244
254
  with a given metric.
@@ -249,6 +259,8 @@ into an actual DateTime value.
249
259
  *`from_seconds`*: should return the number of periods in the given
250
260
  number of seconds. For example, there are 60 seconds in a minute.
251
261
 
262
+ *`to_seconds`*: should return the number of seconds in '1' of these time periods. For example, there are 3600 seconds in an hour.
263
+
252
264
  *`add`*: should add the number of seconds in the given resolution to the
253
265
  supplied timestamp.
254
266
 
@@ -256,7 +268,7 @@ supplied timestamp.
256
268
  For example, the week resolution returns the first hour of the first day
257
269
  of the week.
258
270
 
259
- *NOTE: If you're doing a custom resolution you should probably look into
271
+ *NOTE: If you're doing a custom resolution, you should probably look into
260
272
  the code a bit.*
261
273
 
262
274
  Once you have a module that conforms to the resolution protocol you need
@@ -264,11 +276,19 @@ to register it with Tabs. You can do this in one of two ways:
264
276
 
265
277
  ```ruby
266
278
  # This call can be anywhere before you start using tabs
267
- Tabs::Resolution.register(:second, SecondResolution)
279
+ Tabs::Resolution.register(SecondResolution)
268
280
 
269
281
  # or, you can use the config block described below
270
282
  ```
271
283
 
284
+ #### Removing a Resolution
285
+
286
+ You can also remove any resolution (custom or built-in) by calling the `unregister_resolutions` method in the config block (see config section below). Or, you can remove manually by calling:
287
+
288
+ ```ruby
289
+ Tabs::Resolution.unregister(:minute, :hour)
290
+ ```
291
+
272
292
  ### Inspecting Metrics
273
293
 
274
294
  You can list all metrics using `list_metrics`:
@@ -300,11 +320,31 @@ Tabs.drop_metric!("website-visits")
300
320
 
301
321
  This will drop all recorded values for the metric so it may not be un-done...be careful.
302
322
 
323
+ To drop only a specific resolution for a metric, just call `Tabs#drop_resolution_for_metric!`
324
+
325
+ ```ruby
326
+ Tabs.drop_resolution_for_metric!("website-visits", :minute)
327
+ ```
328
+
303
329
  Even more dangerous, you can drop all metrics...be very careful.
304
330
 
305
331
  ```ruby
306
332
  Tabs.drop_all_metrics!
307
333
  ```
334
+ ### Aging Out Old Metrics
335
+
336
+ You can use the expiration features to age out old metrics that may no longer be in your operational data set. For example, you may want to keep monthly or yearly data around but the minute or day level data isn't necessary past a certain date. You can set expirations for any resolution:
337
+
338
+ ```ruby
339
+ Tabs.configure do |config|
340
+ config.set_expirations(minute: 1.day, day: 1.week)
341
+ end
342
+ ```
343
+
344
+ The expiration date will start counting at the beginning at the end of the given resolution. Meaning that for a month resolution the given expiration time would start at the end of a given month. A month resolution metric recorded in January with an expiration of 2 weeks would expire after the 2nd week of February.
345
+
346
+ *NOTE: You cannot expire task metrics at this time, only counter and
347
+ values.*
308
348
 
309
349
  ### Configuration
310
350
 
@@ -318,6 +358,10 @@ Tabs.configure do |config|
318
358
 
319
359
  # pass a config hash that will be passed to Redis.new
320
360
  config.redis = { :host => 'localhost', :port => 6379 }
361
+
362
+ # pass a prefix that will be used in addition to the "tabs" prefix with Redis keys
363
+ # Example: "tabs:my_app:metric_name"
364
+ config.prefix = "my_app"
321
365
 
322
366
  # override default decimal precision (5)
323
367
  # affects stat averages and task completion rate
@@ -326,10 +370,31 @@ Tabs.configure do |config|
326
370
  # registers a custom resolution
327
371
  config.register_resolution :second, SecondResolution
328
372
 
373
+ # unregisters any resolution
374
+ config.unregister_resolutions(:minute, :hour)
375
+
376
+ # sets TTL for redis keys of specific resolutions
377
+ config.set_expirations({ minute: 1.hour, hour: 1.day })
378
+
329
379
  end
330
380
  ```
331
381
 
332
- ## Breaking Changes
382
+ #### Prefixing
383
+
384
+ Many applications use a single Redis instance for a number of uses:
385
+ background jobs, ephemeral data, Tabs, etc. To avoid key collisions,
386
+ and to make it easier to drop all of your tabs data without affecting
387
+ other parts of your system (or if more than one app shares the Redis
388
+ instance) you can prefix a given 'instance'.
389
+
390
+ Setting the prefix config option will cause all of the keys that tabs
391
+ stores to use this format:
392
+
393
+ ```
394
+ tabs:#{prefix}:#{key}..."
395
+ ```
396
+
397
+ ## Change Log & Breaking Changes
333
398
 
334
399
  ### v0.6.0
335
400
 
@@ -361,6 +426,21 @@ statement to simulate a transaction. Value data that was recorded prior
361
426
  to v0.8.2 will not be accessible in this or future versions so please
362
427
  continue to use v0.8.1 or lower if that is an issue.
363
428
 
429
+ ### v1.0.0
430
+
431
+ _WARNING: Version 1.0.0 is not compatible with previous versions of
432
+ Tabs_
433
+
434
+ We have made a number of changes related to hour metric keys are stored
435
+ in Redis. At this point we'll be following semantec versioning and will
436
+ support backwards compatability between major versions. In this release
437
+ we've added a number of major features:
438
+
439
+ * Metric expiration
440
+ * Key prefixes
441
+ * Support for unregistering resolutions
442
+ * A number of small numeric fixes
443
+
364
444
  ## Contributing
365
445
 
366
446
  1. Fork it
@@ -22,8 +22,43 @@ module Tabs
22
22
  @redis ||= Redis.new
23
23
  end
24
24
 
25
- def register_resolution(resolution, klass)
26
- Tabs::Resolution.register(resolution, klass)
25
+ def prefix=(arg)
26
+ @prefix = arg
27
+ end
28
+
29
+ def prefix
30
+ @prefix
31
+ end
32
+
33
+ def register_resolution(klass)
34
+ Tabs::Resolution.register(klass)
35
+ end
36
+
37
+ def unregister_resolutions(*resolutions)
38
+ Tabs::Resolution.unregister(resolutions)
39
+ end
40
+
41
+ def expiration_settings
42
+ @expiration_settings ||= {}
43
+ end
44
+
45
+ def set_expirations(resolution_hash)
46
+ resolution_hash.each do |resolution, expires_in_seconds|
47
+ raise Tabs::ResolutionMissingError.new(resolution) unless Tabs::Resolution.all.include? resolution
48
+ expiration_settings[resolution] = expires_in_seconds
49
+ end
50
+ end
51
+
52
+ def expires?(resolution)
53
+ expiration_settings.has_key?(resolution)
54
+ end
55
+
56
+ def expires_in(resolution)
57
+ expiration_settings[resolution]
58
+ end
59
+
60
+ def reset_expirations
61
+ @expiration_settings = {}
27
62
  end
28
63
 
29
64
  end
@@ -21,8 +21,8 @@ module Tabs
21
21
 
22
22
  def stats(period, resolution)
23
23
  timestamps = timestamp_range period, resolution
24
- keys = timestamps.map do |ts|
25
- "stat:counter:#{key}:count:#{Tabs::Resolution.serialize(resolution, ts)}"
24
+ keys = timestamps.map do |timestamp|
25
+ storage_key(resolution, timestamp)
26
26
  end
27
27
 
28
28
  values = mget(*keys).map do |v|
@@ -43,12 +43,21 @@ module Tabs
43
43
  del_by_prefix("stat:counter:#{key}")
44
44
  end
45
45
 
46
+ def drop_by_resolution!(resolution)
47
+ del_by_prefix("stat:counter:#{key}:count:#{resolution}")
48
+ end
49
+
50
+ def storage_key(resolution, timestamp)
51
+ formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
52
+ "stat:counter:#{key}:count:#{resolution}:#{formatted_time}"
53
+ end
54
+
46
55
  private
47
56
 
48
57
  def increment_resolution(resolution, timestamp)
49
- formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
50
- stat_key = "stat:counter:#{key}:count:#{formatted_time}"
51
- incr(stat_key)
58
+ store_key = storage_key(resolution, timestamp)
59
+ incr(store_key)
60
+ Tabs::Resolution.expire(resolution, store_key, timestamp)
52
61
  end
53
62
 
54
63
  def increment_total
@@ -37,7 +37,7 @@ module Tabs
37
37
  matching_tokens = started_tokens.select { |token| completed_tokens.include? token }
38
38
  completion_rate = (matching_tokens.size.to_f / range.size).round(Config.decimal_precision)
39
39
  elapsed_times = matching_tokens.map { |t| t.time_elapsed(resolution) }
40
- average_completion_time = (elapsed_times.sum) / matching_tokens.size
40
+ average_completion_time = matching_tokens.blank? ? 0.0 : (elapsed_times.sum) / matching_tokens.size
41
41
  Stats.new(
42
42
  started_tokens.size,
43
43
  completed_tokens.size,
@@ -51,18 +51,20 @@ module Tabs
51
51
  del_by_prefix("stat:task:#{key}")
52
52
  end
53
53
 
54
+ def storage_key(resolution, timestamp, type)
55
+ formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
56
+ "stat:task:#{key}:#{type}:#{resolution}:#{formatted_time}"
57
+ end
58
+
54
59
  private
55
60
 
56
61
  def tokens_for_period(range, resolution, type)
57
62
  keys = keys_for_range(range, resolution, type)
58
- mget(*keys).compact.map(&:to_a).flatten.map { |t| Token.new(t, key) }
63
+ smembers_all(*keys).compact.map(&:to_a).flatten.map { |t| Token.new(t, key) }
59
64
  end
60
65
 
61
66
  def keys_for_range(range, resolution, type)
62
- range.map do |date|
63
- formatted_time = Tabs::Resolution.serialize(resolution, date)
64
- "stat:task:#{key}:#{type}:#{formatted_time}"
65
- end
67
+ range.map { |timestamp| storage_key(resolution, timestamp, type) }
66
68
  end
67
69
 
68
70
  end
@@ -14,13 +14,13 @@ module Tabs
14
14
 
15
15
  def start(timestamp=Time.now)
16
16
  self.start_time = timestamp.utc
17
- sadd("stat:task:#{key}:tokens", token)
17
+ sadd(tokens_storage_key, token)
18
18
  Tabs::Resolution.all.each { |res| record_start(res, start_time) }
19
19
  end
20
20
 
21
21
  def complete(timestamp=Time.now)
22
22
  self.complete_time = timestamp.utc
23
- unless sismember("stat:task:#{key}:tokens", token)
23
+ unless sismember(tokens_storage_key, token)
24
24
  raise UnstartedTaskMetricError.new("No task for metric '#{key}' was started with token '#{token}'")
25
25
  end
26
26
  Tabs::Resolution.all.each { |res| record_complete(res, complete_time) }
@@ -40,32 +40,47 @@ module Tabs
40
40
 
41
41
  private
42
42
 
43
- def record_start(resolution, timestamp)
43
+ def storage_key(resolution, timestamp, type)
44
44
  formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
45
- sadd("stat:task:#{key}:started:#{formatted_time}", token)
45
+ "stat:task:#{key}:#{type}:#{resolution}:#{formatted_time}"
46
+ end
47
+
48
+ def started_storage_key
49
+ "stat:task:#{key}:#{token}:started_time"
50
+ end
51
+
52
+ def completed_storage_key
53
+ "stat:task:#{key}:#{token}:completed_time"
54
+ end
55
+
56
+ def tokens_storage_key
57
+ "stat:task:#{key}:tokens"
58
+ end
59
+
60
+ def record_start(resolution, timestamp)
61
+ sadd(storage_key(resolution, timestamp, "started"), token)
46
62
  end
47
63
 
48
64
  def record_complete(resolution, timestamp)
49
- formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
50
- sadd("stat:task:#{key}:completed:#{formatted_time}", token)
65
+ sadd(storage_key(resolution, timestamp, "completed"), token)
51
66
  end
52
67
 
53
68
  def start_time=(timestamp)
54
- set("stat:task:#{key}:#{token}:started_time", timestamp)
69
+ set(started_storage_key, timestamp)
55
70
  @start_time = timestamp
56
71
  end
57
72
 
58
73
  def start_time
59
- Time.parse(get("stat:task:#{key}:#{token}:started_time"))
74
+ Time.parse(get(started_storage_key))
60
75
  end
61
76
 
62
77
  def complete_time=(timestamp)
63
- set("stat:task:#{key}:#{token}:completed_time", timestamp)
78
+ set(completed_storage_key, timestamp)
64
79
  @complete_time = timestamp
65
80
  end
66
81
 
67
82
  def complete_time
68
- Time.parse(get("stat:task:#{key}:#{token}:completed_time"))
83
+ Time.parse(get(completed_storage_key))
69
84
  end
70
85
 
71
86
  end
@@ -13,23 +13,21 @@ module Tabs
13
13
  def record(value, timestamp=Time.now)
14
14
  timestamp.utc
15
15
  Tabs::Resolution.all.each do |resolution|
16
- formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
17
- stat_key = "stat:value:#{key}:data:#{formatted_time}"
18
- update_values(stat_key, value)
16
+ store_key = storage_key(resolution, timestamp)
17
+ update_values(store_key, value)
18
+ Tabs::Resolution.expire(resolution, store_key, timestamp)
19
19
  end
20
20
  true
21
21
  end
22
22
 
23
23
  def stats(period, resolution)
24
24
  timestamps = timestamp_range period, resolution
25
- keys = timestamps.map do |ts|
26
- formatted_time = Tabs::Resolution.serialize(resolution, ts)
27
- "stat:value:#{key}:data:#{formatted_time}"
25
+ keys = timestamps.map do |timestamp|
26
+ storage_key(resolution, timestamp)
28
27
  end
29
28
 
30
29
  values = mget(*keys).map do |v|
31
- value = v.nil? ? default_value(0) : v
32
- value = Hash[value.map { |k, i| [k, to_numeric(i)] }]
30
+ value = v.nil? ? default_value(0) : JSON.parse(v)
33
31
  value["timestamp"] = timestamps.shift
34
32
  value.with_indifferent_access
35
33
  end
@@ -41,41 +39,51 @@ module Tabs
41
39
  del_by_prefix("stat:value:#{key}")
42
40
  end
43
41
 
42
+ def drop_by_resolution!(resolution)
43
+ del_by_prefix("stat:value:#{key}:data:#{resolution}")
44
+ end
45
+
46
+ def storage_key(resolution, timestamp)
47
+ formatted_time = Tabs::Resolution.serialize(resolution, timestamp)
48
+ "stat:value:#{key}:data:#{resolution}:#{formatted_time}"
49
+ end
50
+
44
51
  private
45
52
 
46
53
  def update_values(stat_key, value)
47
- count = update_count(stat_key)
48
- sum = update_sum(stat_key, value)
49
- update_min(stat_key, value)
50
- update_max(stat_key, value)
51
- update_avg(stat_key, sum, count)
54
+ hash = get_current_hash(stat_key)
55
+ increment(hash, value)
56
+ update_min(hash, value)
57
+ update_max(hash, value)
58
+ update_avg(hash)
59
+ set(stat_key, JSON.generate(hash))
52
60
  end
53
61
 
54
- def update_count(stat_key)
55
- hincrby(stat_key, "count", 1)
62
+ def get_current_hash(stat_key)
63
+ hash = get(stat_key)
64
+ return JSON.parse(hash) if hash
65
+ default_value
56
66
  end
57
67
 
58
- def update_sum(stat_key, value)
59
- hincrby(stat_key, "sum", value)
68
+ def increment(hash, value)
69
+ hash["count"] += 1
70
+ hash["sum"] += value.to_f
60
71
  end
61
72
 
62
- def update_min(stat_key, value)
63
- min = (hget(stat_key, "min") || 0).to_i
64
- hset(stat_key, "min", value) if value < min || min == 0
73
+ def update_min(hash, value)
74
+ hash["min"] = value.to_f if hash["min"].nil? || value.to_f < hash["min"]
65
75
  end
66
76
 
67
- def update_max(stat_key, value)
68
- max = (hget(stat_key, "max") || 0).to_i
69
- hset(stat_key, "max", value) if value > max || max == 0
77
+ def update_max(hash, value)
78
+ hash["max"] = value.to_f if hash["max"].nil? || value.to_f > hash["max"]
70
79
  end
71
80
 
72
- def update_avg(stat_key, sum, count)
73
- avg = sum.to_f / count.to_f
74
- hset(stat_key, "avg", avg)
81
+ def update_avg(hash)
82
+ hash["avg"] = hash["sum"].to_f / hash["count"]
75
83
  end
76
84
 
77
85
  def default_value(nil_value=nil)
78
- { "count" => 0, "min" => nil_value, "max" => nil_value, "sum" => 0, "avg" => 0 }
86
+ { "count" => 0, "min" => nil_value, "max" => nil_value, "sum" => 0.0, "avg" => 0.0 }
79
87
  end
80
88
 
81
89
  end