outpost 0.1.0 → 0.2.0

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