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.
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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 64573d2b1f394251482822e130a7d9ca777554d3
4
- data.tar.gz: ef96805bb0b4dc31541894c950b102e389a6cc05
2
+ SHA256:
3
+ metadata.gz: 0da02f1388bebec96709fa3da4b237960a3bc83a9ef8320921836933b9e1e540
4
+ data.tar.gz: 31193b223254facb6ee7863dfe21811da38dc0874cccfc7d624d1b1aea38d968
5
5
  SHA512:
6
- metadata.gz: 3a66e638ee0a591f9ea4acc1c72f9b20e59a35f682bb147e0bf688f57b7d4e2ee36b0e97e540168218351fc875f22f253a1bad00c4fcc414b689d4e238be6c09
7
- data.tar.gz: 139bf132ff6ab3f18f31f9f4196484889974a1eb7137556f1963e044c1cbffb3c61cfc731d5b4b204b7a4ae8ad51a77a2067279155feab5974f37355b57aac44
6
+ metadata.gz: d0ea0957e804292a8cab5c788afefb7a8eb5299448fcc71894383dc3d6e8c03ae01af1b4f0cde1010a57e2b04a0a8c7930d2c4c4a311f89af01e389598be005f
7
+ data.tar.gz: 6ec94cb380a8860f2b952adc95d87abbd716deea8823c2e4d7fa2299f82975de90ceb6eb86ce3e130df3b5d468b532a90867d7a73b57caefc0a173c35c3a4a7e
data/.gitignore CHANGED
@@ -20,3 +20,6 @@ coverate
20
20
  Gemfile.lock
21
21
  .ruby-version
22
22
  .rake_tasks*
23
+ *.log
24
+ **/log
25
+
@@ -1,7 +1,8 @@
1
1
  rvm:
2
- - 2.2.7
2
+ - 2.2.8
3
3
  - 2.3.4
4
4
  - 2.4.1
5
+ - 2.5.0
5
6
  services:
6
7
  - redis-server
7
8
  env:
@@ -19,4 +20,3 @@ script:
19
20
  - bundle exec rspec
20
21
  after_script:
21
22
  - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
22
-
@@ -1,4 +1,5 @@
1
- Copyright (c) 2012 Wanelo, Inc
1
+ Copyright © 2012 Wanelo, Inc
2
+ Copyright © 2018 Konstantin Gredeskoul
2
3
 
3
4
  MIT License
4
5
 
data/README.md CHANGED
@@ -5,30 +5,175 @@
5
5
 
6
6
  # Turnstile
7
7
 
8
- The goal of this gem is to provide near real time tracking and reporting on the number of users currently online and accessing given application. It requires that the reporting layer is able to uniquely identify each user and provide a unique identifier. It may also optionally assign another dimension to the users accessing, such as, for example, _platform_ -- which in our case denotes how the user is accessing our application: from desktop browser, iOS app, Android app, mobile web, etc. But any other partitioning schemee can be used, or none at all.
8
+ The goal of this gem is to provide near real time tracking and reporting on the number of users currently online and accessing given application. It requires that the reporting layer is able to uniquely identify each user. You may also add one another dimension to the tracking, such as i.e. a _platform_ a coded device or device type the user is using.
9
9
 
10
- The gem uses (and depends on) a [Redis](http://redis.io/) instance in order to keep track of _unique_ users, and can operate in **online* mode (tracking users from a Rack Middleware) or **offline**, by taling a file log.
10
+ For example, you might support platforms: `ios`, `android`, `macos`, `windows`, etc.
11
+
12
+ The gem uses [Redis](http://redis.io/) in order to keep track of the data, and can operate in either the **online* mode** (tracking users from a Rack Middleware) or **offline mode**, by taling an application file log file and searching for a particular pattern.
11
13
 
12
14
  ## Installation
13
15
 
14
16
  Add this line to your application's Gemfile:
15
17
 
16
18
  $ gem install turnstile-rb
19
+ $ turnstile --help
17
20
 
18
21
  ## Usage
19
22
 
20
- ### Tracking
23
+ The gem provides command line interface shown below:
24
+
25
+
26
+ ### Description
27
+
28
+ Turnstile is a Redis-based library that can accurately track total number
29
+ of concurrent users accessing a web/API based server application. It can
30
+ break it down by "platform" or a device type, and returns data in JSON,
31
+ CSV of NAD formats. While user tracking may happen synchronously using a
32
+ Rack middleware, another method is provided that is based on log file
33
+ analysis, and can therefore be performed outside web server process.
34
+
35
+ ### Usage
36
+
37
+ ```
38
+ # Tail the log file as a proper daemon, and optionally HTTP end point
39
+ turnstile -f <file> [ --daemon ] [ --web PORT ] [ options ]
40
+
41
+ # Add a single item and exit
42
+ turnstile -a 'platform:ip:user' [ options ]
43
+
44
+ # Print the summary stats and exit
45
+ turnstile -s [ json | csv | nad ] [ options ]
46
+ ```
47
+
48
+ ### Details
49
+
50
+ Turnstile can be run as a daemon, in which case it watches a given log
51
+ file. Or, you can run turnstile to print the current aggregated stats in
52
+ several supported formats, such as JSON.
53
+
54
+ When Turnstile is used to tail the log files, ideally you should start
55
+ turnstile daemon on each app sever that's generating log file, or be
56
+ content with the effects of sampling.
57
+
58
+ Note that the IP address is not required to track uniqueness. Only
59
+ platform and UID are used. Also note that custom formatter can be
60
+ specified in a config file to parse arbitrary complex log lines.
61
+
62
+
63
+ For tailing a log files, Turnstile must first match a log line expected to
64
+ contain the tokens, and then extract is using one of the matchers. You can
65
+ specify which matcher to use depending on whether you can add Turnstile's
66
+ tokens to your log or not. If you can, great! If not, implement your own
67
+ custom matcher and great again.
68
+
69
+ The following matchers are available, and can be selected with -F:
70
+
71
+ 1. Format named `delimited`, which expects the following token in the
72
+ log file:
73
+
74
+ x-turnstile:platform:ip:user-identifier
75
+
76
+ Where ':' (the delimiter) can be set via -l option, OR you can use one
77
+ of the following formats: `json_formatted`, `pipe_formatted`,
78
+ `comma_formatted`, `colon_formatted` (the default). The match is
79
+ performed on a string `x-turnstile`, other log lines are skipped.
80
+
81
+ 2. Format `json_delimited`, which expects to find a single-line JSON
82
+ Hash, containing keys `platform`, `ip_address`, and `user_id`. The match
83
+ is performed on any log line containing string '`ip_address`', other
84
+ lines are skipped.
85
+
86
+ 3. Format `custom` requires passing an additional flag `-c/--config
87
+ file.rb`, which will be required, and which can define a matcher and
88
+ assign it to the `Turnstile.config.custom_matcher` config variable.
89
+
90
+ Custom matcher is any object that responds to two methods:
91
+
92
+ * `matches?(line)` — must return true if the line is to be used in token extraction, and
93
+ * `tokenlize(line)` — must return a string of the form `platform:ip:user-id` as a result of parsing the `line`.
94
+
95
+ ### Command Line Options
96
+
97
+ The following flags are available:
98
+
99
+ ```
100
+ Mode of Operation:
101
+ -f, --file FILE Starts Turnstile in a file-tailing mode
102
+
103
+ -s, --show [FORMAT] Print current stats and exit. Optional
104
+ format can be "json" (default), "nad",
105
+ "yaml", or "csv"
106
+
107
+ -w, --web [PORT] Starts a Sinatra app on a given port
108
+ offering /turnstile/<json|yaml> end point.
109
+ Can be used with file tailing mode, or
110
+ standalone. Default port is 9090
111
+
112
+ -a, --add TOKEN Registers an event from the token, such as
113
+ "ios:123.4.4.4:32442". Use -l to customize
114
+ the delimiter
115
+
116
+ -p, --print-keys Prints all Turnstile keys in Redis
117
+
118
+ --flushdb Wipes Redis database, and exit
119
+
120
+ Tailing log file:
121
+ -d, --daemonize Daemonize to watch the logs
122
+
123
+ -b, --read-backwards [LINES] Like tail, read last LINES lines
124
+ instead of tailing from the end
125
+
126
+ -F, --format FORMAT Log file format (see above)
127
+
128
+ -l, --delimiter CHAR Forces "delimited" file type, and
129
+ uses CHAR as the delimiter
130
+
131
+ -c, --config FILE Ruby config file that can define the
132
+ custom matcher, supporting arbitrary
133
+ complex logs
134
+
135
+ Redis Server:
136
+ -r, --redis-url URL Redis server URL
137
+ --redis-host HOST Redis server host
138
+ --redis-port PORT Redis server port
139
+ --redis-db DB Redis server db
140
+ --hiredis Use hiredis high performance library
141
+
142
+ Miscellaneous:
143
+ -i, --idle-sleep SECONDS When no work was detected, pause the
144
+ threads for several seconds.
145
+ -v, --verbose Print status to stdout
146
+ -t, --trace Enable trace mode
147
+ -h, --help Show this message
148
+
149
+ ```
150
+
151
+ To summarize, you can run `turnstile` in order to:
152
+
153
+ * start a daemon to tail a log file
154
+ * optionally start a Sinatra web server on port 9090, that returns aggregated results to `/turnstile/json`
155
+ * to print results
156
+ * to print all redis keys
157
+ * to reset all data
158
+ * to add new data
159
+
160
+ ### Data Collection and Reporting
21
161
 
22
162
  Turnstile contains two primary parts: data collection and reporting.
23
163
 
24
164
  Data collection may happen in two way:
25
165
 
26
- 1. Synchronously — in real time — i.e. from a web request
27
- 2. Or asynchronously — by "tailing" the logs on your servers
166
+ 1. Synchronously — in real time — i.e. from a web request
167
+ 2. Or asynchronously — by "tailing" the logs on your servers
168
+
169
+ *Synchronous tracking* is accurate, supports sampling, but introduces a run-time dependency into your application middleware stack that might not be desirable. This is how `Rack::Attack` and other similar middleware operates. Luckily, there is a better way.
28
170
 
29
- Synchronous tracking is more accurate, supports sampling, but introduces a run-time dependency into your application middleware stack that might not be desirable.
171
+ *Asynchronous tracking* has a slight initial setup overhead and it requires a consistently formatted log file that includes lines with the platform, IP and user ID for ALL users at least some of the time. This method introduces zero run-time overhead, as the data collection happens outside of the web request in a standalone daemon, that simply tails your log files.
172
+
173
+ On high performance web applications it is highly recommended to employ the *asynchronous tracking* via the log file, and possibly — a custom matcher.
174
+
175
+ > NOTE: as of Turnstile version 3, you can define a [custom matcher](#custom-matcher) in a ruby-syntax configuration file. Coupled with `-F custom` you are then able to parse arbitrary complex log files and extract the three tokens needed by Turnstile: the `platform` enum type, `IP addresss`, and `User ID`.
30
176
 
31
- Asynchronous tracking has a slight initial setup overhead, but has zero run-time overhead, as the data collection happens outside of the web request.
32
177
 
33
178
  #### Real Time Tracking API
34
179
 
@@ -90,8 +235,8 @@ Turnstile supports two primary formats:
90
235
  { "user_id" :17344742,
91
236
  "platform" :"iphone",
92
237
  "session_id" :"4eKMZJ4nggzvkix29zpS",
93
- "ip_address" :"70.210.128.241",
94
- .... }
238
+ "ip_address" :"70.210.128.241"
239
+ }
95
240
  ```
96
241
 
97
242
  2. Plain text format, where lines are space delimited, and the token is one of the fields of your log line, itself delimited using a configurable character.
@@ -113,6 +258,51 @@ You can also pass the token delimiter on the command line, `-D | --delimiter ","
113
258
 
114
259
  > NOTE: Default format is **`pipe_delimited`**.
115
260
 
261
+ <a name="custom-matcher"></a>
262
+
263
+ ### Custom Log Matchers
264
+
265
+ > This feature is only available in Turnstile version 3.0 or later.
266
+
267
+ To be able to tail a structured log file in any format, create a ruby config file, and pass it with `-c <file>`.
268
+
269
+ For example, below we'll define a custom matcher that extracts our token from a CSV log file.
270
+
271
+ ```
272
+ # config/custom_csv_matcher.rb
273
+
274
+ # This matcher extracts platform, UID and IP from the following CSV string:
275
+ # 2018-05-02 21:51:44.031,25928,3997,th-M4wDQM4w0,web,j5v-dzg0J,69.181.72.240,e2b1be795372c385c92a7df420752992
276
+
277
+ class CSVMatcher
278
+ def tokenize(line)
279
+ words = line.split(',')
280
+ platform, ip, uid = [ words[4], web[6], web[7] ]
281
+ [platform, ip, uid].join(':')
282
+ end
283
+
284
+ def matches?(line)
285
+ line =~ /x-rqst/
286
+ end
287
+ end
288
+
289
+ Turnstile.config.custom_matcher = CSVMatcher.new
290
+ ```
291
+
292
+ The above matcher defines a very simple block that receives a string as input, and returns a string which is a combined value from:
293
+
294
+ ```ruby
295
+ "#{platform}:#{ip}:#{user_id}"
296
+ ```
297
+
298
+ For example, `ios:1.4.54.25:fg7988798779` is a valid token. Note that IP addresses are not necessary for the purposes of tracking, and can be omitted.
299
+
300
+ With the above file defined, we would start turnstile's collector process as follows, tailing the CSV log file:
301
+
302
+ ```bash
303
+ turnstile -f log/production_log.csv -F custom -c config/custom_csv_matcher.rb
304
+ ```
305
+
116
306
  ### Examples
117
307
 
118
308
  For example:
@@ -137,8 +327,7 @@ For example:
137
327
  ^Ctrl-C
138
328
  ```
139
329
 
140
- Note that ideally you should run ```turnstile``` on all app servers, for completeness, and because
141
- this does not incur any additional cost for the application (as user tracking is happening outside web request).
330
+ Note that ideally you should run ```turnstile``` on all app servers, for completeness, and because this does not incur any additional cost for the application (as user tracking is happening outside web request).
142
331
 
143
332
  ### Reporting
144
333
 
data/Rakefile CHANGED
@@ -8,6 +8,7 @@ end
8
8
 
9
9
  task :permissions do
10
10
  shell('rm -rf pkg/')
11
+ shell('rm -f Gemfile.lock')
11
12
  shell("chmod -v o+r,g+r * */* */*/* */*/*/* */*/*/*/* */*/*/*/*/*")
12
13
  shell("find . -type d -exec chmod o+x,g+x {} \\;")
13
14
  end
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
- ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
2
 
4
- require 'rubygems'
5
- require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
6
- require 'turnstile'
3
+ lib_path = File.expand_path(File.dirname(__FILE__) + '/../lib')
4
+ $LOAD_PATH << lib_path if File.exist?(lib_path) && !$LOAD_PATH.include?(lib_path)
7
5
 
8
- require 'turnstile/runner'
6
+ require 'turnstile'
9
7
 
10
- Turnstile::Runner.new(ARGV).execute!
8
+ Turnstile::CLI::Runner.new(ARGV).execute!
11
9
 
@@ -0,0 +1,22 @@
1
+ # For a log file of the following format:
2
+ #
3
+ # I│2018-05-02 07:47:58.770 │ 25887 ⤶ 3997 │ th-M4wDQDbBB ❯❯❯ time: 73.8ms ✔ │ x-rqst │ src=android │ uid=0xFFFFD978998789 │ ip=47.23.6.197 │ sess=BGHSsdCsX5VvsN1jLsAR │ REQ GET │ 200 │ /api/v3/dashboard
4
+ # I│2018-05-02 21:51:43.925 │ 29122 ⤶ 3997 │ th-7lxrgeJZA ❯❯❯ time: 80.6ms ✔ │ x-rqst │ src=android │ uid=0xF9909808f90809 │ ip=47.21.6.197 │ sess=BGHSsdCsX5VvsN1jLsAR │ REQ GET │ 200 │ /api/v4/stuff
5
+ #
6
+ module Application
7
+ class CSVLogFormat
8
+ def tokenize(line)
9
+ platform = line.scan(/src=(\w*)/).flatten.first
10
+ ip = line.scan(/ip=(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/).flatten.first
11
+ uid = line.scan(/sess=(\w*)/).flatten.first
12
+
13
+ [platform, ip, uid].join(':')
14
+ end
15
+
16
+ def matches?(line)
17
+ line =~ /x-rqst/
18
+ end
19
+ end
20
+ end
21
+
22
+ Turnstile.config.custom_matcher = Application::CSVLogFormat.new
@@ -1,19 +1,48 @@
1
1
  require 'turnstile/version'
2
2
  require 'turnstile/configuration'
3
+ require 'turnstile/commands'
4
+ require 'turnstile/logger'
5
+ require 'turnstile/dependencies'
3
6
  require 'turnstile/sampler'
4
- require 'turnstile/adapter'
5
7
  require 'turnstile/tracker'
6
8
  require 'turnstile/observer'
7
- require 'turnstile/logger'
9
+ require 'turnstile/redis/adapter'
8
10
  require 'turnstile/collector'
9
- require 'turnstile/summary'
11
+
12
+ require 'turnstile/cli/runner'
10
13
 
11
14
  module Turnstile
12
- def self.configure(&block)
13
- @configuration = Turnstile::Configuration.new.configure(&block)
14
- end
15
+ class CommandNotFoundError < StandardError; end
16
+ class ConfigFileError < StandardError; end
17
+ class HiredisDriverNotFound < StandardError; end
18
+
19
+ class << self
20
+ attr_accessor :debug
21
+
22
+ def debug?
23
+ self.debug
24
+ end
25
+
26
+ def configure(&block)
27
+ @configuration = create_config.configure(&block)
28
+ end
15
29
 
16
- def self.config
17
- @configuration ||= Turnstile::Configuration.new
30
+ def config
31
+ @configuration ||= create_config
32
+ end
33
+
34
+ private
35
+
36
+ def create_config
37
+ ::Turnstile::Configuration.new
38
+ end
18
39
  end
19
40
  end
41
+
42
+ Kernel.send(:define_method, :tdb) do |msg, io = STDOUT|
43
+ io.puts ''.green + ' debug '.black.on.green+ ''.green + ' —— ' + msg
44
+ end
45
+
46
+ Kernel.send(:define_method, :terr) do |msg, io = STDERR|
47
+ io.puts ''.bold.red + ' error '.bold.white.on.red + ''.red + ' —— ' + msg
48
+ end
@@ -0,0 +1,83 @@
1
+ require 'turnstile/dependencies'
2
+
3
+ module Turnstile
4
+ module CLI
5
+ class Launcher
6
+ include Dependencies
7
+
8
+ attr_reader :stdin, :stdout, :stderr, :sinatra_thread
9
+ attr_accessor :options
10
+
11
+ def initialize(options, stdin = STDIN, stdout = STDOUT, stderr = STDERR)
12
+ self.options = options
13
+ @stdin, @stdout, @stderr= stdin, stdout, stderr
14
+ end
15
+
16
+ def launch
17
+ launch_sinatra_app if options[:web]
18
+ launch_signal_handler
19
+
20
+ tdb "config: #{config.to_h}" if Turnstile.config.trace
21
+ result = if options[:show]
22
+ command(:show).execute(options[:show_format] || :json, options[:delimiter])
23
+
24
+ elsif options[:token]
25
+ tracker.track_token(options[:token], options[:delimiter])
26
+
27
+ elsif options[:flushdb]
28
+ command(:flushdb).execute
29
+
30
+ elsif options[:print_keys]
31
+ command(:print_keys).execute
32
+
33
+ elsif options[:file]
34
+ controller.start
35
+ end
36
+
37
+
38
+ puts result if result && !result.empty?
39
+ rescue SystemExit, SignalException
40
+ exit 6
41
+ rescue Exception => e
42
+ handle_error('Error', e)
43
+ ensure
44
+ sinatra_thread.join if sinatra_thread
45
+ end
46
+
47
+ def launch_signal_handler
48
+ Signal.trap('INT') { sleep 1; Kernel.exit(5) }
49
+ end
50
+
51
+ def launch_sinatra_app
52
+ @sinatra_thread = Thread.new do
53
+ require_relative '../web_app'
54
+ Kernel.exit(0)
55
+ end
56
+ end
57
+
58
+ def command(name)
59
+ ::Turnstile::Commands.command(name).new(options)
60
+ end
61
+
62
+ def handle_error(title, e)
63
+ if options[:trace]
64
+ trace = e.backtrace.reverse
65
+ last = trace.pop
66
+ stderr.puts trace.join("\n")
67
+ stderr.puts last.bold.red
68
+ end
69
+ stderr.puts
70
+ stderr.puts title.bold.yellow
71
+ stderr.puts "\t" + e.message.red
72
+ stderr.puts
73
+ end
74
+
75
+ private
76
+
77
+ def controller
78
+ @controller ||= Turnstile::Collector::Controller.new(options)
79
+ end
80
+
81
+ end
82
+ end
83
+ end