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 +65 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/bin/usage_tracker +4 -0
- data/config/usage_tracker.yml.sample +13 -0
- data/config/usage_tracker_upstart.conf +15 -0
- data/config/views.yml +139 -0
- data/lib/usage_tracker.rb +116 -0
- data/lib/usage_tracker/context.rb +27 -0
- data/lib/usage_tracker/log.rb +45 -0
- data/lib/usage_tracker/middleware.rb +94 -0
- data/lib/usage_tracker/railtie.rb +11 -0
- data/lib/usage_tracker/reactor.rb +132 -0
- data/middleware_test.rb +190 -0
- data/panmind-usage-tracker.gemspec +60 -0
- metadata +118 -0
    
        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,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
         | 
    
        data/middleware_test.rb
    ADDED
    
    | @@ -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 | 
            +
             |