sir_tracks_alot 0.4.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/Gemfile +2 -1
  2. data/Gemfile.lock +54 -0
  3. data/README.rdoc +29 -1
  4. data/VERSION +1 -1
  5. data/benchmarks/activity_benchmark.rb +22 -23
  6. data/benchmarks/benchmark_helper.rb +30 -0
  7. data/benchmarks/count_benchmark.rb +33 -0
  8. data/lib/sir_tracks_alot.rb +16 -14
  9. data/lib/sir_tracks_alot/activity.rb +7 -6
  10. data/lib/sir_tracks_alot/clock.rb +4 -0
  11. data/lib/sir_tracks_alot/count.rb +87 -39
  12. data/lib/sir_tracks_alot/event_helper.rb +1 -1
  13. data/lib/sir_tracks_alot/filter_helper.rb +28 -18
  14. data/lib/sir_tracks_alot/persistable.rb +4 -0
  15. data/lib/sir_tracks_alot/queue/report_cache.rb +2 -0
  16. data/lib/sir_tracks_alot/queue/report_config.rb +3 -0
  17. data/lib/sir_tracks_alot/queue/report_queue.rb +8 -10
  18. data/lib/sir_tracks_alot/reports/actor_activity_report.rb +3 -3
  19. data/lib/sir_tracks_alot/reports/basic_report.rb +1 -1
  20. data/lib/sir_tracks_alot/reports/filter_report.rb +5 -11
  21. data/lib/sir_tracks_alot/reports/report.rb +16 -2
  22. data/lib/sir_tracks_alot/reports/simple_report.rb +43 -0
  23. data/lib/sir_tracks_alot/reports/target_report.rb +2 -7
  24. data/lib/sir_tracks_alot/summary.rb +9 -0
  25. data/sir_tracks_alot.gemspec +140 -0
  26. data/spec/activity_spec.rb +18 -59
  27. data/spec/count_spec.rb +56 -25
  28. data/spec/queue/report_queue_spec.rb +8 -8
  29. data/spec/redis_spec_helper.rb +8 -0
  30. data/spec/reports/actor_report_spec.rb +16 -16
  31. data/spec/reports/filter_report_spec.rb +1 -1
  32. data/spec/reports/report_spec.rb +15 -0
  33. data/spec/reports/root_stem_report_spec.rb +1 -1
  34. data/spec/reports/simple_report_spec.rb +63 -0
  35. data/spec/reports/target_report_spec.rb +9 -4
  36. data/spec/sir_tracks_alot_spec.rb +4 -0
  37. data/spec/spec_helper.rb +1 -9
  38. metadata +38 -10
data/Gemfile CHANGED
@@ -2,11 +2,12 @@ source :gemcutter
2
2
  gem 'logging'
3
3
  gem 'twitter'
4
4
  gem 'ruport'
5
- gem 'ohm'
5
+ gem 'ohm', '=0.0.38'
6
6
  gem 'redis'
7
7
 
8
8
  group :development do
9
9
  gem 'rspec'
10
+ gem 'color'
10
11
  gem 'rspec_hpricot_matchers'
11
12
  gem 'hpricot'
12
13
  end
@@ -0,0 +1,54 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ color (1.4.1)
5
+ crack (0.1.8)
6
+ fastercsv (1.5.3)
7
+ hashie (0.4.0)
8
+ hoe (2.6.2)
9
+ rake (>= 0.8.7)
10
+ rubyforge (>= 2.0.4)
11
+ hpricot (0.8.2)
12
+ httparty (0.6.1)
13
+ crack (= 0.1.8)
14
+ json_pure (1.4.6)
15
+ little-plugger (1.1.2)
16
+ logging (1.4.3)
17
+ little-plugger (>= 1.1.2)
18
+ multi_json (0.0.4)
19
+ oauth (0.4.3)
20
+ ohm (0.0.38)
21
+ redis (>= 1.0.4)
22
+ pdf-writer (1.1.8)
23
+ color (>= 1.4.0)
24
+ transaction-simple (~> 1.3)
25
+ rake (0.8.7)
26
+ redis (2.0.10)
27
+ rspec (1.3.0)
28
+ rspec_hpricot_matchers (1.0)
29
+ rubyforge (2.0.4)
30
+ json_pure (>= 1.1.7)
31
+ ruport (1.6.3)
32
+ fastercsv
33
+ pdf-writer (= 1.1.8)
34
+ transaction-simple (1.4.0)
35
+ hoe (>= 1.1.7)
36
+ twitter (0.9.12)
37
+ hashie (~> 0.4.0)
38
+ httparty (~> 0.6.1)
39
+ multi_json (~> 0.0.4)
40
+ oauth (~> 0.4.3)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ color
47
+ hpricot
48
+ logging
49
+ ohm (= 0.0.38)
50
+ redis
51
+ rspec
52
+ rspec_hpricot_matchers
53
+ ruport
54
+ twitter
@@ -1,6 +1,34 @@
1
1
  = sir_tracks_alot
2
2
 
3
- Description goes here.
3
+ Sir Tracks Alot is a ruby library designed to aid in the tracking, collating and analysis of arbitrary usage data. You can use it to record anything! Then summarize what's been recorded into useful graphs. The primary use case for Sir Tracks Alot was to instrument web usage of Rails applications.
4
+
5
+ For example, to that a specific user has viewed a specific page:
6
+
7
+ SirTracksAlot.record_activity :owner => 'owner', :actor => '/users/123', :target => '/pages/1'
8
+
9
+ To include information taken from a web request (such as the user agent), simple include it:
10
+
11
+ SirTracksAlot.record_activity :owner => 'owner', :actor => '/users/123', :target => '/pages/1', :request => request
12
+
13
+ You can also include the notion of action type:
14
+
15
+ SirTracksAlot.record_activity :owner => 'owner', :actor => '/users/123', :target => '/pages/1', :action => 'create'
16
+
17
+ To investigate what's been stored you can filter activities with a variety of method and any attribute can be queried.
18
+
19
+ Direct queries work on strings. For example, to search for all activities by a certain owner:
20
+
21
+ Activity.filter(:owner => 'owner')
22
+
23
+ Regular expressions can be used:
24
+
25
+ Activity.filter(:owner => 'owner', :target => /\/pages\/\d+/)
26
+
27
+ You can also combine permutations of various options. For example, to find all 'create' and 'view' actions for both '/pages' and '/profiles' targets:
28
+
29
+ Activity.filter(:owner => 'owner', :target => ['/pages', '/profiles'], :actions => ['create', 'destroy'])
30
+
31
+
4
32
 
5
33
  == Note on Patches/Pull Requests
6
34
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 0.6.2
@@ -1,35 +1,34 @@
1
- require 'rubygems'
2
- require 'ohm'
3
- require 'lib/trackable'
1
+ require 'benchmarks/benchmark_helper'
4
2
 
5
- Ohm.connect(:host => 'localhost.com', :timeout => 10)
3
+ @owner = 'owner'
6
4
 
7
- @owner = '/events/md10'
5
+ RedisSpecHelper.reset
6
+ DataBuilder.build(1000)
8
7
 
9
- activities_count = Trackable::Activity.filter(:owner => @owner).size
10
- events_count = 0; Trackable::Activity.filter(:owner => @owner).each{|a| events_count += a.events.size}
11
-
12
- puts "**** Running benchmarks against dataset with #{activities_count} activities and #{events_count} events:"
8
+ Benchmark.bm do |b|
9
+ b.report('filtering by owner') do
10
+ SirTracksAlot::Activity.filter(:owner => @owner)
11
+ end
13
12
 
14
- Benchmark.bmbm do |b|
15
-
16
- b.report('grabbing all activities, and iterating') do
17
- Trackable::Activity.filter(:owner => @owner).collect{|a| a.target}
13
+ b.report('filtering by target with regex (all match)') do
14
+ SirTracksAlot::Activity.filter(:owner => @owner, :target => /.+/)
18
15
  end
19
16
 
20
- b.report('filter only') do
21
- Trackable::Activity.filter(:owner => @owner)
17
+ b.report('filtering by target with regex (no matches)') do
18
+ SirTracksAlot::Activity.filter(:owner => @owner, :target => /xxxxxxx/)
19
+ end
20
+
21
+ b.report('filtering by owner, and iterating') do
22
+ SirTracksAlot::Activity.filter(:owner => @owner).collect{|a| a.target}
22
23
  end
23
24
 
24
- b.report('filter and retrieve last_event') do
25
- Trackable::Activity.filter(:owner => @owner).map{|a| a.last_event}
25
+ b.report('filtering and retrieve last_event') do
26
+ SirTracksAlot::Activity.filter(:owner => @owner).map{|a| a.last_event}
26
27
  end
27
28
 
28
- b.report('filter and retrieve all events') do
29
- Trackable::Activity.filter(:owner => @owner).map{|a| a.events.map{|e| e}}
29
+ b.report('filtering and retrieve all events') do
30
+ SirTracksAlot::Activity.filter(:owner => @owner).map{|a| a.events.map{|e| e}}
30
31
  end
31
-
32
- b.report('count_by daily') do
33
- # counts = Trackable::Activity.count_by(:daily, :owner => @owner)
34
- end
35
32
  end
33
+
34
+ RedisSpecHelper.reset
@@ -0,0 +1,30 @@
1
+ require "rubygems"
2
+ require "bundler"
3
+ Bundler.setup(:development)
4
+ Bundler.require(:development)
5
+ require 'lib/sir_tracks_alot'
6
+ require 'benchmark'
7
+ require 'spec/redis_spec_helper'
8
+
9
+ Ohm.connect(:host => '127.0.0.1', :timeout => 2, :db => 15)
10
+
11
+ class DataBuilder
12
+ START_DATE = 1272559940
13
+ ONE_HOUR = 3600
14
+
15
+ def self.build(count = 1000, owner = 'owner', ref_count = 100)
16
+ puts "** Building activities..."
17
+
18
+ count.times do |i|
19
+ event = START_DATE + (ONE_HOUR * i)
20
+ target = "/targets/#{rand ref_count}"
21
+ actor = "/actors/#{rand ref_count}"
22
+ user_agent = "user_agents-#{rand ref_count}"
23
+ view = 'view'
24
+
25
+ SirTracksAlot.record(:owner => owner, :event => event, :target => target, :actor => actor, :user_agent => user_agent, :action => view)
26
+ end
27
+
28
+ puts "** Done building #{count} activities."
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ require 'benchmarks/benchmark_helper'
2
+
3
+ @owner = 'owner'
4
+ RedisSpecHelper.reset
5
+ DataBuilder.build(1000)
6
+
7
+ @results = {}
8
+
9
+ Benchmark.bm do |b|
10
+ b.report('counting activities') do
11
+ SirTracksAlot::Count.count({:owner => @owner}, {:limit => 999999}, :daily)
12
+ end
13
+
14
+ b.report('building rows for reports') do
15
+ SirTracksAlot::Count.rows(:owner => @owner)
16
+ end
17
+
18
+ b.report('filtering counts by owner') do
19
+ SirTracksAlot::Count.find(:owner => @owner)
20
+ end
21
+
22
+ b.report('filtering counts with regex') do
23
+ SirTracksAlot::Count.rows(:owner => @owner, :target => /.+/)
24
+ end
25
+ end
26
+
27
+ counts_count = SirTracksAlot::Count.all.size
28
+ summaries_count = SirTracksAlot::Summary.all.size
29
+ activities_count = SirTracksAlot::Activity.all.size
30
+
31
+ puts "*** Generated #{activities_count}, #{counts_count} counts (#{(counts_count+0.1)/(activities_count+0.1)}), #{summaries_count} summaries"
32
+
33
+ RedisSpecHelper.reset
@@ -1,16 +1,18 @@
1
1
  $:.unshift(File.dirname(__FILE__)) unless
2
2
  $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
3
 
4
- require "rubygems"
5
- require "bundler"
6
- Bundler.setup
7
- Bundler.require
8
-
9
- # require 'logging'
10
- # require 'twitter'
11
- # require 'ruport'
12
- # require 'ohm'
13
- # require 'redis'
4
+ begin
5
+ require "rubygems"
6
+ require "bundler"
7
+ Bundler.setup
8
+ Bundler.require
9
+ rescue LoadError
10
+ require 'logging'
11
+ require 'twitter'
12
+ require 'ruport'
13
+ require 'ohm'
14
+ require 'redis'
15
+ end
14
16
 
15
17
  module SirTracksAlot
16
18
  TRACKABLE_ROOT = "#{File.dirname(__FILE__)}/.."
@@ -20,6 +22,7 @@ module SirTracksAlot
20
22
  autoload :EventHelper, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot event_helper.rb])
21
23
  autoload :FilterHelper, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot filter_helper.rb])
22
24
  autoload :Count, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot count.rb])
25
+ autoload :Summary, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot summary.rb])
23
26
  autoload :Clock, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot clock.rb])
24
27
 
25
28
  class SirTracksAlotError < StandardError
@@ -57,9 +60,7 @@ module SirTracksAlot
57
60
 
58
61
  raise RecordInvalidError.new("Activity not valid: #{activity.errors.inspect}") unless activity.valid?
59
62
 
60
- raise "EVENT NIL" if event.nil?
61
-
62
- activity.update(:last_event => event)
63
+ activity.update(:last_event => event, :counted => '0')
63
64
  activity.events << event
64
65
  activity
65
66
  end
@@ -94,7 +95,7 @@ raise "EVENT NIL" if event.nil?
94
95
  return @log if @log
95
96
 
96
97
  @log = Logging::Logger[self]
97
- @log.level = :debug
98
+ @log.level = ENV['SIR_TRACKS_ALOT_LOG_LEVEL'] || 'warn'
98
99
  @log
99
100
  end
100
101
 
@@ -121,6 +122,7 @@ raise "EVENT NIL" if event.nil?
121
122
 
122
123
  module Reports
123
124
  autoload :Report, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot reports report.rb])
125
+ autoload :SimpleReport, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot reports simple_report.rb])
124
126
  autoload :BasicReport, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot reports basic_report.rb])
125
127
  autoload :SirTracksAlotReport, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot reports trackable_report.rb])
126
128
  autoload :TargetReport, File.join(File.dirname(__FILE__), *%w[sir_tracks_alot reports target_report.rb])
@@ -4,7 +4,6 @@ module SirTracksAlot
4
4
  extend FilterHelper
5
5
 
6
6
  ACTIONS = [:create, :view, :login, :search, :update, :destroy]
7
- LIMIT = 500
8
7
 
9
8
  attribute :created_at
10
9
  attribute :last_event # Clock.now
@@ -15,7 +14,7 @@ module SirTracksAlot
15
14
  attribute :action # create, view, login, etc.
16
15
  attribute :user_agent # IE/Safari Windows/Mac etc.
17
16
  attribute :counted # true/false
18
- list :events # Clock.now's
17
+ list :events # Clock.now's
19
18
 
20
19
  index :owner
21
20
  index :actor
@@ -36,19 +35,21 @@ module SirTracksAlot
36
35
  end
37
36
 
38
37
  # find recent activities
39
- def self.recent(options_for_find, options_for_sort = {:order => 'DESC', :limit => LIMIT})
38
+ def self.recent(options_for_find, options_for_sort = {:order => 'DESC', :limit => 500})
40
39
  find(options_for_find).sort_by(:last_event, options_for_sort)
41
40
  end
42
41
 
43
- # Delete counted activities, leaving a default of the 500 most recent for the provided search criteria
44
- # If left blank, purges all but the most recent Limit
45
- def self.purge!(options_for_find = {}, options_for_sort = {:order => 'DESC'})
42
+ # Delete 500 counted activities, leaving a default of the 500 most recent for the provided search criteria
43
+ def self.purge!(options_for_find = {}, options_for_sort = {:order => 'DESC', :start => 500, :limit => 500})
46
44
  recent(options_for_find.merge(:counted => 1), options_for_sort).each{|a| a.delete}
47
45
  end
48
46
 
49
47
  # set this activity to counted
48
+ # and clear the recorded events
50
49
  def counted!
51
50
  update(:counted => '1')
51
+ events.clear
52
+ true
52
53
  end
53
54
 
54
55
  # is this activity counted?
@@ -3,5 +3,9 @@ module SirTracksAlot
3
3
  def self.now
4
4
  Time.now.utc.to_i
5
5
  end
6
+
7
+ def self.convert(human)
8
+ Time.parse(human).to_i
9
+ end
6
10
  end
7
11
  end
@@ -5,75 +5,123 @@ module SirTracksAlot
5
5
  ACTIONS = [:create, :view, :login, :search, :update, :destroy]
6
6
 
7
7
  attribute :created_at
8
- attribute :date # 10/01/2010
9
- attribute :hour # 1 - 23
10
8
  attribute :owner # 123123
11
9
  attribute :actor # /users/peter-brown
12
10
  attribute :target # /discussions/23423
13
- attribute :category
14
- attribute :views
15
- attribute :visits
11
+ attribute :category # /discussions
12
+ attribute :action # create
13
+ set :summaries, Summary
16
14
 
17
- index :date
18
- index :hour
19
15
  index :owner
20
16
  index :actor
21
17
  index :target
22
18
  index :category
19
+ index :action
23
20
 
24
- def self.count(options)
25
- options = OpenStruct.new(options) if options.kind_of?(Hash)
21
+ def self.total(what, options_for_find)
22
+ what = [what] unless what.kind_of?(Array)
23
+ views, visits = 0, 0
24
+
25
+ filter(options_for_find).each do |count|
26
+ count.summaries.each do |summary|
27
+ views += summary.views.to_i if what.include?(:views)
28
+ visits += summary.visits.to_i if what.include?(:visits)
29
+ end
30
+ end
31
+
32
+ return [views, visits] if what == [:views, :visits]
33
+ return [visits, views] if what == [:visits, :views]
34
+ return views if what == [:views]
35
+ return visits if what == [:visits]
36
+ raise ArgumentError("what must be one or both of :views, :visits")
37
+ end
38
+
39
+ def self.rows(options_for_find, title = nil)
40
+ groups = {}
26
41
 
27
- rollup(Activity.filter(:owner => options.owner,
28
- :target => options.target,
29
- :action => options.action||[],
30
- :category => options.category || [],
31
- :counted => '0'),
32
- options.session_duration
33
- )
42
+ filter(options_for_find).each do |count|
43
+ t = title || count.target || count.actor # don't currently support counts with both actor and target set
44
+
45
+ count.summaries.each do |summary|
46
+ groups[t] ||= [0,0]
47
+ groups[t][0] += summary.views.to_i
48
+ groups[t][1] += summary.visits.to_i
49
+ end
50
+ end
51
+
52
+ groups.collect{|g| g.flatten}
53
+ end
54
+
55
+ # Convert a default of 500 activities into counts
56
+ def self.count(options_for_find = {}, options_for_sort = {:limit => 500}, resolution = :hourly)
57
+ options_for_find = options_for_find.to_hash
58
+ session_duration = options_for_find.delete(:session_duration)
59
+ activities = Activity.find(options_for_find.merge(:counted => 0, :action => [], :category => [])).sort(options_for_sort)
60
+ rollup(activities, session_duration, resolution)
34
61
  end
35
62
 
36
- def self.rollup(activities, session_duration)
63
+ # Summarize counts daily or hourly
64
+ # All times are in UTC
65
+ def self.summarize(resolution = :daily, options_for_find = {})
66
+ counts = {}
67
+
68
+ filter(options_for_find).each do |count|
69
+ count.summaries.each do |summary|
70
+ key = case resolution
71
+ when :daily
72
+ Time.at(summary.date.to_i).utc.strftime('%Y/%m/%d')
73
+ when :hourly
74
+ Time.at(summary.date.to_i).utc.strftime('%Y/%m/%d %H:00 UTC')
75
+ end
76
+
77
+ counts[key] ||= [0,0]
78
+ counts[key][0] += summary.views.to_i
79
+ counts[key][1] += summary.visits.to_i
80
+ end
81
+ end
82
+
83
+ counts
84
+ end
85
+
86
+ # Rollup all activities by resolution
87
+ # Create one count for actor and one count for target
88
+ def self.rollup(activities, session_duration, resolution = :hourly)
37
89
  counts = []
38
90
 
39
91
  activities.each do |activity|
40
- activity.views(:hourly).each do |time, views|
41
- date, hour = time.split(' ')
42
- counts << create_by_activity({:owner => activity.owner, :category => activity.category, :target => activity.target, :date => date, :hour => hour}, views, 0)
43
- counts << create_by_activity({:owner => activity.owner, :category => activity.category, :actor => activity.actor, :date => date, :hour => hour}, views, 0)
92
+ count = Count.find_or_create(:owner => activity.owner, :category => activity.category, :target => activity.target, :action => activity.action)
93
+
94
+ activity.views(resolution).each do |time, views|
95
+ date = Time.parse(time).to_i
96
+ summary = count.summaries.find(:date => date).first || Summary.create(:date => date)
97
+ count.summaries << summary
98
+ summary.update(:views => (summary.views||0).to_i + views.to_i)
44
99
  end
45
100
 
46
- activity.visits(session_duration, :hourly).each do |time, visits|
47
- date, hour = time.split(' ')
48
- counts << create_by_activity({:owner => activity.owner, :category => activity.category, :target => activity.target, :date => date, :hour => hour}, 0, visits)
49
- counts << create_by_activity({:owner => activity.owner, :category => activity.category, :actor => activity.actor, :date => date, :hour => hour}, 0, visits)
101
+ activity.visits(session_duration, resolution).each do |time, visits|
102
+ date = Time.parse(time).to_i
103
+ summary = count.summaries.find(:date => date).first || Summary.create(:date => date)
104
+ count.summaries << summary
105
+ summary.update(:visits => (summary.visits||0).to_i + visits.to_i)
50
106
  end
51
107
 
52
108
  activity.counted!
109
+ counts << count
53
110
  end
54
111
 
55
112
  counts
56
113
  end
57
114
 
58
- def self.create_by_activity(attributes, views = 0, visits = 0)
59
- count = Count.find_or_create(attributes)
60
-
61
- count.views ||= 0; count.visits ||= 0
115
+ def to_hash
116
+ {:owner => owner, :actor => actor, :target => target, :category => category, :id => id}
117
+ end
118
+
62
119
 
63
- count.views = count.views.to_i + views
64
- count.visits = count.visits.to_i + visits
65
-
66
- count.save
67
- end
68
-
69
-
70
120
  private
71
121
 
72
122
  def validate
73
123
  assert_present :owner
74
- assert_present :views
75
- assert_present :visits
76
- assert_unique([:owner, :actor, :target, :category, :date, :hour])
124
+ assert_unique([:owner, :actor, :target, :category, :action])
77
125
  end
78
126
 
79
127
  end