tocsin 0.1.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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +56 -0
- data/Rakefile +6 -0
- data/lib/tocsin.rb +159 -0
- data/lib/tocsin/config.rb +32 -0
- data/lib/tocsin/notifiers.rb +23 -0
- data/lib/tocsin/notifiers/email_notifier.rb +30 -0
- data/lib/tocsin/rake.rb +13 -0
- data/lib/tocsin/version.rb +3 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/tocsin/notifiers/email_notifier_spec.rb +52 -0
- data/spec/tocsin/notifiers_spec.rb +18 -0
- data/spec/tocsin_spec.rb +259 -0
- data/tocsin.gemspec +26 -0
- metadata +147 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
# Tocsin
|
2
|
+
Tocsin is a library designed to simply the task of notifying interested parties about your site's operation. You may already be tracking errors through [New Relic](http://www.newrelic.com), but not all errors are created equal -- there are probably parts of your site where an exception occuring is significantly more of a problem than a bored URL surfer visiting random links and throwing 404s. Tocsin can help you be informed when these parts of your site break, or when important events happen.
|
3
|
+
|
4
|
+
Currently, Tocsin works only in Rails 3, and supports notification via email.
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
Add Tocsin to your Gemfile:
|
8
|
+
<pre>
|
9
|
+
gem 'tocsin'
|
10
|
+
</pre>
|
11
|
+
|
12
|
+
Update your bundle:
|
13
|
+
<pre>
|
14
|
+
bundle install
|
15
|
+
</pre>
|
16
|
+
|
17
|
+
Use the provided Rake task to generate the migration and model needed by Tocsin. (Angry at the lack of normalization? Install [lookup_by](https://github.com/companygardener/lookup_by/) and rewrite the migration and model to use it; Tocsin won't even notice.)
|
18
|
+
<pre>
|
19
|
+
rake tocsin:install
|
20
|
+
</pre>
|
21
|
+
|
22
|
+
Lastly, configure Tocsin to be useful. Create an initializer in `config/initializers/tocsin.rb` that looks something like this:
|
23
|
+
<pre>
|
24
|
+
Tocsin.configure do |c|
|
25
|
+
c.from_address = 'me@mysite.com'
|
26
|
+
|
27
|
+
c.notify 'you@gmail.com', :of => { :category => /user_registrations/ }, :by => :email
|
28
|
+
c.notify ['you@gmail.com', 'sales@mysite.com'], :of => { :category => /new_sales/ } # N.B. 'email' is the default nofifier.
|
29
|
+
c.notify 'ops@mysite.com', :of => { :severity => /critical/ } # Values in the :of hash should be regexes.
|
30
|
+
end
|
31
|
+
</pre>
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
In anywhere you want to send yourself a notification:
|
35
|
+
<pre>
|
36
|
+
Tocsin.notify :category => :user_registrations,
|
37
|
+
:message => "User #{user.name} registered at #{Time.now}!"
|
38
|
+
</pre>
|
39
|
+
|
40
|
+
If you want to sound the alarm:
|
41
|
+
<pre>
|
42
|
+
begin
|
43
|
+
# ...
|
44
|
+
rescue => e
|
45
|
+
Tocsin.raise_alert e, :category => :user_registrations,
|
46
|
+
:severity => :critical,
|
47
|
+
:message => "An error occurred when a user tried to sign up!"
|
48
|
+
end
|
49
|
+
</pre>
|
50
|
+
|
51
|
+
In any code you want to watch for explosions:
|
52
|
+
<pre>
|
53
|
+
Tocsin.watch! :category => :important_stuff, :severity => :critical, :message => "Error doing important stuff!" do
|
54
|
+
Important::Stuff.do!
|
55
|
+
end
|
56
|
+
</pre>
|
data/Rakefile
ADDED
data/lib/tocsin.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require "tocsin/version"
|
2
|
+
require "tocsin/notifiers"
|
3
|
+
require "tocsin/config"
|
4
|
+
|
5
|
+
require "active_record/errors"
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
# Library for escalating and logging errors.
|
9
|
+
module Tocsin
|
10
|
+
# Raise an alarm and escalate as configured in etc/tocsin.yml.
|
11
|
+
# @param [Exception] exception exception object if one was raised
|
12
|
+
# @option options :severity severity of this notication. Should be low,
|
13
|
+
# medium, high, critical, or notification (arbitrary, though)
|
14
|
+
# @option options :message any info attached to this notification
|
15
|
+
# @option options :category totally arbitrary, but escalation can be configured based on
|
16
|
+
# this field.
|
17
|
+
# @return [Tocsin::Alert] the created alert
|
18
|
+
def self.raise_alert(exception, options={})
|
19
|
+
urgent = options.delete(:now) || options.delete(:urgent) || options.delete(:synchronous)
|
20
|
+
alert = alert_for(exception, options)
|
21
|
+
|
22
|
+
begin
|
23
|
+
urgent ? sound(alert) : queue(alert)
|
24
|
+
rescue => e
|
25
|
+
sound cannot_enqueue_alert(e)
|
26
|
+
sound alert
|
27
|
+
end
|
28
|
+
|
29
|
+
alert
|
30
|
+
end
|
31
|
+
|
32
|
+
# Synonyms for raise_alert when no exception is involved.
|
33
|
+
def self.notify(options)
|
34
|
+
self.raise_alert(nil, {:severity => "notification"}.merge(options))
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.warn!(options)
|
38
|
+
self.raise_alert(nil, options)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Watch the yielded block for exceptions, and log one if it's raised.
|
42
|
+
def self.watch(options={})
|
43
|
+
begin
|
44
|
+
yield
|
45
|
+
rescue => e
|
46
|
+
raise_alert(e, options)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Same as watch, but re-raises the exception after catching it.
|
51
|
+
def self.watch!(options={})
|
52
|
+
begin
|
53
|
+
yield
|
54
|
+
rescue => e
|
55
|
+
raise_alert(e, options)
|
56
|
+
raise e
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Determine the recipients per notification method for a particular alert.
|
61
|
+
def self.recipients(alert)
|
62
|
+
# Each recipient group should look like (where the filter values are regexps):
|
63
|
+
# - category: .*
|
64
|
+
# severity: (critical|high)
|
65
|
+
# recipients:
|
66
|
+
# - rnubel@test.com
|
67
|
+
# notifier: email
|
68
|
+
return {} unless config.recipient_groups
|
69
|
+
|
70
|
+
recipients_per_notifier = config.recipient_groups.inject({}) do |rec_lists, group|
|
71
|
+
if alert_matches_group(alert, group)
|
72
|
+
notifier = group[:notifier] || Tocsin::Notifiers.default_notifier
|
73
|
+
rec_lists[notifier] ||= []
|
74
|
+
rec_lists[notifier] += group[:recipients]
|
75
|
+
end
|
76
|
+
|
77
|
+
rec_lists
|
78
|
+
end
|
79
|
+
|
80
|
+
recipients_per_notifier.each do |k, v| v.uniq!; v.sort! end # Filter duplicates and sort for sanity
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.configure
|
84
|
+
@config = nil
|
85
|
+
yield config
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.config
|
89
|
+
@config ||= Tocsin::Config.new
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.queue
|
93
|
+
@queue ||= config.queue
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.logger
|
97
|
+
@logger ||= config.logger
|
98
|
+
end
|
99
|
+
|
100
|
+
# Job to notify admins via email of a problem.
|
101
|
+
class NotificationJob
|
102
|
+
@queue = Tocsin.queue
|
103
|
+
|
104
|
+
# Look up the given alert and notify recipients of it.
|
105
|
+
def self.perform(alert_id)
|
106
|
+
Tocsin.sound(alert_id)
|
107
|
+
rescue ActiveRecord::RecordNotFound
|
108
|
+
Tocsin.logger.error { "Raised alert with ID=#{alert_id} but couldn't find that alert." }
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def self.sound(alert)
|
116
|
+
alert = Tocsin::Alert.find(alert) unless alert.is_a?(Tocsin::Alert)
|
117
|
+
recipients = Tocsin.recipients(alert)
|
118
|
+
|
119
|
+
recipients.each do |notifier_key, recipient_list|
|
120
|
+
notifier = Tocsin::Notifiers[notifier_key]
|
121
|
+
|
122
|
+
if notifier && recipient_list.any?
|
123
|
+
notifier.notify(recipient_list, alert)
|
124
|
+
Tocsin.logger.info { "Notification sent to #{recipient_list.inspect} via #{notifier_key} for alert #{alert.id}." }
|
125
|
+
elsif recipient_list.empty?
|
126
|
+
Tocsin.logger.error { "No recipients associated with alert: \n #{alert.inspect}" }
|
127
|
+
elsif notifier.nil?
|
128
|
+
Tocsin.logger.error { "Raised alert with ID=#{alert.id} for unregistered notifier '#{notifier_key}'." }
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.alert_for(exception, options = {})
|
134
|
+
alert = Tocsin::Alert.create(
|
135
|
+
:exception => exception && exception.to_s,
|
136
|
+
:backtrace => exception && exception.backtrace.join("\n"),
|
137
|
+
:severity => options[:severity].to_s || "",
|
138
|
+
:message => options[:message].to_s || "",
|
139
|
+
:category => options[:category].to_s || "uncategorized"
|
140
|
+
)
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.queue(alert)
|
144
|
+
alert = Tocsin::Alert.find(alert) unless alert.is_a?(Tocsin::Alert)
|
145
|
+
Resque.enqueue(NotificationJob, alert.id)
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.cannot_enqueue_alert(exception=nil)
|
149
|
+
logger.error "[Tocsin] Enqueuing alert job into Resque failed!"
|
150
|
+
alert_for exception, :severity => "critical",
|
151
|
+
:message => "[Tocsin] Enqueuing alert job into Resque failed!",
|
152
|
+
:category => "system"
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.alert_matches_group(alert, group)
|
156
|
+
alert.category =~ Regexp.new(group[:category] || '.*') &&
|
157
|
+
alert.severity =~ Regexp.new(group[:severity] || '.*')
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Tocsin
|
2
|
+
class Config
|
3
|
+
attr_accessor :logger, :queue, :recipient_groups, :from_address
|
4
|
+
|
5
|
+
# notify [r1, r2], :of => filters, :by => notifier
|
6
|
+
def notify(recipients, parameters)
|
7
|
+
self.recipient_groups ||= []
|
8
|
+
|
9
|
+
recipients = [recipients] unless recipients.is_a? Array
|
10
|
+
filters = parameters[:of] || {}
|
11
|
+
notifier = parameters[:by] || Tocsin::Notifiers.default_notifier
|
12
|
+
|
13
|
+
group_config = { :recipients => recipients,
|
14
|
+
:notifier => notifier}.merge(filters)
|
15
|
+
self.recipient_groups.push(group_config)
|
16
|
+
end
|
17
|
+
|
18
|
+
def queue
|
19
|
+
@queue ||= :high
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= Rails.logger
|
24
|
+
rescue NameError, NoMethodError => e
|
25
|
+
ok = [ /^uninitialized constant .*Rails$/,
|
26
|
+
/^undefined method `logger'/
|
27
|
+
].any?{|regex| e.message =~ regex }
|
28
|
+
raise unless ok
|
29
|
+
@logger ||= Logger.new($stderr)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Tocsin
|
2
|
+
module Notifiers
|
3
|
+
def self.default_notifier
|
4
|
+
:email
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.notifiers
|
8
|
+
@notifiers ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.register!(key, notifier)
|
12
|
+
notifiers.store(key, notifier)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.[](key)
|
16
|
+
notifiers[key]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Dir[File.expand_path("../notifiers/*.rb", __FILE__)].each do |f|
|
22
|
+
require f
|
23
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'mail'
|
2
|
+
|
3
|
+
module Tocsin
|
4
|
+
module Notifiers
|
5
|
+
class EmailNotifier
|
6
|
+
def self.notify(recipients, alert)
|
7
|
+
_body = "
|
8
|
+
Alert raised by Tocsin on your site:
|
9
|
+
\n
|
10
|
+
Message: #{alert.message}
|
11
|
+
Category: #{alert.category}
|
12
|
+
Severity: #{alert.severity}
|
13
|
+
Exception: #{alert.exception}
|
14
|
+
Backtrace: #{alert.backtrace}
|
15
|
+
";
|
16
|
+
|
17
|
+
from_address = Tocsin.config.from_address || recipients.first
|
18
|
+
|
19
|
+
Mail.deliver do
|
20
|
+
from from_address
|
21
|
+
to recipients.join(", ")
|
22
|
+
subject "[Tocsin] [#{alert.severity}] #{alert.message} (#{alert.category})"
|
23
|
+
body _body
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register! :email, EmailNotifier
|
29
|
+
end
|
30
|
+
end
|
data/lib/tocsin/rake.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'tocsin'))
|
2
|
+
|
3
|
+
namespace :tocsin do
|
4
|
+
desc "Generate Tocsin::Alert model & migration"
|
5
|
+
task :generate do
|
6
|
+
if defined?(Rails)
|
7
|
+
puts `rails g model Tocsin::Alert exception:string message:string category:string severity:string backtrace:string --no-test-framework --skip`
|
8
|
+
puts "Run rake db:migrate to finish installation."
|
9
|
+
else
|
10
|
+
puts "Not using rails. Please create Tocsin::Alert manually"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.expand_path("../../lib/tocsin", __FILE__)
|
2
|
+
|
3
|
+
RSpec.configure do |c|
|
4
|
+
c.mock_with :mocha
|
5
|
+
end
|
6
|
+
|
7
|
+
Mail.defaults do
|
8
|
+
delivery_method :test
|
9
|
+
end
|
10
|
+
|
11
|
+
class Tocsin::Alert
|
12
|
+
def id
|
13
|
+
1
|
14
|
+
end
|
15
|
+
|
16
|
+
def category
|
17
|
+
end
|
18
|
+
|
19
|
+
def severity
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.create(*args)
|
23
|
+
self.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.find(*args)
|
27
|
+
self.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.logger
|
31
|
+
@logger ||= Logger.new($stdout)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Resque
|
36
|
+
def self.enqueue(*args)
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Tocsin::Notifiers::EmailNotifier do
|
4
|
+
let(:alert) {
|
5
|
+
stub("Tocsin::Alert", :id => 1, :category => 'category', :severity => 'severity',
|
6
|
+
:message => 'message', :exception => 'exception',
|
7
|
+
:backtrace => 'backtrace')
|
8
|
+
}
|
9
|
+
|
10
|
+
describe "notifying" do
|
11
|
+
let(:origin_email) { "test@test.com" }
|
12
|
+
|
13
|
+
before {
|
14
|
+
Tocsin.configure do |c|
|
15
|
+
c.from_address = "test@test.com"
|
16
|
+
end
|
17
|
+
}
|
18
|
+
|
19
|
+
it "uses the Mail gem to send an alert" do
|
20
|
+
described_class.notify(["a@b.com"], alert)
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "the sent email" do
|
24
|
+
let(:message) { Mail::TestMailer.deliveries.first }
|
25
|
+
let(:body) { message.body.decoded }
|
26
|
+
|
27
|
+
it "was sent" do
|
28
|
+
message.should_not be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "has a subject including the alert's message, severity and category" do
|
32
|
+
message.subject.should == "[Tocsin] [severity] message (category)"
|
33
|
+
end
|
34
|
+
|
35
|
+
it "has a body including all fields" do
|
36
|
+
body.should =~ /severity/
|
37
|
+
body.should =~ /message/
|
38
|
+
body.should =~ /category/
|
39
|
+
body.should =~ /exception/
|
40
|
+
body.should =~ /backtrace/
|
41
|
+
end
|
42
|
+
|
43
|
+
it "is from the configured origin email" do
|
44
|
+
message.from.should == [origin_email]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "registers itself under the key :email" do
|
50
|
+
Tocsin::Notifiers[:email].should == described_class
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
# Fixture for testing notifiers.
|
4
|
+
class DummyNotifier
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Tocsin::Notifiers do
|
9
|
+
context "after a notifier has been registered with a key" do
|
10
|
+
before {
|
11
|
+
Tocsin::Notifiers.register! :dummy, DummyNotifier
|
12
|
+
}
|
13
|
+
|
14
|
+
it "can retrieve the notifier based on that key" do
|
15
|
+
Tocsin::Notifiers[:dummy].should == DummyNotifier
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/spec/tocsin_spec.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Tocsin do
|
4
|
+
let(:alert_options) do
|
5
|
+
{ :severity => :critical,
|
6
|
+
:message => "Raised by someone",
|
7
|
+
:category => :real_important_job }
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:recipient_groups) do
|
11
|
+
[ {:category => /.*/, :severity => /.*/, :recipients => ["x@y.com", "z@w.com"]},
|
12
|
+
{:category => /^test$/, :severity => /(high|low)/, :recipients => ["a@b.com", "z@w.com"], :notifier => :email},
|
13
|
+
{:category => /^test$/, :severity => /(high|low)/, :recipients => ["1234567890"], :notifier => :text_message}
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
before do
|
18
|
+
Tocsin.configure do |c|
|
19
|
+
c.recipient_groups = recipient_groups
|
20
|
+
c.logger = Logger.new('/dev/null')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "when an exception has already been rescued" do
|
25
|
+
it "raises an alert" do
|
26
|
+
Tocsin::Alert.expects(:create).returns(stub("alert", :id => 1))
|
27
|
+
|
28
|
+
begin
|
29
|
+
raise "Testing"
|
30
|
+
rescue => e
|
31
|
+
Tocsin.raise_alert(e, alert_options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context "when considering a block of code which explodes" do
|
37
|
+
it "provides a watch method which raises an alert silently" do
|
38
|
+
Tocsin.expects(:raise_alert).with(is_a(Exception), has_entries(alert_options))
|
39
|
+
|
40
|
+
lambda {
|
41
|
+
Tocsin.watch(alert_options) do
|
42
|
+
raise "Testing"
|
43
|
+
end
|
44
|
+
}.should_not raise_error
|
45
|
+
end
|
46
|
+
|
47
|
+
it "provides a watch! method which both send an alert and re-raises the exception" do
|
48
|
+
Tocsin.expects(:raise_alert).with(is_a(Exception), has_entries(alert_options))
|
49
|
+
|
50
|
+
lambda {
|
51
|
+
Tocsin.watch!(alert_options) do
|
52
|
+
raise "Testing"
|
53
|
+
end
|
54
|
+
}.should raise_error
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "being configured" do
|
59
|
+
it "wipes old configuration" do
|
60
|
+
Tocsin.configure do |c|
|
61
|
+
c.recipient_groups = []
|
62
|
+
end
|
63
|
+
|
64
|
+
Tocsin.configure { }
|
65
|
+
Tocsin.config.recipient_groups.should be_nil
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#recipient_groups=" do
|
69
|
+
it "can set the list of recipient groups directly" do
|
70
|
+
Tocsin.configure do |c|
|
71
|
+
c.recipient_groups = recipient_groups
|
72
|
+
end
|
73
|
+
|
74
|
+
Tocsin.config.recipient_groups.should == recipient_groups
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "#notify" do
|
79
|
+
it "can create a notification group idiomatically" do
|
80
|
+
Tocsin.configure do |c|
|
81
|
+
c.notify ["rnubel@test.com"], :of => { :severity => /critical/ }, :by => :email
|
82
|
+
end
|
83
|
+
|
84
|
+
Tocsin.config.recipient_groups.should == [
|
85
|
+
{ :severity => /critical/, :recipients => ["rnubel@test.com"], :notifier => :email }
|
86
|
+
]
|
87
|
+
end
|
88
|
+
|
89
|
+
it "assumes email as the default notifier if not specified" do
|
90
|
+
Tocsin.configure do |c|
|
91
|
+
c.notify ["rnubel@test.com"], :of => { :severity => /critical/ }
|
92
|
+
end
|
93
|
+
|
94
|
+
Tocsin.config.recipient_groups.should == [
|
95
|
+
{ :severity => /critical/, :recipients => ["rnubel@test.com"], :notifier => :email }
|
96
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
it "converts a single recipient into an array of that single recipient" do
|
100
|
+
Tocsin.configure do |c|
|
101
|
+
c.notify "rnubel@test.com", :of => { :severity => /critical/ }, :by => :email
|
102
|
+
end
|
103
|
+
|
104
|
+
Tocsin.config.recipient_groups.should == [
|
105
|
+
{ :severity => /critical/, :recipients => ["rnubel@test.com"], :notifier => :email }
|
106
|
+
]
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'has a default queue' do
|
110
|
+
Tocsin.configure { }
|
111
|
+
Tocsin.config.queue.should_not be_nil
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'can be confgiured with a default from_address' do
|
115
|
+
Tocsin.configure do |c|
|
116
|
+
c.from_address = "webdude@example.net"
|
117
|
+
end
|
118
|
+
Tocsin.config.from_address.should == "webdude@example.net"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "deciding what recipients apply to a given alert" do
|
124
|
+
context "when given a pattern matching only one group" do
|
125
|
+
it "returns the group in a hash with the notifier as the key" do
|
126
|
+
Tocsin.recipients(stub('alert', :category => "whee", :severity => "test")).should == { :email => ["x@y.com", "z@w.com"] }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should merge all groups which match the pattern" do
|
131
|
+
Tocsin.recipients(stub('alert', :category => "test", :severity => "high")).should == { :email => ["a@b.com", "x@y.com", "z@w.com"],
|
132
|
+
:text_message => ["1234567890"] }
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "synoynms for ::raise_alert" do
|
137
|
+
describe "::notify" do
|
138
|
+
it "calls raise_alert with notification as the default severity" do
|
139
|
+
Tocsin.expects(:raise_alert).with(nil, has_entries(:severity => "notification"))
|
140
|
+
Tocsin.notify({})
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "::warn!" do
|
145
|
+
it "calls raise_alert with the same options as passed" do
|
146
|
+
opts = mock("options")
|
147
|
+
Tocsin.expects(:raise_alert).with(nil, opts)
|
148
|
+
Tocsin.warn!(opts)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context "when raising an alarm" do
|
154
|
+
let(:exception) do
|
155
|
+
begin
|
156
|
+
raise "Exception"
|
157
|
+
rescue => e
|
158
|
+
e
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
let(:alert) do
|
163
|
+
Tocsin.raise_alert(exception, alert_options)
|
164
|
+
end
|
165
|
+
|
166
|
+
describe "the created Tocsin::Alert object" do
|
167
|
+
it "is created with appropriate fields" do
|
168
|
+
Tocsin::Alert.expects(:create).with(has_entries(
|
169
|
+
:exception => exception.to_s,
|
170
|
+
:backtrace => exception.backtrace.join("\n"),
|
171
|
+
:severity => alert_options[:severity].to_s,
|
172
|
+
:message => alert_options[:message].to_s,
|
173
|
+
:category => alert_options[:category].to_s
|
174
|
+
)).returns(stub("alert", :id => 1))
|
175
|
+
alert
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should enqueue a notification job in Resque by default" do
|
180
|
+
Resque.expects(:enqueue).with(Tocsin::NotificationJob, is_a(Integer))
|
181
|
+
Tocsin.expects(:sound).never
|
182
|
+
alert
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should sound an alert immediately when asked" do
|
186
|
+
Resque.expects(:enqueue).never
|
187
|
+
Tocsin.expects(:sound).once
|
188
|
+
alert_options[:urgent] = true
|
189
|
+
alert
|
190
|
+
end
|
191
|
+
|
192
|
+
it "does not explode if Resque.enqueue fails" do
|
193
|
+
Resque.expects(:enqueue).raises("Blah")
|
194
|
+
|
195
|
+
## once for the original alert, once for queuing failure
|
196
|
+
Tocsin.expects(:sound).twice
|
197
|
+
|
198
|
+
expect { alert }.to_not raise_error
|
199
|
+
end
|
200
|
+
|
201
|
+
end
|
202
|
+
|
203
|
+
describe Tocsin::NotificationJob do
|
204
|
+
let(:alert) { stub("alert", :id => 5, :category => "test", :severity => "test", :message => "test", :exception => nil, :backtrace => nil) }
|
205
|
+
before { Mail::TestMailer.deliveries.clear }
|
206
|
+
|
207
|
+
it "locates the alert by id" do
|
208
|
+
Tocsin::Alert.expects(:find).with(5).returns(alert)
|
209
|
+
Tocsin::NotificationJob.perform(5)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "uses the associated notifier to alert recipients" do
|
213
|
+
Tocsin::Alert.expects(:find).with(5).returns(alert)
|
214
|
+
Tocsin.expects(:recipients).with(alert).returns( :email => ["a@b.com"] )
|
215
|
+
Tocsin::Notifiers[:email].expects(:notify).with(["a@b.com"], alert)
|
216
|
+
Tocsin::NotificationJob.perform(5)
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'will use the first email address as the sender when from_address is not present' do
|
220
|
+
Tocsin::Alert.expects(:find).with(5).returns(alert)
|
221
|
+
Tocsin.expects(:recipients).with(alert).returns( :email => ["a@b.com", "x@y.com"] )
|
222
|
+
Tocsin::NotificationJob.perform(5)
|
223
|
+
|
224
|
+
em = Mail::TestMailer.deliveries.first
|
225
|
+
em.from.should include("a@b.com")
|
226
|
+
end
|
227
|
+
|
228
|
+
it "should log an error for an unknown notifier" do
|
229
|
+
Tocsin::Alert.expects(:find).with(5).returns(alert)
|
230
|
+
Tocsin.expects(:recipients).returns({:raven => "Cersei"})
|
231
|
+
Tocsin.logger.expects(:error)
|
232
|
+
Tocsin::NotificationJob.perform(5)
|
233
|
+
end
|
234
|
+
|
235
|
+
context "empty recipient list" do
|
236
|
+
before do
|
237
|
+
Tocsin::Alert.expects(:find).with(5).returns(alert)
|
238
|
+
Tocsin.expects(:recipients).returns({:email => []})
|
239
|
+
end
|
240
|
+
|
241
|
+
after { Tocsin::NotificationJob.perform(5) }
|
242
|
+
|
243
|
+
it "should not attempt notification" do
|
244
|
+
Tocsin::Notifiers[:email].expects(:notify).never
|
245
|
+
end
|
246
|
+
|
247
|
+
it "should log an error" do
|
248
|
+
Tocsin.logger.expects(:error)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
it "should not raise an exception if the alert isn't found (otherwise, possible recursion)" do
|
253
|
+
Tocsin::Alert.expects(:find).with(5).raises(ActiveRecord::RecordNotFound)
|
254
|
+
lambda {
|
255
|
+
Tocsin::NotificationJob.perform(5)
|
256
|
+
}.should_not raise_error
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
data/tocsin.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "tocsin/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "tocsin"
|
7
|
+
s.version = Tocsin::VERSION
|
8
|
+
s.authors = ["Robert Nubel","Blake Thomas"]
|
9
|
+
s.email = ["tocsin.gem@gmail.com"]
|
10
|
+
s.homepage = "https://github.com/tocsin/tocsin"
|
11
|
+
s.summary = %q{Notification and alert library for Rails-like projects.}
|
12
|
+
s.description = %q{Supports wrapping code that you want to be alerted of failures in, as well as sending notifications through the same mechanism.}
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
# specify any dependencies here; for example:
|
20
|
+
s.add_development_dependency "rspec"
|
21
|
+
s.add_development_dependency "mocha"
|
22
|
+
|
23
|
+
s.add_runtime_dependency "rails", ">= 3.0.0"
|
24
|
+
s.add_runtime_dependency "resque"
|
25
|
+
s.add_runtime_dependency "mail"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tocsin
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Nubel
|
9
|
+
- Blake Thomas
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-04-09 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: mocha
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rails
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.0.0
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 3.0.0
|
63
|
+
- !ruby/object:Gem::Dependency
|
64
|
+
name: resque
|
65
|
+
requirement: !ruby/object:Gem::Requirement
|
66
|
+
none: false
|
67
|
+
requirements:
|
68
|
+
- - ! '>='
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '0'
|
71
|
+
type: :runtime
|
72
|
+
prerelease: false
|
73
|
+
version_requirements: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
- !ruby/object:Gem::Dependency
|
80
|
+
name: mail
|
81
|
+
requirement: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ! '>='
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
version: '0'
|
87
|
+
type: :runtime
|
88
|
+
prerelease: false
|
89
|
+
version_requirements: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
description: Supports wrapping code that you want to be alerted of failures in, as
|
96
|
+
well as sending notifications through the same mechanism.
|
97
|
+
email:
|
98
|
+
- tocsin.gem@gmail.com
|
99
|
+
executables: []
|
100
|
+
extensions: []
|
101
|
+
extra_rdoc_files: []
|
102
|
+
files:
|
103
|
+
- .gitignore
|
104
|
+
- Gemfile
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- lib/tocsin.rb
|
108
|
+
- lib/tocsin/config.rb
|
109
|
+
- lib/tocsin/notifiers.rb
|
110
|
+
- lib/tocsin/notifiers/email_notifier.rb
|
111
|
+
- lib/tocsin/rake.rb
|
112
|
+
- lib/tocsin/version.rb
|
113
|
+
- spec/spec_helper.rb
|
114
|
+
- spec/tocsin/notifiers/email_notifier_spec.rb
|
115
|
+
- spec/tocsin/notifiers_spec.rb
|
116
|
+
- spec/tocsin_spec.rb
|
117
|
+
- tocsin.gemspec
|
118
|
+
homepage: https://github.com/tocsin/tocsin
|
119
|
+
licenses: []
|
120
|
+
post_install_message:
|
121
|
+
rdoc_options: []
|
122
|
+
require_paths:
|
123
|
+
- lib
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ! '>='
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
version: '0'
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 1.8.23
|
139
|
+
signing_key:
|
140
|
+
specification_version: 3
|
141
|
+
summary: Notification and alert library for Rails-like projects.
|
142
|
+
test_files:
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
- spec/tocsin/notifiers/email_notifier_spec.rb
|
145
|
+
- spec/tocsin/notifiers_spec.rb
|
146
|
+
- spec/tocsin_spec.rb
|
147
|
+
has_rdoc:
|