turnstile-rb 2.0.1 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +3 -0
- data/.travis.yml +2 -2
- data/LICENSE.txt +2 -1
- data/README.md +200 -11
- data/Rakefile +1 -0
- data/bin/turnstile +4 -6
- data/example/custom_csv_matcher.rb +22 -0
- data/lib/turnstile.rb +37 -8
- data/lib/turnstile/cli/launcher.rb +83 -0
- data/lib/turnstile/cli/parser.rb +166 -0
- data/lib/turnstile/cli/runner.rb +58 -0
- data/lib/turnstile/collector.rb +2 -2
- data/lib/turnstile/collector/actor.rb +81 -0
- data/lib/turnstile/collector/controller.rb +121 -0
- data/lib/turnstile/collector/flusher.rb +36 -0
- data/lib/turnstile/collector/formats.rb +7 -34
- data/lib/turnstile/collector/formats/custom_matcher.rb +19 -0
- data/lib/turnstile/collector/formats/delimited_matcher.rb +30 -0
- data/lib/turnstile/collector/formats/json_matcher.rb +30 -0
- data/lib/turnstile/collector/log_reader.rb +84 -46
- data/lib/turnstile/collector/{matcher.rb → regexp_matcher.rb} +4 -5
- data/lib/turnstile/collector/session.rb +7 -0
- data/lib/turnstile/commands.rb +20 -0
- data/lib/turnstile/commands/base.rb +20 -0
- data/lib/turnstile/commands/flushdb.rb +21 -0
- data/lib/turnstile/commands/print_keys.rb +19 -0
- data/lib/turnstile/commands/show.rb +89 -0
- data/lib/turnstile/configuration.rb +23 -0
- data/lib/turnstile/dependencies.rb +31 -0
- data/lib/turnstile/logger.rb +9 -40
- data/lib/turnstile/logger/helper.rb +42 -0
- data/lib/turnstile/logger/provider.rb +74 -0
- data/lib/turnstile/observer.rb +5 -9
- data/lib/turnstile/redis/adapter.rb +97 -0
- data/lib/turnstile/redis/connection.rb +116 -0
- data/lib/turnstile/redis/spy.rb +42 -0
- data/lib/turnstile/sampler.rb +9 -2
- data/lib/turnstile/tracker.rb +14 -14
- data/lib/turnstile/version.rb +51 -12
- data/lib/turnstile/web_app.rb +29 -0
- data/spec/spec_helper.rb +18 -3
- data/spec/support/logging.rb +17 -0
- data/spec/turnstile/adapter_spec.rb +59 -46
- data/spec/turnstile/collector/flusher_spec.rb +16 -0
- data/spec/turnstile/collector/log_reader_spec.rb +127 -77
- data/spec/turnstile/commands/show_spec.rb +40 -0
- data/spec/turnstile/tracker_spec.rb +21 -7
- data/spec/turnstile_spec.rb +3 -0
- data/turnstile-rb.gemspec +5 -2
- metadata +89 -22
- data/Gemfile +0 -6
- data/lib/turnstile/adapter.rb +0 -61
- data/lib/turnstile/collector/runner.rb +0 -72
- data/lib/turnstile/collector/updater.rb +0 -86
- data/lib/turnstile/parser.rb +0 -107
- data/lib/turnstile/runner.rb +0 -54
- data/lib/turnstile/summary.rb +0 -57
- data/spec/turnstile/summary_spec.rb +0 -41
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'colored2'
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require 'turnstile/version'
|
6
|
+
|
7
|
+
module Turnstile
|
8
|
+
module CLI
|
9
|
+
class Parser
|
10
|
+
extend Forwardable
|
11
|
+
def_delegators :@system, :stdout, :stdin, :stderr
|
12
|
+
|
13
|
+
attr_accessor :options, :argv, :system
|
14
|
+
|
15
|
+
def initialize(argv, system)
|
16
|
+
self.system = system
|
17
|
+
self.argv = argv.dup
|
18
|
+
self.options = Hashie::Mash.new
|
19
|
+
self.argv << '-h' if argv.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse
|
23
|
+
begin
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
opts.banner = ' '
|
26
|
+
|
27
|
+
opts.separator 'DESCRIPTION:'.bold.magenta
|
28
|
+
opts.separator ' ' + ::Turnstile::GEM_DESCRIPTION.gsub(/\n/, "\n ").strip
|
29
|
+
opts.separator ''
|
30
|
+
|
31
|
+
opts.separator "USAGE:\n".bold.magenta +
|
32
|
+
" # Tail the log file as a proper daemon\n".bold.black +
|
33
|
+
" turnstile -f <file> [ --daemon ] [ options ]\n\n".yellow +
|
34
|
+
|
35
|
+
" # Add a single item and exit\n".bold.black +
|
36
|
+
" turnstile -a 'platform:ip:user' [ options ]\n\n".yellow +
|
37
|
+
|
38
|
+
" # Print the summary stats and exit\n".bold.black +
|
39
|
+
' turnstile -s [ json | csv | nad ] [ options ]'.yellow
|
40
|
+
|
41
|
+
opts.separator ''
|
42
|
+
opts.separator 'DETAILS:'.bold.magenta
|
43
|
+
opts.separator ' ' + ::Turnstile::DESCRIPTION.gsub(/\n/, "\n ").strip
|
44
|
+
|
45
|
+
opts.separator ''
|
46
|
+
opts.separator 'OPTIONS:'.bold.magenta
|
47
|
+
|
48
|
+
|
49
|
+
opts.separator "\n Mode of Operation:".bold.green
|
50
|
+
opts.on('-f', '--file FILE',
|
51
|
+
'Starts Turnstile in a file-tailing mode',
|
52
|
+
' ') do |file|
|
53
|
+
options[:file] = file
|
54
|
+
end
|
55
|
+
|
56
|
+
opts.on('-s', '--show [FORMAT]',
|
57
|
+
'Print current stats and exit. Optional ',
|
58
|
+
'format can be "json" (default), "nad",',
|
59
|
+
'"yaml", or "csv"', ' ') do |v|
|
60
|
+
options[:show] = true
|
61
|
+
options[:show_format] = (v || 'json').to_sym
|
62
|
+
end
|
63
|
+
|
64
|
+
opts.on('-w', '--web [PORT]',
|
65
|
+
'Starts a Sinatra app on a given port',
|
66
|
+
'offering /turnstile/<json|yaml> end point.',
|
67
|
+
'Can be used with file tailing mode, or',
|
68
|
+
'standalone. Default port is ' +
|
69
|
+
::Turnstile::DEFAULT_PORT.to_s,
|
70
|
+
' '
|
71
|
+
) do |v|
|
72
|
+
options[:web] = true
|
73
|
+
Turnstile.config.port = v.to_i if v
|
74
|
+
end
|
75
|
+
|
76
|
+
opts.on('-a', '--add TOKEN',
|
77
|
+
'Registers an event from the token, such as ',
|
78
|
+
'"ios:123.4.4.4:32442". Use -l to customize',
|
79
|
+
'the delimiter', ' ') do |v|
|
80
|
+
options[:token] = v
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on('-p', '--print-keys', 'Prints all Turnstile keys in Redis', ' ') do |v|
|
84
|
+
options[:print_keys] = true
|
85
|
+
end
|
86
|
+
|
87
|
+
opts.on('--flushdb', 'Wipes Redis database, and exit', ' ') do |v|
|
88
|
+
options[:flushdb] = true
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
opts.separator ' Tailing log file:'.bold.green
|
93
|
+
|
94
|
+
opts.on('-d', '--daemonize', 'Daemonize to watch the logs', ' ') do |v|
|
95
|
+
options[:daemonize] = true
|
96
|
+
end
|
97
|
+
|
98
|
+
opts.on('-b', '--read-backwards [LINES]',
|
99
|
+
'Like tail, read last LINES lines',
|
100
|
+
'instead of tailing from the end', ' ') do |lines|
|
101
|
+
options[:tail] = lines ? lines.to_i : -1
|
102
|
+
end
|
103
|
+
|
104
|
+
opts.on('-F', '--format FORMAT',
|
105
|
+
'Log file format (see above)', ' ') do |format|
|
106
|
+
options[:filetype] = format
|
107
|
+
end
|
108
|
+
opts.on('-l', '--delimiter CHAR',
|
109
|
+
'Forces "delimited" file type, and ',
|
110
|
+
'uses CHAR as the delimiter', ' ') do |v|
|
111
|
+
options[:delimiter] = v
|
112
|
+
end
|
113
|
+
|
114
|
+
opts.on('-c', '--config FILE',
|
115
|
+
'Ruby config file that can define the',
|
116
|
+
'custom matcher, supporting arbitrary ',
|
117
|
+
'complex logs') do |file|
|
118
|
+
options[:config_file] = file
|
119
|
+
end
|
120
|
+
|
121
|
+
opts.separator "\n Redis Server:".bold.green
|
122
|
+
|
123
|
+
opts.on('-r', '--redis-url URL', 'Redis server URL') { |host| Turnstile.config.redis_url = host }
|
124
|
+
opts.on('--redis-host HOST', 'Redis server host') { |host| Turnstile.config.redis_host = host }
|
125
|
+
opts.on('--redis-port PORT', 'Redis server port') { |port| Turnstile.config.redis_port = port }
|
126
|
+
opts.on('--redis-db DB', 'Redis server db') { |db| Turnstile.config.redis_db = db }
|
127
|
+
opts.on('--hiredis', 'Use hiredis high performance library') do |_value|
|
128
|
+
begin
|
129
|
+
require 'redis/connection/hiredis'
|
130
|
+
rescue LoadError => e
|
131
|
+
raise HiredisDriverNotFound, "Can not use hiredis driver: #{e.message}"
|
132
|
+
end
|
133
|
+
Turnstile.config.redis_use_hiredis = true
|
134
|
+
end
|
135
|
+
|
136
|
+
opts.separator "\n Miscellaneous:".bold.green
|
137
|
+
|
138
|
+
opts.on('-i', '--idle-sleep SECONDS',
|
139
|
+
'When no work was detected, pause the ',
|
140
|
+
'threads for several seconds.') do |v|
|
141
|
+
Turnstile.config.flush_interval = v.to_i
|
142
|
+
end
|
143
|
+
|
144
|
+
opts.on('-v', '--verbose', 'Print status to stdout') { |v| options[:verbose] = true }
|
145
|
+
opts.on('-t', '--trace', 'Enable trace mode') do |v|
|
146
|
+
options[:trace] = true
|
147
|
+
Turnstile.config.trace = true
|
148
|
+
end
|
149
|
+
opts.on_tail('-h', '--help', 'Show this message') do
|
150
|
+
puts opts
|
151
|
+
return
|
152
|
+
end
|
153
|
+
end.parse!(argv)
|
154
|
+
options
|
155
|
+
rescue OptionParser::MissingArgument => e
|
156
|
+
terr 'Invalid Usage: ' + e.message.red, stderr
|
157
|
+
nil
|
158
|
+
rescue Exception => e
|
159
|
+
terr e.message.bold.red, stderr
|
160
|
+
nil
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'colored2'
|
3
|
+
|
4
|
+
require_relative '../version'
|
5
|
+
require_relative '../configuration'
|
6
|
+
|
7
|
+
require_relative 'launcher'
|
8
|
+
require_relative 'parser'
|
9
|
+
|
10
|
+
module Turnstile
|
11
|
+
module CLI
|
12
|
+
class Runner
|
13
|
+
attr_reader :argv, :stdin, :stdout, :stderr, :kernel
|
14
|
+
|
15
|
+
# Allow everything fun to be injected from the outside while defaulting to normal implementations.
|
16
|
+
def initialize(argv, stdin = STDIN, stdout = STDOUT, stderr = STDERR, kernel = Kernel)
|
17
|
+
@argv, @stdin, @stdout, @stderr, @kernel = argv, stdin, stdout, stderr, kernel
|
18
|
+
end
|
19
|
+
|
20
|
+
def execute!
|
21
|
+
exit_code = begin
|
22
|
+
Colored2.disable! unless stdout.tty?
|
23
|
+
|
24
|
+
$stderr = stderr
|
25
|
+
$stdin = stdin
|
26
|
+
$stdout = stdout
|
27
|
+
|
28
|
+
options = Parser.new(argv, self).parse
|
29
|
+
Configuration.from_file(options.config_file) if options && options.config_file
|
30
|
+
Launcher.new(options).launch if options
|
31
|
+
|
32
|
+
# Thor::Base#start does not have a return value, assume success if no exception is raised.
|
33
|
+
0
|
34
|
+
rescue StandardError => e
|
35
|
+
# The ruby interpreter would pipe this to STDERR and exit 1 in the case of an unhandled exception
|
36
|
+
if options && options[:trace]
|
37
|
+
b = e.backtrace
|
38
|
+
terr("#{b.shift}: #{e.message} (#{e.class})")
|
39
|
+
terr(b.map { |s| "\tfrom #{s}" }.join("\n"))
|
40
|
+
else
|
41
|
+
terr(e.message)
|
42
|
+
end
|
43
|
+
1
|
44
|
+
rescue SystemExit => e
|
45
|
+
e.status
|
46
|
+
ensure
|
47
|
+
$stderr = STDERR
|
48
|
+
$stdin = STDIN
|
49
|
+
$stdout = STDOUT
|
50
|
+
end
|
51
|
+
|
52
|
+
# Proxy our exit code back to the injected kernel.
|
53
|
+
@kernel.exit(exit_code)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
data/lib/turnstile/collector.rb
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
require_relative 'session'
|
2
|
+
require_relative '../logger/helper'
|
3
|
+
require_relative '../dependencies'
|
4
|
+
require 'turnstile'
|
5
|
+
require 'thread'
|
6
|
+
module Turnstile
|
7
|
+
module Collector
|
8
|
+
class Actor
|
9
|
+
include Logger::Helper
|
10
|
+
|
11
|
+
@name = 'abstract'
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def actor_name(value = nil)
|
15
|
+
@actor_name = value if value
|
16
|
+
@actor_name
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_accessor :queue,
|
21
|
+
:tracker,
|
22
|
+
:sleep_when_idle,
|
23
|
+
:options,
|
24
|
+
:stopping,
|
25
|
+
:thread
|
26
|
+
|
27
|
+
def initialize(queue:,
|
28
|
+
tracker: nil,
|
29
|
+
sleep_when_idle: Turnstile.config.flush_interval,
|
30
|
+
**opts)
|
31
|
+
|
32
|
+
self.queue = queue
|
33
|
+
self.sleep_when_idle = sleep_when_idle
|
34
|
+
self.tracker = tracker || Turnstile::Tracker.new
|
35
|
+
self.options = opts
|
36
|
+
self.stopping = false
|
37
|
+
|
38
|
+
Kernel.tdb "actor initialized: #{self.to_s}" if Turnstile.config.trace
|
39
|
+
end
|
40
|
+
|
41
|
+
def shutdown
|
42
|
+
self.stopping = true
|
43
|
+
end
|
44
|
+
|
45
|
+
def config
|
46
|
+
@config ||= Turnstile.config
|
47
|
+
end
|
48
|
+
|
49
|
+
def stopping?
|
50
|
+
self.stopping
|
51
|
+
end
|
52
|
+
|
53
|
+
def start
|
54
|
+
self.thread = create_thread(self, sleep_when_idle) do |actor, sleep_period|
|
55
|
+
loop do
|
56
|
+
items_remaining = actor.execute
|
57
|
+
break if actor.stopping?
|
58
|
+
sleep(sleep_period) unless items_remaining && items_remaining > 0
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_s
|
64
|
+
"<turnsile-actor##{self.class.name.gsub(/.*::/, '').downcase}: queue size: #{queue.size}, idle=#{sleep_when_idle}"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return nil when there is nothing else to do
|
68
|
+
# Return ideally a number representing number of remaining items.
|
69
|
+
def execute
|
70
|
+
raise ArgumentError, 'Abstract Method'
|
71
|
+
end
|
72
|
+
|
73
|
+
def create_thread(*args, &_block)
|
74
|
+
Thread.new(self.class.actor_name, *args) do |actor_name, *args|
|
75
|
+
Thread.current[:name] = actor_name
|
76
|
+
yield(*args) if block_given?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'thread'
|
3
|
+
require 'hashie/extensions/symbolize_keys'
|
4
|
+
require 'daemons/daemonize'
|
5
|
+
require 'colored2'
|
6
|
+
|
7
|
+
require_relative 'flusher'
|
8
|
+
require_relative 'actor'
|
9
|
+
|
10
|
+
module Turnstile
|
11
|
+
module Collector
|
12
|
+
class Controller
|
13
|
+
attr_accessor :options, :actors, :threads
|
14
|
+
|
15
|
+
def initialize(*args)
|
16
|
+
self.options = args.last.is_a?(Hash) ? args.pop : {}
|
17
|
+
|
18
|
+
options.verbose ? Turnstile::Logger.enable : Turnstile::Logger.disable
|
19
|
+
|
20
|
+
wait_for_file(file)
|
21
|
+
|
22
|
+
self.actors = [self.reader, self.flusher]
|
23
|
+
|
24
|
+
Daemonize.daemonize if options[:daemonize]
|
25
|
+
STDOUT.sync = true if options[:verbose]
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
self.threads = actors.map(&:start)
|
30
|
+
threads.map(&:join)
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
actors.each(&:shutdown)
|
35
|
+
end
|
36
|
+
|
37
|
+
def flusher
|
38
|
+
@flusher ||= Flusher.new(**flusher_arguments)
|
39
|
+
end
|
40
|
+
|
41
|
+
def tracker
|
42
|
+
@tracker ||= Turnstile::Tracker.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def queue
|
46
|
+
@queue ||= Queue.new
|
47
|
+
end
|
48
|
+
|
49
|
+
def file
|
50
|
+
options.file
|
51
|
+
end
|
52
|
+
|
53
|
+
def reader
|
54
|
+
opts = reader_arguments
|
55
|
+
matcher = opts.delete(:matcher).to_sym
|
56
|
+
@reader ||= if log_reader_class.respond_to?(matcher)
|
57
|
+
log_reader_class.send(matcher, file, queue, **opts)
|
58
|
+
else
|
59
|
+
raise ArgumentError, "Invalid matcher #{matcher}, args #{reader_args}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def symbolize(opts)
|
64
|
+
Hashie::Extensions::SymbolizeKeys.symbolize_keys(opts.to_h)
|
65
|
+
end
|
66
|
+
|
67
|
+
def config
|
68
|
+
@config ||= Turnstile.config
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def flusher_arguments
|
74
|
+
symbolize(actor_argument_hash.merge(sleep_when_idle: config.flush_interval))
|
75
|
+
end
|
76
|
+
|
77
|
+
def reader_arguments
|
78
|
+
reader_args_hash = actor_argument_hash.merge(sleep_when_idle: config.flush_interval)
|
79
|
+
matcher, delimiter = select_matcher
|
80
|
+
|
81
|
+
reader_args_hash.merge!(delimiter: delimiter) if delimiter
|
82
|
+
reader_args_hash.merge!(matcher: matcher) if matcher
|
83
|
+
|
84
|
+
symbolize(reader_args_hash)
|
85
|
+
end
|
86
|
+
|
87
|
+
def actor_argument_hash
|
88
|
+
options.merge(queue: queue, tracker: tracker)
|
89
|
+
end
|
90
|
+
|
91
|
+
def wait_for_file(file)
|
92
|
+
sleep_period = 1
|
93
|
+
while !::File.exist?(file)
|
94
|
+
terr "File #{file.bold.yellow} does not exist, waiting for it to appear..."
|
95
|
+
terr 'Press Ctrl-C to abort.' if sleep_period == 1
|
96
|
+
|
97
|
+
sleep sleep_period
|
98
|
+
sleep_period *= 1.2
|
99
|
+
end
|
100
|
+
tdb "Detected file #{file.bold.yellow} now exists, continue..."
|
101
|
+
end
|
102
|
+
|
103
|
+
def log_reader_class
|
104
|
+
Turnstile::Collector::LogReader
|
105
|
+
end
|
106
|
+
|
107
|
+
def select_matcher
|
108
|
+
matcher = :default
|
109
|
+
delimiter = nil
|
110
|
+
|
111
|
+
if options[:delimiter]
|
112
|
+
matcher = :delimited
|
113
|
+
delimiter = options[:delimiter]
|
114
|
+
elsif options[:filetype]
|
115
|
+
matcher = options[:filetype].to_sym
|
116
|
+
end
|
117
|
+
return matcher, delimiter
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_relative 'session'
|
2
|
+
|
3
|
+
module Turnstile
|
4
|
+
module Collector
|
5
|
+
class Flusher < Actor
|
6
|
+
|
7
|
+
actor_name :flusher
|
8
|
+
|
9
|
+
def execute
|
10
|
+
flush_current_buffer unless queue.empty?
|
11
|
+
queue.size
|
12
|
+
rescue Exception => e
|
13
|
+
puts e.backtrace.reverse.join("\n")
|
14
|
+
puts e.inspect.red
|
15
|
+
raise e
|
16
|
+
end
|
17
|
+
|
18
|
+
def flush_current_buffer
|
19
|
+
item = queue.pop
|
20
|
+
return unless item
|
21
|
+
session = parse(item)
|
22
|
+
tracker.track(session.uid,
|
23
|
+
session.platform,
|
24
|
+
session.ip) if session.uid
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse(token)
|
28
|
+
# platform, IP, user
|
29
|
+
a = token.split(':')
|
30
|
+
|
31
|
+
# session is backwards
|
32
|
+
Session.new(a[2], a[0], a[1])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|