panmind-usage-tracker 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,65 @@
1
+ Panmind Usage Tracker
2
+ ---------------------
3
+
4
+ What is it?
5
+ ===========
6
+
7
+ 1. A `Rack::Middleware` that sends selected parts of the request environment to an UDP socket
8
+ 2. An `EventMachine` daemon that opens an UDP socket and sends out received data to CouchDB
9
+ 3. A set of CouchDB map-reduce views, for analysis
10
+
11
+
12
+ Does it work?
13
+ =============
14
+
15
+ Yes, but the release is still incomplete, because currently tests are too
16
+ tied to Panmind logic.
17
+
18
+ If you can help in complete the test suite, it is much appreciated :-).
19
+
20
+ Deploying
21
+ =========
22
+
23
+ * Add the usage_tracker gem to your Gemfile and require the middleware
24
+
25
+ gem 'usage_tracker', :require => 'usage_tracker/middleware'
26
+
27
+ * Add the Middleware to your application:
28
+
29
+ Your::Application.config.middleware.use UsageTracker::Middleware
30
+
31
+ * The daemon can be started manually with the following command, inside a Rails.root:
32
+
33
+ $ usage_tracker [environment]
34
+
35
+ `environment` is optional and will default to "development" if no command line
36
+ option nor the RAILS_ENV environment variable are set.
37
+
38
+ or can be put under Upstart using the provided configuration file located in
39
+ `config/usage_tracker_upstart.conf`. Check it out and modify it to suit your needs.
40
+
41
+ The daemon logs to `log/usage_tracker.log` and rotates its logs when receives
42
+ the USR1 signal.
43
+
44
+ * The daemon writes its pid into tmp/pids/usage_tracker.pid
45
+
46
+ * The daemon connects to a Couch database named `usage_tracker` running on `localhost`,
47
+ default port `5984/TCP`, and listens on `localhost`, port `5985/UDP` by default.
48
+ You can change these settings via a `config/usage_tracker.yml` file. See the example
49
+ in the `config` directory of the gem distribution.
50
+
51
+ * The CouchDB instance must be running, the database is created (and updated)
52
+ if necessary.
53
+
54
+ * If the daemon cannot start, e.g. because of unavailable database or listening
55
+ address, it will print a diagnostig message to STDERR, log to usage_tracker.log
56
+ and exit with status of 1.
57
+
58
+ * The daemon exits gracefully if it receives the INT or the TERM signals.
59
+
60
+ Testing
61
+ =======
62
+
63
+ The current test suite, brutally extracted from Panmind codebase, is in the
64
+ `middleware_test.rb` file at the root of the Gem distribution. It is of no
65
+ use except Panmind, but it's a start for writing new ones. Please help! :-)
data/Rakefile ADDED
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ require 'rake'
4
+ require 'rake/rdoctask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+
9
+ Jeweler::Tasks.new do |gemspec|
10
+ gemspec.name = 'panmind-usage-tracker'
11
+ gemspec.summary = 'Write your application request logs in CouchDB'
12
+ gemspec.description = 'This software implements a Rails 3 Middleware and ' \
13
+ 'an EventMachine reactor to store into CouchDB the ' \
14
+ 'results of HTTP request processing'
15
+
16
+ gemspec.authors = ['Marcello Barnaba', 'Christian Wörner']
17
+ gemspec.homepage = 'http://github.com/Panmind/usage_tracker'
18
+ gemspec.email = 'vjt@openssl.it'
19
+
20
+ gemspec.add_dependency('rails', '~> 3.0')
21
+ gemspec.add_dependency('eventmachine')
22
+ gemspec.add_dependency('couchrest')
23
+ end
24
+ rescue LoadError
25
+ puts 'Jeweler not available. Install it with: gem install jeweler'
26
+ end
27
+
28
+ desc 'Generate the rdoc'
29
+ Rake::RDocTask.new do |rdoc|
30
+ rdoc.rdoc_files.add %w( README.md lib/**/*.rb )
31
+
32
+ rdoc.main = 'README.md'
33
+ rdoc.title = 'Rails Application Usage Tracker on CouchDB'
34
+ end
35
+
36
+ desc 'Will someone help write tests?'
37
+ task :default do
38
+ puts
39
+ puts 'Can you help in writing tests? Please do :-)'
40
+ puts
41
+ end
42
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
data/bin/usage_tracker ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'usage_tracker/reactor'
@@ -0,0 +1,13 @@
1
+ defaults: &defaults
2
+ couchdb: "http://admin:suxsux@127.0.0.1:5984/usage_tracker"
3
+ listen: "127.0.0.1:5985"
4
+
5
+ development:
6
+ <<: *defaults
7
+
8
+ production:
9
+ <<: *defaults
10
+
11
+ test:
12
+ <<: *defaults
13
+ couchdb: "http://admin:suxsux@127.0.0.1:5984/usage_tracker_test"
@@ -0,0 +1,15 @@
1
+ description "Panmind Usage Tracker Daemon"
2
+ author "Marcello Barnaba <marcello.barnaba@gmail.com>"
3
+ version "1.1"
4
+
5
+ start on runlevel [2345]
6
+ stop on shutdown
7
+ respawn
8
+
9
+ # The following line assumes that you're using RVM, that's why
10
+ # bash is invoked: to load rvm setup scripts.
11
+ #
12
+ # You should change the user under your webapp runs (panmind
13
+ # in this example) and the Rails.root (panmind/deploy).
14
+ #
15
+ exec sudo -i -H -u panmind bash -c 'echo; cd panmind/deploy; exec usage_tracker'
data/config/views.yml ADDED
@@ -0,0 +1,139 @@
1
+ <%
2
+ # Currently defined path prefixes, in JS Regexp format
3
+ #
4
+ _AREAS_RE = '\/(' << %w( inbox res projects users account publish search ).join('|') << ')'
5
+
6
+ # Expands to JS code that defines the "area" variable by
7
+ # executing the _AREAS_RE regexp onto the "path_info"
8
+ # property of the "doc" object.
9
+ #
10
+ _GET_AREA = %(
11
+ var match = doc.env.path_info.match (/#{_AREAS_RE}/);
12
+ var area = match ? match[1] : 'other';
13
+ ).gsub(/\s+/x, ' ').strip
14
+ %>
15
+
16
+ _id : '_design/basic'
17
+ language: 'javascript'
18
+ version : '2010110401' # Format: YYYY MM DD VV
19
+ views :
20
+ by_timestamp:
21
+ map: |
22
+ function (doc) {
23
+ if (doc.env)
24
+ emit (doc._id, doc);
25
+ }
26
+
27
+ by_date:
28
+ map: |
29
+ function (doc) {
30
+ var date = new Date (parseFloat (doc._id) * 1000);
31
+ emit([date.getUTCFullYear (), date.getUTCMonth (), date.getUTCDate ()], 1);
32
+ }
33
+
34
+ reduce: |
35
+ function (keys, values) {
36
+ return sum (values);
37
+ }
38
+
39
+ by_user_and_timestamp:
40
+ map: |
41
+ function (doc) {
42
+ if (doc.env)
43
+ emit ([doc.user_id, doc._id], doc);
44
+ }
45
+
46
+ res_count:
47
+ map: |
48
+ function (doc) {
49
+ if (doc.env && doc.env.path_info.indexOf ('res/') > 0)
50
+ emit (doc.env.path_info.split ('/')[2], 1);
51
+ }
52
+ reduce: |
53
+ function (keys, values, rereduce) {
54
+ return sum (values);
55
+ }
56
+
57
+ res_item_count:
58
+ map: |
59
+ function (doc) {
60
+ if (doc.env && doc.env.path_info.indexOf ('res/') > 0) {
61
+ var pieces = doc.env.path_info.split ('/');
62
+ if (pieces.length > 3)
63
+ emit ([pieces[2], pieces[3]], 1);
64
+ }
65
+ }
66
+ reduce: |
67
+ function (keys, values, rereduce) {
68
+ return sum (values);
69
+ }
70
+
71
+ user_res_count:
72
+ map: |
73
+ function (doc) {
74
+ if (doc.env && doc.env.path_info.indexOf ('res/') != -1)
75
+ emit ([doc.user_id, doc.env.path_info.split ('/')[2]], 1);
76
+ }
77
+ reduce: |
78
+ function (keys, values, rereduce) {
79
+ return sum (values);
80
+ }
81
+
82
+ user_area_count:
83
+ map: |
84
+ function (doc) {
85
+ if (doc.env) {
86
+ <%= _GET_AREA %>
87
+ emit ([doc.user_id, area], 1);
88
+ }
89
+ }
90
+ reduce: |
91
+ function (keys, values, rereduce) {
92
+ return sum (values);
93
+ }
94
+
95
+ area_count:
96
+ map: |
97
+ function (doc) {
98
+ if (doc.env) {
99
+ <%= _GET_AREA %>
100
+ emit (area, 1);
101
+ }
102
+ }
103
+ reduce: |
104
+ function (keys, values, rereduce) {
105
+ return sum (values);
106
+ }
107
+
108
+ average_duration_of_path:
109
+ map: |
110
+ function (doc) {
111
+ if (doc.duration)
112
+ emit (doc.env.path_info, doc.duration);
113
+ }
114
+ reduce: |
115
+ function (keys, values){
116
+ return Math.round (sum (values) / values.length);
117
+ }
118
+
119
+ average_duration_of_area:
120
+ map: |
121
+ function (doc) {
122
+ if (doc.duration) {
123
+ <%= _GET_AREA %>
124
+ emit (area, doc.duration)
125
+ }
126
+ }
127
+ reduce: |
128
+ function (keys, values){
129
+ return Math.round (sum (values) / values.length);
130
+ }
131
+
132
+ by_user_timestamp_area:
133
+ map: |
134
+ function (doc) {
135
+ if (doc.env) {
136
+ <%= _GET_AREA %>
137
+ emit ([doc.user_id, doc._id, area], doc);
138
+ }
139
+ }
@@ -0,0 +1,116 @@
1
+ require 'rubygems'
2
+ require 'erb'
3
+ require 'yaml'
4
+ require 'pathname'
5
+ require 'ostruct'
6
+ require 'couchrest'
7
+ require 'active_support/core_ext/object/blank'
8
+ require 'usage_tracker/log'
9
+
10
+ module UsageTracker
11
+ class << self
12
+ # Memoizes the current environment
13
+ def env
14
+ @env ||= ENV['RAILS_ENV'] || ARGV[0] || 'development'
15
+ end
16
+
17
+ @@defaults = {
18
+ 'couchdb' => 'http://localhost:5984/usage_tracker',
19
+ 'listen' => '127.0.0.1:5985'
20
+ }
21
+
22
+ # Memoizes settings from the ./config/usage_tracker.yml file,
23
+ # relative from __FILE__ and searches for the "usage_tracker"
24
+ # configuration block. Raises RuntimeError if it cannot find
25
+ # the configuration.
26
+ #
27
+ def settings
28
+ @settings ||= begin
29
+ log "Loading #{env} environment"
30
+
31
+ rc_file = Pathname.new('.').join('config', 'usage_tracker.yml')
32
+ settings = YAML.load(rc_file.read)[env] if rc_file.exist?
33
+
34
+ if settings.blank?
35
+ settings = @@defaults
36
+ log "#{env} configuration block not found in #{rc_file}, using defaults"
37
+ elsif settings.values_at(*%w(couchdb listen)).any?(&:blank?)
38
+ raise "Incomplete configuration: please set the 'couchdb' and 'listen' keys"
39
+ end
40
+
41
+ host, port = settings.delete('listen').split(':')
42
+
43
+ if [host, port].any? {|x| x.strip.empty?}
44
+ raise "Please specify where to listen as host:port"
45
+ end
46
+
47
+ settings['host'], settings['port'] = host, port.to_i
48
+
49
+ OpenStruct.new settings
50
+ end
51
+ end
52
+
53
+ def database
54
+ @database or raise "Not connected to the database"
55
+ end
56
+
57
+ # Connects to the configured CouchDB and memoizes the
58
+ # CouchRest::Database connection into an instance variable
59
+ # and calls +load_views!+
60
+ #
61
+ # Raises RuntimeError if the connection could not be established
62
+ #
63
+ def connect!
64
+ @database =
65
+ CouchRest.database!(settings.couchdb).tap do |db|
66
+ db.info
67
+ log "Connected to database #{settings.couchdb}"
68
+ end
69
+
70
+ load_views!
71
+ rescue Errno::ECONNREFUSED, RestClient::Exception => e
72
+ raise "Unable to connect to database #{settings.couchdb}: #{e.message}"
73
+ end
74
+
75
+ def write_pid!(pid = $$)
76
+ dir = Pathname.new('.').join('tmp', 'pids')
77
+ dir = Pathname.new(Dir.tmpdir) unless dir.directory?
78
+ dir.join('usage_tracker.pid').open('w+') {|f| f.write(pid)}
79
+ end
80
+
81
+ def log(message = nil)
82
+ @log ||= Log.new
83
+ message ? @log.info(message) : @log
84
+ end
85
+
86
+ def raise(message)
87
+ log.error message
88
+ Kernel.raise Error, message
89
+ end
90
+
91
+ private
92
+ # Loads CouchDB views from views.yml and verifies that
93
+ # they are loaded in the current instance, upgrading
94
+ # them if necessary.
95
+ def load_views!
96
+ new = YAML.load ERB.new(
97
+ Pathname.new(__FILE__).dirname.join('..', 'config', 'views.yml').read
98
+ ).result
99
+
100
+ id = new['_id']
101
+ old = database.get id
102
+
103
+ if old['version'].to_i < new['version'].to_i
104
+ log "Upgrading Design Document #{id} to v#{new['version']}"
105
+ database.delete_doc old
106
+ database.save_doc new
107
+ end
108
+
109
+ rescue RestClient::ResourceNotFound
110
+ log "Creating Design Document #{id} v#{new['version']}"
111
+ database.save_doc new
112
+ end
113
+ end
114
+
115
+ class Error < StandardError; end
116
+ end
@@ -0,0 +1,27 @@
1
+ require 'usage_tracker/log'
2
+
3
+ module UsageTracker
4
+ module Context
5
+ @@key = 'usage_tracker.context'.freeze
6
+ mattr_reader :key
7
+
8
+ # Sets the env +Key+ variable with the provided +data+
9
+ #
10
+ def usage_tracker_context=(data)
11
+ unless request.env[key].blank?
12
+ unless Rails.env.test? && !caller.grep(/test\/functional/).blank?
13
+ UsageTracker.log 'WARNING: overwriting context data!'
14
+ end
15
+ end
16
+
17
+ request.env[key] = data
18
+ end
19
+
20
+ # Shorthand for self.usage_tracker_context = data
21
+ #
22
+ def usage_tracker_context(data)
23
+ self.usage_tracker_context = data
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ require 'logger'
2
+
3
+ module UsageTracker
4
+ class Log
5
+ attr_reader :path
6
+
7
+ [:debug, :info, :warn, :error, :fatal].each do |severity|
8
+ define_method(severity) {|*args| @logger.send(severity, *args)}
9
+ end
10
+
11
+ def initialize
12
+ open
13
+ end
14
+
15
+ def path
16
+ @path ||= if File.directory?('log')
17
+ Pathname.new('.').join('log', 'usage_tracker.log')
18
+ else
19
+ Pathname.new('usage_tracker.log')
20
+ end
21
+ end
22
+
23
+ def open
24
+ @logger = Logger.new(path.to_s)
25
+ @logger.formatter = Logger::Formatter.new
26
+ @logger.info 'Log opened'
27
+
28
+ rescue
29
+ raise Error, "Cannot open log file #{path}"
30
+ end
31
+
32
+ def close
33
+ return unless @logger
34
+
35
+ @logger.info 'Log closed'
36
+ @logger.close
37
+ @logger = nil
38
+ end
39
+
40
+ def rotate
41
+ close
42
+ open
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,94 @@
1
+ require 'timeout'
2
+ require 'usage_tracker'
3
+ require 'usage_tracker/context'
4
+ require 'usage_tracker/railtie' if defined?(Rails)
5
+
6
+ # This middleware extracts some data from the incoming request
7
+ # and sends it to the reactor, that parses and stores it.
8
+ #
9
+ module UsageTracker
10
+ class Middleware
11
+ @@headers = [
12
+ "REMOTE_ADDR",
13
+ "REQUEST_METHOD",
14
+ "PATH_INFO",
15
+ "REQUEST_URI",
16
+ "SERVER_PROTOCOL",
17
+ #"HTTP_VERSION",
18
+ "HTTP_HOST",
19
+ "HTTP_USER_AGENT",
20
+ "HTTP_ACCEPT",
21
+ "HTTP_ACCEPT_LANGUAGE",
22
+ "HTTP_X_FORWARDED_FOR",
23
+ "HTTP_X_FORWARDED_PROTO",
24
+ #"HTTP_ACCEPT_LANGUAGE",
25
+ #"HTTP_ACCEPT_ENCODING",
26
+ #"HTTP_ACCEPT_CHARSET",
27
+ #"HTTP_KEEP_ALIVE",
28
+ "HTTP_CONNECTION",
29
+ #"HTTP_COOKIE",
30
+ #"HTTP_CACHE_CONTROL",
31
+ #"SERVER_NAME",
32
+ #"SERVER_PORT",
33
+ "QUERY_STRING"
34
+ ].freeze
35
+
36
+ @@backend, @@host, @@port = [
37
+ `hostname`.strip,
38
+ UsageTracker.settings.host,
39
+ UsageTracker.settings.port
40
+ ].each(&:freeze)
41
+
42
+ def initialize(app)
43
+ @app = app
44
+ end
45
+
46
+ def call(env)
47
+ req_start = Time.now.to_f
48
+ response = @app.call env
49
+ req_end = Time.now.to_f
50
+
51
+ begin
52
+ data = {
53
+ :user_id => env['rack.session'][:user_id],
54
+ :duration => ((req_end - req_start) * 1000).to_i,
55
+ :backend => @@backend,
56
+ :xhr => env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest',
57
+ :context => env[Context.key],
58
+ :env => {},
59
+ :status => response[0] # response contains [status, headers, body]
60
+ }
61
+
62
+ @@headers.each {|key| data[:env][key.downcase] = env[key] unless env[key].blank?}
63
+
64
+ self.class.track(data.to_json)
65
+
66
+ rescue
67
+ raise unless response # Error in the application, raise it up
68
+
69
+ # Error in usage tracker itself
70
+ UsageTracker.log($!.message)
71
+ UsageTracker.log($!.backtrace.join("\n"))
72
+ end
73
+
74
+ return response
75
+ end
76
+
77
+ class << self
78
+ # Writes the given `data` to the reactor, using the UDP protocol.
79
+ # Times out after 1 second. If a write error occurs, data is lost.
80
+ #
81
+ def track(data)
82
+ Timeout.timeout(1) do
83
+ UDPSocket.open do |sock|
84
+ sock.connect(@@host, @@port.to_i)
85
+ sock.write_nonblock(data << "\n")
86
+ end
87
+ end
88
+
89
+ rescue Timeout::Error, Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
90
+ UsageTracker.log "Cannot track data: #{$!.message}"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,11 @@
1
+ require 'usage_tracker/context'
2
+
3
+ module UsageTracker
4
+ class Railtie < Rails::Railtie
5
+ initializer 'usage_tracker.insert_into_action_controller' do
6
+ ActiveSupport.on_load :action_controller do
7
+ ActionController::Base.instance_eval { include UsageTracker::Context }
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'usage_tracker'
4
+ require 'eventmachine'
5
+ require 'json'
6
+
7
+ module UsageTracker
8
+ module Reactor
9
+ # This method is called upon every data reception
10
+ #
11
+ def receive_data(data)
12
+ doc = parse(data)
13
+ if doc && check(doc)
14
+ store(doc)
15
+ end
16
+ end
17
+
18
+ # Debug hook
19
+ if UsageTracker.env == 'test'
20
+ alias :real_receive_data :receive_data
21
+ def receive_data(data)
22
+ UsageTracker.log.debug "Received #{data.inspect}"
23
+ ret = real_receive_data(data)
24
+ UsageTracker.log.debug ret ? "Stored #{ret}" : 'Failed to store input data'
25
+ end
26
+ end
27
+
28
+ private
29
+ def parse(data)
30
+ JSON(data).tap {|h| h.reject! {|k,v| v.nil?}}
31
+ rescue JSON::ParserError
32
+ UsageTracker.log.error "Tossing out invalid JSON #{data.inspect} (#{$!.message.inspect})"
33
+ return nil
34
+ end
35
+
36
+ def check(doc)
37
+ error =
38
+ if !doc.kind_of?(Hash) then 'invalid'
39
+ elsif doc.empty? then 'empty'
40
+ elsif !(missing = check_keys(doc)).empty?
41
+ "#{missing.join(', ')} missing"
42
+ end
43
+
44
+ if error
45
+ UsageTracker.log.error "Tossing out invalid document #{doc.inspect}: #{error}"
46
+ return nil
47
+ else
48
+ return true
49
+ end
50
+ end
51
+
52
+ def check_keys(doc)
53
+ %w( duration env status ).reject {|k| doc.has_key?(k)}
54
+ end
55
+
56
+ def store(doc)
57
+ tries = 0
58
+
59
+ begin
60
+ doc['_id'] = make_id
61
+ UsageTracker.database.save_doc(doc)
62
+
63
+ rescue RestClient::Conflict => e
64
+ if (tries += 1) < 10
65
+ UsageTracker.log.warn "Retrying to save #{doc.inspect}, try #{tries}"
66
+ retry
67
+ else
68
+ UsageTracker.log.error "Losing '#{doc.inspect}' because of too many conflicts"
69
+ end
70
+
71
+ rescue Encoding::UndefinedConversionError
72
+ UsageTracker.log.error "Losing '#{doc.inspect}' because #$!" # FIXME handle this error properly
73
+ end
74
+ end
75
+
76
+ # Timestamp as _id has the advantage that documents
77
+ # are sorted automatically by CouchDB.
78
+ #
79
+ # Eventual duplication (multiple servers) is (possibly)
80
+ # avoided by adding a random digit at the end.
81
+ #
82
+ def make_id
83
+ Time.now.to_f.to_s.ljust(16, '0') + rand(10).to_s
84
+ end
85
+ end
86
+
87
+ connect!
88
+
89
+ # Setup signal handlers
90
+ #
91
+ # * INT, TERM: graceful exit
92
+ # * USR1 : rotate logs
93
+ #
94
+ def self.sigexit(sig)
95
+ log "Received SIG#{sig}"
96
+ EventMachine.stop_event_loop
97
+ end
98
+
99
+ trap('INT') { sigexit 'INT' }
100
+ trap('TERM') { sigexit 'TERM' }
101
+ trap('USR1') { log.rotate }
102
+
103
+ # Run the Event Loop
104
+ #
105
+ EventMachine.run do
106
+ begin
107
+ host, port = UsageTracker.settings.host, UsageTracker.settings.port
108
+
109
+ unless (1024..65535).include? port.to_i
110
+ raise "Please set a listening port between 1024 and 65535"
111
+ end
112
+
113
+ EventMachine.open_datagram_socket host, port, Reactor
114
+ log "Listening on #{host}:#{port} UDP"
115
+ write_pid!
116
+
117
+ $stderr.puts "Started, logging to #{log.path}"
118
+ [$stdin, $stdout, $stderr].each {|io| io.reopen '/dev/null'}
119
+
120
+ rescue Exception => e
121
+ message = e.message == 'no datagram socket' ? "Unable to bind #{host}:#{port}" : e
122
+ log.fatal message
123
+ $stderr.puts message unless $stderr.closed?
124
+ EventMachine.stop_event_loop
125
+ exit 1
126
+ end
127
+ end
128
+
129
+ # Goodbye!
130
+ #
131
+ log 'Exiting'
132
+ end
@@ -0,0 +1,190 @@
1
+ ################################################################
2
+ # ATTENTION
3
+ # make sure that a event-machine test reactor process is running
4
+ # ruby extras/usage_tracker/reactor.rb test
5
+ #
6
+ ################################################################
7
+
8
+ # this checks the end-point of the usage tracking (arrival in the database) ->
9
+ # consider checking intermediate steps.......
10
+ require 'usage_tracker'
11
+
12
+ module UsageTracker
13
+ class IntegrationTest < ActionController::IntegrationTest
14
+ UsageTracker.connect!
15
+
16
+ context 'a request from a guest' do
17
+ should 'get tracked when successful' do
18
+ assert_difference 'doc_count' do
19
+ get '/'
20
+ assert_response :success
21
+ end
22
+
23
+ doc = last_tracking
24
+ assert_equal '/', doc.env.request_uri
25
+ assert_equal nil, doc.user_id
26
+ assert_equal 200, doc.status
27
+ assert doc.duration > 0
28
+ end
29
+
30
+ should 'get tracked when not found' do
31
+ get '/nonexistant'
32
+ assert_response :not_found
33
+
34
+ doc = last_tracking
35
+ assert_equal '/nonexistant', doc.env.request_uri
36
+ assert_equal 404, doc.status
37
+ end
38
+ end
39
+
40
+ context 'a request from a logged-in user' do
41
+ setup do
42
+ @user = Factory.create(:confirmed_user)
43
+ post '/login', {:email => @user.email, :password => @user.password}, {'HTTPS' => 'on'}
44
+ assert_redirected_to plain_root_url
45
+ end
46
+
47
+ should 'get tracked when successful' do
48
+ assert_difference 'doc_count' do
49
+ get '/_'
50
+ assert_response :success
51
+ end
52
+
53
+ doc = last_tracking
54
+
55
+ assert_equal '/_', doc.env.request_uri
56
+ assert_equal @user.id, doc.user_id
57
+ assert_equal 200, doc.status
58
+ end
59
+
60
+ should 'get tracked when not found' do
61
+ get '/nonexistant'
62
+ assert_response :not_found
63
+
64
+ doc = last_tracking
65
+
66
+ assert_equal '/nonexistant', doc.env.request_uri
67
+ assert_equal @user.id, doc.user_id
68
+ assert_equal 404, doc.status
69
+ assert_equal false, doc.xhr
70
+ end
71
+
72
+ should 'get tracked when failed' do
73
+ xhr :get, '/projects/1/error', {}, {'HTTPS' => 'on'}
74
+ assert_response :internal_server_error
75
+
76
+ doc = last_tracking
77
+
78
+ assert_equal '/projects/1/error', doc.env.request_uri
79
+ assert_equal @user.id, doc.user_id
80
+ assert_equal 500, doc.status
81
+ assert_equal true, doc.xhr
82
+ assert_equal `hostname`.strip, doc.backend
83
+ end
84
+ end
85
+
86
+ fast_context "a search request" do
87
+ setup do
88
+ @res, @users, @assets =
89
+ mock_search_results_for(Array.new(2) { Factory.create(:res) }),
90
+ mock_search_results_for(Array.new(3) { Factory.create(:user) }),
91
+ mock_search_results_for(Network::AssetsController::ContentModels.map {|name|
92
+ Array.new(2) { Factory(name.underscore.to_sym).reload.asset } }.flatten)
93
+
94
+ Res.stubs(:search).returns(@res)
95
+ User.stubs(:search).returns(@users)
96
+ NetworkAsset.stubs(:search).returns(@assets)
97
+ end
98
+
99
+ should "be tracked with results" do
100
+ get '/search/e'
101
+ assert_response :success
102
+
103
+ doc = last_tracking
104
+ assert !doc.context.blank?
105
+
106
+ assert_equal 'e', doc.context.query
107
+ assert_equal [], doc.context.tags
108
+ assert_equal nil, doc.context.cat
109
+
110
+ assert_equal @res.map(&:id), doc.context.results.res
111
+ assert_equal @users.map(&:id), doc.context.results.users
112
+ assert_equal @assets.map(&:id), doc.context.results.assets
113
+ end
114
+
115
+ should "be tracked with tags" do
116
+ get '/search', :tag => 'a,b,c'
117
+ assert_response :success
118
+
119
+ doc = last_tracking
120
+ assert !doc.context.blank?
121
+
122
+ assert_equal '', doc.context.query
123
+ assert_equal %w(a b c), doc.context.tags
124
+ assert_equal nil, doc.context.cat
125
+ end
126
+
127
+ should "be tracked with tags and query" do
128
+ get '/search/antani', :tag => 'd,e,f'
129
+ assert_response :success
130
+
131
+ doc = last_tracking
132
+ assert !doc.context.blank?
133
+
134
+ assert_equal 'antani', doc.context.query
135
+ assert_equal %w(d e f), doc.context.tags
136
+ assert_equal nil, doc.context.cat
137
+ end
138
+
139
+ should "be tracked with category" do
140
+ cat = Factory.create(:res_category)
141
+ get '/search', :cat => cat.shortcut
142
+ assert_response :success
143
+
144
+ doc = last_tracking
145
+ assert !doc.context.blank?
146
+
147
+ assert_equal '', doc.context.query
148
+ assert_equal [], doc.context.tags
149
+ assert_equal cat.id, doc.context.cat
150
+ end
151
+
152
+ should "be tracked with category and query" do
153
+ cat = Factory.create(:res_category)
154
+ get '/search/res/asd', :cat => cat.shortcut
155
+ assert_response :success
156
+
157
+ doc = last_tracking
158
+ assert !doc.context.blank?
159
+
160
+ assert_equal 'asd', doc.context.query
161
+ assert_equal [], doc.context.tags
162
+ assert_equal cat.id, doc.context.cat
163
+ end
164
+ end
165
+
166
+ context "the middleware" do
167
+ should "not wait for more than a second before aborting" do
168
+ UDPSocket.expects(:open).once.yields(Class.new do
169
+ def write_nonblock(*args); sleep 0.7 end
170
+ def connect(*args) ; sleep 0.7 end
171
+ end.new)
172
+
173
+ assert_no_difference 'doc_count' do
174
+ get '/_'
175
+ assert_response :success
176
+ end
177
+ end
178
+ end
179
+
180
+ def last_tracking
181
+ sleep 0.3
182
+ UsageTracker.database.view('basic/by_timestamp', :descending => true, :limit => 1).rows.first.value
183
+ end
184
+
185
+ def doc_count
186
+ sleep 0.3
187
+ UsageTracker.database.info['doc_count']
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,60 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{panmind-usage-tracker}
8
+ s.version = "0.4.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Marcello Barnaba", "Christian Wo\u0308rner"]
12
+ s.date = %q{2010-12-03}
13
+ s.default_executable = %q{usage_tracker}
14
+ s.description = %q{This software implements a Rails 3 Middleware and an EventMachine reactor to store into CouchDB the results of HTTP request processing}
15
+ s.email = %q{vjt@openssl.it}
16
+ s.executables = ["usage_tracker"]
17
+ s.extra_rdoc_files = [
18
+ "README.md"
19
+ ]
20
+ s.files = [
21
+ "README.md",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "bin/usage_tracker",
25
+ "config/usage_tracker.yml.sample",
26
+ "config/usage_tracker_upstart.conf",
27
+ "config/views.yml",
28
+ "lib/usage_tracker.rb",
29
+ "lib/usage_tracker/context.rb",
30
+ "lib/usage_tracker/log.rb",
31
+ "lib/usage_tracker/middleware.rb",
32
+ "lib/usage_tracker/railtie.rb",
33
+ "lib/usage_tracker/reactor.rb",
34
+ "middleware_test.rb"
35
+ ]
36
+ s.homepage = %q{http://github.com/Panmind/usage_tracker}
37
+ s.require_paths = ["lib"]
38
+ s.rubygems_version = %q{1.3.7}
39
+ s.summary = %q{Write your application request logs in CouchDB}
40
+
41
+ if s.respond_to? :specification_version then
42
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
43
+ s.specification_version = 3
44
+
45
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
46
+ s.add_runtime_dependency(%q<rails>, ["~> 3.0"])
47
+ s.add_runtime_dependency(%q<eventmachine>, [">= 0"])
48
+ s.add_runtime_dependency(%q<couchrest>, [">= 0"])
49
+ else
50
+ s.add_dependency(%q<rails>, ["~> 3.0"])
51
+ s.add_dependency(%q<eventmachine>, [">= 0"])
52
+ s.add_dependency(%q<couchrest>, [">= 0"])
53
+ end
54
+ else
55
+ s.add_dependency(%q<rails>, ["~> 3.0"])
56
+ s.add_dependency(%q<eventmachine>, [">= 0"])
57
+ s.add_dependency(%q<couchrest>, [">= 0"])
58
+ end
59
+ end
60
+
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: panmind-usage-tracker
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 4
8
+ - 0
9
+ version: 0.4.0
10
+ platform: ruby
11
+ authors:
12
+ - Marcello Barnaba
13
+ - "Christian Wo\xCC\x88rner"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-03 00:00:00 +01:00
19
+ default_executable: usage_tracker
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rails
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ segments:
30
+ - 3
31
+ - 0
32
+ version: "3.0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: eventmachine
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: couchrest
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ type: :runtime
60
+ version_requirements: *id003
61
+ description: This software implements a Rails 3 Middleware and an EventMachine reactor to store into CouchDB the results of HTTP request processing
62
+ email: vjt@openssl.it
63
+ executables:
64
+ - usage_tracker
65
+ extensions: []
66
+
67
+ extra_rdoc_files:
68
+ - README.md
69
+ files:
70
+ - README.md
71
+ - Rakefile
72
+ - VERSION
73
+ - bin/usage_tracker
74
+ - config/usage_tracker.yml.sample
75
+ - config/usage_tracker_upstart.conf
76
+ - config/views.yml
77
+ - lib/usage_tracker.rb
78
+ - lib/usage_tracker/context.rb
79
+ - lib/usage_tracker/log.rb
80
+ - lib/usage_tracker/middleware.rb
81
+ - lib/usage_tracker/railtie.rb
82
+ - lib/usage_tracker/reactor.rb
83
+ - middleware_test.rb
84
+ - panmind-usage-tracker.gemspec
85
+ has_rdoc: true
86
+ homepage: http://github.com/Panmind/usage_tracker
87
+ licenses: []
88
+
89
+ post_install_message:
90
+ rdoc_options: []
91
+
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ segments:
108
+ - 0
109
+ version: "0"
110
+ requirements: []
111
+
112
+ rubyforge_project:
113
+ rubygems_version: 1.3.7
114
+ signing_key:
115
+ specification_version: 3
116
+ summary: Write your application request logs in CouchDB
117
+ test_files: []
118
+