outpost 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/.gitignore +1 -0
  2. data/CHANGELOG.rdoc +16 -0
  3. data/Gemfile +9 -3
  4. data/Gemfile.lock +39 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.markdown +40 -8
  7. data/Rakefile +14 -0
  8. data/TODO.md +7 -0
  9. data/lib/outpost.rb +1 -1
  10. data/lib/outpost/application.rb +143 -0
  11. data/lib/outpost/expectations/response_body.rb +23 -9
  12. data/lib/outpost/expectations/response_code.rb +5 -0
  13. data/lib/outpost/expectations/response_time.rb +10 -1
  14. data/lib/outpost/notifiers.rb +2 -0
  15. data/lib/outpost/notifiers/campfire.rb +50 -0
  16. data/lib/outpost/notifiers/email.rb +59 -0
  17. data/lib/outpost/report.rb +7 -1
  18. data/lib/outpost/scout.rb +82 -0
  19. data/lib/outpost/scout_config.rb +10 -4
  20. data/lib/outpost/scouts/http.rb +28 -7
  21. data/lib/outpost/scouts/ping.rb +26 -6
  22. data/lib/outpost/version.rb +4 -1
  23. data/outpost.gemspec +0 -2
  24. data/test/integration/{basic_dsl_test.rb → basic_application_test.rb} +5 -5
  25. data/test/integration/more_complex_test.rb +20 -8
  26. data/test/integration/notifiers_test.rb +51 -0
  27. data/test/outpost/dsl_test.rb +74 -3
  28. data/test/outpost/expectations/response_body_test.rb +13 -15
  29. data/test/outpost/expectations/response_code_test.rb +5 -7
  30. data/test/outpost/expectations/response_time_test.rb +9 -11
  31. data/test/outpost/notifiers/campfire_test.rb +72 -0
  32. data/test/outpost/notifiers/email_test.rb +90 -0
  33. data/test/outpost/scout_test.rb +5 -6
  34. data/test/outpost/scouts/http_test.rb +51 -0
  35. data/test/outpost/scouts/ping_test.rb +41 -0
  36. data/test/support/nothing_raised.rb +10 -0
  37. data/test/support/server.rb +0 -1
  38. data/test/support/stubs.rb +11 -0
  39. data/test/test_helper.rb +8 -6
  40. metadata +32 -22
  41. data/lib/outpost/dsl.rb +0 -54
@@ -1,10 +1,15 @@
1
1
  module Outpost
2
2
  module Expectations
3
+ # Module containing response_code logic. It is the simplest of all,
4
+ # it is a simple direct equality check. Extend your Scout to win instant
5
+ # response_code checking.
3
6
  module ResponseCode
7
+ # Installs the response code expectation
4
8
  def self.extended(base)
5
9
  base.expect :response_code, base.method(:evaluate_response_code)
6
10
  end
7
11
 
12
+ # Method that will be used as an expectation to evaluate response code
8
13
  def evaluate_response_code(scout, response_code)
9
14
  scout.response_code == response_code.to_i
10
15
  end
@@ -1,17 +1,26 @@
1
1
  module Outpost
2
2
  module Expectations
3
+ # Module containing response_time matching expectations. Extend your Scout
4
+ # with ResponseTime and it will evaluate timely expressions, all in
5
+ # milisseconds.
6
+ #
7
+ # It respond to the following rules:
8
+ # * less_than => If the response time is less than the associated number
9
+ # * more_than => If the response time is below than the associated number
3
10
  module ResponseTime
4
11
  RESPONSE_TIME_MAPPING = {
5
12
  :less_than => "<",
6
13
  :more_than => ">",
7
14
  }.freeze
8
15
 
16
+ # Installs the response time expectation
9
17
  def self.extended(base)
10
18
  base.expect :response_time, base.method(:evaluate_response_time)
11
19
  end
12
20
 
21
+ # Method that will be used as an expectation to evaluate response time
13
22
  def evaluate_response_time(scout, rules)
14
- rules.all? do |rule,comparison|
23
+ rules.all? do |rule, comparison|
15
24
  scout.response_time.send(RESPONSE_TIME_MAPPING[rule], comparison)
16
25
  end
17
26
  end
@@ -0,0 +1,2 @@
1
+ require 'outpost/notifiers/email'
2
+ require 'outpost/notifiers/campfire'
@@ -0,0 +1,50 @@
1
+ begin
2
+ require 'tinder'
3
+ rescue LoadError => e
4
+ puts "Please install tinder gem: gem install tinder"
5
+ raise
6
+ end
7
+
8
+ module Outpost
9
+ module Notifiers
10
+ # The Campfire notifier issues Outpost notifications to the 37signals'
11
+ # Campfire web app (http://campfirenow.com). It issues messages about
12
+ # the system status in the specified subdomain and room.
13
+ #
14
+ # This requires the 'tinder' gem to be installed.
15
+ class Campfire
16
+
17
+ # @param [Hash] Options to create a campfire notification.
18
+ # @option options [String] :subdomain The subdomain of your campfire
19
+ # rooms
20
+ # @option options [String] :token An access token, can be found
21
+ # in your Account info
22
+ # @option options [String] :room The room notifications will be sent to
23
+ # @option options [Class] :campfire_notifier Another Campfire
24
+ # notification class. Defaults to Tinder's gem
25
+ def initialize(options={})
26
+ @subdomain = options[:subdomain] || ''
27
+ @token = options[:token] || ''
28
+ @room = options[:room] || ''
29
+ @campfire_notifier = options[:campfire_notifier] || Tinder::Campfire
30
+
31
+ if [@subdomain, @token, @room].any?(&:empty?)
32
+ raise ArgumentError, 'You need to supply :token, :subdomain and :room.'
33
+ end
34
+ end
35
+
36
+ # Issues a notification to a Campfire room. This is a callback, called by
37
+ # an Outpost instance.
38
+ # @param [Outpost::Application, #read] outpost an instance of an outpost, containing
39
+ # latest status, messages and reports that can be queried to build
40
+ # a notification message.
41
+ def notify(outpost)
42
+ campfire = @campfire_notifier.new @subdomain, :token => @token
43
+ room = campfire.find_room_by_name @room
44
+
45
+ status = outpost.last_status.to_s
46
+ room.speak "System is #{status}: #{outpost.messages.join(',')}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,59 @@
1
+ begin
2
+ require 'mail'
3
+ rescue LoadError => e
4
+ puts "Please install mail gem: gem install mail"
5
+ raise
6
+ end
7
+
8
+ module Outpost
9
+ module Notifiers
10
+ # The Email notifier issues Outpost notifications to through email. It
11
+ # uses the 'mail' gem send the emails. You can see mail's documentation
12
+ # in order to change how emails will be delivered:
13
+ # https://github.com/mikel/mail
14
+ #
15
+ # This requires the 'mail' gem to be installed.
16
+ class Email
17
+
18
+ # @param [Hash] Options to create an email notification.
19
+ # @option options [String] :from The "from" email field
20
+ # @option options [String] :to Where e-mails will be delivered
21
+ # @option options [String] :subject The email's subject
22
+ def initialize(options={})
23
+ @from = options[:from]
24
+ @to = options[:to]
25
+ @subject = options[:subject] || 'Outpost notification'
26
+
27
+ unless @from && @to
28
+ raise ArgumentError, 'You need to set :from and :to to send emails.'
29
+ end
30
+ end
31
+
32
+ # Issues a notification through email. This is a callback, called by
33
+ # an Outpost instance.
34
+ # @param [Outpost::Application, #read] outpost an instance of an outpost, containing
35
+ # latest status, messages and reports that can be queried to build
36
+ # a notification message.
37
+ def notify(outpost)
38
+ mail = Mail.new
39
+ mail.from = @from
40
+ mail.to = @to
41
+ mail.subject = @subject
42
+ mail.body = build_message(outpost)
43
+
44
+ mail.deliver
45
+ end
46
+
47
+ private
48
+ def build_message(outpost)
49
+ status = outpost.last_status.to_s
50
+
51
+ message = "This is the report for #{outpost.name}: "
52
+ message += "System is #{status.upcase}!\n\n"
53
+
54
+ message += outpost.messages.join("\n")
55
+ end
56
+ end
57
+ end
58
+ end
59
+
@@ -1,13 +1,19 @@
1
1
  module Outpost
2
+ # Contain the status report of an Outpost execution. Holds the name,
3
+ # description and status of the reported item.
2
4
  class Report
3
- # summarizes the list of statuses in a single status only.
5
+ # Summarizes the list of statuses in a single status only.
4
6
  # The logic is rather simple - it will return the lowest status
5
7
  # present in the list.
6
8
  #
7
9
  # Examples:
8
10
  #
9
11
  # if passed [:up, :up, :up], will result on :up
12
+ #
10
13
  # if passed [:up, :down, :up], will result on :down
14
+ #
15
+ # @params [Array] status_list a list of statuses to be analyzed
16
+ # @return [Symbol] the final status to be considered.
11
17
  def self.summarize(status_list)
12
18
  return :down if status_list.empty? || status_list.include?(:down)
13
19
  return :up
data/lib/outpost/scout.rb CHANGED
@@ -1,11 +1,69 @@
1
1
  module Outpost
2
+ # Scouts are responsible for gathering data about a something specific that is
3
+ # a part of your system. For example, a HTTP Scout is responsible for getting
4
+ # the response code (200 for OK, 404 for Page Not Found, etc.), response
5
+ # body and any other thing you might want to know.
6
+ #
7
+ #
8
+ # Scouts must also contain expectations. They must be registered into the
9
+ # Scout that wish to support different types of expectations. You can supply
10
+ # a block or an object that respond to #call and return true if any of the
11
+ # rules match. It will receive an instance of the scout (so you can query
12
+ # current system state) as the first parameter and the state defined by the
13
+ # Outpost as the second.
14
+ #
15
+ #
16
+ # Example:
17
+ # expect(:response_code) do |scout, code|
18
+ # scout.response_code == code
19
+ # end
20
+ #
21
+ # @example an example of a Scout that parses the HTTP response code
22
+ # module Outpost
23
+ # module Scouts
24
+ # class Http < Outpost::Scout
25
+ # expect(:response_code) { |scout,code| scout.response_code == code }
26
+ #
27
+ # attr_reader :response_code
28
+ #
29
+ # def setup(options)
30
+ # @host = options[:host]
31
+ # @port = options[:port] || 80
32
+ # @path = options[:path] || '/'
33
+ # end
34
+ #
35
+ # def execute
36
+ # response = Net::HTTP.get_response(@host, @path, @port)
37
+ # @response_code = response.code.to_i
38
+ # end
39
+ # end
40
+ # end
41
+ # end
42
+ #
43
+ # @abstract Subclasses must override {#setup} and {#execute} to be a valid
44
+ # Scout
2
45
  class Scout
3
46
  class << self
4
47
 
48
+ # Returns the hash of expectations, where the key is the name of the
49
+ # expectation and the value is the callable Object (object that responds
50
+ # to #call)
51
+ #
52
+ # @return [Hash<Symbol, Object>]
5
53
  def expectations
6
54
  @expectations ? @expectations.dup : []
7
55
  end
8
56
 
57
+ # Registers a new expectation into the Scout. If the callable does not
58
+ # respond to #call, an ArgumentError will be raised.
59
+ #
60
+ # @param [Symbol, #read] expectation The name of the expectation
61
+ # (examples: :response_code, :response_time)
62
+ # @param [Object, #read] callable An object that responds to call and
63
+ # returns a Boolean. It will be executed whenever a Scout is run.
64
+ # A block is also accepted.
65
+ # @raise [ArgumentError] raised when the callable parameter does not
66
+ # respond to #call method.
9
67
  def expect(expectation, callable=nil, &callable_block)
10
68
  callable ||= callable_block
11
69
 
@@ -18,6 +76,10 @@ module Outpost
18
76
  end
19
77
  end
20
78
 
79
+ # @param [String, #read] description A string containing a description of
80
+ # the Scout so it may be identified in the construction of status reports.
81
+ # @param [Hash, #read] config A hash containing any number of
82
+ # configurations that will be passed to the #setup method
21
83
  def initialize(description, config)
22
84
  @description = description
23
85
  @config = config
@@ -25,9 +87,21 @@ module Outpost
25
87
  setup(config.options)
26
88
  end
27
89
 
90
+ # Executes the Scout and go through all the registered expectations to find
91
+ # out all expectations that match and return the associated status.
92
+ #
93
+ # @return [Symbol] the current status of the Scout (:up, :down)
94
+ # @raise [NotImplementedError] raised when a configured expectation was not
95
+ # registered in the Scout.
28
96
  def run
29
97
  statuses = []
30
98
  execute
99
+ # Response_pair contains the expectation as key and the expected value as
100
+ # value.
101
+ # Example: {:response_time => 200}
102
+ #
103
+ # status is the status (:up or :down, for example) that will be returned
104
+ # in case the expectation match current system status.
31
105
  @config.reports.each do |response_pair, status|
32
106
  response_pair.each do |expectation, value|
33
107
  if self.class.expectations[expectation].nil?
@@ -44,10 +118,18 @@ module Outpost
44
118
  Report.summarize(statuses)
45
119
  end
46
120
 
121
+ # Called when the scout object is being constructed. Arguments can be
122
+ # everything the developer set in the creation of Outpost.
123
+ #
124
+ # @raise [NotImplementedError] raised when method is not overriden.
47
125
  def setup(*args)
48
126
  raise NotImplementedError, 'You must implement the setup method for Scout to work correctly.'
49
127
  end
50
128
 
129
+ # Called when the Scout must take action and gather all the data needed to
130
+ # be analyzed.
131
+ #
132
+ # @raise [NotImplementedError] raised when method is not overriden.
51
133
  def execute(*args)
52
134
  raise NotImplementedError, 'You must implement the execute method for Scout to work correctly.'
53
135
  end
@@ -1,9 +1,16 @@
1
1
  module Outpost
2
+ # Provides the DSL used to configure Scouts in Outposts.
2
3
  class ScoutConfig
3
4
  attr_reader :options, :reports
4
5
 
5
- # Reads/writes any options. It will passed
6
- # down to the scout.
6
+ def initialize
7
+ @reports = {}
8
+ @options = {}
9
+ end
10
+
11
+ # Reads/writes any options. It will passed down to the Scout.
12
+ # @param [Object] args Any argument that will be passed to the Scout.
13
+ # Rreturn [Object] The associated option
7
14
  def options(args=nil)
8
15
  if args.nil?
9
16
  @options
@@ -21,10 +28,9 @@ module Outpost
21
28
  # status = 200
22
29
  # params = {:response_code => 200}
23
30
  #
24
- # gets stored as:
31
+ # gets stored as:
25
32
  # {:response_code => 200} = up
26
33
  def report(status, params)
27
- @reports ||= {}
28
34
  @reports[params] = status
29
35
  end
30
36
  end
@@ -1,27 +1,48 @@
1
1
  require 'net/http'
2
- require 'outpost'
3
-
4
2
  require 'outpost/expectations'
5
3
 
6
4
  module Outpost
7
5
  module Scouts
6
+ # Uses ruby's own Net:HTTP to send HTTP requests and evaluate response
7
+ # body, response time and response code.
8
+ #
9
+ # * Responds to response_time expectation
10
+ # ({Outpost::Expectations::ResponseTime})
11
+ # * Responds to response_code expectation
12
+ # ({Outpost::Expectations::ResponseCode})
13
+ # * Responds to response_body expectation
14
+ # ({Outpost::Expectations::ResponseBody})
15
+ #
8
16
  class Http < Outpost::Scout
9
17
  extend Outpost::Expectations::ResponseCode
10
18
  extend Outpost::Expectations::ResponseBody
11
19
 
12
20
  attr_reader :response_code, :response_body
13
21
 
22
+ # Configure the scout with given options.
23
+ # @param [Hash] Options to setup the scout
24
+ # @option options [String] :host The host that will be connected to.
25
+ # @option options [Number] :port The port that will be used to.
26
+ # @option options [String] :path The path that will be fetched from the
27
+ # host.
28
+ # @option options [String] :http_class The class that will be used to
29
+ # fetch the page, defaults to Net::HTTP
14
30
  def setup(options)
15
- @host = options[:host]
16
- @port = options[:port] || 80
17
- @path = options[:path] || '/'
31
+ @host = options[:host]
32
+ @port = options[:port] || 80
33
+ @path = options[:path] || '/'
34
+ @http_class = options[:http_class] || Net::HTTP
18
35
  end
19
36
 
37
+ # Runs the scout, connecting to the host and getting the response code,
38
+ # body and time.
20
39
  def execute
21
- # FIXME Apply Dependency Injection Principle here
22
- response = Net::HTTP.get_response(@host, @path, @port)
40
+ response = @http_class.get_response(@host, @path, @port)
41
+
23
42
  @response_code = response.code.to_i
24
43
  @response_body = response.body
44
+ rescue SocketError, Errno::ECONNREFUSED
45
+ @response_code = @response_body = nil
25
46
  end
26
47
  end
27
48
  end
@@ -1,21 +1,41 @@
1
- require 'net/ping/external'
1
+ begin
2
+ require 'net/ping/external'
3
+ rescue LoadError => e
4
+ puts "Please install net-ping gem: gem install net-ping".
5
+ raise
6
+ end
7
+
8
+ require 'outpost/expectations'
2
9
 
3
10
  module Outpost
4
11
  module Scouts
12
+ # Uses system's "ping" command line tool to check if the server is
13
+ # responding in a timely manner.
14
+ #
15
+ # * Responds to response_time expectation
16
+ # ({Outpost::Expectations::ResponseTime})
17
+ #
18
+ # It needs the 'net-ping' gem.
5
19
  class Ping < Outpost::Scout
6
20
  extend Outpost::Expectations::ResponseTime
7
21
  attr_reader :response_time
8
22
 
23
+
24
+ # Configure the scout with given options.
25
+ # @param [Hash] Options to setup the scout
26
+ # @option options [String] :host The host that will be "pinged".
27
+ # @option options [Object] :pinger An object that can ping hosts.
28
+ # Defaults to Net::Ping::External.new
9
29
  def setup(options)
10
- @host = options[:host]
30
+ @host = options[:host]
31
+ @pinger = options[:pinger] || Net::Ping::External.new
11
32
  end
12
33
 
34
+ # Runs the scout, pinging the host and getting the duration.
13
35
  def execute
14
- # FIXME Apply Dependency Injection Principle here
15
- pinger = Net::Ping::External.new
16
- if pinger.ping(@host)
36
+ if @pinger.ping(@host)
17
37
  # Miliseconds
18
- @response_time = pinger.duration * 100
38
+ @response_time = @pinger.duration * 1000
19
39
  end
20
40
  end
21
41
  end
@@ -1,3 +1,6 @@
1
1
  module Outpost
2
- VERSION = "0.1.0".freeze
2
+ PATCH = 0
3
+ MINOR = 2
4
+ MAJOR = 0
5
+ VERSION = "#{MAJOR}.#{MINOR}.#{PATCH}".freeze
3
6
  end