turnstile-rb 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3cd7be07aee113fee488b2839dc8eadc611a3be257e6c37a7de50618e55fce94
4
+ data.tar.gz: f13265ed097e7bb4f4625ed6fcf7da9e45208f33e382c6af0df7157345af3efa
5
+ SHA512:
6
+ metadata.gz: d68205747142bb284446c06221046a8746c7cf301e4d0306624872fc9ecc8a443386325a00581a22add14816822ac08fa98356ae36246bad49e1080001f04f02
7
+ data.tar.gz: 3f84e37343fd99ae8bed3fad5182b5f66c716d945db21e2c1d8be27258a1b97fd26aa5f8696a1f52226ad3a07b303b56ff5ccdf678bef8f2089c8b9dd128253f
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ InstalledFiles
7
+ _yardoc
8
+ coverage
9
+ doc/
10
+ lib/bundler/man
11
+ pkg
12
+ rdoc
13
+ spec/reports
14
+ test/tmp
15
+ test/version_tmp
16
+ tmp
17
+ .idea
18
+ .DS_Store
19
+ coverate
20
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
@@ -0,0 +1,22 @@
1
+ rvm:
2
+ - 2.2.7
3
+ - 2.3.4
4
+ - 2.4.1
5
+ services:
6
+ - redis-server
7
+ env:
8
+ global:
9
+ - CC_TEST_REPORTER_ID=d17e91219ca5c271ca9bdd9eb447611f1be6b750abe8e4bc48146f5dec1154d4
10
+ sudo: false
11
+ language: ruby
12
+ cache: bundler
13
+ before_install: gem install bundler -v 1.15.4
14
+ before_script:
15
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
16
+ - chmod +x ./cc-test-reporter
17
+ - ./cc-test-reporter before-build
18
+ script:
19
+ - bundle exec rspec
20
+ after_script:
21
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
22
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in turnstile.gemspec
6
+ gemspec
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+ #^syntax detection
3
+
4
+ # A sample Guardfile
5
+ # More info at https://github.com/guard/guard#readme
6
+
7
+ guard 'rspec' do
8
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
9
+ watch(%r{^spec/.+_spec\.rb$})
10
+ watch('spec/spec_helper.rb') { "spec" }
11
+ watch(%r{spec/support/.*}) { "spec" }
12
+ end
13
+
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Wanelo, Inc
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,197 @@
1
+ [![Build Status](https://travis-ci.org/kigster/turnstile.svg?branch=master)](https://travis-ci.org/kigster/turnstile)
2
+ [![Test Coverage](https://codeclimate.com/github/wanelo/turnstile/badges/coverage.svg)](https://codeclimate.com/github/kigster/turnstile/coverage)
3
+ [![Code Climate](https://codeclimate.com/github/wanelo/turnstile/badges/gpa.svg)](https://codeclimate.com/github/kigster/turnstile)
4
+ [![Issue Count](https://codeclimate.com/github/wanelo/turnstile/badges/issue_count.svg)](https://codeclimate.com/github/kigster/turnstile)
5
+
6
+ # Turnstile
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.
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.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile (Note that another gem with a competing name is on RubyGems, so you **must** specify the path below):
15
+
16
+ gem 'turnstile-rb'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install turnstile
25
+
26
+ ## Usage
27
+
28
+ ### Tracking
29
+
30
+ Turnstile contains two primary parts: data collection and reporting.
31
+
32
+ Data collection may happen in two way:
33
+
34
+ 1. Synchronously — in real time — i.e. from a web request
35
+ 2. Or asynchronously — by "tailing" the logs on your servers
36
+
37
+ Synchronous tracking is more accurate, supports sampling, but introduces a run-time dependency into your application middleware stack that might not be desirable.
38
+
39
+ Asynchronous tracking has a slight initial setup overhead, but has zero run-time overhead, as the data collection happens outside of the web request.
40
+
41
+ #### Real Time Tracking API
42
+
43
+ With Real Time tracking you can use sampling to _estimate_ the number of online users.
44
+
45
+ * To do sampling, use the ```Turnstile::Tracker#track``` method
46
+ * To store and analyze 100% of your data use ```Turnstile::Adapter#add```.
47
+
48
+ **Example:**
49
+
50
+ ```ruby
51
+ @turnstile = Turnstile::Adapter.new
52
+
53
+ user_id = 12345
54
+ platform = 'desktop'
55
+ ip = '224.247.12.4'
56
+
57
+ # register user
58
+ @turnstile.add(user_id, platform, ip)
59
+ ```
60
+
61
+ Without any further calls to ```track()``` method for this particular user/platform/ip combination, the user is considered _online_ for 60 seconds. Each subsequent call to ```track()``` resets the TTL.
62
+
63
+ #### Offline Log Parsing by "Tailing Logs"
64
+
65
+ If adding latency to a web request is not desirable, another option is to run Turnstile ```turnstile``` process as a daemon on each application server. The daemon "tails" the `production.log` file (or any other file), while scanning for lines matching a particular configurable pattern, and then extracting `user id`, `IP` and `platform` based on another configurable regular expression setting.
66
+
67
+ ##### Data Structure
68
+
69
+ The logging approach expects that you print a special token into your log file, which contains three fields separated by a delimiter:
70
+
71
+ * `x-turnstile` is a hardcoded string used in finding this token,
72
+ * `platform` — ideally a short string, such as 'desktop', 'ios', 'android', etc
73
+ * `remote IP` — is the IP address of the request
74
+ * `user_id` — can be encoded, i.e. digest of user_id)
75
+
76
+ for example, a token such as this:
77
+
78
+ ```
79
+ x-turnstile:desktop:125.4.5.13:3456
80
+ ```
81
+
82
+ is colon-separated, and easily extractable from the log.
83
+
84
+ ##### Log File Formats
85
+
86
+ Turnstile supports two primary formats:
87
+
88
+ 1. JSON format, where each **log line** contains the following JSON fields:
89
+
90
+ ```json
91
+ { "user_id" :17344742,
92
+ "platform" :"iphone",
93
+ "session_id" :"4eKMZJ4nggzvkix29zpS",
94
+ "ip_address" :"70.210.128.241",
95
+ .... }
96
+ ```
97
+
98
+ 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.
99
+
100
+ ```
101
+ 2017-10-06 11:03:21 x-turnstile|desktop|124.5.4.3|234324 GET /...
102
+ ```
103
+
104
+ You can specify the file format using the `-t | --file-type` switch.
105
+
106
+ Possible values are:
107
+
108
+ * `json_formatted`
109
+ * `pipe_delimited`
110
+ * `colon_delimited`
111
+ * `comma_delimited`
112
+
113
+ You can also pass the token delimiter on the command line, `-D | --delimiter "," ` in which case the `delimited` file type is used, with your custom delimiter.
114
+
115
+ > NOTE: Default format is **`pipe_delimited`**.
116
+
117
+ ### Examples
118
+
119
+ For example:
120
+
121
+ ```bash
122
+ > gem install turnstile
123
+
124
+ > turnstile -v -f log/production/log -t json_formatted | \
125
+ tee -a /var/log/turnstile.log
126
+
127
+ > turnstile -v -f log/production/log -h 127.0.0.1 -p 6432 | \
128
+ tee -a /var/log/turnstile.log
129
+
130
+ 2014-04-12 05:16:41 -0700: updater:flush - nothing to flush, sleeping 6s..
131
+ 2014-04-12 05:16:41 -0700: updater:queue - nothing in the queue, sleeping 5s...
132
+ 2014-04-12 05:16:41 -0700: log-reader - starting to tail file log....
133
+ 2014-04-12 05:16:46 -0700: updater:queue - nothing in the queue, sleeping 5s...
134
+ 2014-04-12 05:16:53 -0700: updater:flush - nothing to flush, sleeping 6s..
135
+ 2014-04-12 05:16:56 -0700: updater:queue - ( 0.65ms) caching [746] keys locally
136
+ 2014-04-12 05:16:59 -0700: updater:flush - ( 91.73ms) flushing cache with [602] keys
137
+ 2014-04-12 05:17:05 -0700: updater:flush - nothing to flush, sleeping 6s..
138
+ ^Ctrl-C
139
+ ```
140
+
141
+ Note that ideally you should run ```turnstile``` on all app servers, for completeness, and because
142
+ this does not incur any additional cost for the application (as user tracking is happening outside web request).
143
+
144
+ ### Reporting
145
+
146
+ Once the tracking information is sent, the data can be queried.
147
+
148
+ If you used sampling, then you should query using ```Turnstile::Observer``` class that provides
149
+ exprapolation of the results based on sample size configuration.
150
+ ```ruby
151
+ # Return data for sampled users and the summary
152
+ Turnstile::Observer.new.stats
153
+ # => { stats: { total: 3, platforms: 2 }, users: [ { uid: 1, platform: 'desktop', ip: '123.2.4.54' }, ... ]
154
+ ```
155
+ If you did not use sampling, you can get some answers from the ```Turnstile::Adapter``` class:
156
+ ```ruby
157
+ Turntstile::Adapter.new.fetch
158
+ # => [ { uid: 213, :platform: 'desktop', '123.2.4.54' }, { uid: 215, ... } ]
159
+ ```
160
+ You can also request an aggregate results, suitable for sending to graphing systems or displaying on a dashboard:
161
+ ```ruby
162
+ Turntstile::Adapter.new.aggregate
163
+ # => { 'desktop' => 234, 'ios' => 3214, ..., 'total' => 4566 }
164
+ ```
165
+
166
+ ## Circonus NAD Integration
167
+
168
+ We use Circonus to collect and graph data. You can use ```turnstile```
169
+ to dump the current aggregate statistics from redis to standard output,
170
+ which is a tab-delimited format consumable by the nad daemon.
171
+
172
+ (below output is formatted to show tabs as aligned for readability).
173
+
174
+ ```ruby
175
+ > turnstile --summary
176
+
177
+ turnstile.iphone n 383
178
+ turnstile.ipad n 34
179
+ turnstile.android n 108
180
+ turnstile.ipod_touch n 34
181
+ turnstile.unknown n 36
182
+ turnstile.total n 595
183
+ ```
184
+
185
+ ## TODO:
186
+
187
+ * Allow users of the gem to easier customize log reader to fit their own custom log files
188
+ * Export configuration into a YAML file and load from there by defaul
189
+ * Refactor commands to have a single ```turnstile``` CLI with sub-commands ```watch``` and ```report```.
190
+
191
+ ## Contributing
192
+
193
+ 1. Fork it ( http://github.com/<my-github-username>/turnstile/fork )
194
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
195
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
196
+ 4. Push to the branch (`git push origin my-new-feature`)
197
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3
+ require 'rubygems'
4
+ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
5
+ require 'turnstile'
6
+ require 'optparse'
7
+ require 'colored2'
8
+
9
+ options = {}
10
+ ARGV << '-?' if ARGV.empty?
11
+
12
+ Colored2.disable! unless STDOUT.tty?
13
+
14
+ DESCRIPTION = <<-EOF
15
+ Turnstile consists of two components:
16
+
17
+ 1. A daemon process started with #{'turnstile -f <log-file> [ --daemon ]'.bold.green}
18
+ This process should run on every server that generates a log
19
+ file, and they should all connect to the same Redis server.
20
+
21
+ 2. Query command, ran as #{'turnstile --summary'.bold.green}
22
+
23
+ In the first form, turnstile can tail a given log file,
24
+ parsing and tallying and periodically syncing to the master
25
+ Redis server.
26
+
27
+ When the query is invoked, data from all reporters is shown on STDOUT.
28
+ EOF
29
+
30
+ OptionParser.new do |opts|
31
+ opts.banner = "Usage:\n".bold.magenta +
32
+ " turnstile -f <file> [ --daemon ] [options]\n".green +
33
+ " turnstile --summary [options]\n".green
34
+
35
+ opts.separator "Description:".bold.magenta
36
+ opts.separator ' ' + DESCRIPTION.gsub(/\n/, "\n ")
37
+
38
+ opts.separator "Log File Specification:".bold.magenta
39
+ opts.on('-f', '--file FILE', 'File to monitor') do |file|
40
+ options[:file] = file
41
+ end
42
+ opts.on('-t', '--file-type TYPE',
43
+ 'Either: json_formatted, pipe_delimited,',
44
+ 'or comma_delimited (default).') do |type|
45
+ options[:filetype] = type
46
+ end
47
+ opts.on('-D', '--delimiter CHAR',
48
+ 'Forces "delimited" file type, and uses ',
49
+ 'the character in the argument as the delimiter') do |v|
50
+ options[:delimiter] = v
51
+ end
52
+ opts.separator "\nRedis Server:".bold.magenta
53
+ opts.on('-h', '--redis-host HOST', 'Redis server host') do |host|
54
+ Turnstile.config.redis_host = host
55
+ end
56
+ opts.on('-p', '--redis-port PORT', 'Redis server port') do |port|
57
+ Turnstile.config.redis_port = port
58
+ end
59
+ opts.on('-n', '--redis-db DB', 'Redis server db') do |db|
60
+ Turnstile.config.redis_db = db
61
+ end
62
+ opts.separator "\nMode of Operation:".bold.magenta
63
+ opts.on('-d', '--daemonize', 'Daemonize to watch the logs') do |v|
64
+ options[:daemonize] = true
65
+ end
66
+ opts.on('-s', '--summary', 'Print current stats (using NAD format) and exit') do |v|
67
+ options[:summary] = true
68
+ end
69
+ opts.separator "\nTiming Adjustments:".bold.magenta
70
+ opts.on('-b', '--buffer-interval INTERVAL', 'Buffer for this many seconds') do |v|
71
+ options[:buffer_interval] = v.to_i
72
+ end
73
+ opts.on('-i', '--flush-interval INTERVAL', 'Flush then sleep for this many seconds') do |v|
74
+ options[:flush_interval] = v.to_i
75
+ end
76
+ opts.separator "\nMiscellaneous:".bold.magenta
77
+ opts.on('-v', '--verbose', 'Print status to stdout') do |v|
78
+ options[:debug] = true
79
+ end
80
+ opts.on_tail('-?', '--help', 'Show this message') do
81
+ puts opts
82
+ exit
83
+ end
84
+ end.parse!
85
+
86
+ if options[:summary]
87
+ STDOUT.puts Turnstile::Nad.new.data
88
+ else
89
+ Turnstile::Collector::Runner.new(options).run
90
+ end
91
+
@@ -0,0 +1,19 @@
1
+ require 'turnstile/version'
2
+ require 'turnstile/configuration'
3
+ require 'turnstile/sampler'
4
+ require 'turnstile/adapter'
5
+ require 'turnstile/tracker'
6
+ require 'turnstile/observer'
7
+ require 'turnstile/logger'
8
+ require 'turnstile/collector'
9
+ require 'turnstile/nad'
10
+
11
+ module Turnstile
12
+ def self.configure(&block)
13
+ @configuration = Turnstile::Configuration.new.configure(&block)
14
+ end
15
+
16
+ def self.config
17
+ @configuration ||= Turnstile::Configuration.new
18
+ end
19
+ end
@@ -0,0 +1,60 @@
1
+ require 'redis'
2
+ require 'timeout'
3
+
4
+ module Turnstile
5
+ class Adapter
6
+ attr_accessor :redis
7
+ include Timeout
8
+
9
+ def initialize
10
+ self.redis = ::Redis.new(host: config.redis.host,
11
+ port: config.redis.port,
12
+ db: config.redis.db)
13
+ end
14
+
15
+ def add(uid, platform, ip)
16
+ key = compose_key(uid, platform, ip)
17
+ timeout(config.redis.timeout) do
18
+ redis.setex(key, config.activity_interval, 1)
19
+ end
20
+ rescue StandardError => e
21
+ Turnstile::Logger.log "exception while writing to redis: #{e.inspect}"
22
+ end
23
+
24
+ def fetch
25
+ redis.keys('t:*').map do |key|
26
+ fields = key.split(':')
27
+ {
28
+ uid: fields[1],
29
+ platform: fields[2],
30
+ ip: fields[3],
31
+ }
32
+ end
33
+ end
34
+
35
+ def aggregate
36
+ redis.keys('t:*').inject({}) { |hash, key| increment_platform(hash, key) }.tap do |h|
37
+ h['total'] = h.values.inject(&:+) || 0
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def increment_platform(hash, key)
44
+ platform = key.split(':')[2]
45
+ hash[platform] ||= 0
46
+ hash[platform] += 1
47
+ hash
48
+ end
49
+
50
+ def config
51
+ Turnstile.config
52
+ end
53
+
54
+
55
+ def compose_key(uid, platform = nil, ip = nil)
56
+ "t:#{uid}:#{platform}:#{ip}"
57
+ end
58
+
59
+ end
60
+ end