panmind-usage-tracker 0.4.0 → 1.0.1

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/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source :rubygems
2
+
3
+ gem 'bson_ext'
4
+ gem 'mongo'
5
+ gem 'couchrest'
6
+ gem 'activesupport', '~>3.0.3'
7
+ gem 'json'
8
+ gem 'eventmachine'
9
+ gem 'freegenie-em-spec', '0.2.3'
10
+ gem 'rspec'
11
+
12
+
13
+
14
+
15
+
data/README.md CHANGED
@@ -5,54 +5,48 @@ What is it?
5
5
  ===========
6
6
 
7
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
-
8
+ 2. An `EventMachine` daemon that opens an UDP socket and sends out received data to a database
11
9
 
12
10
  Does it work?
13
11
  =============
14
12
 
15
- Yes, but the release is still incomplete, because currently tests are too
16
- tied to Panmind logic.
17
-
13
+ Yes, we are using it in production.
18
14
  If you can help in complete the test suite, it is much appreciated :-).
19
15
 
20
16
  Deploying
21
17
  =========
22
18
 
23
- * Add the usage_tracker gem to your Gemfile and require the middleware
19
+ * Add the usage\_tracker gem to your Gemfile and require the middleware
24
20
 
25
- gem 'usage_tracker', :require => 'usage_tracker/middleware'
21
+ gem 'usage\_tracker', :require => 'usage\_tracker/middleware'
26
22
 
27
- * Add the Middleware to your application:
23
+ * Configure the middleware and plug it into your application:
28
24
 
25
+ UsageTracker::Middleware.config(:host => '192.168.1.20', :port => '8840')
29
26
  Your::Application.config.middleware.use UsageTracker::Middleware
30
27
 
31
- * The daemon can be started manually with the following command, inside a Rails.root:
28
+ * Install the gem on the target machine and run it with this command:
32
29
 
33
30
  $ usage_tracker [environment]
34
31
 
32
+ If you run it into a Rails.root it will log and write pids in canonical dirs.
33
+
35
34
  `environment` is optional and will default to "development" if no command line
36
- option nor the RAILS_ENV environment variable are set.
35
+ option nor the RAILS\_ENV environment variable are set.
37
36
 
38
37
  or can be put under Upstart using the provided configuration file located in
39
38
  `config/usage_tracker_upstart.conf`. Check it out and modify it to suit your needs.
40
39
 
41
- The daemon logs to `log/usage_tracker.log` and rotates its logs when receives
42
- the USR1 signal.
40
+ The daemon logs to `usage_tracker.log` if the log directory exists and rotates its
41
+ logs when receives the USR1 signal.
43
42
 
44
- * The daemon writes its pid into tmp/pids/usage_tracker.pid
43
+ * The daemon writes its pid into usage\_tracker.pid
45
44
 
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.
45
+ * The daemon can be configured to work with couchdb or mongodb adapter. Look at the
46
+ sample configuration file for hints.
53
47
 
54
48
  * 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
49
+ address, it will print a diagnostig message to STDERR, log to usage\_tracker.log
56
50
  and exit with status of 1.
57
51
 
58
52
  * The daemon exits gracefully if it receives the INT or the TERM signals.
@@ -60,6 +54,15 @@ Deploying
60
54
  Testing
61
55
  =======
62
56
 
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! :-)
57
+ Our will is to test the Evented code in isolation using rspec and em-rspec gem.
58
+ Tests are still incomplete. You can start the running:
59
+
60
+ > bundle exec rspec spec
61
+
62
+ All required gems for testing should be installed running:
63
+
64
+ > bundle install
65
+
66
+ About the middleware, it's probably better for you to test that in your own
67
+ app's integration test suite.
68
+
data/Rakefile CHANGED
@@ -8,18 +8,21 @@ begin
8
8
 
9
9
  Jeweler::Tasks.new do |gemspec|
10
10
  gemspec.name = 'panmind-usage-tracker'
11
- gemspec.summary = 'Write your application request logs in CouchDB'
11
+ gemspec.summary = 'Write your application request logs on CouchDB or MongoDB'
12
12
  gemspec.description = 'This software implements a Rails 3 Middleware and ' \
13
- 'an EventMachine reactor to store into CouchDB the ' \
13
+ 'an EventMachine reactor to store into a database the ' \
14
14
  'results of HTTP request processing'
15
15
 
16
- gemspec.authors = ['Marcello Barnaba', 'Christian Wörner']
16
+ gemspec.authors = ['Marcello Barnaba', 'Christian Wörner', 'Fabrizio Regini']
17
17
  gemspec.homepage = 'http://github.com/Panmind/usage_tracker'
18
- gemspec.email = 'vjt@openssl.it'
18
+ gemspec.email = 'info@panmind.org'
19
19
 
20
20
  gemspec.add_dependency('rails', '~> 3.0')
21
21
  gemspec.add_dependency('eventmachine')
22
22
  gemspec.add_dependency('couchrest')
23
+ gemspec.add_dependency('mongo')
24
+ gemspec.add_dependency('bson')
25
+ gemspec.add_dependency('bson_ext')
23
26
  end
24
27
  rescue LoadError
25
28
  puts 'Jeweler not available. Install it with: gem install jeweler'
@@ -30,7 +33,7 @@ Rake::RDocTask.new do |rdoc|
30
33
  rdoc.rdoc_files.add %w( README.md lib/**/*.rb )
31
34
 
32
35
  rdoc.main = 'README.md'
33
- rdoc.title = 'Rails Application Usage Tracker on CouchDB'
36
+ rdoc.title = 'Rails Application Usage Tracker'
34
37
  end
35
38
 
36
39
  desc 'Will someone help write tests?'
@@ -39,4 +42,3 @@ task :default do
39
42
  puts 'Can you help in writing tests? Please do :-)'
40
43
  puts
41
44
  end
42
-
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.0
1
+ 1.0.1
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
2
  $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
3
 
4
- require 'usage_tracker/reactor'
4
+ require 'usage_tracker/runner'
@@ -1,6 +1,20 @@
1
1
  defaults: &defaults
2
- couchdb: "http://admin:suxsux@127.0.0.1:5984/usage_tracker"
2
+ # Available adapters: 'couchdb', 'mongodb'
3
+ # adapter: 'couchdb'
4
+
5
+ # CouchDB connect settings
6
+ # database: "http://127.0.0.1:5984/usage_tracker"
7
+
8
+ # MongoDB connect settings
9
+ adapter: 'mongodb'
10
+ database:
11
+ name: "usage_tracker"
12
+ host: "127.0.0.1"
13
+ port: 27017
14
+ collection: "data"
15
+
3
16
  listen: "127.0.0.1:5985"
17
+ log_level: 'debug'
4
18
 
5
19
  development:
6
20
  <<: *defaults
@@ -10,4 +24,7 @@ production:
10
24
 
11
25
  test:
12
26
  <<: *defaults
13
- couchdb: "http://admin:suxsux@127.0.0.1:5984/usage_tracker_test"
27
+
28
+ spec:
29
+ <<: *defaults
30
+
@@ -6,6 +6,7 @@ require 'ostruct'
6
6
  require 'couchrest'
7
7
  require 'active_support/core_ext/object/blank'
8
8
  require 'usage_tracker/log'
9
+ require 'usage_tracker/adapter'
9
10
 
10
11
  module UsageTracker
11
12
  class << self
@@ -14,11 +15,6 @@ module UsageTracker
14
15
  @env ||= ENV['RAILS_ENV'] || ARGV[0] || 'development'
15
16
  end
16
17
 
17
- @@defaults = {
18
- 'couchdb' => 'http://localhost:5984/usage_tracker',
19
- 'listen' => '127.0.0.1:5985'
20
- }
21
-
22
18
  # Memoizes settings from the ./config/usage_tracker.yml file,
23
19
  # relative from __FILE__ and searches for the "usage_tracker"
24
20
  # configuration block. Raises RuntimeError if it cannot find
@@ -32,10 +28,9 @@ module UsageTracker
32
28
  settings = YAML.load(rc_file.read)[env] if rc_file.exist?
33
29
 
34
30
  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"
31
+ raise "Configuration missing for #{env}"
32
+ elsif settings.values_at(*%w(adapter database listen)).any?(&:blank?)
33
+ raise "Incomplete configuration: please set the 'adapter', 'database' and 'listen' keys"
39
34
  end
40
35
 
41
36
  host, port = settings.delete('listen').split(':')
@@ -46,6 +41,9 @@ module UsageTracker
46
41
 
47
42
  settings['host'], settings['port'] = host, port.to_i
48
43
 
44
+ settings['log_level'] ||= :warn
45
+ log.level = settings['log_level']
46
+
49
47
  OpenStruct.new settings
50
48
  end
51
49
  end
@@ -54,6 +52,10 @@ module UsageTracker
54
52
  @database or raise "Not connected to the database"
55
53
  end
56
54
 
55
+ def adapter
56
+ @adapter or raise "Not connected to the database adapter"
57
+ end
58
+
57
59
  # Connects to the configured CouchDB and memoizes the
58
60
  # CouchRest::Database connection into an instance variable
59
61
  # and calls +load_views!+
@@ -61,17 +63,23 @@ module UsageTracker
61
63
  # Raises RuntimeError if the connection could not be established
62
64
  #
63
65
  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}"
66
+ @adapter = Adapter::new settings
67
+ @database = @adapter.database
73
68
  end
74
69
 
70
+ # Code to run inside EventMachine
71
+ def run!
72
+ host, port = UsageTracker.settings.host, UsageTracker.settings.port
73
+
74
+ unless (1024..65535).include? port.to_i
75
+ raise "Please set a listening port between 1024 and 65535"
76
+ end
77
+
78
+ EventMachine.open_datagram_socket host, port, Reactor
79
+ log "Listening on #{host}:#{port} UDP"
80
+ write_pid!
81
+ end
82
+
75
83
  def write_pid!(pid = $$)
76
84
  dir = Pathname.new('.').join('tmp', 'pids')
77
85
  dir = Pathname.new(Dir.tmpdir) unless dir.directory?
@@ -87,29 +95,6 @@ module UsageTracker
87
95
  log.error message
88
96
  Kernel.raise Error, message
89
97
  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
98
  end
114
99
 
115
100
  class Error < StandardError; end
@@ -0,0 +1,19 @@
1
+ require 'usage_tracker/adapters/couchdb'
2
+ require 'usage_tracker/adapters/mongodb'
3
+
4
+ module UsageTracker
5
+ class Adapter
6
+ def self::new(settings)
7
+ klass =
8
+ case settings.adapter
9
+ when 'couchdb'
10
+ Adapters::Couchdb
11
+ when 'redis'
12
+ Adapters::Redis
13
+ when 'mongodb'
14
+ Adapters::Mongodb
15
+ end
16
+ klass::new(settings)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ require 'pathname'
2
+ require 'couchrest'
3
+ require 'yaml'
4
+
5
+ module UsageTracker
6
+ module Adapters
7
+ class Couchdb
8
+ attr_accessor :database
9
+ def initialize (settings)
10
+ @database =
11
+ CouchRest.database!(settings.database).tap do |db|
12
+ db.info
13
+ end
14
+ rescue Errno::ECONNREFUSED, RestClient::Exception => e
15
+ raise "Unable to connect to database #{settings.database}: #{e.message}"
16
+ end
17
+
18
+ def save_doc (doc)
19
+ doc['_id'] = make_id if doc['_id'].nil?
20
+ @database.save_doc(doc)
21
+ end
22
+
23
+ # Timestamp as _id has the advantage that documents
24
+ # are sorted automatically by CouchDB.
25
+ #
26
+ # Eventual duplication (multiple servers) is (possibly)
27
+ # avoided by adding a random digit at the end.
28
+ #
29
+ def make_id
30
+ Time.now.to_f.to_s.ljust(16, '0') + rand(10).to_s
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,23 @@
1
+ require 'pathname'
2
+ require 'mongo'
3
+ require 'yaml'
4
+
5
+ module UsageTracker
6
+ module Adapters
7
+ class Mongodb
8
+ attr_accessor :database
9
+ def initialize (settings)
10
+ @database =
11
+ db = Mongo::Connection.new(settings.database['host'], settings.database['port']).db(settings.database['name'])
12
+ @collection = db[settings.database['collection']]
13
+ db
14
+ rescue Errno::ECONNREFUSED, Mongo::ConnectionError => e
15
+ raise "Unable to connect to database #{settings.database['name']} with #{settings.adapter} adapter: #{e.message}"
16
+ end
17
+
18
+ def save_doc(doc)
19
+ @collection.insert(doc)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -2,6 +2,15 @@ require 'logger'
2
2
 
3
3
  module UsageTracker
4
4
  class Log
5
+
6
+ Levels = {
7
+ :fatal => Logger::FATAL,
8
+ :erorr => Logger::ERROR,
9
+ :warn => Logger::WARN,
10
+ :info => Logger::INFO,
11
+ :debug => Logger::DEBUG
12
+ }
13
+
5
14
  attr_reader :path
6
15
 
7
16
  [:debug, :info, :warn, :error, :fatal].each do |severity|
@@ -41,5 +50,11 @@ module UsageTracker
41
50
  close
42
51
  open
43
52
  end
53
+
54
+ def level=(level)
55
+ level = level.to_sym
56
+ raise "Invalid log level" unless Levels.keys.include?(level)
57
+ @logger.level = Levels[level]
58
+ end
44
59
  end
45
60
  end
@@ -1,15 +1,22 @@
1
1
  require 'timeout'
2
- require 'usage_tracker'
2
+ require 'usage_tracker/log'
3
3
  require 'usage_tracker/context'
4
4
  require 'usage_tracker/railtie' if defined?(Rails)
5
5
 
6
6
  # This middleware extracts some data from the incoming request
7
7
  # and sends it to the reactor, that parses and stores it.
8
8
  #
9
+
9
10
  module UsageTracker
10
11
  class Middleware
12
+
13
+ @@host = 'localhost'
14
+ @@port = 5985
15
+ @@backend = `hostname`.strip
16
+ @@logger = UsageTracker::Log.new
17
+
11
18
  @@headers = [
12
- "REMOTE_ADDR",
19
+ # "REMOTE_ADDR",
13
20
  "REQUEST_METHOD",
14
21
  "PATH_INFO",
15
22
  "REQUEST_URI",
@@ -33,14 +40,11 @@ module UsageTracker
33
40
  "QUERY_STRING"
34
41
  ].freeze
35
42
 
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
43
+ def initialize(app, options={})
44
+ @@host = options[:host] if options.keys.include?(:host)
45
+ @@port = options[:port] if options.keys.include?(:port)
46
+ @@backend = options[:backend] if options.keys.include?(:backend)
47
+ @app = app
44
48
  end
45
49
 
46
50
  def call(env)
@@ -50,13 +54,14 @@ module UsageTracker
50
54
 
51
55
  begin
52
56
  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]
57
+ :user_id => env['rack.session'][:user_id],
58
+ :remote_ip => env['action_dispatch.remote_ip'].to_s,
59
+ :duration => ((req_end - req_start) * 1000).to_i,
60
+ :backend => @@backend,
61
+ :xhr => env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest',
62
+ :context => env[Context.key],
63
+ :env => {},
64
+ :status => response[0] # response contains [status, headers, body]
60
65
  }
61
66
 
62
67
  @@headers.each {|key| data[:env][key.downcase] = env[key] unless env[key].blank?}
@@ -65,21 +70,30 @@ module UsageTracker
65
70
 
66
71
  rescue
67
72
  raise unless response # Error in the application, raise it up
68
-
69
73
  # Error in usage tracker itself
70
- UsageTracker.log($!.message)
71
- UsageTracker.log($!.backtrace.join("\n"))
74
+ @@logger.error($!.message)
75
+ @@logger.error($!.backtrace.join("\n"))
76
+
72
77
  end
73
78
 
74
79
  return response
75
80
  end
76
81
 
77
82
  class << self
83
+
84
+ def development?
85
+ defined?(Rails) && Rails.env.development?
86
+ end
87
+
78
88
  # Writes the given `data` to the reactor, using the UDP protocol.
79
89
  # Times out after 1 second. If a write error occurs, data is lost.
80
90
  #
91
+
81
92
  def track(data)
82
93
  Timeout.timeout(1) do
94
+
95
+ @@logger.debug("Sending to #{@@host}:#{@@port} : #{data.to_json}") if development?
96
+
83
97
  UDPSocket.open do |sock|
84
98
  sock.connect(@@host, @@port.to_i)
85
99
  sock.write_nonblock(data << "\n")
@@ -87,7 +101,7 @@ module UsageTracker
87
101
  end
88
102
 
89
103
  rescue Timeout::Error, Errno::EWOULDBLOCK, Errno::EAGAIN, Errno::EINTR
90
- UsageTracker.log "Cannot track data: #{$!.message}"
104
+ @@logger.error "Cannot track data: #{$!.message}"
91
105
  end
92
106
  end
93
107
  end