Welcome to the CodeRack contest!
"Rack is a beautiful thing" - asciicasts.com
"the most important change in Rails over the past year - its move to become Rack-compatible" - www.viget.com
Welcome to the CodeRack - a competition to develop most useful and top quality Rack middlewares. Through our contest we want to spread the word about middleware, and most importantly, encourage people to implement middlewares that will benefit the entire Ruby community, and expand the public library of Open Source solutions.
Winners Announced!
The CodeRack contest has ended and the three finalists who had the highest ranking from the public voting are:
First Place
GeoIP Country
About
Rack::GeoIPCountry uses the geoip gem and the GeoIP database to lookup the country of a request by its IP address.
The country data is then passed to the application as custom X_GEOIP_* headers. You can use the included Mapping class to trigger lookup only for certain requests (matching the given prefix).
Usage
The database can be downloaded from:
http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
Usage:
use Rack::GeoIPCountry, :db => “path/to/GeoIP.dat”
By default all requests are looked up and the X_GEOIP_* headers are added to the request
The headers can then be read in the application
The country name is added to the request header as X_GEOIP_COUNTRY, eg:
X_GEOIP_COUNTRY: United Kingdom
The full set of GEOIP request headers is below:
X_GEOIP_COUNTRY_ID – The GeoIP country-ID as an integer, if not found set to 0
X_GEOIP_COUNTRY_CODE – The ISO3166-1 two-character country code, if not found set to “—”
X_GEOIP_COUNTRY_CODE3 – The ISO3166-2 three-character country code, if not found set to “—”
X_GEOIP_COUNTRY – The ISO3166 English-language name of the country, if not found set to N/A
X_GEOIP_CONTINENT – The two-character continent code, if not found set to “—”
You can use the included Mapping class to trigger lookup only for certain requests by specifying matching path prefix in options, eg:
use Rack::GeoIPCountry::Mapping, :prefix => ‘/video_tracking’
The above will lookup IP addresses only for requests matching /video_tracking etc.
MIT License – Karol Hosiawa (hosiawak at gmail.com)
Code
require 'geoip' module Rack # Rack::GeoIPCountry uses the geoip gem and the GeoIP database to lookup the country of a request by its IP address # The database can be downloaded from: # http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz # # Usage: # use Rack::GeoIPCountry, :db => "path/to/GeoIP.dat" # # By default all requests are looked up and the X_GEOIP_* headers are added to the request # The headers can then be read in the application # The country name is added to the request header as X_GEOIP_COUNTRY, eg: # X_GEOIP_COUNTRY: United Kingdom # # The full set of GEOIP request headers is below: # X_GEOIP_COUNTRY_ID - The GeoIP country-ID as an integer, if not found set to 0 # X_GEOIP_COUNTRY_CODE - The ISO3166-1 two-character country code, if not found set to -- # X_GEOIP_COUNTRY_CODE3 - The ISO3166-2 three-character country code, if not found set to -- # X_GEOIP_COUNTRY - The ISO3166 English-language name of the country, if not found set to N/A # X_GEOIP_CONTINENT - The two-character continent code, if not found set to -- # # # You can use the included Mapping class to trigger lookup only for certain requests by specifying matching path prefix in options, eg: # use Rack::GeoIPCountry::Mapping, :prefix => '/video_tracking' # The above will lookup IP addresses only for requests matching /video_tracking etc. # # MIT License - Karol Hosiawa ( http://twitter.com/hosiawak ) # class GeoIPCountry def initialize(app, options = {}) options[:db] ||= 'GeoIP.dat' @db = GeoIP.new(options[:db]) @app = app end def call(env) res = @db.country(env['REMOTE_ADDR']) env['X_GEOIP_COUNTRY_ID'] = res[2] env['X_GEOIP_COUNTRY_CODE'] = res[3] env['X_GEOIP_COUNTRY_CODE3'] = res[4] env['X_GEOIP_COUNTRY'] = res[5] env['X_GEOIP_CONTINENT'] = res[6] @app.call(env) end class Mapping def initialize(app, options = {}) @app, @prefix = app, /^#{options.delete(:prefix)}/ @geoip_country = GeoIPCountry.new(app, options) end def call(env) if env['PATH_INFO'] =~ @prefix @geoip_country.call(env) else @app.call(env) end end end endend Second Place
Superlogger
About
Superlogger allows you to unify your log files across all the applications – regardless the framework you use. Web application coded with Merb or Rails and API or some other “subapp” with Sinatra – sounds familiar? Every framework has its own log format. Not a big deal, they are just logs. But when you want to analyze them, it becomes an obstacle. Awsome Request Log Analyzer supports Rails, Merb and few other log formats. Though, sometimes you want to inject some additional information (e.g. ApplicationController#current_user ) into your logs or completely change log format. I didn’t like complex regular expressions which are needed by RLA, so I created this middleware to simplify all my logs.
It is not 100% finished – there is probably some faster way to compose log message from data given by the app, but current version shows the general idea. Check my rack-contrib fork.
And if you haven’t tried Request Log Analyzer, I really encourage you to do so. It’s great piece of software.
Usage
As always:
use Rack::Superlogger, :logger => logger, :template => %{":my_var" ":sth_else"} , :type => :templated
:logger must be an object which responds to an "info" method :type is type of log processor and currently :templated is the only one available :the rest of the argumets are passed to the LogProcessor constructor
LogProcessor::Templated simply substitutes the tokens from :template with values corresponding with these tokens from env["rack.superlogger.data"] or values returned by Rack::Request#token_name mathods. Additionally Superlogger gives you an access to raw logger using env["rack.superlogger.raw_logger"].
Here is the test which shows a basic usage:
context "Rack::Superlogger" do
def test_response(logger, template)
app = lambda { |env|
env["rack.superlogger.data"][:some_var] = "foobar"
env["rack.superlogger.data"][:something_else] = "kiszonka"
[200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]]
}
Rack::Superlogger.new(app, {:type => "Templated", :logger => logger, :template => template}).call(Rack::MockRequest.env_for("?super=logger"))
end
specify "should substitute :keys in template with values from 'rack.logger'" do
logger = mock("logger")
logger.expects("info").with("foobar kiszonka").once
test_response(logger, ':some_var :something_else')
end
end
Additionally it substitutes :duration, :content_length and :status with proper values. See tests for more examples.
Code
module Rack class Superlogger module LogProcessor def self.[](type) case type when Class type when String, Symbol const_get type.to_s.capitalize else raise ArgumentError, "Unexpected type class #{type.class}" end end class Base attr_reader :logger def initialize(options) @options = options end def process(env) raise "Not implemented" end end class Templated < Base def initialize(options) @logger = options.delete(:logger) or raise ArgumentError, "You must specify a logger" @template = options.delete(:template) or raise ArgumentError, "You must specify a template" super options end def process(env) request = Rack::Request.new(env) message = @template.dup Rack::Superlogger::REQUEST_METHODS.each do |method_name| env["rack.superlogger.data"][method_name.to_sym] = request.send(method_name.to_sym) if message.include?(":#{method_name}") end env["rack.superlogger.data"].each do |k, v| message.gsub! ":#{k}", v.to_s end @logger.info message end end end REQUEST_METHODS = Rack::Request.public_instance_methods(false). reject { |method_name| method_name =~ /[=\[]|content_length/ }.freeze def initialize(app, options) @app, @processor = app, LogProcessor[options.delete(:type)].new(options) end def call(env) env["rack.superlogger.data"], env["rack.superlogger.raw_logger"] = {}, @processor.logger before = Time.now.to_f status, headers, body = @app.call(env) duration = ((Time.now.to_f - before.to_f) * 1000).floor env["rack.superlogger.data"][:duration] = duration.to_s env["rack.superlogger.data"][:status] = status.to_s env["rack.superlogger.data"][:content_length] = headers["Content-length"] @processor.process env [status, headers, body] end endendThird Place
Rack Proctitle
About
Allows you to see what request a given server process is handling. By running "watch -n 0.1 ‘ps aux | grep “USER\|rack” ’ you can see all your rack processes, what request they’re currently handling and for how long (or if they’re idle), what their last request was and how long it took, how many requests have been handled in the life of this process, and the requests in queue.
It ends up being really useful to diagnose problems.
Example output:
rack/14a0b0 [1/682]: handling 0.0s /users/6014086
You can set a prefix, which will replace “rack” in the line above, and you can set an APPLICATION_VERSION constant which will be after the prefix. You can use this to distinguish between different rack processes if you run multiple applications on the same machine.
It’s based off Ryan Tomayko’s Mongrel Proctitle but has more features and is Rack compatible.
Usage
In some file somewhere (optional):
APPLICATION_VERSION = File.read(File.join(RAILS_ROOT, “REVISION”))[0,6] # if you use capistrano which creates the REVISION file
In your rackup file (prefix is optional):
use RackProctitle, :prefix => “application_name”
We use watch to get a continuously updating status of our rack processes:
watch -n 0.1 ‘ps aux | grep “USER\|application_name” | grep -v grep ; uptime; hostname’
Code
class RackProctitle def initialize(app, options = nil) @app = app @prefix = options.delete(:prefix) if options @revision = APPLICATION_VERSION if defined?(APPLICATION_VERSION) @mutex = Mutex.new @titles = [] @request_threads = [] @queue_length = 0 @request_count = 0 @updater_thread = Thread.new do while true @mutex.synchronize do set_request_list_title end sleep 0.5 end end end def call(env) Thread.current[:request_str] = ((env["REQUEST_URI"].nil? || env["REQUEST_URI"].empty?) ? "/" : env["REQUEST_URI"]).split("?", 2)[0] Thread.current[:arrived_at] = Time.now.to_f @mutex.synchronize do @request_threads.push(Thread.current) @queue_length += 1 set_request_list_title end begin @app.call(env) ensure @mutex.synchronize do @queue_length -= 1 @request_count += 1 @last_time = Time.now.to_f - Thread.current[:arrived_at].to_f @last_request_str = Thread.current[:request_str].to_s @request_threads.delete(Thread.current) set_request_list_title end end end protected def set_request_list_title @title = if @request_threads.empty? idle_message else now = Time.now.to_f list = @request_threads.inject([]) do |str, thread| str << "#{time_delta_abbriv(now - thread[:arrived_at])} #{thread[:request_str]}" end.join(" | ") "handling #{list}" end update_process_title end def idle_message str = "idle#{' '*20}" str << "[last #{time_delta_abbriv(@last_time)} #{@last_request_str}]" if @last_time && @last_request_str str end def update_process_title title = "#{@prefix || 'rack'}" title << "/#{@revision}" if @revision title << " [" title << "#{@queue_length}/" title << "#{@request_count}" title << "]: #{@title}" $0 = title end def time_delta_abbriv(delta) if delta < 60 "%.1fs" % delta elsif delta < 3600 "#{delta.to_i / 60}m#{delta.to_i % 60}s" elsif delta < 86400 "#{delta.to_i / 3600}h#{(delta.to_i % 3600) / 60}m" else "#{delta.to_i / 86400}d#{(delta.to_i % 86400) / 3600}h" end endend Honorable Mention
RedisRequestLimiter
About
A configurable (yet still simple) set of middleware that makes it easy to limit user requests (typically for an api)
based on a # / hour limit. Combined with other middleware it’d be easy to do routing.
Usage
It accepts a variety of hash-style options, with defaults specified in RedisRequestLimiter::DEFAULT_OPTIONS:
- :per_user_limit – the number of requests / period a user normally can use, Defaults to 100
- :reset_requests_every – Number of seconds to reset after (Defaults to 5 minutes)
- :override_limit_proc – proc returning [limit_count, reset_every] ([nil, nil] to use defaults) based on a specified env
- :redis_rate_key – the default redis key prefix, api-requests out of the box
- :header_status_prefix – prefix for the added api limit headers, defaults to X-API-Limit
- :env_to_api_key_proc – returns a string to use in the key per-user, defaults to the requester’s IP
- :limiter_app – a rack app that is called when limited.
Additionally, RedisRequestLimiter.redis(=) is defined to let you set a default Redis client
Code
require 'rubygems'require 'rack'require 'redis_request_limiter' # Give 5 requests a minute to make testing really easyuse RedisAPILimiter, :per_user_limit => 5, :reset_requests_every => 60run proc { |env| [200, {"Content-Type" => "text/html", "Content-Length" => "5"}, ["Hello"]]} require 'redis' class RedisRequestLimiter DEFAULT_OPTIONS = { :per_user_limit => 100, :reset_requests_every => 300, :override_limit_proc => proc { |env| [nil, nil] }, # limit, every :redis_rate_key => "api-requests".freeze, :header_status_prefix => "X-API-Limit".freeze, :env_to_api_key_proc => proc { |env| env["REMOTE_ADDR"] }, :limiter_app => proc do |env| headers = {"Content-Type" => "text/html"} resets_in = env['x-rack.rate-limiter.resets-at'] - Time.now.to_i body = "You are currently limited and will be reset in appx. %s seconds." % resets_in headers["Content-Length"] = body.length.to_s return 503, headers, [body] end } attr_accessor :per_user_limit, :reset_requests_every, :redis_rate_key, :override_limit_proc, :header_status_prefix, :limiter_app, :redis, :env_to_api_key_proc def initialize(app, opts = {}) @app = app # Setup options w/ defaults real_opts = DEFAULT_OPTIONS.merge(opts) real_opts.each_pair do |key, value| send(:"#{key}=", value) if respond_to?(:"#{key}=") end end def call(env) self.dup._call(env) end def _call(env) self.preload_request_info(env) env = self.env_with_limit_information(env) if self.under_request_limit?(env) count_request!(env) status, headers, body = @app.call(env) self.add_headers(headers) [status, headers, body] else status, headers, body = @limiter_app.call(env) self.add_headers(headers) [status, headers, body] end end def self.redis @@redis ||= Redis.new end def self.redis=(value) @@redis = value end protected def env_with_limit_information(env) env = env.dup env['x-rack.rate-limiter.current-count'] = self.current_request_count env['x-rack.rate-limiter.last-period'] = self.last_period env['x-rack.rate-limiter.resets-at'] = self.last_period + @reset_requests_every env['x-rack.rate-limiter.request-limit'] = @per_user_limit return env end def add_headers(header_hash) remaining_count = [(@per_user_limit - self.current_request_count), 0].max header_hash["#{@header_status_prefix}-Requests-Used"] = @current_request_count.to_s header_hash["#{@header_status_prefix}-Requests-Remaining"] = remaining_count.to_s header_hash["#{@header_status_prefix}-Period"] = self.last_period.to_s header_hash["#{@header_status_prefix}-Seconds-Remaining"] = ((self.last_period + @reset_requests_every) - Time.now.to_i).to_s header_hash["#{@header_status_prefix}-Is-Limited"] = (remaining_count > 0 ? "No" : "Yes") end def under_request_limit?(env) if self.last_period == self.current_period self.current_request_count < @per_user_limit else @current_request_count = 0 @last_period = current_period @redis.set(@count_env_key, "0") @redis.set(@period_env_key, @current_period) true end end def count_request!(env) @current_request_count = @redis.incr(@count_env_key) end def env_rate_limit_key(env, key = "count") "#{@redis_rate_key}:#{@env_to_api_key_proc.call(env)}:#{key}" end def preload_request_info(env) current_request_limit, current_reset_every = @override_limit_proc.call(env) # Since we dup, we can change ivars @reset_requests_every = current_reset_every if !current_reset_every.nil? @per_user_limit = current_request_limit if !current_request_limit.nil? @count_env_key = env_rate_limit_key(env) @period_env_key = env_rate_limit_key(env, "current-limit-period") @redis ||= self.class.redis end def last_period @last_period ||= begin raw_stored_period = @redis.get(@period_env_key).to_i (raw_stored_period / @reset_requests_every) * @reset_requests_every end end def current_period @current_period ||= (Time.now.to_i / @reset_requests_every) * @reset_requests_every end def current_request_count @current_request_count ||= @redis.get(@count_env_key).to_i end end You are all Rackstars!
A hearty congratulations to all our finalists, and to the sponsors and judges who helped make the CodeRack contest such a success. We hope that you all continue to find CodeRack a great place to hang your code. Our future plans include adding tagging and searching so that coderack.org can become a great resource for developers looking for the best rack middleware solutions.
CodeRack Finalists
Firebug Logger
About
Firebug Logger
Allows logging from your Rack-based app in Firebug (or the WebKit inspector)
Usage
HOWTO
Currently takes one option when initialised, :group. This allows you to specify the group under which your logs appear - see "Nested grouping" at http://getfirebug.com/logging.html for an example.
To log, set env['firebug.logs'] to a list of pairs. The first element in each pair should be the log level: :info, :warn, :debug or :error. The second element should be the message.
For example
env['firebug.logs'] = [[:info, "beginning"]]
env['firebug.logs'] << [:error, "something went wrong"]
Code
# See also http://github.com/simonjefford/rack_firebug_logger# for this middleware + tests + a rails pluginclass FirebugLogger def initialize(app, options = {}) @app = app @options = options end def call(env) dup._call(env) end def _call(env) status, headers, body = @app.call(env) return [status, headers, body] unless (headers["Content-Type"] =~ /html/ && env['firebug.logs']) response = Rack::Response.new([], status, headers) js = generate_js(env['firebug.logs']) body.each do |line| line.gsub!("</body>", js) response.write(line) end response.finish end private def generate_js(logs) js = ["<script type=\"text/javascript\">"] start_group(js) logs.each do |level, log| level = sanitise_level(level) log.gsub!('"', '\"') js << "console.#{level.to_s}(\"#{log}\");" end end_group(js) js << "</script>" js << "</body>" js.join("\n") end def start_group(js) if @options[:group] js << "console.group(\"#{@options[:group]}\");" end end def sanitise_level(level) if [:info, :debug, :warn, :error].include?(level) level else :debug end end def end_group(js) if @options[:group] js << "console.groupEnd();" end endendGeoIP Country
About
Rack::GeoIPCountry uses the geoip gem and the GeoIP database to lookup the country of a request by its IP address.
The country data is then passed to the application as custom X_GEOIP_* headers. You can use the included Mapping class to trigger lookup only for certain requests (matching the given prefix).
Usage
The database can be downloaded from:
http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
Usage:
use Rack::GeoIPCountry, :db => “path/to/GeoIP.dat”
By default all requests are looked up and the X_GEOIP_* headers are added to the request
The headers can then be read in the application
The country name is added to the request header as X_GEOIP_COUNTRY, eg:
X_GEOIP_COUNTRY: United Kingdom
The full set of GEOIP request headers is below:
X_GEOIP_COUNTRY_ID – The GeoIP country-ID as an integer, if not found set to 0
X_GEOIP_COUNTRY_CODE – The ISO3166-1 two-character country code, if not found set to “—”
X_GEOIP_COUNTRY_CODE3 – The ISO3166-2 three-character country code, if not found set to “—”
X_GEOIP_COUNTRY – The ISO3166 English-language name of the country, if not found set to N/A
X_GEOIP_CONTINENT – The two-character continent code, if not found set to “—”
You can use the included Mapping class to trigger lookup only for certain requests by specifying matching path prefix in options, eg:
use Rack::GeoIPCountry::Mapping, :prefix => ‘/video_tracking’
The above will lookup IP addresses only for requests matching /video_tracking etc.
MIT License – Karol Hosiawa (hosiawak at gmail.com)
Code
require 'geoip' module Rack # Rack::GeoIPCountry uses the geoip gem and the GeoIP database to lookup the country of a request by its IP address # The database can be downloaded from: # http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz # # Usage: # use Rack::GeoIPCountry, :db => "path/to/GeoIP.dat" # # By default all requests are looked up and the X_GEOIP_* headers are added to the request # The headers can then be read in the application # The country name is added to the request header as X_GEOIP_COUNTRY, eg: # X_GEOIP_COUNTRY: United Kingdom # # The full set of GEOIP request headers is below: # X_GEOIP_COUNTRY_ID - The GeoIP country-ID as an integer, if not found set to 0 # X_GEOIP_COUNTRY_CODE - The ISO3166-1 two-character country code, if not found set to -- # X_GEOIP_COUNTRY_CODE3 - The ISO3166-2 three-character country code, if not found set to -- # X_GEOIP_COUNTRY - The ISO3166 English-language name of the country, if not found set to N/A # X_GEOIP_CONTINENT - The two-character continent code, if not found set to -- # # # You can use the included Mapping class to trigger lookup only for certain requests by specifying matching path prefix in options, eg: # use Rack::GeoIPCountry::Mapping, :prefix => '/video_tracking' # The above will lookup IP addresses only for requests matching /video_tracking etc. # # MIT License - Karol Hosiawa ( http://twitter.com/hosiawak ) # class GeoIPCountry def initialize(app, options = {}) options[:db] ||= 'GeoIP.dat' @db = GeoIP.new(options[:db]) @app = app end def call(env) res = @db.country(env['REMOTE_ADDR']) env['X_GEOIP_COUNTRY_ID'] = res[2] env['X_GEOIP_COUNTRY_CODE'] = res[3] env['X_GEOIP_COUNTRY_CODE3'] = res[4] env['X_GEOIP_COUNTRY'] = res[5] env['X_GEOIP_CONTINENT'] = res[6] @app.call(env) end class Mapping def initialize(app, options = {}) @app, @prefix = app, /^#{options.delete(:prefix)}/ @geoip_country = GeoIPCountry.new(app, options) end def call(env) if env['PATH_INFO'] =~ @prefix @geoip_country.call(env) else @app.call(env) end end end endend MemoryBloat
About
detect memory bloat
Usage
require ‘memory_bloat’
require ‘logger’
use Rack::MemoryBloat, Logger.new(STDOUT)
or for Rails, use initializers:
require ‘rack/memory_bloat’
Rails.configuration.middleware.use Rack::MemoryBloat, Rails.logger
Code
module Rack class MemoryBloat def initialize(app, logger) @app = app @logger = logger end def call(env) memory_usage_before = memory_usage result = @app.call(env) memory_usage_after = memory_usage @logger.info "MemoryBloat: #{memory_usage_after - memory_usage_before} URL: #{Rack::Request.new(env).url}" result end private def memory_usage `ps -o rss= -p #{$$}`.to_i end endendRack::CacheBuster
About
So your content expires in an hour. How the hell can you deploy cleanly?
"So this middleware disables the cache?" you ask. "No" I respond. That would be dumb.
This middleware makes all ETags code revision number specific, and gradually aligns all expiry times to a single point in time.
So you tell the server you are going to deploy in an hour, it makes sure all clients have forgotten their cache in an hour, and then when you deploy the ETags change, so they will get fresh content.
By sitting it in front of Rack::Cache your app will see no more dynamic hits than it did before! Rack::CacheBuster only invalidates the cache on the clients, so Rack::Cache still has a valid for the duration of the wind down!
And for bonus points, I've even given you some cap tasks, and a Rails specific helper!
Usage
Rack::CacheBuster
Use to wind down a running app when needed.
Limits the max-age to the contents of the WIND_DOWN file, and salts the ETags to unsure clients see different deployments as having different contents.
Usage:
require "rack/cache_buster"
…
use Rack::CacheBuster, APP_VERSION, WIND_DOWN_TIME
Rails Usage:
require "rack/cache_buster"
…
use Rack::CacheBuster::Rails
Before you deploy (capistrano):
Add this to your deploy.rb:
begin
gem "rack-cache-buster"
require 'rack_cache_buster_recipes'
rescue LoadError
puts "\n\n*** Please gem install rack-cache-buster before trying to deploy.\n\n"
end
# set :wind_down_time, 60*60 # defaults to 1hr
Then:
cap cache:winddown
… wait 1 hr …
cap deploy:migrations
… profit!
Before you deploy (other):
Find the maximum cache duration for all pages in your app, start this process with at least that amount time before you deploy.
Create a WIND_DOWN file in the app root of all your app servers.
Restart all instances (touch tmp/restart.txt will be fine for passenger apps).
Once all the caches should have expired you can deploy as normal.
Notes
If you are using Rack::Cache, you can safely put the CacheBuster below it in the stack, but only if you clear Rack::Cache's cache upon deploy.
This is actually the recommended approach as it will prevent the increased amount of hits between WIND_DOWN_TIME and your actual deployment from making it through to your app.
Code
# Full code is at http://github.com/cwninja/rack-cache-buster require 'digest/md5' module Rack class CacheBuster def initialize(app, key, target_time = nil) @app, @target_time = app, target_time @key = "-"+Digest::MD5.hexdigest(key || "blank-key").freeze @key_regexp = /#{@key}/.freeze end def call(env) status, headers, body = app.call(unpatch_etag(env)) [status, patch_etag(limit_cache(headers)), body] end protected QUOTE_STRIPPER=/^"|"$/.freeze ETAGGY_HEADERS = ["HTTP_IF_NONE_MATCH", "HTTP_IF_MATCH", "HTTP_IF_RANGE"].freeze ETag = "ETag".freeze attr_reader :app attr_reader :key def limit_cache(headers) return headers if @target_time.nil? cache_control_header = CacheControlHeader.new(headers) if cache_control_header.expired? headers else cache_control_header.expire_time = [@target_time, cache_control_header.expire_time].min headers.merge(CacheControlHeader::CacheControl => cache_control_header.to_s) end end def unpatch_etag(headers) ETAGGY_HEADERS.inject(headers){|memo, k| memo.has_key?(k) ? memo.merge(k => strip_etag(memo[k])) : memo } end def strip_etag(s) s.gsub(@key_regexp, "") end def modify_etag(s) s = s.to_s.gsub(QUOTE_STRIPPER, "") s.empty? ? s : %Q{"#{s}#{key}"} end def patch_etag(headers) headers.merge(ETag => modify_etag(headers[ETag])) end autoload :CacheControlHeader, "rack/cache_buster/cache_control_header" autoload :Rails, "rack/cache_buster/rails" endend require "rack/cache_buster" class Rack::CacheBuster::CacheControlHeader CacheControl = "Cache-Control".freeze Age = "Age".freeze MaxAge = "max-age".freeze def initialize(env) @age = env[Age].to_i @max_age = 0 if env[CacheControl] parts = env[CacheControl].split(/ *, */) settings, options = parts.partition{|part| part =~ /=/ } settings = settings.inject({}){|acc, part| k, v = part.split(/ *= */, 2) acc.merge(k => v) } @max_age = settings.delete(MaxAge).to_i @other_parts = options + settings.map{|k,v| "#{k}=#{v}"} end end def expires_in @max_age - @age end def expired? expires_in <= 0 end def expire_time Time.now + expires_in end def expire_time=(t) @max_age = [t.to_i - Time.now.to_i + @age, @age].max end def update_env(env) env[CacheControl] = to_s end def to_s ["max-age=#{@max_age}", *@other_parts].join(", ") endend require "rack/cache_buster" class Rack::CacheBuster::Rails < Rack::CacheBuster def initialize(app) app_version = File.read(File.join(::Rails.root, "REVISION")) rescue nil wind_down_time = Time.parse(File.read(File.join(::Rails.root, "WIND_DOWN"))) rescue nil super(app, app_version, wind_down_time) endend RedisRequestLimiter
About
A configurable (yet still simple) set of middleware that makes it easy to limit user requests (typically for an api)
based on a # / hour limit. Combined with other middleware it’d be easy to do routing.
Usage
It accepts a variety of hash-style options, with defaults specified in RedisRequestLimiter::DEFAULT_OPTIONS:
- :per_user_limit – the number of requests / period a user normally can use, Defaults to 100
- :reset_requests_every – Number of seconds to reset after (Defaults to 5 minutes)
- :override_limit_proc – proc returning [limit_count, reset_every] ([nil, nil] to use defaults) based on a specified env
- :redis_rate_key – the default redis key prefix, api-requests out of the box
- :header_status_prefix – prefix for the added api limit headers, defaults to X-API-Limit
- :env_to_api_key_proc – returns a string to use in the key per-user, defaults to the requester’s IP
- :limiter_app – a rack app that is called when limited.
Additionally, RedisRequestLimiter.redis(=) is defined to let you set a default Redis client
Code
require 'rubygems'require 'rack'require 'redis_request_limiter' # Give 5 requests a minute to make testing really easyuse RedisAPILimiter, :per_user_limit => 5, :reset_requests_every => 60run proc { |env| [200, {"Content-Type" => "text/html", "Content-Length" => "5"}, ["Hello"]]} require 'redis' class RedisRequestLimiter DEFAULT_OPTIONS = { :per_user_limit => 100, :reset_requests_every => 300, :override_limit_proc => proc { |env| [nil, nil] }, # limit, every :redis_rate_key => "api-requests".freeze, :header_status_prefix => "X-API-Limit".freeze, :env_to_api_key_proc => proc { |env| env["REMOTE_ADDR"] }, :limiter_app => proc do |env| headers = {"Content-Type" => "text/html"} resets_in = env['x-rack.rate-limiter.resets-at'] - Time.now.to_i body = "You are currently limited and will be reset in appx. %s seconds." % resets_in headers["Content-Length"] = body.length.to_s return 503, headers, [body] end } attr_accessor :per_user_limit, :reset_requests_every, :redis_rate_key, :override_limit_proc, :header_status_prefix, :limiter_app, :redis, :env_to_api_key_proc def initialize(app, opts = {}) @app = app # Setup options w/ defaults real_opts = DEFAULT_OPTIONS.merge(opts) real_opts.each_pair do |key, value| send(:"#{key}=", value) if respond_to?(:"#{key}=") end end def call(env) self.dup._call(env) end def _call(env) self.preload_request_info(env) env = self.env_with_limit_information(env) if self.under_request_limit?(env) count_request!(env) status, headers, body = @app.call(env) self.add_headers(headers) [status, headers, body] else status, headers, body = @limiter_app.call(env) self.add_headers(headers) [status, headers, body] end end def self.redis @@redis ||= Redis.new end def self.redis=(value) @@redis = value end protected def env_with_limit_information(env) env = env.dup env['x-rack.rate-limiter.current-count'] = self.current_request_count env['x-rack.rate-limiter.last-period'] = self.last_period env['x-rack.rate-limiter.resets-at'] = self.last_period + @reset_requests_every env['x-rack.rate-limiter.request-limit'] = @per_user_limit return env end def add_headers(header_hash) remaining_count = [(@per_user_limit - self.current_request_count), 0].max header_hash["#{@header_status_prefix}-Requests-Used"] = @current_request_count.to_s header_hash["#{@header_status_prefix}-Requests-Remaining"] = remaining_count.to_s header_hash["#{@header_status_prefix}-Period"] = self.last_period.to_s header_hash["#{@header_status_prefix}-Seconds-Remaining"] = ((self.last_period + @reset_requests_every) - Time.now.to_i).to_s header_hash["#{@header_status_prefix}-Is-Limited"] = (remaining_count > 0 ? "No" : "Yes") end def under_request_limit?(env) if self.last_period == self.current_period self.current_request_count < @per_user_limit else @current_request_count = 0 @last_period = current_period @redis.set(@count_env_key, "0") @redis.set(@period_env_key, @current_period) true end end def count_request!(env) @current_request_count = @redis.incr(@count_env_key) end def env_rate_limit_key(env, key = "count") "#{@redis_rate_key}:#{@env_to_api_key_proc.call(env)}:#{key}" end def preload_request_info(env) current_request_limit, current_reset_every = @override_limit_proc.call(env) # Since we dup, we can change ivars @reset_requests_every = current_reset_every if !current_reset_every.nil? @per_user_limit = current_request_limit if !current_request_limit.nil? @count_env_key = env_rate_limit_key(env) @period_env_key = env_rate_limit_key(env, "current-limit-period") @redis ||= self.class.redis end def last_period @last_period ||= begin raw_stored_period = @redis.get(@period_env_key).to_i (raw_stored_period / @reset_requests_every) * @reset_requests_every end end def current_period @current_period ||= (Time.now.to_i / @reset_requests_every) * @reset_requests_every end def current_request_count @current_request_count ||= @redis.get(@count_env_key).to_i end end Superlogger
About
Superlogger allows you to unify your log files across all the applications – regardless the framework you use. Web application coded with Merb or Rails and API or some other “subapp” with Sinatra – sounds familiar? Every framework has its own log format. Not a big deal, they are just logs. But when you want to analyze them, it becomes an obstacle. Awsome Request Log Analyzer supports Rails, Merb and few other log formats. Though, sometimes you want to inject some additional information (e.g. ApplicationController#current_user ) into your logs or completely change log format. I didn’t like complex regular expressions which are needed by RLA, so I created this middleware to simplify all my logs.
It is not 100% finished – there is probably some faster way to compose log message from data given by the app, but current version shows the general idea. Check my rack-contrib fork.
And if you haven’t tried Request Log Analyzer, I really encourage you to do so. It’s great piece of software.
Usage
As always:
use Rack::Superlogger, :logger => logger, :template => %{":my_var" ":sth_else"} , :type => :templated
:logger must be an object which responds to an "info" method :type is type of log processor and currently :templated is the only one available :the rest of the argumets are passed to the LogProcessor constructor
LogProcessor::Templated simply substitutes the tokens from :template with values corresponding with these tokens from env["rack.superlogger.data"] or values returned by Rack::Request#token_name mathods. Additionally Superlogger gives you an access to raw logger using env["rack.superlogger.raw_logger"].
Here is the test which shows a basic usage:
context "Rack::Superlogger" do
def test_response(logger, template)
app = lambda { |env|
env["rack.superlogger.data"][:some_var] = "foobar"
env["rack.superlogger.data"][:something_else] = "kiszonka"
[200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]]
}
Rack::Superlogger.new(app, {:type => "Templated", :logger => logger, :template => template}).call(Rack::MockRequest.env_for("?super=logger"))
end
specify "should substitute :keys in template with values from 'rack.logger'" do
logger = mock("logger")
logger.expects("info").with("foobar kiszonka").once
test_response(logger, ':some_var :something_else')
end
end
Additionally it substitutes :duration, :content_length and :status with proper values. See tests for more examples.
Code
module Rack class Superlogger module LogProcessor def self.[](type) case type when Class type when String, Symbol const_get type.to_s.capitalize else raise ArgumentError, "Unexpected type class #{type.class}" end end class Base attr_reader :logger def initialize(options) @options = options end def process(env) raise "Not implemented" end end class Templated < Base def initialize(options) @logger = options.delete(:logger) or raise ArgumentError, "You must specify a logger" @template = options.delete(:template) or raise ArgumentError, "You must specify a template" super options end def process(env) request = Rack::Request.new(env) message = @template.dup Rack::Superlogger::REQUEST_METHODS.each do |method_name| env["rack.superlogger.data"][method_name.to_sym] = request.send(method_name.to_sym) if message.include?(":#{method_name}") end env["rack.superlogger.data"].each do |k, v| message.gsub! ":#{k}", v.to_s end @logger.info message end end end REQUEST_METHODS = Rack::Request.public_instance_methods(false). reject { |method_name| method_name =~ /[=\[]|content_length/ }.freeze def initialize(app, options) @app, @processor = app, LogProcessor[options.delete(:type)].new(options) end def call(env) env["rack.superlogger.data"], env["rack.superlogger.raw_logger"] = {}, @processor.logger before = Time.now.to_f status, headers, body = @app.call(env) duration = ((Time.now.to_f - before.to_f) * 1000).floor env["rack.superlogger.data"][:duration] = duration.to_s env["rack.superlogger.data"][:status] = status.to_s env["rack.superlogger.data"][:content_length] = headers["Content-length"] @processor.process env [status, headers, body] end endendLiveStats
About
If Google Analytics (formerly Urchin) has become one of the most popular tracking systems, it fails at being live because of its architecture. You only get valid data the day after which is the default view anyways. I tried to make something to improve the actual solution we are using at work.
What we do have at work is a basic session counter. It helps us not pushing a release when a lot of people are surfing the website even it has been easier since we switched from Mongrel to Passenger. From a simple number, like “280 sessions” I’d try to move that to the next level and having a view of the actual usage.
The idea is to count views and to store that into memcached using the current time as a key and putting a timeout of the length of the needed graph.
Usage
How to activate it:
cache = MemCache.new("127.0.0.1:11211")
use LiveStats, cache, :period => 3600, :precision => 10
Example application and screenshot there: Live statistics for your website
Code
class LiveStats def initialize(app, cache, options={}) @app = app @cache = cache @period = options[:period] || 3600 @precision = options[:precision] || 10 end def inc(key, value=1, timeout=nil) if @cache.incr(key, value).nil? @cache.add(key, value, timeout || @period) end end def call(env) env["rack.livestats.track"] = true now = Time::now response = @app.call(env) if env["rack.livestats.track"] ts = ((Time::now - now) * 1000).to_i # in ms key = Time::now.to_i / @precision # time spent inc("t"+key.to_s, ts) # visits inc("v"+key.to_s) end response endendRack::DomainSprinkler
About
Modifies outgoing HTML markup such that requests for common static assets (currently fixed, <link>, <script>, and <img> tags) will be distributed across a user-defined set of domains in order to improve parallel downloading and speed page load times. Resource URL mappings are stored upon first use in order to ensure that browser caching is not broken.
Usage
Options:
- an Array of domain names to be used for sprinklin’ (DNS zone configuration sold separately)
- the filename to be used for the URL mapping cache (uses Ruby PStore)
require "rack/domain_sprinkler"
config.middleware.use Rack::DomainSprinkler, ["dummy1.foo.com", "dummy2.foo.com", "dummy3.foo.com"], "tmp/url_mappings.pstore"
Code
require 'nokogiri'require 'pstore' ## Rack::DomainSprinkler## Modifies outgoing HTML markup so that common static assets like script source# files, images, and stylesheets will be served from one of a number of domains# rather than just a single one in order to improve parallelization of resource# downloads during page loads.#module Rack class DomainSprinkler # # Options: # A list of domains across which to spread requests # The name of the file to be used for caching URL/path mappings # def initialize(app, domains=[], cache_loc="tmp/sprinklins.pstore") @app = app @@domains = domains @@url_cache = PStore.new(cache_loc) end def call(env) dup._call(env) end # # Currently only looks for <link>, <script>, and <img> tags. # Should be refactored to provide more generic mapping. # def _call(env) status, headers, response = @app.call(env) if headers["Content-Type"] =~ /^text\/html/ document = Nokogiri::HTML(response.body) sprinkle(env, document, 'link', 'href') sprinkle(env, document, 'script', 'src') sprinkle(env, document, 'img', 'src') response.body = document.to_html end [status, headers, response] end def sprinkle(env, document, selector, property) document.search(selector).each do |node| url = node[property] if !url.empty? and url !~ /^\#/ and url !~ /^http\:\/\// node[property] = domainify(url, env) end end end def domainify(path, env) if path !~ /\// path = absolutify(path) end full_url = nil @@url_cache.transaction do unless @@url_cache[path] domain = @@domains.shift and @@domains.push(domain) @@url_cache[path] = "http://#{ domain }#{ path }" end full_url = @@url_cache[path] end full_url end def absolutify(path, env) path_prefix = env['REQUEST_PATH'] unless path_prefix =~ /\/$/ path_prefix.sub!(/\/[^\/]+$/, '') end "#{ tmp_path }/#{ path }" end endend Rack::Validate
About
Validate your pages with the w3c validator and present the errors/warnings in the page.
See it in action here: Screenshot Link
Report any bugs or contribute here: Github Page
Usage
Enable Rack::Validate:
use Rack::Validate
Enable Rack::Validate w/Rails:
config.middleware.use Rack::Validate
Pass in the rack-validate parameter in the request:
http://development.localhost/?rack-validate=true
Code
module Rack # A rack middleware for validating HTML via w3c validator class Validate def initialize( app ) @app = app end def call( env ) status, headers, response = @app.call( env ) request = Rack::Request.new( env ) if !request.params['rack-validate'].blank? if headers['Content-Type'] =~ /text\/html|application\/xhtml\+xml/ body = response.body issues = Validator.validate( body ) body.insert( 0, Validator.generate_report( issues ) ) headers["Content-Length"] = body.length.to_s response = [body] end end [status, headers, response] end class Validator include W3CValidators def self.validate( response ) validator = MarkupValidator.new validator.validate_text( response ) end def self.generate_report( issues ) report = "" report << STYLES report << SCRIPT report << "<div id='message_toolbar'>" report << '<div id="controls">' summary = "<span>There were " + issues.errors.size.to_s + " errors and " + issues.warnings.size.to_s + " warnings</span><br/><br/>" report << summary report << LINKS report << '</div>' if !issues.errors.empty? report << "<table id='errors_table' style='display:none;'>" report << "<tr><td colspan='2' class='header_column'>Errors</td></tr>" issues.errors.each do |item| report << "<tr><td class='line_number_column'>Line #{item.line}</td><td class='message_column'>#{html_escape( item.message )}</td></tr>" end report << "</table>" end if !issues.warnings.empty? report << "<table id='warnings_table' style='display:none;'>" report << "<tr><td colspan='2' class='header_column'>Warnings</td></tr>" issues.warnings.each do |item| report << "<tr><td class='line_number_column'>--</td><td class='message_column'>#{html_escape( item.message )}</td></tr>" end report << "</table>" end report << "</div>" end private # Stealing HTML escape method from rails def self.html_escape( string ) string.to_s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] } end # Stealing HTML escape constant from rails HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"' } LINKS = <<-HERE <a href="#" onclick="$( '#errors_table' ).toggle();updateText( this, 'Show Errors', 'Hide Errors' );">Show Errors</a> <a href="#" onclick="$( '#warnings_table' ).toggle();updateText( this, 'Show Warnings', 'Hide Warnings' );">Show Warnings</a> <a href="#" onclick="$( '#message_toolbar' ).toggle();">Close Toolbar</a> HERE SCRIPT = <<-HERE <script src="http://www.google.com/jsapi"></script> <script type="text/javascript"> google.load("jquery", "1.3.2"); google.load("jqueryui", "1.7.2"); function updateText( element, textOne, textTwo ) { if( element.innerHTML == textOne ) { element.innerHTML = textTwo; } else { element.innerHTML = textOne; } } </script> HERE STYLES = <<-HERE <style type='text/css'> #message_toolbar { width: 100%; min-height: 50px; background: black; position: absolute; top:0; left:0; -moz-opacity:.80; filter:alpha(opacity=80); opacity:.80; font-size: smaller; font-family: "Courier New"; color: white; text-align: center; } #message_toolbar a{ color: white; font-weight: bold; } .line_number_column { text-align: left; width: 100px; } .error_message_column { text-align: left; } #errors_table, #warnings_table { width: 75%; margin: 0 auto; border: none; color: white; margin-top: 10px; } #errors_table .header_column, #warnings_table .header_column { text-align: center; border-bottom: 2px dashed; } #errors_table .header_column { border-color: red; } #warnings_table .header_column { border-color: yellow; } </style> HERE end endendRack Proctitle
About
Allows you to see what request a given server process is handling. By running "watch -n 0.1 ‘ps aux | grep “USER\|rack” ’ you can see all your rack processes, what request they’re currently handling and for how long (or if they’re idle), what their last request was and how long it took, how many requests have been handled in the life of this process, and the requests in queue.
It ends up being really useful to diagnose problems.
Example output:
rack/14a0b0 [1/682]: handling 0.0s /users/6014086
You can set a prefix, which will replace “rack” in the line above, and you can set an APPLICATION_VERSION constant which will be after the prefix. You can use this to distinguish between different rack processes if you run multiple applications on the same machine.
It’s based off Ryan Tomayko’s Mongrel Proctitle but has more features and is Rack compatible.
Usage
In some file somewhere (optional):
APPLICATION_VERSION = File.read(File.join(RAILS_ROOT, “REVISION”))[0,6] # if you use capistrano which creates the REVISION file
In your rackup file (prefix is optional):
use RackProctitle, :prefix => “application_name”
We use watch to get a continuously updating status of our rack processes:
watch -n 0.1 ‘ps aux | grep “USER\|application_name” | grep -v grep ; uptime; hostname’
Code
class RackProctitle def initialize(app, options = nil) @app = app @prefix = options.delete(:prefix) if options @revision = APPLICATION_VERSION if defined?(APPLICATION_VERSION) @mutex = Mutex.new @titles = [] @request_threads = [] @queue_length = 0 @request_count = 0 @updater_thread = Thread.new do while true @mutex.synchronize do set_request_list_title end sleep 0.5 end end end def call(env) Thread.current[:request_str] = ((env["REQUEST_URI"].nil? || env["REQUEST_URI"].empty?) ? "/" : env["REQUEST_URI"]).split("?", 2)[0] Thread.current[:arrived_at] = Time.now.to_f @mutex.synchronize do @request_threads.push(Thread.current) @queue_length += 1 set_request_list_title end begin @app.call(env) ensure @mutex.synchronize do @queue_length -= 1 @request_count += 1 @last_time = Time.now.to_f - Thread.current[:arrived_at].to_f @last_request_str = Thread.current[:request_str].to_s @request_threads.delete(Thread.current) set_request_list_title end end end protected def set_request_list_title @title = if @request_threads.empty? idle_message else now = Time.now.to_f list = @request_threads.inject([]) do |str, thread| str << "#{time_delta_abbriv(now - thread[:arrived_at])} #{thread[:request_str]}" end.join(" | ") "handling #{list}" end update_process_title end def idle_message str = "idle#{' '*20}" str << "[last #{time_delta_abbriv(@last_time)} #{@last_request_str}]" if @last_time && @last_request_str str end def update_process_title title = "#{@prefix || 'rack'}" title << "/#{@revision}" if @revision title << " [" title << "#{@queue_length}/" title << "#{@request_count}" title << "]: #{@title}" $0 = title end def time_delta_abbriv(delta) if delta < 60 "%.1fs" % delta elsif delta < 3600 "#{delta.to_i / 60}m#{delta.to_i % 60}s" elsif delta < 86400 "#{delta.to_i / 3600}h#{(delta.to_i % 3600) / 60}m" else "#{delta.to_i / 86400}d#{(delta.to_i % 86400) / 3600}h" end endend













