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 +15 -0
- data/README.md +28 -25
- data/Rakefile +8 -6
- data/VERSION +1 -1
- data/bin/usage_tracker +1 -1
- data/config/usage_tracker.yml.sample +19 -2
- data/lib/usage_tracker.rb +26 -41
- data/lib/usage_tracker/adapter.rb +19 -0
- data/lib/usage_tracker/adapters/couchdb.rb +35 -0
- data/lib/usage_tracker/adapters/mongodb.rb +23 -0
- data/lib/usage_tracker/log.rb +15 -0
- data/lib/usage_tracker/middleware.rb +35 -21
- data/lib/usage_tracker/reactor.rb +3 -71
- data/lib/usage_tracker/runner.rb +54 -0
- data/panmind-usage-tracker.gemspec +48 -10
- data/spec/spec_helper.rb +6 -0
- data/spec/usage_tracker_spec.rb +39 -0
- metadata +143 -31
- data/config/views.yml +0 -139
- data/middleware_test.rb +0 -190
data/Gemfile
ADDED
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
|
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,
|
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
|
19
|
+
* Add the usage\_tracker gem to your Gemfile and require the middleware
|
24
20
|
|
25
|
-
gem '
|
21
|
+
gem 'usage\_tracker', :require => 'usage\_tracker/middleware'
|
26
22
|
|
27
|
-
*
|
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
|
-
*
|
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
|
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 `
|
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
|
43
|
+
* The daemon writes its pid into usage\_tracker.pid
|
45
44
|
|
46
|
-
* The daemon
|
47
|
-
|
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
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
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
|
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 = '
|
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
|
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.
|
1
|
+
1.0.1
|
data/bin/usage_tracker
CHANGED
@@ -1,6 +1,20 @@
|
|
1
1
|
defaults: &defaults
|
2
|
-
|
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
|
-
|
27
|
+
|
28
|
+
spec:
|
29
|
+
<<: *defaults
|
30
|
+
|
data/lib/usage_tracker.rb
CHANGED
@@ -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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
@
|
65
|
-
|
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
|
data/lib/usage_tracker/log.rb
CHANGED
@@ -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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
54
|
-
:
|
55
|
-
:
|
56
|
-
:
|
57
|
-
:
|
58
|
-
:
|
59
|
-
:
|
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
|
-
|
71
|
-
|
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
|
-
|
104
|
+
@@logger.error "Cannot track data: #{$!.message}"
|
91
105
|
end
|
92
106
|
end
|
93
107
|
end
|