mongoid_traffic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
1
+ module MongoidTraffic
2
+ module ControllerAdditions
3
+
4
+ def log_traffic scope: nil
5
+ MongoidTraffic::Logger.log(
6
+ ip_address: request.remote_ip,
7
+ referer: request.headers['Referer'],
8
+ unique_id: request.session_options[:id], # FIXME: not sure about this
9
+ user_agent: request.headers['User-Agent']
10
+ )
11
+ end
12
+
13
+ def log_scoped_traffic scope: nil
14
+ log_traffic(scope: (scope || request.fullpath.split('?').first))
15
+ end
16
+
17
+ # ---------------------------------------------------------------------
18
+
19
+ def self.included base
20
+ base.extend ClassMethods
21
+ base.helper_method :log_traffic
22
+ base.helper_method :log_scoped_traffic
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ if defined? ActionController::Base
29
+ ActionController::Base.class_eval do
30
+ include MongoidTraffic::ControllerAdditions
31
+ end
32
+ end
@@ -0,0 +1,86 @@
1
+ module MongoidTraffic
2
+ class Log
3
+
4
+ include Mongoid::Document
5
+
6
+ # ---------------------------------------------------------------------
7
+
8
+ field :s, as: :scope, type: String
9
+
10
+ field :ac, as: :access_count, type: Integer
11
+
12
+ field :b, as: :browsers, type: Hash, default: {}
13
+ field :c, as: :countries, type: Hash, default: {}
14
+ field :r, as: :referers, type: Hash, default: {}
15
+ field :u, as: :unique_ids, type: Hash, default: {}
16
+
17
+ field :uat, as: :updated_at, type: Time
18
+
19
+ # ---------------------------------------------------------------------
20
+
21
+ field :df, as: :date_from, type: Date
22
+ field :dt, as: :date_to, type: Date
23
+
24
+ # ---------------------------------------------------------------------
25
+
26
+ validates :date_from, presence: true
27
+ validates :date_to, presence: true
28
+
29
+ # ---------------------------------------------------------------------
30
+
31
+ default_scope -> { where(scope: nil) }
32
+
33
+ scope :for_dates, -> date_from, date_to { where(date_from: date_from, date_to: date_to) }
34
+
35
+ scope :yearly, -> year { self.for_dates(Date.parse("01/01/#{year}"), Date.parse("01/01/#{year}").at_end_of_year) }
36
+ scope :monthly, -> month, year { self.for_dates(Date.parse("01/#{month}/#{year}"), Date.parse("01/#{month}/#{year}").at_end_of_month) }
37
+ scope :weekly, -> week, year { self.for_dates(Date.commercial(year, week), Date.commercial(year, week).at_end_of_week) }
38
+ scope :daily, -> date { self.for_dates(date, date) }
39
+
40
+ scope :scoped_to, -> scope { where(scope: scope) }
41
+
42
+ # ---------------------------------------------------------------------
43
+
44
+ index({ scope: 1, date_from: 1, date_to: 1 })
45
+
46
+ # =====================================================================
47
+
48
+ def self.aggregate_on att
49
+ case find_field_by_name(att).type.to_s
50
+ when 'Integer' then sum(att)
51
+ when 'Hash' then sum_hash(att)
52
+ end
53
+ end
54
+
55
+ def self.sum att
56
+ if att.to_sym == :unique_ids
57
+ aggregate_on(:unique_ids).keys.count
58
+ else
59
+ super(att)
60
+ end
61
+ end
62
+
63
+ private # =============================================================
64
+
65
+ def self.find_field_by_name field_name
66
+ return unless f = fields.detect{ |k,v| k == field_name.to_s or v.options[:as].to_s == field_name.to_s }
67
+ f.last
68
+ end
69
+
70
+ def self.sum_hash field_name
71
+ self.pluck(field_name).inject({}) do |res, h|
72
+ merger = proc { |key, v1, v2|
73
+ if Hash === v1 && Hash === v2
74
+ v1.merge(v2, &merger)
75
+ elsif Hash === v2
76
+ v2
77
+ else
78
+ v1.to_i + v2.to_i
79
+ end
80
+ }
81
+ res = res.merge(h, &merger)
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,34 @@
1
+ require 'nokogiri'
2
+
3
+ # sourced from https://github.com/charlotte-ruby/impressionist/blob/master/lib/impressionist/bots.rb
4
+
5
+ module MongoidTraffic
6
+ class Logger
7
+ class Bots
8
+
9
+ DATA_URL = "http://www.user-agents.org/allagents.xml"
10
+ FILE_PATH = "vendor/mongoid_traffic/allagents.xml"
11
+
12
+ class << self
13
+ def list
14
+ @list ||= begin
15
+ response = File.open(FILE_PATH).read
16
+ doc = Nokogiri::XML(response)
17
+ list = []
18
+ doc.xpath('//user-agent').each do |agent|
19
+ type = agent.xpath("Type").text
20
+ list << agent.xpath('String').text.gsub("&lt;","<") if %w(R S).include?(type)
21
+ end
22
+ list
23
+ end
24
+ end
25
+
26
+ def is_a_bot? referer
27
+ return false unless referer.present?
28
+ list.include?(referer)
29
+ end
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+ module MongoidTraffic
2
+ class Logger
3
+ class Browser
4
+
5
+ def initialize user_agent_string
6
+ @user_agent_string = user_agent_string
7
+ end
8
+
9
+ # ---------------------------------------------------------------------
10
+
11
+ def platform
12
+ user_agent.platform
13
+ end
14
+
15
+ def name
16
+ user_agent.browser
17
+ end
18
+
19
+ def version
20
+ user_agent.version.to_s.split('.')[0..1].join('.')
21
+ end
22
+
23
+ private # =============================================================
24
+
25
+ def user_agent
26
+ @user_agent ||= ::UserAgent.parse(@user_agent_string)
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ require 'geoip'
2
+
3
+ module MongoidTraffic
4
+ class Logger
5
+ class GeoIp
6
+
7
+ DATA_URL = "http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz"
8
+ FILE_URL = "vendor/mongoid_traffic/GeoIP.dat"
9
+
10
+ class << self
11
+ def geoip
12
+ @geoip ||= ::GeoIP.new(FILE_URL)
13
+ end
14
+
15
+ def country_code2 str
16
+ geoip.country(str).country_code2
17
+ end
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ require 'uri'
2
+
3
+ module MongoidTraffic
4
+ class Logger
5
+ class Referer
6
+
7
+ def initialize referer_string
8
+ @referer_string = referer_string
9
+ end
10
+
11
+ def host
12
+ uri.host
13
+ end
14
+
15
+ def to_s
16
+ @referer_string
17
+ end
18
+
19
+ private # =============================================================
20
+
21
+ def uri
22
+ URI.parse(@referer_string)
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,125 @@
1
+ require 'geoip'
2
+ require 'uri'
3
+ require 'useragent'
4
+
5
+ require_relative './log'
6
+ require_relative './logger/bots'
7
+ require_relative './logger/browser'
8
+ require_relative './logger/geo_ip'
9
+ require_relative './logger/referer'
10
+
11
+ module MongoidTraffic
12
+ class Logger
13
+
14
+ TIME_SCOPE_OPTIONS = %i(year month week day)
15
+
16
+ # ---------------------------------------------------------------------
17
+
18
+ def self.log *args
19
+ new(*args).log
20
+ end
21
+
22
+ # ---------------------------------------------------------------------
23
+
24
+ def initialize ip_address: nil, referer: nil, scope: nil, time_scope: %i(month day), unique_id: nil, user_agent: nil
25
+ @ip_address = ip_address
26
+ @referer_string = referer
27
+ @scope = scope
28
+ @time_scope = time_scope
29
+ @unique_id = unique_id
30
+ @user_agent_string = user_agent
31
+ end
32
+
33
+ def log
34
+ return if Bots.is_a_bot?(@referer_string)
35
+ raise "Invalid time scope definition: #{@time_scope}" unless @time_scope.all?{ |ts| TIME_SCOPE_OPTIONS.include?(ts) }
36
+
37
+ @time_scope.each do |ts|
38
+ Log.collection.find( find_query(ts) ).upsert( upsert_query )
39
+ end
40
+ end
41
+
42
+ # ---------------------------------------------------------------------
43
+
44
+ def upsert_query
45
+ {
46
+ '$inc' => access_count_query.
47
+ merge(browser_query).
48
+ merge(country_query).
49
+ merge(referer_query).
50
+ merge(unique_id_query),
51
+ '$set' => { uat: Time.now }
52
+ }
53
+ end
54
+
55
+ # ---------------------------------------------------------------------
56
+
57
+ def access_count_query
58
+ { ac: 1 }
59
+ end
60
+
61
+ def browser_query
62
+ return {} unless browser.present?
63
+ browser_path = [browser.platform, browser.name, browser.version].map{ |s| escape_key(s) }.join('.')
64
+ { "b.#{browser_path}" => 1 }
65
+ end
66
+
67
+ def country_query
68
+ return {} unless @ip_address.present?
69
+ return {} unless country_code2 = GeoIp.country_code2(@ip_address)
70
+ country_code_key = escape_key(country_code2)
71
+ { "c.#{country_code_key}" => 1 }
72
+ end
73
+
74
+ def referer_query
75
+ return {} unless referer.present?
76
+ referer_key = escape_key(referer.to_s)
77
+ { "r.#{referer_key}" => 1 }
78
+ end
79
+
80
+ def unique_id_query
81
+ return {} unless @unique_id.present?
82
+ unique_id_key = escape_key(@unique_id.to_s)
83
+ { "u.#{unique_id_key}" => 1 }
84
+ end
85
+
86
+ # ---------------------------------------------------------------------
87
+
88
+ def escape_key key
89
+ CGI::escape(key).gsub('.', '%2E')
90
+ end
91
+
92
+ # ---------------------------------------------------------------------
93
+
94
+ def find_query ts
95
+ res = time_query(ts)
96
+ res = res.merge(scope_query) if @scope.present?
97
+ res
98
+ end
99
+
100
+ def scope_query
101
+ { s: @scope }
102
+ end
103
+
104
+ def time_query ts
105
+ date = Date.today
106
+ case ts
107
+ when :day then { df: date, dt: date }
108
+ else { df: date.send("at_beginning_of_#{ts}"), dt: date.send("at_end_of_#{ts}") }
109
+ end
110
+ end
111
+
112
+ private # =============================================================
113
+
114
+ def browser
115
+ return unless @user_agent_string.present?
116
+ @browser ||= Browser.new(@user_agent_string)
117
+ end
118
+
119
+ def referer
120
+ return unless @referer_string.present?
121
+ @referer ||= Referer.new(@referer_string)
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ module MongoidTraffic
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,3 @@
1
+ require "mongoid_traffic/version"
2
+
3
+ require "mongoid_traffic/controller_additions"
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongoid_traffic/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongoid_traffic"
8
+ spec.version = MongoidTraffic::VERSION
9
+ spec.authors = ["Tomas Celizna"]
10
+ spec.email = ["tomas.celizna@gmail.com"]
11
+ spec.description = %q{Aggregated traffic logs stored in MongoDB.}
12
+ spec.summary = %q{Aggregated traffic logs stored in MongoDB.}
13
+ spec.homepage = "https://github.com/tomasc/mongoid_traffic"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "geoip"
22
+ spec.add_dependency "mongoid", "~> 4.0"
23
+ spec.add_dependency "nokogiri"
24
+ spec.add_dependency "useragent", "~> 0.10.0"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.3"
27
+ spec.add_development_dependency "coveralls"
28
+ spec.add_development_dependency "database_cleaner"
29
+ spec.add_development_dependency "guard"
30
+ spec.add_development_dependency "guard-minitest"
31
+ spec.add_development_dependency "minitest"
32
+ spec.add_development_dependency "rake"
33
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+
3
+ require_relative '../../lib/mongoid_traffic/controller_additions'
4
+
5
+ module MongoidTraffic
6
+ describe 'ControllerAdditions' do
7
+
8
+ before(:each) do
9
+ @controller_class = Class.new
10
+ @controller = @controller_class.new
11
+ def @controller_class.helper_method(name); end
12
+ @controller_class.send(:include, MongoidTraffic::ControllerAdditions)
13
+ end
14
+
15
+ describe '.log_traffic' do
16
+ it 'logs with :user_agent'
17
+ it 'logs with :referer'
18
+ it 'logs with :ip_address'
19
+ it 'logs with :unique_id'
20
+ end
21
+
22
+ describe '.log_scoped_traffic' do
23
+ it 'infers scope from request path'
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,153 @@
1
+ require 'test_helper'
2
+
3
+ require_relative '../../lib/mongoid_traffic/log'
4
+
5
+ module MongoidTraffic
6
+ describe 'Log' do
7
+ subject { Log.new }
8
+
9
+ describe 'fields' do
10
+ it 'has :scope' do
11
+ subject.must_respond_to :scope
12
+ end
13
+ it 'has :access_count' do
14
+ subject.must_respond_to :access_count
15
+ end
16
+ it 'has :browsers' do
17
+ subject.must_respond_to :browsers
18
+ subject.browsers.must_be_kind_of Hash
19
+ end
20
+ it 'has :referers' do
21
+ subject.must_respond_to :referers
22
+ subject.referers.must_be_kind_of Hash
23
+ end
24
+ it 'has :countries' do
25
+ subject.must_respond_to :countries
26
+ subject.countries.must_be_kind_of Hash
27
+ end
28
+ it 'has :unique_ids' do
29
+ subject.must_respond_to :unique_ids
30
+ subject.unique_ids.must_be_kind_of Hash
31
+ end
32
+ it 'has :updated_at' do
33
+ subject.must_respond_to :updated_at
34
+ end
35
+ end
36
+
37
+ describe 'scopes' do
38
+ it 'has :default_scope that assumes no :scope' do
39
+ Log.criteria.selector.fetch('s').must_be_nil
40
+ end
41
+
42
+ it('has :for_dates') { Log.must_respond_to :for_dates }
43
+ it('has :yearly') { Log.must_respond_to :yearly }
44
+ it('has :monthly') { Log.must_respond_to :monthly }
45
+ it('has :weekly') { Log.must_respond_to :weekly }
46
+ it('has :daily') { Log.must_respond_to :daily }
47
+ it('has :scoped_to') { Log.must_respond_to :scoped_to }
48
+ end
49
+
50
+ describe '.aggregate_on' do
51
+ let(:log_1) { Log.new(date_from: Date.today, date_to: Date.today) }
52
+ let(:log_2) { Log.new(date_from: Date.tomorrow, date_to: Date.tomorrow) }
53
+
54
+ describe '.aggregate_on(:access_count)' do
55
+ before do
56
+ log_1.tap{ |l| l.access_count = 1 }.save
57
+ log_2.tap{ |l| l.access_count = 2 }.save
58
+ end
59
+
60
+ it 'sums the access_counts' do
61
+ Log.aggregate_on(:access_count).must_equal 3
62
+ end
63
+ end
64
+
65
+ describe '.aggregate_on(:browsers)' do
66
+ # not the key names have been abbreviated
67
+ before do
68
+ log_1.tap do |l|
69
+ l.browsers = {
70
+ "Mac" => {
71
+ "Saf" => { "8" => 1 }
72
+ },
73
+ "Win" => {
74
+ "Saf" => { "7" => 5 }
75
+ }
76
+ }
77
+ end.save
78
+
79
+ log_2.tap do |l|
80
+ l.browsers = {
81
+ "Mac" => {
82
+ "Saf" => { "8" => 10, "7" => 100 },
83
+ "Chr" => { "3" => 5 }
84
+ },
85
+ "Win" => {
86
+ "Saf" => { "7" => 100 },
87
+ "IE" => { "10" => 1 }
88
+ }
89
+ }
90
+ end.save
91
+ end
92
+
93
+ it 'sums the browsers' do
94
+ Log.aggregate_on(:browsers).must_equal({
95
+ "Mac" => {
96
+ "Saf" => { "8" => 11, "7" => 100 },
97
+ "Chr" => { "3" => 5 }
98
+ },
99
+ "Win" => {
100
+ "Saf" => { "7" => 105 },
101
+ "IE" => { "10" => 1 }
102
+ }
103
+ })
104
+ end
105
+ end
106
+
107
+ describe '.aggregate_on(:referers)' do
108
+ before do
109
+ log_1.tap{ |l| l.referers = { 'google' => 100, 'apple' => 1000 } }.save
110
+ log_2.tap{ |l| l.referers = { 'google' => 10, 'apple' => 100, 'ms' => 1 } }.save
111
+ end
112
+
113
+ it 'sums the referers' do
114
+ Log.aggregate_on(:referers).must_equal({ 'google' => 110, 'apple' => 1100, 'ms' => 1 })
115
+ end
116
+ end
117
+
118
+ describe '.aggregate_on(:countries)' do
119
+ before do
120
+ log_1.tap{ |l| l.countries = { 'CZ' => 100 } }.save
121
+ log_2.tap{ |l| l.countries = { 'DE' => 10 } }.save
122
+ end
123
+
124
+ it 'sums the countries' do
125
+ Log.aggregate_on(:countries).must_equal({ 'CZ' => 100, 'DE' => 10 })
126
+ end
127
+ end
128
+
129
+ describe '.aggregate_on(:unique_ids)' do
130
+ before do
131
+ log_1.tap{ |l| l.unique_ids = { '01234' => 100, '56789' => 100 } }.save
132
+ log_2.tap{ |l| l.unique_ids = { '56789' => 100 } }.save
133
+ end
134
+
135
+ it 'sums the unique_ids' do
136
+ Log.aggregate_on(:unique_ids).must_equal({ '01234' => 100, '56789' => 200 })
137
+ end
138
+ end
139
+
140
+ describe '.sum(:unique_ids)' do
141
+ before do
142
+ log_1.tap{ |l| l.unique_ids = { '01234' => 100, '56789' => 100 } }.save
143
+ log_2.tap{ |l| l.unique_ids = { '56789' => 100, 'ABCDE' => 1 } }.save
144
+ end
145
+
146
+ it 'sums the unique_ids' do
147
+ Log.sum(:unique_ids).must_equal 3
148
+ end
149
+ end
150
+ end
151
+
152
+ end
153
+ end