opticon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +0 -0
- data/History.txt +0 -0
- data/Manifest.txt +24 -0
- data/README.txt +15 -0
- data/Rakefile +55 -0
- data/examples/sample.rb +58 -0
- data/lib/opticon.rb +19 -0
- data/lib/opticon/failure.rb +40 -0
- data/lib/opticon/http.rb +39 -0
- data/lib/opticon/mailer.rb +20 -0
- data/lib/opticon/mailer/failure_notification.rhtml +5 -0
- data/lib/opticon/notifier.rb +18 -0
- data/lib/opticon/runner.rb +4 -0
- data/lib/opticon/service.rb +67 -0
- data/lib/opticon/tester.rb +118 -0
- data/lib/opticon/version.rb +9 -0
- data/setup.rb +1585 -0
- data/test/opticon/http_test.rb +45 -0
- data/test/opticon/mailer_test.rb +26 -0
- data/test/opticon/tester_test.rb +77 -0
- data/test/opticon_test.rb +11 -0
- data/test/test_helper.rb +2 -0
- metadata +78 -0
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
|
data/examples/sample.rb
ADDED
@@ -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
|
data/lib/opticon/http.rb
ADDED
@@ -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,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,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
|