opticon 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.
data/CHANGELOG.txt ADDED
File without changes
data/History.txt ADDED
File without changes
data/Manifest.txt ADDED
@@ -0,0 +1,24 @@
1
+ Rakefile
2
+ README.txt
3
+ CHANGELOG.txt
4
+ History.txt
5
+ Manifest.txt
6
+ README.txt
7
+ Rakefile
8
+ examples/sample.rb
9
+ lib/opticon.rb
10
+ lib/opticon/failure.rb
11
+ lib/opticon/http.rb
12
+ lib/opticon/mailer.rb
13
+ lib/opticon/mailer/failure_notification.rhtml
14
+ lib/opticon/notifier.rb
15
+ lib/opticon/runner.rb
16
+ lib/opticon/service.rb
17
+ lib/opticon/tester.rb
18
+ lib/opticon/version.rb
19
+ setup.rb
20
+ test/opticon/http_test.rb
21
+ test/opticon/mailer_test.rb
22
+ test/opticon/tester_test.rb
23
+ test/opticon_test.rb
24
+ test/test_helper.rb
data/README.txt ADDED
@@ -0,0 +1,15 @@
1
+ Opticon is a no-nonsense utility for monitoring HTTP-based services (websites).
2
+
3
+ When triggered, opticon connects to a list of URIs and checks to make sure that
4
+ they pass some arbitrary test, such as responding with a non-error HTTP response
5
+ code, or returning content that matches a regular expression. In case of failure,
6
+ opticon generates emails that can be forwarded to your mailbox, blackberry,
7
+ cellphone, etc.
8
+
9
+
10
+ For installation instructions and other info, visit opticon's Google Code
11
+ page at:
12
+
13
+ http://code.google.com/p/opticon/
14
+
15
+ (Have a look at the Wiki in particular)
data/Rakefile ADDED
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ include FileUtils
12
+ require File.join(File.dirname(__FILE__), 'lib', 'opticon', 'version')
13
+
14
+ AUTHOR = "Matt Zukowski" # can also be an array of Authors
15
+ EMAIL = "matt [at] roughest.net"
16
+ DESCRIPTION = "Peace of mind through automated monitoring of your HTTP services."
17
+ GEM_NAME = "opticon" # what ppl will type to install your gem
18
+ RUBYFORGE_PROJECT = "opticon" # The unix name for your project
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+ DEPS = ['actionmailer']
21
+
22
+ NAME = "opticon"
23
+ REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
24
+ VERS = ENV['VERSION'] || (Opticon::VERSION::STRING + (REV ? ".#{REV}" : ""))
25
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config']
26
+ RDOC_OPTS = ['--quiet', '--title', "opticon documentation",
27
+ "--opname", "index.html",
28
+ "--line-numbers",
29
+ "--main", "README",
30
+ "--inline-source"]
31
+
32
+ class Hoe
33
+ def extra_deps
34
+ @extra_deps.reject { |x| Array(x).first == 'hoe' }
35
+ end
36
+ end
37
+
38
+ # Generate all the Rake tasks
39
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
40
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
41
+ p.author = AUTHOR
42
+ p.description = DESCRIPTION
43
+ p.email = EMAIL
44
+ p.summary = DESCRIPTION
45
+ p.url = HOMEPATH
46
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
47
+ p.test_globs = ["test/**/*_test.rb"]
48
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
49
+ p.extra_deps = DEPS
50
+
51
+ # == Optional
52
+ #p.changes - A description of the release's latest changes.
53
+ #p.extra_deps - An array of rubygem dependencies.
54
+ #p.spec_extras - A hash of extra values to set in the gemspec.
55
+ end
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'; gem 'opticon'; require 'opticon'
3
+
4
+ # Where notifications will be emailed
5
+ SEND_TO = "sample@nowhere.foo"
6
+
7
+ # If you want to send to multiple mailboxes, you can specify an array of
8
+ # addresses like so:
9
+ #
10
+ # SEND_TO = ['robert@nowhere.foo', 'sally@nowhere.foo']
11
+
12
+ # Email notifications will show up as coming from this address.
13
+ FROM = "opticon@nowhere.foo"
14
+
15
+ # You may have to set the FROM value to be a real email address (e.g. your
16
+ # email address). Otherwise your mail server may decide to drop Opticon's
17
+ # notification messages.
18
+
19
+ # You don't have to change anything on this next line. This configures the
20
+ # default notification behaviour for Opticon, which is to email failures to the
21
+ # addresses you configured above.
22
+ Opticon.default_notifiers = Opticon::Notifier::Email.new(SEND_TO, :from => FROM)
23
+
24
+ # You may need to configure the Mailer with your mail server info. Here's how to
25
+ # use an SMTP mail server (unless you do this, local sendmail will be used used
26
+ # by default):
27
+ #
28
+ # Opticon::Mailer.smtp_settings = {
29
+ # :address => 'mail.nowhere.foo',
30
+ # :username => 'johnny',
31
+ # :password => 'topsecret'
32
+ # }
33
+ #
34
+ # See http://api.rubyonrails.com/classes/ActionMailer/Base.html for more details
35
+ # on setting up the mailer.
36
+
37
+ # Now you can set up some tests. Note that this is just plain Ruby code, formatted
38
+ # a bit strangely (note the dot at the end of each line -- we are calling the test()
39
+ # method on the URI string, and then on the result of each successive test).
40
+ # Here are some examples:
41
+
42
+ "http://www.yahoo.com/".
43
+ # check that the HTTP response code is in the 200 range
44
+ test(:responds_with_code, :success).
45
+ # check that the page includes the string "Privacy Policy"
46
+ test(:response_body_contains, "Privacy Policy")
47
+
48
+ "http://google.com".
49
+ # Google will try to redirect to http://www.google.com/
50
+ test(:responds_with_code, 302)
51
+ # Note that currently there is no test for checking where you are being
52
+ # redirected to. This functionality will be added in the future.
53
+
54
+
55
+ # For a comprehensive guide on configuring and using Opticon, please see:
56
+ #
57
+ # http://code.google.com/p/opticon/wiki/ConfiguringOpticon
58
+ #
data/lib/opticon.rb ADDED
@@ -0,0 +1,19 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless
2
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
+
4
+ module Opticon
5
+
6
+ @@default_notifiers = nil
7
+ def default_notifiers
8
+ @@default_notifiers
9
+ end
10
+ def default_notifiers=(notifiers)
11
+ notifiers = [notifiers] unless notifiers.kind_of? Array
12
+ @@default_notifiers = notifiers
13
+ end
14
+ module_function :default_notifiers, 'default_notifiers='
15
+
16
+ end
17
+
18
+ require 'opticon/service'
19
+ require 'opticon/notifier'
@@ -0,0 +1,40 @@
1
+ module Opticon
2
+ module Failure
3
+ class Base
4
+ attr_reader :uri, :condition, :response
5
+ attr_accessor :exception
6
+
7
+ def initialize(uri, condition, response)
8
+ @uri = uri
9
+ @condition = condition
10
+ @response = response
11
+ end
12
+
13
+ def to_s
14
+ "#<#{self.class} [#{uri}] #{failure_message}>"
15
+ end
16
+ end
17
+
18
+ class ConnectionFailure < Base
19
+ def failure_message
20
+ "#{exception.message} (#{exception.class})"
21
+ end
22
+ end
23
+
24
+ class ResponseCodeTestFailure < Base
25
+ def failure_message
26
+ "Expected response code #{condition.inspect} but got '#{response.message}' (#{response.code})"
27
+ end
28
+ end
29
+
30
+ class ContentTestFailure < Base
31
+ def failure_message
32
+ if exception
33
+ return exception.message
34
+ else
35
+ "Content did not include the expected #{condition.class} #{condition.inspect}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+
5
+ module Opticon
6
+ module HTTP
7
+ def response
8
+ raise "URI cannot be empty" if uri.kind_of? String and (uri.nil? or uri.empty?)
9
+
10
+ self.uri = URI.parse(self.uri) unless self.uri.kind_of? URI
11
+ if @response and self.uri == @last_fetched_uri
12
+ @response
13
+ else
14
+ @response = get(self.uri)
15
+ end
16
+ end
17
+
18
+ def get(get_uri)
19
+ if self.uri.kind_of? URI
20
+ get_uri = self.uri
21
+ else
22
+ get_uri = URI.parse(self.uri) unless self.uri.kind_of? URI
23
+ end
24
+
25
+ @last_fetched_uri = get_uri
26
+
27
+ http = Net::HTTP.new(uri.host, uri.port)
28
+ http.use_ssl = uri.scheme == "https"
29
+
30
+ response = nil
31
+ http.start do
32
+ path = uri.path.empty? ? '/' : uri.path
33
+ response = http.request_get(path)
34
+ end
35
+
36
+ response
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+
3
+ gem 'actionmailer'
4
+ require 'action_mailer'
5
+
6
+ module Opticon
7
+ class Mailer < ::ActionMailer::Base
8
+ # we use sendmail by default since it might just work out of the box
9
+ # (:smtp, which is the normal default, definetly won't work without manual configuration)
10
+ self.delivery_method = :sendmail
11
+ self.template_root = File.dirname(File.expand_path(__FILE__))+'/..'
12
+
13
+ def failure_notification(failure, recipients, from)
14
+ self.recipients recipients
15
+ self.from from
16
+ subject "HTTP Service Failure: #{failure.uri}"
17
+ body :failure => failure
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,5 @@
1
+ Service at '<%= @failure.uri %>' failed:
2
+
3
+ <%= @failure.failure_message %>
4
+
5
+ (Generated by Opticon on host '<%= ENV['HOSTNAME'] %>' at <%= Time.now %>)
@@ -0,0 +1,18 @@
1
+ require 'opticon/mailer'
2
+
3
+ module Opticon
4
+ module Notifier
5
+ class Email
6
+ attr_accessor :recipients, :from
7
+
8
+ def initialize(recipients, options = {:from => "opticon@#{ENV['HOSTNAME']}"})
9
+ @recipients = recipients
10
+ @from = options[:from]
11
+ end
12
+
13
+ def notify(failure)
14
+ Opticon::Mailer.deliver_failure_notification(failure, recipients, from)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ module Opticon
2
+ class Runner
3
+ end
4
+ end
@@ -0,0 +1,67 @@
1
+ require 'uri'
2
+ require 'opticon/tester'
3
+
4
+ module Opticon
5
+ class Service
6
+ attr_accessor :notifiers
7
+
8
+ def initialize(uri, notifiers = Opticon.default_notifiers)
9
+ raise ArgumentError, "You must either provide a valid Notifier or set Opticon.default_notifiers." unless
10
+ notifiers and (notifiers.respond_to? :notify or notifiers.first.respond_to? :notify)
11
+ @uri = URI.parse(uri)
12
+
13
+ if notifiers.kind_of? Array
14
+ @notifiers = notifiers
15
+ else
16
+ @notifiers = [notifiers]
17
+ end
18
+ end
19
+
20
+ def to_s
21
+ @uri.to_s
22
+ end
23
+
24
+ def test(tester, *args)
25
+ case tester
26
+ when :responds_with_code, 'responds_with_code'
27
+ tester = Opticon::Tester::ResponseCodeTester.new
28
+ when :response_body_contains, 'response_body_contains'
29
+ tester = Opticon::Tester::ContentTester.new
30
+ when Class
31
+ tester = tester.new
32
+ when Tester::Base
33
+ tester = tester
34
+ else
35
+ begin
36
+ tester_class = "Opitcon::Tester::#{tester.to_s}".constantize
37
+ tester = tester_class.new
38
+ rescue NameError
39
+ bad_tester = true
40
+ end
41
+
42
+ if bad_tester or !tester.respond_to? :test
43
+ raise ArgumentError, "'#{tester}' is not a valid tester. The parameter must be: 'responds_with_code', 'response_body_contains', or"+
44
+ " an Object or Class that responds to a 'test' method."
45
+ end
46
+ end
47
+
48
+ tester.uri = @uri
49
+
50
+ @failures = []
51
+ unless tester.test(*args)
52
+ notifiers.each {|n| n.notify(tester.failure)}
53
+ @failures << tester.failure
54
+ end
55
+
56
+ # return self to allow for test chaining
57
+ return self
58
+ end
59
+ end
60
+ end
61
+
62
+ # this makes it possible to treat strings as Service objects
63
+ class String
64
+ def test(tester, *args)
65
+ Opticon::Service.new(to_s).test(tester, *args)
66
+ end
67
+ end
@@ -0,0 +1,118 @@
1
+ require 'opticon/http'
2
+ require 'opticon/failure'
3
+
4
+ module Opticon
5
+ module Tester
6
+ class Base
7
+ attr_accessor :uri
8
+ attr_reader :failure
9
+
10
+ include Opticon::HTTP
11
+
12
+ def test(*args)
13
+ begin
14
+ test_without_exception_handling(*args)
15
+ rescue SocketError, TimeoutError, Net::HTTPError, Errno::ECONNREFUSED
16
+ @failure = Opticon::Failure::ConnectionFailure.new(uri, args, nil)
17
+ @failure.exception = $!
18
+ false
19
+ end
20
+ end
21
+ end
22
+
23
+ class ResponseCodeTester < Base
24
+ def test_without_exception_handling(condition)
25
+ case condition
26
+ when :ok
27
+ responds_with_code(200)
28
+ when :success
29
+ responds_with_success
30
+ when :failure, :error
31
+ responds_with_error
32
+ when :redirect
33
+ responds_with_redirect
34
+ when :client_error, :bad_request
35
+ responds_with_client_error
36
+ when :server_error
37
+ responds_with_server_error
38
+ when Integer, Array, Range
39
+ # TODO: if array, assert that each element is an integer (an HTTP response code)
40
+ responds_with_code(condition)
41
+ when String, Regexp
42
+ response_contains(condition)
43
+ else
44
+ raise ArgumentError, "'#{condition.inspect}' is not a valid argument for responds_with"
45
+ end
46
+ end
47
+
48
+ private
49
+ def responds_with_code(codes)
50
+
51
+ if codes.kind_of? Integer
52
+ result = codes == response.code.to_i
53
+ else
54
+ result = codes.include? response.code.to_i
55
+ end
56
+
57
+ @failure = Opticon::Failure::ResponseCodeTestFailure.new(uri, codes, response) unless result
58
+ return result
59
+ end
60
+
61
+ def responds_with_success
62
+ responds_with_code(200..206)
63
+ end
64
+
65
+ def responds_with_redirect
66
+ responds_with_code(300..207)
67
+ end
68
+
69
+ def responds_with_client_error
70
+ responds_with_code(400..417)
71
+ end
72
+
73
+ def responds_with_server_error
74
+ responds_with_code(500..505)
75
+ end
76
+
77
+ def responds_with_error
78
+ responds_with_client_error or responds_with_client_error
79
+ end
80
+ end
81
+
82
+ class ContentTester < Base
83
+ def test_without_exception_handling(condition)
84
+ unless (200..206).include?(response.code.to_i)
85
+ @failure = Opticon::Failure::ContentTestFailure.new(uri, condition, response)
86
+ if (300..307).include?(response.code.to_i)
87
+ @failure.exception = RuntimeError.new("Request was redirected to #{response['location']}.")
88
+ else
89
+ @failure.exception = RuntimeError.new("Request returned code '#{response.message}'"+
90
+ " (#{response.code}) but response must be a 2xx HTTP status code.")
91
+ end
92
+ return false
93
+ end
94
+
95
+ unless response.class.body_permitted?
96
+ @failure = Opticon::Failure::ContentTestFailure.new(uri, condition, response)
97
+ @failure.exception = RuntimeError.new("Response did not include a body (HTTP response: #{response.inspect})")
98
+ return false
99
+ end
100
+
101
+ case condition
102
+ when Regexp
103
+ result = condition =~ response.body
104
+ when String
105
+ result = response.body.include? condition
106
+ end
107
+
108
+ if result
109
+ @failure = nil
110
+ else
111
+ @failure = Opticon::Failure::ContentTestFailure.new(uri, condition, response)
112
+ end
113
+
114
+ result
115
+ end
116
+ end
117
+ end
118
+ end