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 +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
|