panmind-usage-tracker-server 1.1.5

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.
@@ -0,0 +1,39 @@
1
+ Panmind Usage Tracker Server
2
+ ----------------------------
3
+
4
+ What is it?
5
+ ===========
6
+
7
+ An `EventMachine`-based server that opens an UDP socket and sends out received data to a database
8
+
9
+ Does it work?
10
+ =============
11
+
12
+ Yes, we are using it in production.
13
+
14
+ Deploying
15
+ =========
16
+
17
+ * Install the gem on the target machine and run it with this command:
18
+
19
+ $ usage_tracker [environment]
20
+
21
+ If you run it into a Rails.root it will log and write pids in canonical dirs.
22
+
23
+ or can be put under Upstart using the provided configuration file located in
24
+ `config/usage_tracker_server_upstart.conf`. Check it out and modify it to suit your needs.
25
+
26
+ The daemon logs to `usage_tracker_server.log` if the log directory exists and rotates its
27
+ logs when receives the USR1 signal.
28
+
29
+ * The daemon writes its pid into usage\_tracker\_server.pid
30
+
31
+ * The daemon can be configured to work with couchdb or mongodb adapter. Look at the
32
+ sample configuration file for hints.
33
+
34
+ * If the daemon cannot start, e.g. because of unavailable database or listening
35
+ address, it will print a diagnostic message to STDERR, log to usage\_tracker.log
36
+ and exit with status of 1.
37
+
38
+ * The daemon exits gracefully if it receives the INT or the TERM signals.
39
+
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+ #
3
+ require 'rake'
4
+ require 'rdoc/task'
5
+
6
+ begin
7
+ require 'jeweler'
8
+
9
+ Jeweler::Tasks.new do |gemspec|
10
+ gemspec.name = 'panmind-usage-tracker-server'
11
+ gemspec.summary = 'server component for usage-tracker-middleware (Rack)'
12
+ gemspec.description = 'this software implements an EventMachine reactor ' \
13
+ 'which stores json-objects received via UDP in a database.'
14
+
15
+ gemspec.authors = ['Christian Woerner', 'Fabrizio Regini', 'Marcello Barnaba']
16
+ gemspec.homepage = 'http://github.com/Panmind/usage_tracker_server'
17
+ gemspec.email = 'info@panmind.com'
18
+
19
+ gemspec.add_dependency('eventmachine')
20
+ gemspec.add_dependency('couchrest')
21
+ gemspec.add_dependency('mongo')
22
+ gemspec.add_dependency('bson')
23
+ gemspec.add_dependency('bson_ext', '=1.6.0')
24
+ end
25
+ rescue LoadError
26
+ puts 'Jeweler not available. Install it with: gem install jeweler'
27
+ end
28
+
29
+ desc 'Generate the rdoc'
30
+ Rake::RDocTask.new do |rdoc|
31
+ rdoc.rdoc_files.add %w( README.md lib/**/*.rb )
32
+
33
+ rdoc.main = 'README.md'
34
+ rdoc.title = 'Usage Tracker Server'
35
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.1.5
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'usage_tracker_server/runner'
@@ -0,0 +1,18 @@
1
+ # Available adapters: 'couchdb', 'mongodb'
2
+ # adapter: 'couchdb'
3
+
4
+ # CouchDB connect settings
5
+ # database: "http://127.0.0.1:5984/usage_tracker"
6
+
7
+ # MongoDB connect settings
8
+ adapter: 'mongodb'
9
+ database:
10
+ name: "usage_tracker"
11
+ host: "127.0.0.1"
12
+ port: 27017
13
+ collection: "data"
14
+
15
+ listen: "127.0.0.1:5985"
16
+ log_level: 'warn'
17
+
18
+
@@ -0,0 +1,14 @@
1
+ description "Panmind Usage Tracker Server 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 and the directory
13
+ #
14
+ exec sudo -i -H -u panmind bash -c 'echo; cd panmind/deploy; exec usage_tracker_server'
@@ -0,0 +1,94 @@
1
+ require 'rubygems'
2
+ require 'erb'
3
+ require 'yaml'
4
+ require 'pathname'
5
+ require 'ostruct'
6
+ require 'couchrest'
7
+ require 'usage_tracker_server/log'
8
+ require 'usage_tracker_server/adapter'
9
+
10
+ module UsageTrackerServer
11
+ class << self
12
+ # Memoizes settings from the ./config/usage_tracker_server.yml file,
13
+ # relative from __FILE__ and searches for the "usage_tracker_server"
14
+ # configuration block. Raises RuntimeError if it cannot find
15
+ # the configuration.
16
+ #
17
+ def settings
18
+ @settings ||= begin
19
+
20
+ rc_file = Pathname.new('.').join('config', 'usage_tracker_server.yml')
21
+ settings = YAML.load(rc_file.read) if rc_file.exist?
22
+
23
+ if settings == nil
24
+ raise "Configuration missing."
25
+ elsif settings.values_at(*%w(adapter database listen)).any?(&:nil?)
26
+ raise "Incomplete configuration: please set the 'adapter', 'database' and 'listen' keys"
27
+ end
28
+
29
+ host, port = settings.delete('listen').split(':')
30
+
31
+ if [host, port].any? {|x| x.strip.empty?}
32
+ raise "Please specify where to listen as host:port"
33
+ end
34
+
35
+ settings['host'], settings['port'] = host, port.to_i
36
+
37
+ settings['log_level'] ||= :warn
38
+ log.level = settings['log_level']
39
+
40
+ OpenStruct.new settings
41
+ end
42
+ end
43
+
44
+ def database
45
+ @database or raise "Not connected to the database"
46
+ end
47
+
48
+ def adapter
49
+ @adapter or raise "Not connected to the database adapter"
50
+ end
51
+
52
+ # Connects to the configured CouchDB and memoizes the
53
+ # CouchRest::Database connection into an instance variable
54
+ # and calls +load_views!+
55
+ #
56
+ # Raises RuntimeError if the connection could not be established
57
+ #
58
+ def connect!
59
+ @adapter = Adapter::new settings
60
+ @database = @adapter.database
61
+ end
62
+
63
+ # Code to run inside EventMachine
64
+ def run!
65
+ host, port = UsageTrackerServer.settings.host, UsageTrackerServer.settings.port
66
+
67
+ unless (1024..65535).include? port.to_i
68
+ raise "Please set a listening port between 1024 and 65535"
69
+ end
70
+
71
+ EventMachine.open_datagram_socket host, port, Reactor
72
+ log "Listening on #{host}:#{port} UDP"
73
+ write_pid!
74
+ end
75
+
76
+ def write_pid!(pid = $$)
77
+ dir = Pathname.new('.').join('tmp', 'pids')
78
+ dir = Pathname.new(Dir.tmpdir) unless dir.directory?
79
+ dir.join('usage_tracker_server.pid').open('w+') {|f| f.write(pid)}
80
+ end
81
+
82
+ def log(message = nil)
83
+ @log ||= Log.new
84
+ message ? @log.info(message) : @log
85
+ end
86
+
87
+ def raise(message)
88
+ log.error message
89
+ Kernel.raise Error, message
90
+ end
91
+ end
92
+
93
+ class Error < StandardError; end
94
+ end
@@ -0,0 +1,17 @@
1
+ require 'usage_tracker_server/adapters/couchdb'
2
+ require 'usage_tracker_server/adapters/mongodb'
3
+
4
+ module UsageTrackerServer
5
+ class Adapter
6
+ def self::new(settings)
7
+ klass =
8
+ case settings.adapter
9
+ when 'couchdb'
10
+ Adapters::Couchdb
11
+ when 'mongodb'
12
+ Adapters::Mongodb
13
+ end
14
+ klass::new(settings)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ require 'pathname'
2
+ require 'couchrest'
3
+ require 'yaml'
4
+
5
+ module UsageTrackerServer
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,28 @@
1
+ require 'pathname'
2
+ require 'mongo'
3
+ require 'yaml'
4
+
5
+ module UsageTrackerServer
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
+
13
+ if settings.database['username'] || settings.database['password']
14
+ db.authenticate(settings.database['username'], settings.database['password'])
15
+ end
16
+
17
+ @collection = db[settings.database['collection']]
18
+ db
19
+ rescue Errno::ECONNREFUSED, Mongo::ConnectionError => e
20
+ raise "Unable to connect to database #{settings.database['name']} with #{settings.adapter} adapter: #{e.message}"
21
+ end
22
+
23
+ def save_doc(doc)
24
+ @collection.insert(doc)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ require 'logger'
2
+
3
+ module UsageTrackerServer
4
+ class Log
5
+
6
+ Levels = {
7
+ :fatal => Logger::FATAL,
8
+ :error => Logger::ERROR,
9
+ :warn => Logger::WARN,
10
+ :info => Logger::INFO,
11
+ :debug => Logger::DEBUG
12
+ }
13
+
14
+ attr_reader :path
15
+
16
+ [:debug, :info, :warn, :error, :fatal].each do |severity|
17
+ define_method(severity) {|*args| @logger.send(severity, *args)}
18
+ end
19
+
20
+ def initialize
21
+ open
22
+ end
23
+
24
+ def path
25
+ @path ||= if File.directory?('log')
26
+ Pathname.new('.').join('log', 'usage_tracker_server.log')
27
+ else
28
+ Pathname.new('usage_tracker_server.log')
29
+ end
30
+ end
31
+
32
+ def open
33
+ @logger = Logger.new(path.to_s)
34
+ @logger.formatter = Logger::Formatter.new
35
+ @logger.info 'Log opened'
36
+
37
+ rescue
38
+ raise Error, "Cannot open log file #{path}"
39
+ end
40
+
41
+ def close
42
+ return unless @logger
43
+
44
+ @logger.info 'Log closed'
45
+ @logger.close
46
+ @logger = nil
47
+ end
48
+
49
+ def rotate
50
+ close
51
+ open
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
59
+ end
60
+ end
@@ -0,0 +1,64 @@
1
+ require 'usage_tracker_server'
2
+ require 'json'
3
+
4
+ module UsageTrackerServer
5
+ module Reactor
6
+ # This method is called upon every data reception
7
+ #
8
+ def receive_data(data)
9
+ doc = parse(data)
10
+ if doc && check(doc)
11
+ store(doc)
12
+ end
13
+ end
14
+
15
+ private
16
+ def parse(data)
17
+ JSON(data).tap {|h| h.reject! {|k,v| v.nil?}}
18
+ rescue JSON::ParserError
19
+ UsageTrackerServer.log.error "Tossing out invalid JSON #{data.inspect} (#{$!.message.inspect})"
20
+ return nil
21
+ end
22
+
23
+ def check(doc)
24
+ error =
25
+ if !doc.kind_of?(Hash) then 'invalid'
26
+ elsif doc.empty? then 'empty'
27
+ elsif !(missing = check_keys(doc)).empty?
28
+ "#{missing.join(', ')} missing"
29
+ end
30
+
31
+ if error
32
+ UsageTrackerServer.log.error "Tossing out invalid document #{doc.inspect}: #{error}"
33
+ return nil
34
+ else
35
+ return true
36
+ end
37
+ end
38
+
39
+ def check_keys(doc)
40
+ %w( duration env status ).reject {|k| doc.has_key?(k)}
41
+ end
42
+
43
+ def store(doc)
44
+ tries = 0
45
+
46
+ begin
47
+ UsageTrackerServer.log.debug "Received #{doc.inspect}"
48
+ UsageTrackerServer.adapter.save_doc(doc)
49
+
50
+ rescue RestClient::Conflict => e
51
+ if (tries += 1) < 10
52
+ UsageTrackerServer.log.warn "Retrying to save #{doc.inspect}, try #{tries}"
53
+ retry
54
+ else
55
+ UsageTrackerServer.log.error "Losing '#{doc.inspect}' because of too many conflicts"
56
+ end
57
+
58
+ rescue
59
+ UsageTrackerServer.log.error "Losing '#{doc.inspect}' because #$!"
60
+ end
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This is the runner script where EventMachine gets
4
+ # actually invocated.
5
+ # Behaviour is defined in the UsageTrackerServer::Reactor module
6
+
7
+ $:<< File.expand_path(File.dirname(__FILE__) + '/..')
8
+
9
+ require 'usage_tracker_server'
10
+ require 'usage_tracker_server/reactor'
11
+ require 'eventmachine'
12
+
13
+ module UsageTrackerServer
14
+
15
+ connect!
16
+
17
+ # Setup signal handlers
18
+ #
19
+ # * INT, TERM: graceful exit
20
+ # * USR1 : rotate logs
21
+ #
22
+ def self.sigexit(sig)
23
+ log "Received SIG#{sig}"
24
+ EventMachine.stop_event_loop
25
+ end
26
+
27
+ trap('INT') { sigexit 'INT' }
28
+ trap('TERM') { sigexit 'TERM' }
29
+ trap('USR1') { log.rotate }
30
+
31
+ # Run the Event Loop
32
+ #
33
+ EventMachine.run do
34
+ begin
35
+
36
+ run!
37
+
38
+ $stderr.puts "Started, logging to #{log.path}"
39
+ [$stdin, $stdout, $stderr].each {|io| io.reopen '/dev/null'}
40
+
41
+ rescue Exception => e
42
+ message = e.message == 'no datagram socket' ? "Unable to bind #{UsageTrackerServer.settings.host}:#{UsageTrackerServer.settings.port}" : e
43
+ log.fatal message
44
+ $stderr.puts message unless $stderr.closed?
45
+ EventMachine.stop_event_loop
46
+ exit 1
47
+ end
48
+ end
49
+
50
+ # Goodbye!
51
+ #
52
+ log 'Exiting'
53
+ end
54
+
File without changes
@@ -0,0 +1,66 @@
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
+ require 'yaml'
8
+ YAML::ENGINE.yamler = 'syck'
9
+ s.name = "panmind-usage-tracker-server"
10
+ s.version = "1.1.5"
11
+
12
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
13
+ s.authors = ["Christian Woerner", "Fabrizio Regini", "Marcello Barnaba"]
14
+ s.date = "2012-02-24"
15
+ s.description = "this software implements an EventMachine reactor which stores json-objects received via UDP in a database."
16
+ s.email = "info@panmind.com"
17
+ s.executables = ["usage_tracker_server"]
18
+ s.extra_rdoc_files = [
19
+ "README.md"
20
+ ]
21
+ s.files = [
22
+ "README.md",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "bin/usage_tracker_server",
26
+ "config/usage_tracker_server.yml.sample",
27
+ "config/usage_tracker_server_upstart.conf",
28
+ "lib/usage_tracker_server.rb",
29
+ "lib/usage_tracker_server/adapter.rb",
30
+ "lib/usage_tracker_server/adapters/couchdb.rb",
31
+ "lib/usage_tracker_server/adapters/mongodb.rb",
32
+ "lib/usage_tracker_server/log.rb",
33
+ "lib/usage_tracker_server/reactor.rb",
34
+ "lib/usage_tracker_server/runner.rb",
35
+ "log/.placeholder",
36
+ "panmind-usage-tracker-server.gemspec"
37
+ ]
38
+ s.homepage = "http://github.com/Panmind/usage_tracker_server"
39
+ s.require_paths = ["lib"]
40
+ s.summary = "server component for usage-tracker-middleware (Rack)"
41
+
42
+ if s.respond_to? :specification_version then
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<eventmachine>, [">= 0"])
47
+ s.add_runtime_dependency(%q<couchrest>, [">= 0"])
48
+ s.add_runtime_dependency(%q<mongo>, [">= 0"])
49
+ s.add_runtime_dependency(%q<bson>, [">= 0"])
50
+ s.add_runtime_dependency(%q<bson_ext>, ["= 1.6.0"])
51
+ else
52
+ s.add_dependency(%q<eventmachine>, [">= 0"])
53
+ s.add_dependency(%q<couchrest>, [">= 0"])
54
+ s.add_dependency(%q<mongo>, [">= 0"])
55
+ s.add_dependency(%q<bson>, [">= 0"])
56
+ s.add_dependency(%q<bson_ext>, ["= 1.6.0"])
57
+ end
58
+ else
59
+ s.add_dependency(%q<eventmachine>, [">= 0"])
60
+ s.add_dependency(%q<couchrest>, [">= 0"])
61
+ s.add_dependency(%q<mongo>, [">= 0"])
62
+ s.add_dependency(%q<bson>, [">= 0"])
63
+ s.add_dependency(%q<bson_ext>, ["= 1.6.0"])
64
+ end
65
+ end
66
+
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: panmind-usage-tracker-server
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.1.5
6
+ platform: ruby
7
+ authors:
8
+ - Christian Woerner
9
+ - Fabrizio Regini
10
+ - Marcello Barnaba
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+
15
+ date: 2012-02-24 00:00:00 Z
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: eventmachine
19
+ prerelease: false
20
+ requirement: &id001 !ruby/object:Gem::Requirement
21
+ none: false
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: "0"
26
+ type: :runtime
27
+ version_requirements: *id001
28
+ - !ruby/object:Gem::Dependency
29
+ name: couchrest
30
+ prerelease: false
31
+ requirement: &id002 !ruby/object:Gem::Requirement
32
+ none: false
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: "0"
37
+ type: :runtime
38
+ version_requirements: *id002
39
+ - !ruby/object:Gem::Dependency
40
+ name: mongo
41
+ prerelease: false
42
+ requirement: &id003 !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ type: :runtime
49
+ version_requirements: *id003
50
+ - !ruby/object:Gem::Dependency
51
+ name: bson
52
+ prerelease: false
53
+ requirement: &id004 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ type: :runtime
60
+ version_requirements: *id004
61
+ - !ruby/object:Gem::Dependency
62
+ name: bson_ext
63
+ prerelease: false
64
+ requirement: &id005 !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - "="
68
+ - !ruby/object:Gem::Version
69
+ version: 1.6.0
70
+ type: :runtime
71
+ version_requirements: *id005
72
+ description: this software implements an EventMachine reactor which stores json-objects received via UDP in a database.
73
+ email: info@panmind.com
74
+ executables:
75
+ - usage_tracker_server
76
+ extensions: []
77
+
78
+ extra_rdoc_files:
79
+ - README.md
80
+ files:
81
+ - README.md
82
+ - Rakefile
83
+ - VERSION
84
+ - bin/usage_tracker_server
85
+ - config/usage_tracker_server.yml.sample
86
+ - config/usage_tracker_server_upstart.conf
87
+ - lib/usage_tracker_server.rb
88
+ - lib/usage_tracker_server/adapter.rb
89
+ - lib/usage_tracker_server/adapters/couchdb.rb
90
+ - lib/usage_tracker_server/adapters/mongodb.rb
91
+ - lib/usage_tracker_server/log.rb
92
+ - lib/usage_tracker_server/reactor.rb
93
+ - lib/usage_tracker_server/runner.rb
94
+ - log/.placeholder
95
+ - panmind-usage-tracker-server.gemspec
96
+ homepage: http://github.com/Panmind/usage_tracker_server
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options: []
101
+
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: "0"
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ none: false
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: "0"
116
+ requirements: []
117
+
118
+ rubyforge_project:
119
+ rubygems_version: 1.8.17
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: server component for usage-tracker-middleware (Rack)
123
+ test_files: []
124
+