turnstile-rb 2.0.1 → 3.0.2
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 +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
|