panmind-usage-tracker 0.4.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
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