ops 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 (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