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.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +3 -0
  3. data/.travis.yml +2 -2
  4. data/LICENSE.txt +2 -1
  5. data/README.md +200 -11
  6. data/Rakefile +1 -0
  7. data/bin/turnstile +4 -6
  8. data/example/custom_csv_matcher.rb +22 -0
  9. data/lib/turnstile.rb +37 -8
  10. data/lib/turnstile/cli/launcher.rb +83 -0
  11. data/lib/turnstile/cli/parser.rb +166 -0
  12. data/lib/turnstile/cli/runner.rb +58 -0
  13. data/lib/turnstile/collector.rb +2 -2
  14. data/lib/turnstile/collector/actor.rb +81 -0
  15. data/lib/turnstile/collector/controller.rb +121 -0
  16. data/lib/turnstile/collector/flusher.rb +36 -0
  17. data/lib/turnstile/collector/formats.rb +7 -34
  18. data/lib/turnstile/collector/formats/custom_matcher.rb +19 -0
  19. data/lib/turnstile/collector/formats/delimited_matcher.rb +30 -0
  20. data/lib/turnstile/collector/formats/json_matcher.rb +30 -0
  21. data/lib/turnstile/collector/log_reader.rb +84 -46
  22. data/lib/turnstile/collector/{matcher.rb → regexp_matcher.rb} +4 -5
  23. data/lib/turnstile/collector/session.rb +7 -0
  24. data/lib/turnstile/commands.rb +20 -0
  25. data/lib/turnstile/commands/base.rb +20 -0
  26. data/lib/turnstile/commands/flushdb.rb +21 -0
  27. data/lib/turnstile/commands/print_keys.rb +19 -0
  28. data/lib/turnstile/commands/show.rb +89 -0
  29. data/lib/turnstile/configuration.rb +23 -0
  30. data/lib/turnstile/dependencies.rb +31 -0
  31. data/lib/turnstile/logger.rb +9 -40
  32. data/lib/turnstile/logger/helper.rb +42 -0
  33. data/lib/turnstile/logger/provider.rb +74 -0
  34. data/lib/turnstile/observer.rb +5 -9
  35. data/lib/turnstile/redis/adapter.rb +97 -0
  36. data/lib/turnstile/redis/connection.rb +116 -0
  37. data/lib/turnstile/redis/spy.rb +42 -0
  38. data/lib/turnstile/sampler.rb +9 -2
  39. data/lib/turnstile/tracker.rb +14 -14
  40. data/lib/turnstile/version.rb +51 -12
  41. data/lib/turnstile/web_app.rb +29 -0
  42. data/spec/spec_helper.rb +18 -3
  43. data/spec/support/logging.rb +17 -0
  44. data/spec/turnstile/adapter_spec.rb +59 -46
  45. data/spec/turnstile/collector/flusher_spec.rb +16 -0
  46. data/spec/turnstile/collector/log_reader_spec.rb +127 -77
  47. data/spec/turnstile/commands/show_spec.rb +40 -0
  48. data/spec/turnstile/tracker_spec.rb +21 -7
  49. data/spec/turnstile_spec.rb +3 -0
  50. data/turnstile-rb.gemspec +5 -2
  51. metadata +89 -22
  52. data/Gemfile +0 -6
  53. data/lib/turnstile/adapter.rb +0 -61
  54. data/lib/turnstile/collector/runner.rb +0 -72
  55. data/lib/turnstile/collector/updater.rb +0 -86
  56. data/lib/turnstile/parser.rb +0 -107
  57. data/lib/turnstile/runner.rb +0 -54
  58. data/lib/turnstile/summary.rb +0 -57
  59. 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
+
@@ -4,5 +4,5 @@ module Turnstile
4
4
  end
5
5
 
6
6
  require_relative 'collector/log_reader'
7
- require_relative 'collector/updater'
8
- require_relative 'collector/runner'
7
+ require_relative 'collector/flusher'
8
+ require_relative 'collector/controller'
@@ -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