panmind-usage-tracker 0.4.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.
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
+