turnstile-rb 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|