turnstile-rb 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ module Turnstile
2
+ module Collector
3
+ end
4
+ end
5
+
6
+ require_relative 'collector/log_reader'
7
+ require_relative 'collector/updater'
8
+ require_relative 'collector/runner'
@@ -0,0 +1,46 @@
1
+ require 'json'
2
+ require_relative 'matcher'
3
+
4
+ module Turnstile
5
+ module Collector
6
+ module Formats
7
+ MARKER_TURNSTILE = 'x-turnstile'.freeze
8
+
9
+ # Extracts from the log file of the form:
10
+ # {"method":"GET","path":"/api/v1/saves/4SB8U-1Am9u-4ixC5","format":"json","duration":49.01,.....}
11
+ def json_matcher(*_args)
12
+ @json_matcher ||= Matcher.new(%r{"ip_address":"\d+},
13
+ ->(line) {
14
+ begin
15
+ data = JSON.parse(line)
16
+ [
17
+ data['platform'],
18
+ data['ip_address'],
19
+ data['user_id']
20
+ ].join(':')
21
+ rescue
22
+ nil
23
+ end
24
+ })
25
+ end
26
+
27
+ # Expects the form of '..... x-turnstile|desktop|10.10.2.4|1234456 ....'
28
+ def delimited_matcher(delimiter = '|')
29
+ @default_matcher ||= Matcher.new(%r{#{MARKER_TURNSTILE}},
30
+ ->(line) {
31
+ marker = line.split(/ /).find { |w| w =~ /^#{MARKER_TURNSTILE}/ }
32
+ if marker
33
+ list = marker.split(delimiter)
34
+ if list && list.size == 4
35
+ return(list[1..-1].join(':'))
36
+ end
37
+ end
38
+ nil
39
+ })
40
+ end
41
+
42
+ alias default_matcher delimited_matcher
43
+ end
44
+ end
45
+ end
46
+
@@ -0,0 +1,81 @@
1
+ require 'file-tail'
2
+ require 'turnstile/collector/formats'
3
+
4
+ module Turnstile
5
+ module Collector
6
+ class LogFile < ::File
7
+ include ::File::Tail
8
+ end
9
+
10
+ class LogReader
11
+ class << self
12
+ include Formats
13
+
14
+ def pipe_delimited(file, queue)
15
+ new(file, queue, delimited_matcher)
16
+ end
17
+
18
+ def comma_delimited(file, queue)
19
+ new(file, queue, delimited_matcher(','))
20
+ end
21
+
22
+ def colon_delimited(file, queue)
23
+ new(file, queue, delimited_matcher(':'))
24
+ end
25
+
26
+ def delimited(file, queue, delimiter)
27
+ new(file, queue, delimited_matcher(delimiter))
28
+ end
29
+
30
+ def json_formatted(file, queue)
31
+ new(file, queue, json_matcher)
32
+ end
33
+
34
+ alias default pipe_delimited
35
+ end
36
+
37
+ attr_accessor :file, :queue, :matcher
38
+
39
+ def initialize(log_file, queue, matcher)
40
+ self.matcher = matcher
41
+ self.queue = queue
42
+
43
+ self.file = LogFile.new(log_file)
44
+
45
+ file.interval = 1
46
+ file.backward(0)
47
+ end
48
+
49
+ def run
50
+ Thread.new do
51
+ Thread.current[:name] = 'log-reader'
52
+ Turnstile::Logger.log "starting to tail file #{file.path}...."
53
+ process!
54
+ end
55
+ end
56
+
57
+ def read(&_block)
58
+ file.tail do |line|
59
+ token = matcher.token_from(line)
60
+ yield(token) if block_given? && token
61
+ end
62
+ end
63
+
64
+ def process!
65
+ self.read do |token|
66
+ queue << token if token
67
+ end
68
+ end
69
+
70
+ def close
71
+ (file.close if file) rescue nil
72
+ end
73
+
74
+ private
75
+
76
+ def extract(line)
77
+ end
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,21 @@
1
+ module Turnstile
2
+ module Collector
3
+ class Matcher < Struct.new(:regexp, :extractor)
4
+ # checks if the line matches +regexp+, and if yes
5
+ # runs it through +extractor+ to grab the token
6
+ #
7
+ # @param [String] line read from a log file
8
+ # @return [String] a token in the form 'platform:ip:user'
9
+
10
+ def token_from(line)
11
+ return nil unless matches?(line)
12
+ return nil unless extractor
13
+ extractor ? extractor[line] : nil
14
+ end
15
+
16
+ def matches?(line)
17
+ regexp && regexp.match?(line)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,71 @@
1
+ require 'thread'
2
+ require 'daemons/daemonize'
3
+
4
+
5
+ module Turnstile
6
+ module Collector
7
+ class Runner
8
+ attr_accessor :config, :queue, :reader, :updater, :file
9
+
10
+ def initialize(*args)
11
+ @config = args.last.is_a?(Hash) ? args.pop : {}
12
+ @file = config[:file]
13
+ @queue = Queue.new
14
+
15
+ config[:debug] ? Turnstile::Logger.enable : Turnstile::Logger.disable
16
+
17
+ wait_for_file(@file)
18
+
19
+ self.reader
20
+ self.updater
21
+
22
+ Daemonize.daemonize if config[:daemonize]
23
+ STDOUT.sync = true if config[:debug]
24
+ end
25
+
26
+ def wait_for_file(file)
27
+ sleep_period = 1
28
+ while !File.exist?(file)
29
+ STDERR.puts "File #{file} does not exist, waiting for it to appear..."
30
+ STDERR.puts 'Press Ctrl-C to abort.' if sleep_period == 1
31
+
32
+ sleep sleep_period
33
+ sleep_period *= 1.2
34
+ end
35
+ end
36
+
37
+ def run
38
+ threads = [reader, updater].map(&:run)
39
+ threads.last.join
40
+ end
41
+
42
+ def updater
43
+ @updater ||= Turnstile::Collector::Updater.new(queue,
44
+ config[:buffer_interval] || 5,
45
+ config[:flush_interval] || 6)
46
+ end
47
+
48
+ def log_reader_class
49
+ Turnstile::Collector::LogReader
50
+ end
51
+
52
+ def reader
53
+ args = [file, queue]
54
+ matcher = :default
55
+
56
+ if config[:delimiter]
57
+ matcher = :delimited
58
+ args << config[:delimiter]
59
+ elsif config[:filetype]
60
+ matcher = config[:filetype].to_sym
61
+ end
62
+
63
+ @reader ||= if log_reader_class.respond_to?(matcher)
64
+ log_reader_class.send(matcher, *args)
65
+ else
66
+ raise ArgumentError, "Invalid matcher #{matcher}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,86 @@
1
+ module Turnstile
2
+ module Collector
3
+ class Updater
4
+
5
+ class Session < ::Struct.new(:uid, :platform, :ip); end
6
+
7
+ attr_accessor :queue, :semaphore, :cache, :tracker, :buffer_interval, :flush_interval
8
+
9
+ def initialize(queue, buffer_interval = 5, flush_interval = 6)
10
+ @queue = queue
11
+ @semaphore = Mutex.new
12
+ @cache = Hash.new(0)
13
+ @tracker = Turnstile::Tracker.new
14
+ @buffer_interval = buffer_interval
15
+ @buffer_interval = 6 if @buffer_interval <= 0
16
+ @flush_interval = flush_interval
17
+ @flush_interval = 5 if @flush_interval <= 0
18
+ end
19
+
20
+ def run
21
+ run_queue_popper
22
+ run_flusher
23
+ end
24
+
25
+ private
26
+
27
+ def run_flusher
28
+ Thread.new do
29
+ Thread.current[:name] = 'updater:flush'
30
+ loop do
31
+ semaphore.synchronize {
32
+ unless cache.empty?
33
+ Turnstile::Logger.logging "flushing cache with [#{cache.keys.size}] keys" do
34
+ cache.keys.each do |key|
35
+ session = parse(key)
36
+ if session.uid && !session.uid.empty?
37
+ tracker.track(session.uid, session.platform, session.ip)
38
+ end
39
+ end
40
+ reset_cache
41
+ end
42
+ else
43
+ Turnstile::Logger.log "nothing to flush, sleeping #{flush_interval}s.."
44
+ end
45
+ }
46
+ sleep flush_interval
47
+ end
48
+ end
49
+ end
50
+
51
+ def run_queue_popper
52
+ Thread.new do
53
+ Thread.current[:name] = 'updater:queue'
54
+ loop do
55
+ unless queue.empty?
56
+ Turnstile::Logger.logging "caching [#{queue.size}] keys locally" do
57
+ while !queue.empty?
58
+ semaphore.synchronize {
59
+ add(queue.pop)
60
+ }
61
+ end
62
+ end
63
+ else
64
+ Turnstile::Logger.log "nothing in the queue, sleeping #{buffer_interval}s..."
65
+ end
66
+ sleep buffer_interval
67
+ end
68
+ end
69
+ end
70
+
71
+ def add(token)
72
+ cache[token] = 1
73
+ end
74
+
75
+ def parse(token)
76
+ a = token.split(':')
77
+ Session.new(a[2], a[0], a[1])
78
+ end
79
+
80
+ def reset_cache
81
+ @cache.clear
82
+ GC.start
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,38 @@
1
+ require 'hashie/dash'
2
+ require 'hashie/extensions/dash/property_translation'
3
+
4
+ module Turnstile
5
+ class RedisConfig < ::Hashie::Dash
6
+ include Hashie::Extensions::Dash::PropertyTranslation
7
+
8
+ property :host, default: '127.0.0.1', required: true
9
+ property :port, default: 6379, required: true, transform_with: ->(value) { value.to_i }
10
+ property :db, default: 1, required: true, transform_with: ->(value) { value.to_i }
11
+ property :timeout, default: 0.05, required: true, transform_with: ->(value) { value.to_f }
12
+
13
+ def configure
14
+ yield self if block_given?
15
+ self
16
+ end
17
+ end
18
+
19
+ class Configuration < ::Hashie::Dash
20
+ include Hashie::Extensions::Dash::PropertyTranslation
21
+ property :activity_interval, default: 60, required: true, transform_with: ->(value) { value.to_i }
22
+ property :sampling_rate, default: 100, required: true, transform_with: ->(value) { value.to_i }
23
+ property :redis, default: ::Turnstile::RedisConfig.new
24
+
25
+ def configure
26
+ yield self if block_given?
27
+ self
28
+ end
29
+
30
+ def method_missing(method, *args, &block)
31
+ return super unless method.to_s =~ /^redis_/
32
+ prop = method.to_s.gsub(/^redis_/, '').to_sym
33
+ if self.redis.respond_to?(prop)
34
+ prop.to_s.end_with?('=') ? self.redis.send(prop, *args, &block) : self.redis.send(prop)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ module Turnstile
2
+ module Logger
3
+
4
+ class << self
5
+ def enable
6
+ class << self
7
+ self.send(:define_method, :log, proc { |msg| _log(msg) })
8
+ self.send(:define_method, :logging, proc { |msg, &block| _logging(msg, &block) })
9
+ end
10
+ end
11
+
12
+ def disable
13
+ class << self
14
+ self.send(:define_method, :log, proc { |msg|})
15
+ self.send(:define_method, :logging, proc { |msg, &block| block.call })
16
+ end
17
+ end
18
+
19
+ def log(msg)
20
+ end
21
+
22
+ def logging(msg, &block)
23
+ block.call
24
+ end
25
+
26
+ private
27
+
28
+ def _log(msg)
29
+ puts "#{Time.now}: #{sprintf("%-20s", Thread.current[:name])} - #{msg}"
30
+ end
31
+
32
+ def _logging(message, &block)
33
+ start = Time.now
34
+ returned_from_block = yield
35
+ elapsed_time = Time.now - start
36
+ if returned_from_block.is_a?(String) && returned_from_block != ""
37
+ message += " - #{returned_from_block}"
38
+ end
39
+ log "(#{"%9.2f" % (1000 * elapsed_time)}ms) #{message}"
40
+ returned_from_block
41
+ rescue Exception => e
42
+ elapsed_time = Time.now - start
43
+ log "(#{"%9.2f" % (1000 * elapsed_time)}ms) error: #{e.message} for #{message} "
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ module Turnstile
2
+ class Nad
3
+
4
+ def data
5
+ out = ""
6
+ aggregate.each_pair do |key, value|
7
+ out << %Q(turnstile:#{key}#{"\tn\t"}#{value}\n)
8
+ end
9
+ out
10
+ end
11
+
12
+ def aggregate
13
+ Turnstile::Adapter.new.aggregate
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,33 @@
1
+ require 'hashie/mash'
2
+ require 'hashie/extensions/mash/symbolize_keys'
3
+
4
+ module Turnstile
5
+ class Stats < ::Hashie::Mash
6
+ include ::Hashie::Extensions::Mash::SymbolizeKeys
7
+ end
8
+
9
+ class Observer
10
+ def stats
11
+ data = adapter.fetch
12
+ platforms = Hash[data.group_by { |d| d[:platform] }.map { |k, v| [k, sampler.extrapolate(v.count)] }]
13
+ total = platforms.values.inject(:+) || 0
14
+ Stats.new({
15
+ stats: {
16
+ total: total,
17
+ platforms: platforms
18
+ },
19
+ users: data
20
+ })
21
+ end
22
+
23
+ private
24
+
25
+ def adapter
26
+ @adapter ||= Adapter.new
27
+ end
28
+
29
+ def sampler
30
+ @sampler ||= Sampler.new
31
+ end
32
+ end
33
+ end