Skip to content

Commit 035cc5d

Browse files
committed
Add discord notifications; restructure code
1 parent ceb7f30 commit 035cc5d

4 files changed

Lines changed: 201 additions & 108 deletions

File tree

config.sample.yml

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# Emails From address
2-
from: status@codidact.com
3-
41
# Credentials for SES for sending notification emails
52
ses:
63
access_key_id: ~
@@ -15,11 +12,6 @@ monitors:
1512
# endpoint is up or down.
1613
test_url: https://meta.codidact.com/
1714

18-
# This is an email address to which to send emails indicating the current
19-
# status of the endpoint. This is designed for StatusPage but may work for
20-
# other services.
21-
notification_address: example@statuspage
22-
2315
# How many seconds apart should each test be?
2416
frequency: 300
2517

@@ -34,3 +26,28 @@ monitors:
3426
# This many tests must succeed when the endpoint is DOWN before an UP
3527
# notification is sent.
3628
success_count: 2
29+
30+
# Defines endpoints to which to send notifications when the status of components changes.
31+
notifications:
32+
# Email notifications, for example to StatusPage, just need an email address to send to.
33+
- type: email
34+
from: status@codidact.org
35+
address: example@statuspage
36+
subject: '$Status'
37+
body: '$Component is $Status'
38+
39+
# You can also set up notifications in Discord channels like this:
40+
- type: discord
41+
42+
# This is the webhook URL copied when you create your webhook in Discord.
43+
url: https://discord.com/api/webhooks/123/token
44+
45+
# This will override the default username of your webhook bot.
46+
username: Captain Hook
47+
48+
# List users or roles the message should mention. Use just the user ID for users, prefix with & for roles.
49+
mentions:
50+
- 1234
51+
- '&1234'
52+
53+
content: '$Mentions ping! $Component is $Status!'

lib/helpers.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'date'
2+
require 'net/http'
3+
4+
module Uptime
5+
module Helpers
6+
def now
7+
DateTime.now.strftime '%Y-%m-%d %H:%M:%S'
8+
end
9+
10+
def log(text)
11+
puts "[#{now}] #{text}"
12+
end
13+
14+
def test(url)
15+
begin
16+
res = Net::HTTP.get_response(URI(url))
17+
18+
unless res.is_a? Net::HTTPSuccess
19+
check_res = Net::HTTP.get_response(URI('https://www.google.com/'))
20+
if check_res.is_a? Net::HTTPSuccess
21+
return [res.is_a?(Net::HTTPSuccess), res.code]
22+
else
23+
return [true, '-99 (client fail)']
24+
end
25+
end
26+
27+
[res.is_a?(Net::HTTPSuccess), res.code]
28+
rescue
29+
[false, '-53 (client fail)']
30+
end
31+
end
32+
end
33+
end

lib/monitor.rb

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
require 'json'
2+
require 'net/http'
3+
require_relative 'helpers'
4+
5+
module Uptime
6+
class Monitor
7+
include Uptime::Helpers
8+
9+
attr_accessor :name, :test_url, :frequency, :failed_retest, :failure_count, :success_count, :notifications
10+
11+
##
12+
# Create a new Monitor instance.
13+
# @param config [Hash<Symbol, String>] The monitor's configuration as parsed from YAML.
14+
# @return [Monitor]
15+
def initialize(config, ses)
16+
[:name, :test_url, :frequency, :failed_retest, :failure_count, :success_count, :notifications].each do |sym|
17+
send "#{sym}=", config[sym]
18+
end
19+
@ses = ses
20+
end
21+
22+
##
23+
# Start a thread monitoring the endpoint as defined.
24+
# @return [Thread]
25+
def monitor
26+
Thread.new do
27+
failures = 0
28+
successes = 0
29+
currently_up = true
30+
31+
up = 'UP'.green
32+
down = 'DOWN'.red
33+
34+
while true do
35+
result, code = test(@test_url)
36+
37+
if currently_up && result
38+
log "#{@name}: currently #{up}, tested #{up} (#{code}) 💤 #{@frequency}"
39+
sleep @frequency
40+
elsif currently_up && !result
41+
failures += 1
42+
log "#{@name}: currently #{up}, tested #{down} #{failures}/#{@failure_count} (#{code}) 💤 #{@failed_retest}"
43+
if failures >= @failure_count
44+
if send_notifications 'DOWN'
45+
currently_up = false
46+
successes = 0
47+
log "#{@name}: #{down} notification sent, status set to #{down}"
48+
else
49+
log "#{@name}: failed to send notification, will retry next round"
50+
end
51+
end
52+
sleep @failed_retest
53+
elsif !currently_up && result
54+
successes += 1
55+
if successes >= @success_count
56+
log "#{@name}: currently #{down}, tested #{up} #{successes}/#{@success_count} (#{code}) 💤 #{@frequency}"
57+
if send_notifications 'UP'
58+
currently_up = true
59+
failures = 0
60+
log "#{@name}: #{up} notification sent, status set to #{up}"
61+
else
62+
log "#{@name}: failed to send notification, will retry next round"
63+
end
64+
sleep @frequency
65+
else
66+
log "#{@name}: currently #{down}, tested #{up} #{successes}/#{@success_count} (#{code}) 💤 #{@failed_retest}"
67+
sleep @failed_retest
68+
end
69+
elsif !currently_up && !result
70+
log "#{@name}: currently #{down}, tested #{down} (#{code}) 💤 #{@failed_retest}"
71+
successes = 0
72+
sleep @failed_retest
73+
end
74+
end
75+
end
76+
end
77+
78+
private
79+
80+
def send_notifications(status)
81+
notifications.all? do |notif|
82+
case notif[:type]
83+
when 'email'
84+
send_email_notification(notif, status)
85+
when 'discord'
86+
send_discord_webhook(notif, status)
87+
else
88+
log "#{@name}: unrecognized notification type #{notif[:type]}"
89+
end
90+
end
91+
end
92+
93+
def send_email_notification(notif, status)
94+
address = notif[:to]
95+
begin
96+
if address.is_a? String
97+
@ses.send_email(to: address, source: notif[:from], subject: status,
98+
text_body: subbed_content(notif[:content], status))
99+
elsif address.is_a? Array
100+
address.each do |a|
101+
@ses.send_email(to: address, source: notif[:from], subject: status,
102+
text_body: subbed_content(notif[:content], status))
103+
end
104+
end
105+
true
106+
rescue
107+
false
108+
end
109+
end
110+
111+
def send_discord_webhook(notif, status)
112+
uri = URI(notif[:url])
113+
mentions = notif[:mentions].nil? ? '' : notif[:mentions].map { |m| "<@#{m}>" }.join(' ')
114+
content = subbed_content(notif[:content], status).gsub('$Mentions', mentions)
115+
params = { content: content }
116+
params[:username] = notif[:username] unless notif[:username].nil?
117+
headers = { 'Content-Type': 'application/json' }
118+
begin
119+
response = Net::HTTP.post(uri, params.to_json, headers)
120+
unless response.is_a? Net::HTTPSuccess
121+
log "#{@name}: failed to send Discord webhook (fail) #{notif[:url]}"
122+
end
123+
response.is_a? Net::HTTPSuccess
124+
rescue
125+
log "#{@name}: failed to send Discord webhook (error) #{notif[:url]}"
126+
false
127+
end
128+
end
129+
130+
def subbed_content(content, status)
131+
content.gsub('$Component', @name)
132+
.gsub('$Status', status)
133+
end
134+
end
135+
end

monitor.rb

Lines changed: 8 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,23 @@
1-
require 'net/http'
21
require 'yaml'
32
require 'active_support/core_ext/hash/keys'
43
require 'io/console'
5-
require 'aws/ses'
64
require 'colorize'
5+
require 'aws/ses'
6+
require_relative 'lib/helpers'
7+
require_relative 'lib/monitor'
8+
9+
include Uptime::Helpers
710

811
$stdout.sync = true
912

1013
@config = OpenStruct.new(YAML.load_file('config.yml').deep_symbolize_keys)
1114
@ses = AWS::SES::Base.new(@config.ses)
12-
@monitors = @config.monitors
13-
14-
def test(url)
15-
begin
16-
res = Net::HTTP.get_response(URI(url))
17-
18-
unless res.is_a? Net::HTTPSuccess
19-
check_res = Net::HTTP.get_response(URI('https://www.google.com/'))
20-
if check_res.is_a? Net::HTTPSuccess
21-
return [res.is_a?(Net::HTTPSuccess), res.code]
22-
else
23-
return [true, '-99 (client fail)']
24-
end
25-
end
26-
27-
[res.is_a?(Net::HTTPSuccess), res.code]
28-
rescue
29-
[false, '-53 (client fail)']
30-
end
31-
end
32-
33-
def send_notification(address, type, body: nil)
34-
begin
35-
if address.is_a? String
36-
@ses.send_email(to: address, source: @config.from, subject: type, text_body: body)
37-
elsif address.is_a? Array
38-
address.each do |a|
39-
@ses.send_email(to: address, source: @config.from, subject: type, text_body: body)
40-
end
41-
end
42-
true
43-
rescue
44-
false
45-
end
46-
end
47-
48-
def now
49-
DateTime.now.strftime '%Y-%m-%d %H:%M:%S'
50-
end
51-
52-
def log(text)
53-
puts "[#{now}] #{text}"
54-
end
15+
@monitors = @config.monitors.map { |m| Uptime::Monitor.new(m, @ses) }
5516

5617
log "Initialized. #{@monitors.count} monitors pending."
5718

58-
threads = @monitors.map do |monitor|
59-
monitor = OpenStruct.new(monitor)
60-
61-
Thread.new do
62-
failures = 0
63-
successes = 0
64-
currently_up = true
65-
66-
up = 'UP'.green
67-
down = 'DOWN'.red
68-
69-
while true do
70-
result, code = test(monitor.test_url)
71-
72-
if currently_up && result
73-
log "#{monitor.name}: currently #{up}, tested #{up} (#{code}) 💤 #{monitor.frequency}"
74-
sleep monitor.frequency
75-
elsif currently_up && !result
76-
failures += 1
77-
log "#{monitor.name}: currently #{up}, tested #{down} #{failures}/#{monitor.failure_count} (#{code}) 💤 #{monitor.failed_retest}"
78-
if failures >= monitor.failure_count
79-
if send_notification(monitor.notification_address, 'DOWN',
80-
body: "#{monitor.name} detected DOWN (#{failures} failures, latest #{now})")
81-
currently_up = false
82-
successes = 0
83-
log "#{' ' * monitor.name.length}: #{down} notification sent, status set to #{down}"
84-
else
85-
log "#{' ' * monitor.name.length}: failed to send notification, will retry next round"
86-
end
87-
end
88-
sleep monitor.failed_retest
89-
elsif !currently_up && result
90-
successes += 1
91-
if successes >= monitor.success_count
92-
log "#{monitor.name}: currently #{down}, tested #{up} #{successes}/#{monitor.success_count} (#{code}) 💤 #{monitor.frequency}"
93-
if send_notification(monitor.notification_address, 'UP',
94-
body: "#{monitor.name} detected UP (#{successes} successes, latest #{now})")
95-
currently_up = true
96-
failures = 0
97-
log "#{' ' * monitor.name.length}: #{up} notification sent, status set to #{up}"
98-
else
99-
log "#{' ' * monitor.name.length}: failed to send notification, will retry next round"
100-
end
101-
sleep monitor.frequency
102-
else
103-
log "#{monitor.name}: currently #{down}, tested #{up} #{successes}/#{monitor.success_count} (#{code}) 💤 #{monitor.failed_retest}"
104-
sleep monitor.failed_retest
105-
end
106-
elsif !currently_up && !result
107-
log "#{monitor.name}: currently #{down}, tested #{down} (#{code}) 💤 #{monitor.frequency}"
108-
successes = 0
109-
sleep monitor.failed_retest
110-
end
111-
end
112-
end
19+
threads = @monitors.map do |m|
20+
m.monitor
11321
end
11422

11523
threads.map(&:join)

0 commit comments

Comments
 (0)