rack_monitor 0.0.1

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 +13 -0
  3. data/Rakefile +141 -0
  4. data/lib/rack_monitor.rb +279 -0
  5. data/rack_monitor.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,13 @@
1
+ # RackMonitor
2
+
3
+ Some tiny Rack apps for monitoring Rack apps in production.
4
+
5
+ * RackMonitor::RequestStatus - Adds a status URL for health checks.
6
+ * RackMonitor::RequestHostname - Shows which what code is running on
7
+ which node for a given request.
8
+ * RackMonitor::ProcessUtilization - Tracks how long Unicorns spend
9
+ processing requests. Optioanally sends metrics to a StatsD server.
10
+
11
+ This code has been extracted from GitHub.com and is used on
12
+ http://git.io currently.
13
+
@@ -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,279 @@
1
+ module RackMonitor
2
+ VERSION = "0.0.1"
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 horizon
95
+ # resets.
96
+ # :stats - Optional StatsD client.
97
+ # :hostname - Optional String hostname.
98
+ def initialize(app, domain, revision, options = {})
99
+ @app = app
100
+ @domain = domain
101
+ @revision = revision
102
+ @window = options[:window] || 100
103
+ @horizon = nil
104
+ @active_time = nil
105
+ @requests = nil
106
+ @total_requests = 0
107
+ @worker_number = nil
108
+ @track_gc = GC.respond_to?(:time)
109
+
110
+ if @stats = options[:stats]
111
+ @hostname = options[:hostname] || `hostname -s`.chomp
112
+ end
113
+ end
114
+
115
+ # the app's domain name - shown in proctitle
116
+ attr_accessor :domain
117
+
118
+ # the currently running git revision as a 7-sha
119
+ attr_accessor :revision
120
+
121
+ # time when we began sampling. this is reset every once in a while so
122
+ # averages don't skew over time.
123
+ attr_accessor :horizon
124
+
125
+ # total number of requests that have been processed by this worker since
126
+ # the horizon time.
127
+ attr_accessor :requests
128
+
129
+ # decimal number of seconds the worker has been active within a request
130
+ # since the horizon time.
131
+ attr_accessor :active_time
132
+
133
+ # total requests processed by this worker process since it started
134
+ attr_accessor :total_requests
135
+
136
+ # the unicorn worker number
137
+ attr_accessor :worker_number
138
+
139
+ # the amount of time since the horizon
140
+ def horizon_time
141
+ Time.now - horizon
142
+ end
143
+
144
+ # decimal number of seconds this process has been active since the horizon
145
+ # time. This is the inverse of the active time.
146
+ def idle_time
147
+ horizon_time - active_time
148
+ end
149
+
150
+ # percentage of time this process has been active since the horizon time.
151
+ def percentage_active
152
+ (active_time / horizon_time) * 100
153
+ end
154
+
155
+ # percentage of time this process has been idle since the horizon time.
156
+ def percentage_idle
157
+ (idle_time / horizon_time) * 100
158
+ end
159
+
160
+ # number of requests processed per second since the horizon
161
+ def requests_per_second
162
+ requests / horizon_time
163
+ end
164
+
165
+ # average response time since the horizon in milliseconds
166
+ def average_response_time
167
+ (active_time / requests.to_f) * 1000
168
+ end
169
+
170
+ # called exactly once before the first request is processed by a worker
171
+ def first_request
172
+ reset_horizon
173
+ record_worker_number
174
+ end
175
+
176
+ # resets the horizon and all dependent variables
177
+ def reset_horizon
178
+ @horizon = Time.now
179
+ @active_time = 0.0
180
+ @requests = 0
181
+ end
182
+
183
+ # extracts the worker number from the unicorn procline
184
+ def record_worker_number
185
+ if $0 =~ /^.* worker\[(\d+)\].*$/
186
+ @worker_number = $1.to_i
187
+ else
188
+ @worker_number = nil
189
+ end
190
+ end
191
+
192
+ # the generated procline
193
+ def procline
194
+ "unicorn %s[%s] worker[%02d]: %5d reqs, %4.1f req/s, %4dms avg, %5.1f%% util" % [
195
+ domain,
196
+ revision,
197
+ worker_number.to_i,
198
+ total_requests.to_i,
199
+ requests_per_second.to_f,
200
+ average_response_time.to_i,
201
+ percentage_active.to_f
202
+ ]
203
+ end
204
+
205
+ # called immediately after a request to record statistics, update the
206
+ # procline, and dump information to the logfile
207
+ def record_request(status)
208
+ now = Time.now
209
+ diff = (now - @start)
210
+ @active_time += diff
211
+ @requests += 1
212
+
213
+ $0 = procline
214
+
215
+ if @stats
216
+ @stats.timing("unicorn.#{@hostname}.response_time", diff * 1000)
217
+ if suffix = status_suffix(status)
218
+ @stats.increment "unicorn.#{Gitio.host}.status_code.#{status_suffix(status)}"
219
+ end
220
+ if @track_gc && GC.time > 0
221
+ @stats.timing "unicorn.#{@hostname}.gc.time", GC.time / 1000
222
+ @stats.count "unicorn.#{@hostname}.gc.collections", GC.collections
223
+ end
224
+ end
225
+
226
+ reset_horizon if now - horizon > @window
227
+ rescue => boom
228
+ warn "ProcessUtilization#record_request failed: #{boom}"
229
+ end
230
+
231
+ def status_suffix(status)
232
+ suffix = case status.to_i
233
+ when 404 then :missing
234
+ when 422 then :invalid
235
+ when 503 then :node_down
236
+ when 500 then :error
237
+ end
238
+ end
239
+
240
+ # Body wrapper. Yields to the block when body is closed. This is used to
241
+ # signal when a response is fully finished processing.
242
+ class Body
243
+ def initialize(body, &block)
244
+ @body = body
245
+ @block = block
246
+ end
247
+
248
+ def each(&block)
249
+ @body.each(&block)
250
+ end
251
+
252
+ def close
253
+ @body.close if @body.respond_to?(:close)
254
+ @block.call
255
+ nil
256
+ end
257
+ end
258
+
259
+ # Rack entry point.
260
+ def call(env)
261
+ @start = Time.now
262
+ GC.clear_stats if @track_gc
263
+
264
+ @total_requests += 1
265
+ first_request if @total_requests == 1
266
+
267
+ env['process.request_start'] = @start.to_f
268
+ env['process.total_requests'] = total_requests
269
+
270
+ # newrelic X-Request-Start
271
+ env.delete('HTTP_X_REQUEST_START')
272
+
273
+ status, headers, body = @app.call(env)
274
+ body = Body.new(body) { record_request(status) }
275
+ [status, headers, body]
276
+ end
277
+ end
278
+ end
279
+
@@ -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_monitor'
16
+ s.version = '0.0.1'
17
+ s.date = '2011-12-02'
18
+ s.rubyforge_project = 'rack_monitor'
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_monitor'
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_monitor.rb
53
+ rack_monitor.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_monitor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
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-02 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rack-test
17
+ requirement: &70363258145480 !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: *70363258145480
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_monitor.rb
36
+ - rack_monitor.gemspec
37
+ homepage: https://github.com/github/rack_monitor
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_monitor
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: []