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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +2 -0
- data/.travis.yml +22 -0
- data/Gemfile +6 -0
- data/Guardfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +197 -0
- data/Rakefile +1 -0
- data/bin/turnstile +91 -0
- data/lib/turnstile.rb +19 -0
- data/lib/turnstile/adapter.rb +60 -0
- data/lib/turnstile/collector.rb +8 -0
- data/lib/turnstile/collector/formats.rb +46 -0
- data/lib/turnstile/collector/log_reader.rb +81 -0
- data/lib/turnstile/collector/matcher.rb +21 -0
- data/lib/turnstile/collector/runner.rb +71 -0
- data/lib/turnstile/collector/updater.rb +86 -0
- data/lib/turnstile/configuration.rb +38 -0
- data/lib/turnstile/logger.rb +47 -0
- data/lib/turnstile/nad.rb +16 -0
- data/lib/turnstile/observer.rb +33 -0
- data/lib/turnstile/rb.rb +1 -0
- data/lib/turnstile/sampler.rb +20 -0
- data/lib/turnstile/tracker.rb +17 -0
- data/lib/turnstile/version.rb +3 -0
- data/spec/fixtures/sample-production.log +4 -0
- data/spec/fixtures/sample-production.log.json +37 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/turnstile/adapter_spec.rb +71 -0
- data/spec/turnstile/collector/log_reader_spec.rb +114 -0
- data/spec/turnstile/configuration_spec.rb +78 -0
- data/spec/turnstile/nad_spec.rb +31 -0
- data/spec/turnstile/observer_spec.rb +65 -0
- data/spec/turnstile/sampler_spec.rb +35 -0
- data/spec/turnstile/tracker_spec.rb +25 -0
- data/spec/turnstile_spec.rb +68 -0
- data/turnstile-rb.gemspec +36 -0
- metadata +277 -0
@@ -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,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
|