turnstile-rb 2.0.0

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