rack-statsd 0.1.0

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 (6) hide show
  1. data/LICENSE +22 -0
  2. data/README.md +16 -0
  3. data/Rakefile +141 -0
  4. data/lib/rack-statsd.rb +290 -0
  5. data/rack-statsd.gemspec +61 -0
  6. metadata +61 -0
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License
2
+
3
+ Copyright (c) GitHub, Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
22
+
@@ -0,0 +1,16 @@
1
+ # RackStatsD
2
+
3
+ Some tiny middleware for monitoring Rack apps in production.
4
+
5
+ * RackStatsD::RequestStatus - Adds a status URL for health checks.
6
+ * RackStatsD::RequestHostname - Shows which what code is running on
7
+ which node for a given request.
8
+ * RackStatsD::ProcessUtilization - Tracks how long Unicorns spend
9
+ processing requests. Optionally sends metrics to a StatsD server.
10
+
11
+ Note: The request tracking code isn't thread safe. It should work fine
12
+ for apps on Unicorn.
13
+
14
+ This code has been extracted from GitHub.com and is used on
15
+ http://git.io currently.
16
+
@@ -0,0 +1,141 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
48
+ if false
49
+ require 'rake/testtask'
50
+ Rake::TestTask.new(:test) do |test|
51
+ test.libs << 'lib' << 'test'
52
+ test.pattern = 'test/**/*_test.rb'
53
+ test.verbose = true
54
+ end
55
+ else
56
+ task :test do
57
+ puts "haven't setup tests yet"
58
+ end
59
+ end
60
+
61
+ desc "Open an irb session preloaded with this library"
62
+ task :console do
63
+ sh "irb -rubygems -r ./lib/#{name}.rb"
64
+ end
65
+
66
+ #############################################################################
67
+ #
68
+ # Custom tasks (add your own tasks here)
69
+ #
70
+ #############################################################################
71
+
72
+
73
+
74
+ #############################################################################
75
+ #
76
+ # Packaging tasks
77
+ #
78
+ #############################################################################
79
+
80
+ desc "Create tag v#{version} and build and push #{gem_file} to Rubygems"
81
+ task :release => :build do
82
+ unless `git branch` =~ /^\* master$/
83
+ puts "You must be on the master branch to release!"
84
+ exit!
85
+ end
86
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
87
+ sh "git tag v#{version}"
88
+ sh "git push origin master"
89
+ sh "git push origin v#{version}"
90
+ sh "gem push pkg/#{name}-#{version}.gem"
91
+ end
92
+
93
+ desc "Build #{gem_file} into the pkg directory"
94
+ task :build => :gemspec do
95
+ sh "mkdir -p pkg"
96
+ sh "gem build #{gemspec_file}"
97
+ sh "mv #{gem_file} pkg"
98
+ end
99
+
100
+ desc "Generate #{gemspec_file}"
101
+ task :gemspec => :validate do
102
+ # read spec file and split out manifest section
103
+ spec = File.read(gemspec_file)
104
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
105
+
106
+ # replace name version and date
107
+ replace_header(head, :name)
108
+ replace_header(head, :version)
109
+ replace_header(head, :date)
110
+ #comment this out if your rubyforge_project has a different name
111
+ replace_header(head, :rubyforge_project)
112
+
113
+ # determine file list from git ls-files
114
+ files = `git ls-files`.
115
+ split("\n").
116
+ sort.
117
+ reject { |file| file =~ /^\./ }.
118
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
119
+ map { |file| " #{file}" }.
120
+ join("\n")
121
+
122
+ # piece file back together and write
123
+ manifest = " s.files = %w[\n#{files}\n ]\n"
124
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
125
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
126
+ puts "Updated #{gemspec_file}"
127
+ end
128
+
129
+ desc "Validate #{gemspec_file}"
130
+ task :validate do
131
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
132
+ unless libfiles.empty?
133
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
134
+ exit!
135
+ end
136
+ unless Dir['VERSION*'].empty?
137
+ puts "A `VERSION` file at root level violates Gem best practices."
138
+ exit!
139
+ end
140
+ end
141
+
@@ -0,0 +1,290 @@
1
+ module RackStatsD
2
+ VERSION = "0.1.0"
3
+
4
+ # Simple middleware to add a quick status URL for tools like Nagios.
5
+ class RequestStatus
6
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
7
+ GET = 'GET'.freeze
8
+ PATH_INFO = 'PATH_INFO'.freeze
9
+ STATUS_PATH = '/status'
10
+
11
+ # Initializes the middleware.
12
+ #
13
+ # # Responds with "OK" on /status
14
+ # use RequestStatus, "OK"
15
+ #
16
+ # You can change what URL to look for:
17
+ #
18
+ # use RequestStatus, "OK", "/ping"
19
+ #
20
+ # You can also check internal systems and return something more informative.
21
+ #
22
+ # use RequestStatus, lambda {
23
+ # status = MyApp.status # A Hash of some live counters or something
24
+ # [200, {"Content-Type" => "application/json"}, status.to_json]
25
+ # }
26
+ #
27
+ # app - The next Rack app in the pipeline.
28
+ # callback_or_response - Either a Proc or a Rack response.
29
+ # status_path - Optional String path that returns the status.
30
+ # Default: "/status"
31
+ #
32
+ # Returns nothing.
33
+ def initialize(app, callback_or_response, status_path = nil)
34
+ @app = app
35
+ @status_path = (status_path || STATUS_PATH).freeze
36
+ @callback = callback_or_response
37
+ end
38
+
39
+ def call(env)
40
+ if env[REQUEST_METHOD] == GET
41
+ if env[PATH_INFO] == @status_path
42
+ if @callback.respond_to?(:call)
43
+ return @callback.call
44
+ else
45
+ return @callback
46
+ end
47
+ end
48
+ end
49
+
50
+ @app.call env
51
+ end
52
+ end
53
+
54
+ # Simple middleware that adds the current host name and current git SHA to
55
+ # the response headers. This can help diagnose problems by letting you
56
+ # know what code is running from what machine.
57
+ class RequestHostname
58
+ # Initializes the middlware.
59
+ #
60
+ # app - The next Rack app in the pipeline.
61
+ # options - Hash of options.
62
+ # :host - String hostname.
63
+ # :revision - String SHA that describes the version of code
64
+ # this process is running.
65
+ #
66
+ # Returns nothing.
67
+ def initialize(app, options = {})
68
+ @app = app
69
+ @host = options[:host] || `hostname -s`.chomp
70
+ @sha = options[:revision] || '<none>'
71
+ end
72
+
73
+ def call(env)
74
+ status, headers, body = @app.call(env)
75
+ headers['X-Node'] = @host
76
+ headers['X-Revision'] = @sha
77
+ [status, headers, body]
78
+ end
79
+ end
80
+
81
+ # Middleware that tracks the amount of time this process spends processing
82
+ # requests, as opposed to being idle waiting for a connection. Statistics
83
+ # are dumped to rack.errors every 5 minutes.
84
+ #
85
+ # NOTE This middleware is not thread safe. It should only be used when
86
+ # rack.multiprocess is true and rack.multithread is false.
87
+ class ProcessUtilization
88
+ # Initializes the middleware.
89
+ #
90
+ # app - The next Rack app in the pipeline.
91
+ # domain - The String domain name the app runs in.
92
+ # revision - The String SHA that describes the current version of code.
93
+ # options - Hash of options.
94
+ # :window - The Integer number of seconds before the
95
+ # horizon resets.
96
+ # :stats - Optional StatsD client.
97
+ # :hostname - Optional String hostname. Set to nil
98
+ # to exclude.
99
+ # :stats_prefix - Optional String prefix for StatsD keys.
100
+ # Default: "rack"
101
+ def initialize(app, domain, revision, options = {})
102
+ @app = app
103
+ @domain = domain
104
+ @revision = revision
105
+ @window = options[:window] || 100
106
+ @horizon = nil
107
+ @active_time = nil
108
+ @requests = nil
109
+ @total_requests = 0
110
+ @worker_number = nil
111
+ @track_gc = GC.respond_to?(:time)
112
+
113
+ if @stats = options[:stats]
114
+ prefix = [options[:stats_prefix] || :rack]
115
+ if options.has_key?(:hostname)
116
+ prefix << options[:hostname] unless options[:hostname].nil?
117
+ else
118
+ prefix << `hostname -s`.chomp
119
+ end
120
+ @stats_prefix = prefix.join(".")
121
+ end
122
+ end
123
+
124
+ # the app's domain name - shown in proctitle
125
+ attr_accessor :domain
126
+
127
+ # the currently running git revision as a 7-sha
128
+ attr_accessor :revision
129
+
130
+ # time when we began sampling. this is reset every once in a while so
131
+ # averages don't skew over time.
132
+ attr_accessor :horizon
133
+
134
+ # total number of requests that have been processed by this worker since
135
+ # the horizon time.
136
+ attr_accessor :requests
137
+
138
+ # decimal number of seconds the worker has been active within a request
139
+ # since the horizon time.
140
+ attr_accessor :active_time
141
+
142
+ # total requests processed by this worker process since it started
143
+ attr_accessor :total_requests
144
+
145
+ # the unicorn worker number
146
+ attr_accessor :worker_number
147
+
148
+ # the amount of time since the horizon
149
+ def horizon_time
150
+ Time.now - horizon
151
+ end
152
+
153
+ # decimal number of seconds this process has been active since the horizon
154
+ # time. This is the inverse of the active time.
155
+ def idle_time
156
+ horizon_time - active_time
157
+ end
158
+
159
+ # percentage of time this process has been active since the horizon time.
160
+ def percentage_active
161
+ (active_time / horizon_time) * 100
162
+ end
163
+
164
+ # percentage of time this process has been idle since the horizon time.
165
+ def percentage_idle
166
+ (idle_time / horizon_time) * 100
167
+ end
168
+
169
+ # number of requests processed per second since the horizon
170
+ def requests_per_second
171
+ requests / horizon_time
172
+ end
173
+
174
+ # average response time since the horizon in milliseconds
175
+ def average_response_time
176
+ (active_time / requests.to_f) * 1000
177
+ end
178
+
179
+ # called exactly once before the first request is processed by a worker
180
+ def first_request
181
+ reset_horizon
182
+ record_worker_number
183
+ end
184
+
185
+ # resets the horizon and all dependent variables
186
+ def reset_horizon
187
+ @horizon = Time.now
188
+ @active_time = 0.0
189
+ @requests = 0
190
+ end
191
+
192
+ # extracts the worker number from the unicorn procline
193
+ def record_worker_number
194
+ if $0 =~ /^.* worker\[(\d+)\].*$/
195
+ @worker_number = $1.to_i
196
+ else
197
+ @worker_number = nil
198
+ end
199
+ end
200
+
201
+ # the generated procline
202
+ def procline
203
+ "unicorn %s[%s] worker[%02d]: %5d reqs, %4.1f req/s, %4dms avg, %5.1f%% util" % [
204
+ domain,
205
+ revision,
206
+ worker_number.to_i,
207
+ total_requests.to_i,
208
+ requests_per_second.to_f,
209
+ average_response_time.to_i,
210
+ percentage_active.to_f
211
+ ]
212
+ end
213
+
214
+ # called immediately after a request to record statistics, update the
215
+ # procline, and dump information to the logfile
216
+ def record_request(status)
217
+ now = Time.now
218
+ diff = (now - @start)
219
+ @active_time += diff
220
+ @requests += 1
221
+
222
+ $0 = procline
223
+
224
+ if @stats
225
+ @stats.timing("#{@stats_prefix}.response_time", diff * 1000)
226
+ if suffix = status_suffix(status)
227
+ @stats.increment "#{@stats_prefix}.status_code.#{status_suffix(status)}"
228
+ end
229
+ if @track_gc && GC.time > 0
230
+ @stats.timing "#{@stats_prefix}.gc.time", GC.time / 1000
231
+ @stats.count "#{@stats_prefix}.gc.collections", GC.collections
232
+ end
233
+ end
234
+
235
+ reset_horizon if now - horizon > @window
236
+ rescue => boom
237
+ warn "ProcessUtilization#record_request failed: #{boom}"
238
+ end
239
+
240
+ def status_suffix(status)
241
+ suffix = case status.to_i
242
+ when 400 then :bad_request
243
+ when 401 then :unauthorized
244
+ when 404 then :missing
245
+ when 422 then :invalid
246
+ when 503 then :node_down
247
+ when 500 then :error
248
+ end
249
+ end
250
+
251
+ # Body wrapper. Yields to the block when body is closed. This is used to
252
+ # signal when a response is fully finished processing.
253
+ class Body
254
+ def initialize(body, &block)
255
+ @body = body
256
+ @block = block
257
+ end
258
+
259
+ def each(&block)
260
+ @body.each(&block)
261
+ end
262
+
263
+ def close
264
+ @body.close if @body.respond_to?(:close)
265
+ @block.call
266
+ nil
267
+ end
268
+ end
269
+
270
+ # Rack entry point.
271
+ def call(env)
272
+ @start = Time.now
273
+ GC.clear_stats if @track_gc
274
+
275
+ @total_requests += 1
276
+ first_request if @total_requests == 1
277
+
278
+ env['process.request_start'] = @start.to_f
279
+ env['process.total_requests'] = total_requests
280
+
281
+ # newrelic X-Request-Start
282
+ env.delete('HTTP_X_REQUEST_START')
283
+
284
+ status, headers, body = @app.call(env)
285
+ body = Body.new(body) { record_request(status) }
286
+ [status, headers, body]
287
+ end
288
+ end
289
+ end
290
+
@@ -0,0 +1,61 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'rack-statsd'
16
+ s.version = '0.1.0'
17
+ s.date = '2011-12-14'
18
+ s.rubyforge_project = 'rack-statsd'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "Tools for monitoring Rack apps in production."
23
+ s.description = "Tools for monitoring Rack apps in production."
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["Ryan Tomayko", "Rick Olson"]
29
+ s.email = 'technoweenie@gmail.com'
30
+ s.homepage = 'https://github.com/github/rack-statsd'
31
+
32
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
33
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
34
+ s.require_paths = %w[lib]
35
+
36
+ ## List your runtime dependencies here. Runtime dependencies are those
37
+ ## that are needed for an end user to actually USE your code.
38
+ #s.add_dependency('rack', "~> 1.2.6")
39
+
40
+ ## List your development dependencies here. Development dependencies are
41
+ ## those that are only needed during development
42
+ s.add_development_dependency('rack-test')
43
+
44
+ ## Leave this section as-is. It will be automatically generated from the
45
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
46
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
47
+ # = MANIFEST =
48
+ s.files = %w[
49
+ LICENSE
50
+ README.md
51
+ Rakefile
52
+ lib/rack-statsd.rb
53
+ rack-statsd.gemspec
54
+ ]
55
+ # = MANIFEST =
56
+
57
+ ## Test files will be grabbed from the file list. Make sure the path glob
58
+ ## matches what you actually use.
59
+ s.test_files = s.files.select { |path| path =~ /^test\/.*_test\.rb/ }
60
+ end
61
+
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-statsd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan Tomayko
9
+ - Rick Olson
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2011-12-14 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack-test
17
+ requirement: &70279099842980 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *70279099842980
26
+ description: Tools for monitoring Rack apps in production.
27
+ email: technoweenie@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - LICENSE
33
+ - README.md
34
+ - Rakefile
35
+ - lib/rack-statsd.rb
36
+ - rack-statsd.gemspec
37
+ homepage: https://github.com/github/rack-statsd
38
+ licenses: []
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ requirements: []
56
+ rubyforge_project: rack-statsd
57
+ rubygems_version: 1.8.11
58
+ signing_key:
59
+ specification_version: 2
60
+ summary: Tools for monitoring Rack apps in production.
61
+ test_files: []