├── .gitignore ├── Gemfile ├── Gemfile.lock ├── History.txt ├── README.md ├── Rakefile ├── VERSION ├── ci.rb ├── e20_ops_middleware.gemspec ├── lib └── e20 │ └── ops │ ├── hostname.rb │ ├── middleware.rb │ ├── middleware │ ├── hostname_middleware.rb │ ├── revision_middleware.rb │ └── transaction_id_middleware.rb │ └── revision.rb └── spec ├── ops ├── hostname_spec.rb ├── middleware │ ├── hostname_middleware_spec.rb │ ├── revision_middleware_spec.rb │ └── transaction_id_middleware_spec.rb └── revision_spec.rb ├── spec.opts └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | vendor 3 | pkg 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubyforge 2 | 3 | gem "jeweler", "~> 1.4.0" 4 | gem "rspec", "~> 1.3.0" 5 | gem "rake", "~> 0.8.7" 6 | gem "uuid" 7 | gem "activesupport", "~> 2.3.8" 8 | gem "json_pure", "= 1.4.3" 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | activesupport (2.3.8) 5 | gemcutter (0.6.1) 6 | git (1.2.5) 7 | jeweler (1.4.0) 8 | gemcutter (>= 0.1.0) 9 | git (>= 1.2.5) 10 | rubyforge (>= 2.0.0) 11 | json_pure (1.4.3) 12 | macaddr (1.0.0) 13 | rake (0.8.7) 14 | rspec (1.3.0) 15 | rubyforge (2.0.4) 16 | json_pure (>= 1.1.7) 17 | uuid (2.3.2) 18 | macaddr (~> 1.0) 19 | 20 | PLATFORMS 21 | ruby 22 | 23 | DEPENDENCIES 24 | activesupport (~> 2.3.8) 25 | jeweler (~> 1.4.0) 26 | json_pure (= 1.4.3) 27 | rake (~> 0.8.7) 28 | rspec (~> 1.3.0) 29 | uuid 30 | -------------------------------------------------------------------------------- /History.txt: -------------------------------------------------------------------------------- 1 | == 2.0.1 / 2010-08-29 2 | 3 | * 1 major enhancement 4 | 5 | * First public release! 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Efficiency 2.0 Ops Middleware 2 | ============================= 3 | 4 | A collection of useful middleware for exposing information about deployed Rack 5 | applications. Efficiency 2.0 uses this to track distributed transactions 6 | across its Ruby-based service oriented architecture. 7 | 8 | Features 9 | -------- 10 | 11 | * Adds a `X-Served-By` header with the hostname of the server that processed 12 | the request. 13 | * Adds a `X-Transaction` header with a unique ID for the web request. 14 | * Adds a `X-Revision` header with the running Git revision. 15 | * Adds an endpoint of `/system/revision` for easily checking the running 16 | revision. This can be leveraged in a post-deployment sanity check to ensure 17 | the application servers restarted properly. 18 | 19 | Install 20 | ------- 21 | 22 | ### Rails ### 23 | 24 | Add to your `Gemfile`: 25 | 26 | gem "e20_ops_middleware", :require => "e20/ops/middleware" 27 | 28 | Install the gem: 29 | 30 | $ bundle install 31 | 32 | Create a `config/initializers/ops_middleware.rb` with the following: 33 | 34 | Rails.application.middleware.with_options :logger => Rails.logger do |m| 35 | m.use E20::Ops::Middleware::RevisionMiddleware 36 | m.use E20::Ops::Middleware::HostnameMiddleware 37 | m.use E20::Ops::Middleware::TransactionIdMiddleware 38 | end 39 | 40 | ### Rack ### 41 | 42 | In `config.ru`, add: 43 | 44 | use E20::Ops::Middleware::RevisionMiddleware 45 | use E20::Ops::Middleware::HostnameMiddleware 46 | use E20::Ops::Middleware::TransactionIdMiddleware 47 | 48 | Usage 49 | ----- 50 | 51 | The information exposed by the middleware can be viewed manually with `curl` 52 | and will also be logged to the provided logger (or STDOUT). Additionally, 53 | we've found it useful to log this information when receiving responses from 54 | REST web services. 55 | 56 | ### Revision Middleware ### 57 | 58 | Revisions can be queried directly by using the `/system/revision` endpoint: 59 | 60 | $ curl http://instance/system/revision 61 | fe09f24b4a927b6eab5db66b6a89fe960e2ff03b 62 | 63 | The current revision will be passed as an HTTP header for other requests: 64 | 65 | $ curl -I http://instance/ 66 | HTTP/1.1 200 OK 67 | Content-Type: text/html; charset=utf-8 68 | Content-Length: 3304 69 | X-Revision: fe09f24b4a927b6eab5db66b6a89fe960e2ff03b 70 | 71 | The current revision will also be logged upon application start: 72 | 73 | $ grep RevisionMiddleware log/production.log 74 | [E20::Ops::Middleware::RevisionMiddleware] Running: fe09f24b4a927b6eab5db66b6a89fe960e2ff03b 75 | 76 | ### Hostname Middleware ### 77 | 78 | The hostname of the system that processed the request will be passed as an 79 | HTTP header: 80 | 81 | $ curl -I http://instance/ 82 | HTTP/1.1 200 OK 83 | Content-Type: text/html; charset=utf-8 84 | Content-Length: 3304 85 | X-Served-By: fulton 86 | 87 | The hostname will also be logged upon application start: 88 | 89 | $ grep HostnameMiddleware log/production.log 90 | [E20::Ops::Middleware::HostnameMiddleware] Running on: fulton 91 | 92 | ### Transaction ID Middleware ### 93 | 94 | A transaction ID will be logged for each incoming request: 95 | 96 | $ grep TransactionIdMiddleware log/production.log 97 | [E20::Ops::Middleware::TransactionIdMiddleware] Transaction ID: 111d3180-91f4-012d-ce1a-549a20d01d99 98 | 99 | The transaction ID will also be passed as an HTTP header: 100 | 101 | $ curl -I http://instance/ 102 | HTTP/1.1 200 OK 103 | Content-Type: text/html; charset=utf-8 104 | Content-Length: 3304 105 | X-Transaction: 111d3180-91f4-012d-ce1a-549a20d01d99 106 | 107 | Thanks 108 | ------ 109 | 110 | Thanks to Efficiency 2.0 ([http://efficiency20.com](http://efficiency20.com)) 111 | for sponsoring development of this gem. 112 | 113 | Development 114 | ----------- 115 | 116 | To run the tests: 117 | 118 | $ bundle install 119 | $ rake 120 | 121 | License 122 | ------- 123 | 124 | (The MIT License) 125 | 126 | Copyright © 2010 Efficiency 2.0, LLC 127 | 128 | Permission is hereby granted, free of charge, to any person obtaining a copy 129 | of this software and associated documentation files (the ‘Software’), to deal 130 | in the Software without restriction, including without limitation the rights 131 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 132 | copies of the Software, and to permit persons to whom the Software is 133 | furnished to do so, subject to the following conditions: 134 | 135 | The above copyright notice and this permission notice shall be included in all 136 | copies or substantial portions of the Software. 137 | 138 | THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 139 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 140 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 141 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 142 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 143 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 144 | SOFTWARE. 145 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rubygems" 2 | require "bundler" 3 | Bundler.setup 4 | 5 | require 'jeweler' 6 | 7 | Jeweler::Tasks.new do |gem| 8 | gem.name = "e20_ops_middleware" 9 | gem.summary = "Collection of useful middleware for exposing information about deployed Rack applications" 10 | gem.email = "tech@efficiency20.com" 11 | gem.homepage = "http://github.com/efficiency20/ops_middleware" 12 | gem.description = "Adds middleware for debugging purposes" 13 | gem.authors = ["Efficiency 2.0"] 14 | gem.add_dependency "uuid", "~> 2.3.2" 15 | gem.add_development_dependency "rspec", "~> 1.3.0" 16 | end 17 | 18 | require "spec/rake/spectask" 19 | 20 | desc "Run all specs" 21 | Spec::Rake::SpecTask.new("spec") do |t| 22 | t.spec_files = FileList["spec/**/*_spec.rb"] 23 | end 24 | 25 | task :default => :spec 26 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.1.2 2 | -------------------------------------------------------------------------------- /ci.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ["bundle install vendor/bundle", 4 | "rake" 5 | ].each do |stage| 6 | exit 1 unless system(stage) 7 | end -------------------------------------------------------------------------------- /e20_ops_middleware.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{e20_ops_middleware} 8 | s.version = "2.1.2" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Efficiency 2.0"] 12 | s.date = %q{2011-06-15} 13 | s.description = %q{Adds middleware for debugging purposes} 14 | s.email = %q{tech@efficiency20.com} 15 | s.extra_rdoc_files = [ 16 | "README.md" 17 | ] 18 | s.files = [ 19 | ".gitignore", 20 | "Gemfile", 21 | "Gemfile.lock", 22 | "History.txt", 23 | "README.md", 24 | "Rakefile", 25 | "VERSION", 26 | "ci.rb", 27 | "e20_ops_middleware.gemspec", 28 | "lib/e20/ops/hostname.rb", 29 | "lib/e20/ops/middleware.rb", 30 | "lib/e20/ops/middleware/hostname_middleware.rb", 31 | "lib/e20/ops/middleware/revision_middleware.rb", 32 | "lib/e20/ops/middleware/transaction_id_middleware.rb", 33 | "lib/e20/ops/revision.rb", 34 | "spec/ops/hostname_spec.rb", 35 | "spec/ops/middleware/hostname_middleware_spec.rb", 36 | "spec/ops/middleware/revision_middleware_spec.rb", 37 | "spec/ops/middleware/transaction_id_middleware_spec.rb", 38 | "spec/ops/revision_spec.rb", 39 | "spec/spec.opts", 40 | "spec/spec_helper.rb" 41 | ] 42 | s.homepage = %q{http://github.com/efficiency20/ops_middleware} 43 | s.rdoc_options = ["--charset=UTF-8"] 44 | s.require_paths = ["lib"] 45 | s.rubygems_version = %q{1.4.2} 46 | s.summary = %q{Collection of useful middleware for exposing information about deployed Rack applications} 47 | s.test_files = [ 48 | "spec/ops/hostname_spec.rb", 49 | "spec/ops/middleware/hostname_middleware_spec.rb", 50 | "spec/ops/middleware/revision_middleware_spec.rb", 51 | "spec/ops/middleware/transaction_id_middleware_spec.rb", 52 | "spec/ops/revision_spec.rb", 53 | "spec/spec_helper.rb" 54 | ] 55 | 56 | if s.respond_to? :specification_version then 57 | s.specification_version = 3 58 | 59 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 60 | s.add_runtime_dependency(%q, ["~> 2.3.2"]) 61 | s.add_development_dependency(%q, ["~> 1.3.0"]) 62 | else 63 | s.add_dependency(%q, ["~> 2.3.2"]) 64 | s.add_dependency(%q, ["~> 1.3.0"]) 65 | end 66 | else 67 | s.add_dependency(%q, ["~> 2.3.2"]) 68 | s.add_dependency(%q, ["~> 1.3.0"]) 69 | end 70 | end 71 | 72 | -------------------------------------------------------------------------------- /lib/e20/ops/hostname.rb: -------------------------------------------------------------------------------- 1 | module E20 2 | module Ops 3 | class Hostname 4 | 5 | def to_s 6 | @hostname ||= `hostname`.strip 7 | end 8 | 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/e20/ops/middleware.rb: -------------------------------------------------------------------------------- 1 | module E20 2 | module Ops 3 | autoload :Revision, "e20/ops/revision" 4 | autoload :Hostname, "e20/ops/hostname" 5 | 6 | module Middleware 7 | autoload :HostnameMiddleware, "e20/ops/middleware/hostname_middleware" 8 | autoload :RevisionMiddleware, "e20/ops/middleware/revision_middleware" 9 | autoload :TransactionIdMiddleware, "e20/ops/middleware/transaction_id_middleware" 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/e20/ops/middleware/hostname_middleware.rb: -------------------------------------------------------------------------------- 1 | module E20 2 | module Ops 3 | module Middleware 4 | class HostnameMiddleware 5 | 6 | def initialize(app, options = {}) 7 | @app = app 8 | @hostname = options[:hostname] || Hostname.new 9 | 10 | if (logger = options[:logger]) 11 | logger.info "[#{self.class.name}] Running on: #{@hostname}" 12 | end 13 | end 14 | 15 | def call(env) 16 | status, headers, body = @app.call(env) 17 | headers["X-Served-By"] = @hostname.to_s 18 | [status, headers, body] 19 | end 20 | 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/e20/ops/middleware/revision_middleware.rb: -------------------------------------------------------------------------------- 1 | module E20 2 | module Ops 3 | module Middleware 4 | class RevisionMiddleware 5 | 6 | def initialize(app, options = {}) 7 | @app = app 8 | @revision = options[:revision] || Revision.new 9 | 10 | if (logger = options[:logger]) 11 | logger.info "[#{self.class.name}] Running: #{@revision}" 12 | end 13 | end 14 | 15 | def call(env) 16 | if env["PATH_INFO"] == "/system/revision" 17 | body = "#{@revision}\n" 18 | [200, { "Content-Type" => "text/plain", "Content-Length" => body.size.to_s }, [body]] 19 | else 20 | status, headers, body = @app.call(env) 21 | headers["X-Revision"] = @revision.to_s 22 | [status, headers, body] 23 | end 24 | end 25 | 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/e20/ops/middleware/transaction_id_middleware.rb: -------------------------------------------------------------------------------- 1 | require "uuid" 2 | require "logger" 3 | 4 | module E20 5 | module Ops 6 | module Middleware 7 | class TransactionIdMiddleware 8 | 9 | def initialize(app, options = {}) 10 | @app = app 11 | @uuid_generator = options[:uuid_generator] || UUID.method(:generate) 12 | @logger = options[:logger] || Logger.new(STDOUT) 13 | end 14 | 15 | def call(env) 16 | uuid = @uuid_generator.call.to_s 17 | @logger.info "[#{self.class.name}] Transaction ID: #{uuid}" 18 | 19 | status, headers, body = @app.call(env) 20 | headers["X-Transaction"] = uuid 21 | [status, headers, body] 22 | end 23 | 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/e20/ops/revision.rb: -------------------------------------------------------------------------------- 1 | require "active_support" 2 | 3 | module E20 4 | module Ops 5 | class Revision 6 | 7 | def initialize(root = Pathname.new(Dir.pwd)) 8 | @root = root 9 | end 10 | 11 | def to_s 12 | @revision ||= begin 13 | if revision_file.exist? 14 | revision_file.read.strip 15 | elsif revision_from_git.present? 16 | revision_from_git 17 | else 18 | "unknown" 19 | end 20 | end 21 | end 22 | 23 | private 24 | 25 | def revision_from_git 26 | @revision_from_git ||= `git rev-parse HEAD`.strip 27 | end 28 | 29 | def revision_file 30 | @root.join("REVISION") 31 | end 32 | 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/ops/hostname_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe E20::Ops::Hostname do 4 | it "returns the hostname" do 5 | hostname = E20::Ops::Hostname.new 6 | hostname.should_receive(:`).with("hostname").and_return("Computer.local\n") 7 | hostname.to_s.should == "Computer.local" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/ops/middleware/hostname_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe E20::Ops::Middleware::HostnameMiddleware do 4 | let(:app) { Proc.new { |env| [200, {}, "OK!"] } } 5 | 6 | it "is initialized with an app" do 7 | E20::Ops::Middleware::HostnameMiddleware.new(app) 8 | end 9 | 10 | it "delegates to the app" do 11 | middleware = E20::Ops::Middleware::HostnameMiddleware.new(app) 12 | status, headers, body = middleware.call({}) 13 | body.should == "OK!" 14 | end 15 | 16 | it "logs the hostname when initialized" do 17 | log_io = StringIO.new 18 | E20::Ops::Middleware::HostnameMiddleware.new(app, :logger => Logger.new(log_io)) 19 | log_io.string.should include("[E20::Ops::Middleware::HostnameMiddleware] Running on: ") 20 | end 21 | 22 | it "sets an X-Served-By header" do 23 | middleware = E20::Ops::Middleware::HostnameMiddleware.new(app, :hostname => "Computer.local") 24 | status, headers, body = middleware.call({}) 25 | headers["X-Served-By"].should == "Computer.local" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/ops/middleware/revision_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe E20::Ops::Middleware::RevisionMiddleware do 4 | let(:app) { Proc.new { |env| [200, {}, "OK!"] } } 5 | 6 | it "is initialized with an app" do 7 | E20::Ops::Middleware::RevisionMiddleware.new(app) 8 | end 9 | 10 | context "/system/revision" do 11 | it "returns the current running revision" do 12 | middleware = E20::Ops::Middleware::RevisionMiddleware.new(app, :revision => "rev") 13 | status, headers, body = middleware.call({"PATH_INFO" => "/system/revision"}) 14 | body.should == ["rev\n"] 15 | end 16 | end 17 | 18 | context "any other endpoint" do 19 | it "delegates to the app" do 20 | middleware = E20::Ops::Middleware::RevisionMiddleware.new(app) 21 | status, headers, body = middleware.call({}) 22 | body.should == "OK!" 23 | end 24 | 25 | it "logs the running revision when initialized" do 26 | log_io = StringIO.new 27 | E20::Ops::Middleware::RevisionMiddleware.new(app, :revision => "rev", :logger => Logger.new(log_io)) 28 | log_io.string.should include("[E20::Ops::Middleware::RevisionMiddleware] Running: rev") 29 | end 30 | 31 | it "sets an X-Revision header" do 32 | middleware = E20::Ops::Middleware::RevisionMiddleware.new(app, :revision => "the_revision", :logger => nil) 33 | status, headers, body = middleware.call({}) 34 | headers["X-Revision"].should == "the_revision" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/ops/middleware/transaction_id_middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe E20::Ops::Middleware::TransactionIdMiddleware do 4 | let(:app) { Proc.new { |env| [200, {}, "OK!"] } } 5 | let(:uuid) { stub(:call => "abc123") } 6 | let(:logger) { Logger.new(StringIO.new) } 7 | 8 | it "is initialized with an app" do 9 | E20::Ops::Middleware::TransactionIdMiddleware.new(app) 10 | end 11 | 12 | it "delegates to the app" do 13 | middleware = E20::Ops::Middleware::TransactionIdMiddleware.new(app, :logger => logger) 14 | status, headers, body = middleware.call({}) 15 | body.should == "OK!" 16 | end 17 | 18 | it "defaults to a 36 character transaction id with dashes" do 19 | middleware = E20::Ops::Middleware::TransactionIdMiddleware.new(app, :logger => logger) 20 | status, headers, body = middleware.call({}) 21 | headers["X-Transaction"].size.should == 36 22 | headers["X-Transaction"].should include('-') 23 | end 24 | 25 | it "sets an X-Transaction header" do 26 | middleware = E20::Ops::Middleware::TransactionIdMiddleware.new(app, :uuid_generator => uuid, :logger => logger) 27 | status, headers, body = middleware.call({}) 28 | headers["X-Transaction"].should == "abc123" 29 | end 30 | 31 | it "logs a line for each request" do 32 | log_io = StringIO.new 33 | middleware = E20::Ops::Middleware::TransactionIdMiddleware.new(app, :uuid_generator => uuid, :logger => Logger.new(log_io)) 34 | middleware.call({}) 35 | log_io.string.should include("[E20::Ops::Middleware::TransactionIdMiddleware] Transaction ID: abc123") 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/ops/revision_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "tmpdir" 3 | 4 | describe E20::Ops::Revision do 5 | context "when a REVISION file is present" do 6 | it "adds a X-Revision header with the REVISION" do 7 | tmp_path = Pathname.new(Dir.tmpdir) 8 | tmp_path.join("REVISION").open("w") { |f| f.write "hello\n" } 9 | E20::Ops::Revision.new(tmp_path).to_s.should == "hello" 10 | end 11 | end 12 | 13 | context "when a REVISION file is not present" do 14 | it "adds a X-Revision header with the git rev-parse HEAD" do 15 | revision = E20::Ops::Revision.new 16 | revision.should_receive(:`).with("git rev-parse HEAD").and_return("abc123") 17 | revision.to_s.should == "abc123" 18 | end 19 | end 20 | 21 | context "when neither a REVISION file or a git revision are available" do 22 | it "adds a X-Revision header of 'unknown'" do 23 | revision = E20::Ops::Revision.new 24 | revision.stub(:` => "") 25 | revision.to_s.should == "unknown" 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "e20/ops/middleware" 3 | --------------------------------------------------------------------------------