opticon 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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