ops 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/.gitignore +14 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +14 -0
  4. data/Gemfile.lock +134 -0
  5. data/LICENSE +7 -0
  6. data/README.md +81 -0
  7. data/Rakefile +27 -0
  8. data/examples/rails3/.gitignore +15 -0
  9. data/examples/rails3/.rspec +3 -0
  10. data/examples/rails3/Gemfile +5 -0
  11. data/examples/rails3/Gemfile.lock +130 -0
  12. data/examples/rails3/Rakefile +13 -0
  13. data/examples/rails3/config/application.rb +34 -0
  14. data/examples/rails3/config/boot.rb +6 -0
  15. data/examples/rails3/config/environment.rb +5 -0
  16. data/examples/rails3/config/initializers/secret_token.rb +1 -0
  17. data/examples/rails3/config/locales/en.yml +5 -0
  18. data/examples/rails3/config/routes.rb +3 -0
  19. data/examples/rails3/config.ru +4 -0
  20. data/examples/rails3/public/404.html +26 -0
  21. data/examples/rails3/public/422.html +26 -0
  22. data/examples/rails3/public/500.html +25 -0
  23. data/examples/rails3/public/favicon.ico +0 -0
  24. data/examples/rails3/public/index.html +14 -0
  25. data/examples/rails3/public/robots.txt +5 -0
  26. data/examples/rails3/script/rails +6 -0
  27. data/examples/rails3/spec/functional/ops_routes_spec.rb +63 -0
  28. data/examples/rails3/spec/spec_helper.rb +5 -0
  29. data/examples/sample_deploys/1234/REVISION +1 -0
  30. data/examples/sample_deploys/1234/VERSION +1 -0
  31. data/examples/sample_deploys/2341/REVISION +1 -0
  32. data/examples/sample_deploys/2341/VERSION +1 -0
  33. data/examples/sample_deploys/3412/REVISION +1 -0
  34. data/examples/sample_deploys/3412/VERSION +1 -0
  35. data/examples/sample_deploys/4123/REVISION +1 -0
  36. data/examples/sample_deploys/4123/VERSION +1 -0
  37. data/examples/sinatra/.rspec +3 -0
  38. data/examples/sinatra/Rakefile +15 -0
  39. data/examples/sinatra/app.rb +14 -0
  40. data/examples/sinatra/config.ru +17 -0
  41. data/examples/sinatra/spec/functional/ops_routes_spec.rb +63 -0
  42. data/examples/sinatra/spec/spec_helper.rb +3 -0
  43. data/lib/ops/config.rb +43 -0
  44. data/lib/ops/heartbeat.rb +41 -0
  45. data/lib/ops/revision.rb +117 -0
  46. data/lib/ops/server/helpers.rb +44 -0
  47. data/lib/ops/server/views/layout.html.slim +22 -0
  48. data/lib/ops/server/views/version.html.slim +50 -0
  49. data/lib/ops/server/views/version.json.rabl +9 -0
  50. data/lib/ops/server.rb +45 -0
  51. data/lib/ops/version.rb +3 -0
  52. data/lib/ops.rb +14 -0
  53. data/ops.gemspec +25 -0
  54. data/spec/functional/ops_routes_spec.rb +63 -0
  55. data/spec/ops/config_spec.rb +0 -0
  56. data/spec/ops/heartbeat_spec.rb +49 -0
  57. data/spec/ops/revision_spec.rb +41 -0
  58. data/spec/ops/server_spec.rb +0 -0
  59. data/spec/ops/version_spec.rb +7 -0
  60. data/spec/ops_spec.rb +9 -0
  61. data/spec/spec_helper.rb +15 -0
  62. metadata +192 -0
@@ -0,0 +1,63 @@
1
+ require 'rspec'
2
+ require 'rack/test'
3
+ require 'spec_helper'
4
+ require 'json'
5
+
6
+ RSpec::Matchers.define :have_content_type do |content_type|
7
+ CONTENT_HEADER_MATCHER = /^([A-Za-z]+\/[\w\-+\.]+)(;charset=(.*))?/
8
+
9
+ chain :with_charset do |charset|
10
+ @charset = charset
11
+ end
12
+
13
+ match do |response|
14
+ _, content, _, charset = *content_type_header.match(CONTENT_HEADER_MATCHER).to_a
15
+
16
+ if @charset
17
+ @charset == charset && content == content_type
18
+ else
19
+ content == content_type
20
+ end
21
+ end
22
+
23
+ failure_message_for_should do |response|
24
+ if @charset
25
+ "Content type #{content_type_header.inspect} should match #{content_type.inspect} with charset #{@charset}"
26
+ else
27
+ "Content type #{content_type_header.inspect} should match #{content_type.inspect}"
28
+ end
29
+ end
30
+
31
+ failure_message_for_should_not do |model|
32
+ if @charset
33
+ "Content type #{content_type_header.inspect} should not match #{content_type.inspect} with charset #{@charset}"
34
+ else
35
+ "Content type #{content_type_header.inspect} should not match #{content_type.inspect}"
36
+ end
37
+ end
38
+
39
+ def content_type_header
40
+ last_response.headers['Content-Type']
41
+ end
42
+ end
43
+
44
+ describe 'routes', :type => :controller do
45
+ include Rack::Test::Methods
46
+
47
+ it 'renders a version page' do
48
+ get "/ops/version"
49
+ last_response.should be_ok
50
+ end
51
+
52
+ it 'renders a json version page' do
53
+ get "/ops/version.json"
54
+ last_response.should be_ok
55
+ last_response.should have_content_type('application/json').with_charset('utf-8')
56
+ end
57
+
58
+ it 'renders a heartbeat page' do
59
+ get "/ops/heartbeat"
60
+ last_response.should be_ok
61
+ end
62
+
63
+ end
@@ -0,0 +1,5 @@
1
+ require File.expand_path("../../config/environment", __FILE__)
2
+
3
+ def app
4
+ Rails3::Application
5
+ end
@@ -0,0 +1 @@
1
+ 1234567890
@@ -0,0 +1 @@
1
+ v1.0.0^{}
@@ -0,0 +1 @@
1
+ 2345678901
@@ -0,0 +1 @@
1
+ v1.0.1^{}
@@ -0,0 +1 @@
1
+ 3456789012
@@ -0,0 +1 @@
1
+ v1.1.1^{}
@@ -0,0 +1 @@
1
+ 4567890123
@@ -0,0 +1 @@
1
+ v2.0.0^{}
@@ -0,0 +1,3 @@
1
+ --color
2
+ -fn
3
+ --tty
@@ -0,0 +1,15 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib'
2
+ $LOAD_PATH.unshift File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
3
+
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc "Start the demo using `rackup`"
7
+ task :start do
8
+ exec "rackup config.ru"
9
+ end
10
+
11
+ task default: [:start]
12
+
13
+ RSpec::Core::RakeTask.new('test:ops') do |test|
14
+ test.pattern = "spec/functional/*_spec.rb"
15
+ end
@@ -0,0 +1,14 @@
1
+ require 'sinatra/base'
2
+
3
+ module Demo
4
+ class App < Sinatra::Base
5
+ get '/' do
6
+ "Hello, world. I'm a demo app."
7
+ end
8
+
9
+ get '/test' do
10
+ "Still me, the demo app."
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ require 'logger'
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../../lib'
4
+ $LOAD_PATH.unshift File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
5
+ require 'app'
6
+ require 'ops'
7
+
8
+ use Rack::ShowExceptions
9
+
10
+ Ops.setup do |config|
11
+ config.file_root = '../sample_deploys/4123'
12
+ config.environment = ENV['RACK_ENV']
13
+ end
14
+
15
+ run Rack::URLMap.new \
16
+ "/" => Demo::App.new,
17
+ "/ops" => Ops::Server.new
@@ -0,0 +1,63 @@
1
+ require 'rspec'
2
+ require 'rack/test'
3
+ require 'spec_helper'
4
+ require 'json'
5
+
6
+ RSpec::Matchers.define :have_content_type do |content_type|
7
+ CONTENT_HEADER_MATCHER = /^([A-Za-z]+\/[\w\-+\.]+)(;charset=(.*))?/
8
+
9
+ chain :with_charset do |charset|
10
+ @charset = charset
11
+ end
12
+
13
+ match do |response|
14
+ _, content, _, charset = *content_type_header.match(CONTENT_HEADER_MATCHER).to_a
15
+
16
+ if @charset
17
+ @charset == charset && content == content_type
18
+ else
19
+ content == content_type
20
+ end
21
+ end
22
+
23
+ failure_message_for_should do |response|
24
+ if @charset
25
+ "Content type #{content_type_header.inspect} should match #{content_type.inspect} with charset #{@charset}"
26
+ else
27
+ "Content type #{content_type_header.inspect} should match #{content_type.inspect}"
28
+ end
29
+ end
30
+
31
+ failure_message_for_should_not do |model|
32
+ if @charset
33
+ "Content type #{content_type_header.inspect} should not match #{content_type.inspect} with charset #{@charset}"
34
+ else
35
+ "Content type #{content_type_header.inspect} should not match #{content_type.inspect}"
36
+ end
37
+ end
38
+
39
+ def content_type_header
40
+ last_response.headers['Content-Type']
41
+ end
42
+ end
43
+
44
+ describe 'routes', :type => :controller do
45
+ include Rack::Test::Methods
46
+
47
+ it 'renders a version page' do
48
+ get "/ops/version"
49
+ last_response.should be_ok
50
+ end
51
+
52
+ it 'renders a json version page' do
53
+ get "/ops/version.json"
54
+ last_response.should be_ok
55
+ last_response.should have_content_type('application/json').with_charset('utf-8')
56
+ end
57
+
58
+ it 'renders a heartbeat page' do
59
+ get "/ops/heartbeat"
60
+ last_response.should be_ok
61
+ end
62
+
63
+ end
@@ -0,0 +1,3 @@
1
+ def app
2
+ eval "Rack::Builder.new {( " + File.read(File.dirname(__FILE__) + '/../config.ru') + "\n )}"
3
+ end
data/lib/ops/config.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Ops
2
+ class Config
3
+ def initialize(data={})
4
+ @data = {}
5
+ update!(data)
6
+ end
7
+
8
+ def update!(data)
9
+ data.each do |key, value|
10
+ self[key] = value
11
+ end
12
+ end
13
+
14
+ def [](key)
15
+ @data[key.to_sym]
16
+ end
17
+
18
+ def []=(key, value)
19
+ if value.class == Hash
20
+ @data[key.to_sym] = Config.new(value)
21
+ else
22
+ @data[key.to_sym] = value
23
+ end
24
+ end
25
+
26
+ def method_missing(sym, *args)
27
+ if sym.to_s =~ /(.+)=$/
28
+ self[$1] = args.first
29
+ else
30
+ self[sym]
31
+ end
32
+ end
33
+ end
34
+
35
+ class << self
36
+ attr_accessor :config
37
+
38
+ def setup
39
+ self.config ||= Config.new
40
+ yield config
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ module Ops
2
+ class Heartbeat
3
+ class << self
4
+ def add(name, &block)
5
+ instance.add name, &block
6
+ end
7
+
8
+ def check(name)
9
+ instance.check(name)
10
+ end
11
+
12
+ def instance
13
+ @singleton ||= new
14
+ end
15
+ end
16
+
17
+ def heartbeats
18
+ @heartbeats ||= {}
19
+ end
20
+
21
+ def add(name, &block)
22
+ heartbeats[name] = block
23
+ end
24
+
25
+ def check(name)
26
+ begin
27
+ return heartbeats[name.to_sym].call
28
+ rescue Exception => e
29
+ # print stacktrace for error raised by executing block
30
+ puts "Exception: #{e}\n#{e.backtrace[2..-1].join("\n")}" unless heartbeats[name.to_sym].nil?
31
+ return false
32
+ end
33
+ end
34
+ end
35
+
36
+ class << self
37
+ def add_heartbeat(name, &block)
38
+ Heartbeat.add name, &block
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,117 @@
1
+ module Ops
2
+ class Revision
3
+ def initialize(new_headers={}, opts = Ops.config)
4
+ @file_root = opts.file_root.to_s # convert to string in case they pass us a Pathname
5
+ @environment = opts.environment
6
+ @headers = new_headers
7
+ end
8
+
9
+ def version_or_branch
10
+ @version ||= if version_file?
11
+ chomp(version_file).gsub('^{}', '')
12
+ elsif development? && `git branch` =~ /^\* (.*)$/
13
+ $1
14
+ else
15
+ 'Unknown (VERSION file is missing)'
16
+ end
17
+ end
18
+
19
+ def previous_versions
20
+ @previous_versions ||= get_previous_by_time
21
+ end
22
+
23
+ def get_previous_by_time
24
+ get_previous_versions.sort { |a, b| a[:time] <=> b[:time] }
25
+ end
26
+
27
+ def get_previous_versions
28
+ Dir["#{path}/../*"].each_with_object([]) do |dir, array|
29
+ next if dir =~ /#{current_dir}$/
30
+ version, revision = File.join(dir, 'VERSION'), File.join(dir, 'REVISION')
31
+ array << stats_hash(version: version, revision: revision) if File.exists?(version) && File.exists?(revision)
32
+ end
33
+ end
34
+
35
+ def path
36
+ File.absolute_path file_root
37
+ end
38
+
39
+ def current_dir
40
+ file_root.split('/').last
41
+ end
42
+
43
+ def stats_hash(files)
44
+ { version: get_version(files[:version]),
45
+ revision: get_revision(files[:revision]),
46
+ time: get_time(files[:revision]) }
47
+ end
48
+
49
+ def get_version(file)
50
+ chomp(file).gsub('^{}', '')
51
+ end
52
+
53
+ def get_revision(file)
54
+ chomp file
55
+ end
56
+
57
+ def chomp(file)
58
+ File.read(file).chomp
59
+ end
60
+
61
+ def get_time(file)
62
+ File.stat(file).mtime
63
+ end
64
+
65
+ def file_root
66
+ @file_root
67
+ end
68
+
69
+ def environment
70
+ @environment
71
+ end
72
+
73
+ def development?
74
+ environment == 'development'
75
+ end
76
+
77
+ def version_file
78
+ @version_file ||= File.join(file_root, 'VERSION')
79
+ end
80
+
81
+ def version_file?
82
+ File.exists? version_file
83
+ end
84
+
85
+ def revision_file
86
+ @revision_file ||= File.join(file_root, 'REVISION')
87
+ end
88
+
89
+ def revision_file?
90
+ File.exists? revision_file
91
+ end
92
+
93
+ def deploy_date
94
+ @deploy_date ||= if version_file?
95
+ get_time version_file
96
+ elsif development?
97
+ 'Live'
98
+ else
99
+ 'Unknown (VERSION file is missing)'
100
+ end
101
+ end
102
+
103
+ def last_commit
104
+ @last_commit ||= if revision_file?
105
+ chomp revision_file
106
+ elsif development? && `git show` =~ /^commit (.*)$/
107
+ $1
108
+ else
109
+ 'Unknown (REVISION file is missing)'
110
+ end
111
+ end
112
+
113
+ def headers
114
+ @headers.select{|k,v| k.match(/^[-A-Z_].*$/) }
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,44 @@
1
+ module Ops
2
+ module Helpers
3
+ def hostname
4
+ @hostname ||= `/bin/hostname` || 'Unknown'
5
+ end
6
+
7
+ def app_name
8
+ @app_name ||= begin
9
+ dirs = Dir.pwd.split('/')
10
+ if dirs.last =~ /^\d+$/
11
+ dirs[-3]
12
+ else
13
+ dirs.last
14
+ end.sub(/\.com$/, '')
15
+ end
16
+ end
17
+
18
+ def version_link(version)
19
+ github_link 'tree', version
20
+ end
21
+
22
+ def commit_link(commit)
23
+ github_link 'commit', commit
24
+ end
25
+
26
+ def github_link(resource, subresource)
27
+ "https://github.com/primedia/#{app_name}/#{resource}/#{subresource}" unless subresource =~ /^Unknown/
28
+ end
29
+
30
+ def print_detail(object, indent = 0)
31
+ output = ""
32
+ if object.kind_of? Hash
33
+ output << "{\n"
34
+ output << object.collect { |key, value|
35
+ " " * indent + " #{print_detail key} => #{print_detail value, indent+1}"
36
+ }.join(",\n") << "\n"
37
+ output << " " * indent + "}"
38
+ else
39
+ output << object.inspect
40
+ end
41
+ output
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ doctype html
2
+ html
3
+ head
4
+ title= app_name
5
+ css:
6
+ td {padding: 0 5px; vertical-align: top;}
7
+ pre {margin: 0}
8
+ tr.even {background: #dddddd;}
9
+ .container {font-size: 20px; width: 1000px; padding-bottom: 100px;}
10
+ .label {float: left; width: 400px; font-weight: bold}
11
+ .spacer {clear: both; padding: 10px;}
12
+ .value {float: left; width: 500px; text-align: right;}
13
+ td {padding: 0 5px}
14
+ #previous_versions .value {clear: both; width: 1000px; text-align: left;}
15
+ #previous_versions tr.header td {font-weight: bold}
16
+ #previous_versions .value tr.even {background: #dddddd;}
17
+ #headers .value {clear: both; width: 1000px; text-align: left;}
18
+ #headers .value tr.even {background: #dddddd;}
19
+ .clear {clear: both;}
20
+ body
21
+ ==yield
22
+
@@ -0,0 +1,50 @@
1
+ .container
2
+ .spacer
3
+ #version
4
+ .label= "#{app_name} Version"
5
+ .value
6
+ a href=version_link(@version.version_or_branch) = @version.version_or_branch
7
+ .spacer
8
+ #date
9
+ .label Date Deployed
10
+ .value= @version.deploy_date
11
+ .spacer
12
+ #commit
13
+ .label Last Commit
14
+ .value
15
+ a href=commit_link(@version.last_commit) = @version.last_commit
16
+ .spacer
17
+ #host
18
+ .label Host
19
+ .value= hostname
20
+ .spacer
21
+ #environment
22
+ .label Environment
23
+ .value= @version.environment
24
+ .spacer
25
+ #previous_versions
26
+ .label Previous Versions
27
+ .value
28
+ - unless @previous_versions.empty?
29
+ table
30
+ tr.header
31
+ td Time
32
+ td Version
33
+ td Commit
34
+ - @previous_versions.each_with_index do |version, i|
35
+ tr class=(i%2==0 ? 'even' : nil)
36
+ td= version[:time].strftime('%x %X')
37
+ td
38
+ a href=version_link(version[:version]) = version[:version]
39
+ td
40
+ a href=commit_link(version[:revision]) = version[:revision]
41
+ .spacer
42
+ #headers
43
+ .label Headers
44
+ .value
45
+ table
46
+ - @headers.sort_by{ |h| h.first }.each_with_index do |(header, value), i|
47
+ tr class=(i%2==0 ? 'even' : nil)
48
+ td= header
49
+ td= value
50
+ .clear
@@ -0,0 +1,9 @@
1
+ node :version do
2
+ @version.version_or_branch
3
+ end
4
+ node :previous_versions do
5
+ @previous_versions
6
+ end
7
+ node :headers do
8
+ @headers
9
+ end
data/lib/ops/server.rb ADDED
@@ -0,0 +1,45 @@
1
+ require 'sinatra/base'
2
+ require 'sinatra/respond_to'
3
+ require 'ops/server/helpers'
4
+ require 'rabl'
5
+ require 'slim'
6
+
7
+ module Ops
8
+ class Server < Sinatra::Base
9
+ Rabl.register!
10
+ Server.register Sinatra::RespondTo
11
+ dir = File.dirname(File.expand_path(__FILE__))
12
+ set :views, "#{dir}/server/views"
13
+ Slim::Engine.set_default_options shortcut: { '#' => 'id', '.' => 'class' }
14
+
15
+ helpers Ops::Helpers
16
+
17
+ def request_headers
18
+ env.each_with_object({}) { |(k,v), headers| headers[k] = v }
19
+ end
20
+
21
+ get '/version/?' do
22
+ @version = Revision.new request_headers
23
+ @previous_versions = @version.previous_versions
24
+ @headers = @version.headers
25
+ respond_to do |wants|
26
+ wants.html { slim :version }
27
+ wants.json { render :rabl, :version, format: 'json' }
28
+ end
29
+ end
30
+
31
+ get '/heartbeat/?' do
32
+ 'OK'
33
+ end
34
+
35
+ get '/heartbeat/:name/?' do
36
+ name = params[:name]
37
+ if Heartbeat.check name
38
+ "#{name} is OK"
39
+ else
40
+ status 503
41
+ "#{name} does not have a heartbeat"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ module Ops
2
+ VERSION = '0.0.1'
3
+ end
data/lib/ops.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'ops/config'
2
+ require 'ops/version'
3
+ require 'ops/revision'
4
+ require 'ops/heartbeat'
5
+ require 'ops/server'
6
+
7
+ module Ops
8
+ class << self
9
+ def new
10
+ Server.new
11
+ end
12
+ end
13
+ end
14
+
data/ops.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/ops/version', __FILE__)
3
+ require 'date'
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ["Michael Pelz-Sherman", "Colin Rymer", "Primedia Team"]
7
+ gem.email = ["mpelzsherman@gmail.com", "colin.rymer@gmail.com"]
8
+ gem.description = 'This gem provides standardized support for obtaining version and heartbeat information from Sinatra or Rails-based web applications.'
9
+ gem.summary = "Provide ops info endpoints."
10
+ gem.date = Date.today.to_s
11
+ gem.homepage = "http://github.com/primedia/ops"
12
+ gem.license = 'MIT'
13
+ gem.executables = []
14
+ gem.files = `git ls-files | grep -v myapp`.split("\n")
15
+ gem.test_files = `git ls-files -- test/*`.split("\n")
16
+ gem.name = "ops"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Ops::VERSION
19
+ gem.required_ruby_version = '>= 1.9'
20
+ gem.add_dependency 'oj', '~> 1.4.5'
21
+ gem.add_dependency 'rabl', '~> 0.7.6'
22
+ gem.add_dependency 'slim', '~> 1.3.4'
23
+ gem.add_dependency 'sinatra', '~> 1.3.3'
24
+ gem.add_dependency 'sinatra-respond_to', '~> 0.8.0'
25
+ end