Last active
August 29, 2015 14:11
-
-
Save johntdyer/aec1672ee21aede1f899 to your computer and use it in GitHub Desktop.
Sensu slack extension
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Sends events to slack for wonderful chatty notifications | |
# | |
# This extension requires the slack gem | |
# | |
# The reason I wrote this instead of using the normal slack handler, is that with Flapjack | |
# all events are handled unless you do crazy filtering stuff. Also with a large number of events | |
# and checks the sensu server can get overloaded with forking stuff. So anyway, slack extension :) | |
# | |
# Here is an example of what the Sensu configuration for slack should | |
# look like. It's fairly configurable: | |
# | |
# { | |
# "slack": { | |
# "apikey": "webhook_url", | |
# 'keepalive': { | |
# 'occurrences': {slack_keepalive_occurrences} | |
# } | |
# } | |
# } | |
# | |
# | |
# Options that can be passed in event data are as follows:u) | |
# "playbook" => URL or HTML for playbook for that check | |
# | |
# Copyright 2014 John Dyer and contributors. | |
# | |
# Released under the same terms as Sensu (the MIT license); see LICENSE for details. | |
require 'rubygems' if RUBY_VERSION < '1.9.0' | |
require 'timeout' | |
require 'rest-client' | |
require 'net/https' | |
require "uri" | |
module Sensu::Extension | |
class Slack < Handler | |
# The post_init hook is called after the main event loop has started | |
# At this time EventMachine is available for interaction. | |
def post_init | |
end | |
# Must at a minimum define type and name. These are used by | |
# Sensu during extension initialization. | |
def definition | |
{ | |
type: 'extension', # Always. | |
name: 'slack', # Usually just class name lower case. | |
mutator: 'ruby_hash' | |
} | |
end | |
# Simple accessor for the extension's name. You could put a different | |
# value here that is slightly more descriptive, or you could simply | |
# refer to the definition name. | |
def name | |
definition[:name] | |
end | |
# A simple, brief description of the extension's function. | |
def description | |
'Slack extension. Because otherwise the sensu server will forking die.' | |
end | |
# Sends an event to the specified slack room etc | |
def send_slack(room, from, message) | |
webhook_url = @settings['slack']['webhook_url'] | |
uri = URI.parse(webhook_url) | |
http = Net::HTTP.new(uri.host, uri.port) | |
http.use_ssl = true | |
request = Net::HTTP::Post.new(uri.request_uri) | |
request.body = message.to_json | |
begin | |
timeout(3) do | |
response = http.request(request) | |
return 'Sent slack message' | |
end | |
rescue Timeout::Error | |
return "Timed out while attempting to message #{room} [#{message}]" | |
rescue => e | |
return "Unexpected error sending #{message} to #{room} [ #{e} - #{e.backtrace.join('\n')} ]" | |
end | |
end | |
# Log something and return false. | |
def bail(msg, event) | |
@logger.info("Slack handler: #{msg} : #{event[:client][:name]}/#{event[:check][:name]}") | |
false | |
end | |
# Lifted from the sensu-plugin gem, makes an api request to sensu | |
def api_request(method, path, &blk) | |
http = Net::HTTP.new(@settings['api']['host'], @settings['api']['port']) | |
req = net_http_req_class(method).new(path) | |
if @settings['api']['user'] && @settings['api']['password'] | |
req.basic_auth(@settings['api']['user'], @settings['api']['password']) | |
end | |
yield(req) if block_given? | |
http.request(req) | |
end | |
# also lifted from the sensu-plugin gem. In fact, most of the rest was. | |
def net_http_req_class(method) | |
case method.to_s.upcase | |
when 'GET' | |
Net::HTTP::Get | |
when 'POST' | |
Net::HTTP::Post | |
when 'DELETE' | |
Net::HTTP::Delete | |
when 'PUT' | |
Net::HTTP::Put | |
end | |
end | |
def stash_exists?(path) | |
api_request(:GET, '/stash' + path).code == '200' | |
end | |
def event_exists?(client, check) | |
api_request(:GET, '/event/' + client + '/' + check).code == '200' | |
end | |
# Has this check been disabled from handlers? | |
def filter_disabled(event) | |
if event[:check].key?(:alert) | |
if event[:check][:alert] == false | |
bail 'alert disabled', event | |
end | |
end | |
true | |
end | |
# Don't spam slack too much! | |
def filter_repeated(event) | |
defaults = { | |
'occurrences' => 1, | |
'interval' => 60, | |
'refresh' => 1800 | |
} | |
occurrences = event[:check][:occurrences] || defaults['occurrences'] | |
interval = event[:check][:interval] || defaults['interval'] | |
refresh = event[:check][:refresh] || defaults['refresh'] | |
if event[:occurrences] < occurrences | |
return bail 'not enough occurrences', event | |
end | |
if event[:occurrences] > occurrences && event[:action] == :create | |
number = refresh.fdiv(interval).to_i | |
unless number == 0 || event[:occurrences] % number == 0 | |
return bail 'only handling every ' + number.to_s + ' occurrences', event | |
end | |
end | |
true | |
end | |
# Has the event been silenced through the API? | |
def filter_silenced(event) | |
stashes = [ | |
['client', '/silence/' + event[:client][:name]], | |
['check', '/silence/' + event[:client][:name] + '/' + event[:check][:name]], | |
['check', '/silence/all/' + event[:check][:name]] | |
] | |
stashes.each do |(scope, path)| | |
begin | |
timeout(2) do | |
if stash_exists?(path) | |
return bail scope + ' alerts silenced', event | |
end | |
end | |
rescue Timeout::Error | |
@logger.warn('timed out while attempting to query the sensu api for a stash') | |
end | |
end | |
true | |
end | |
# Does this event have dependencies? | |
def filter_dependencies(event) | |
if event[:check].has_key?(:dependencies) | |
if event[:check][:dependencies].is_a?(Array) | |
event[:check][:dependencies].each do |dependency| | |
begin | |
timeout(2) do | |
check, client = dependency.split('/').reverse | |
if event_exists?(client || event[:client][:name], check) | |
return bail 'check dependency event exists', event | |
end | |
end | |
rescue Timeout::Error | |
@logger.warn('timed out while attempting to query the sensu api for an event') | |
end | |
end | |
end | |
end | |
true | |
end | |
# Run all the filters in some order. Only run the handler if they all return true | |
def filters(event_data) | |
return false unless filter_repeated(event_data) | |
return false unless filter_silenced(event_data) | |
return false unless filter_dependencies(event_data) | |
return false unless filter_disabled(event_data) | |
@logger.info("#{event_data[:client][:name]}/#{event_data[:check][:name]} not being filtered!") | |
true | |
end | |
def build_slack_message(event, state_msg, status_msg, color) | |
#"#{status_msg} #{client_name}/#{check_name} - #{state_msg}: #{output} #{playbook}" | |
check = event[:check] | |
client_name = check[:source] || event[:client][:name] | |
check_name = check[:name] | |
output = check[:notification] || check[:output] | |
history = check[:history] || [] | |
playbook = check[:playbook] | |
data = { | |
fallback: "#{status_msg} #{client_name}/#{check_name} - #{state_msg} ", | |
text: "#{status_msg} #{client_name}/#{check_name} - #{state_msg} ", | |
color: color, | |
fields: [] | |
} | |
# We dont need to see any of this data if the alert is resolving | |
unless status_msg =~ /RESOLVE/ | |
# Use :pretext instead of :text, this is mainly for aesthetics | |
data[:pretext] = data.delete :text | |
data[:fields] = [ | |
{ | |
title: 'Output: ', | |
value: state_msg, | |
short: false | |
}, | |
{ | |
title: 'Occurances', | |
value: 3, | |
short: true | |
}, | |
{ | |
title: 'History', | |
value: "#{history.join(',')}", | |
short: true | |
} | |
] | |
# Include playbook link if event includes it | |
if playbook | |
# Append the playbook url to the fallback url | |
data[:fallback] = data[:fallback] + "- #{playbook}" | |
data[:fields] << { | |
title: 'Playbook', | |
value: "<#{playbook}|#{playbook.split('/')[-1]}>", | |
short: true | |
} | |
end | |
end | |
return {attachments:[data]} | |
end | |
def clarify_state(state, check) | |
if state == 0 | |
state_msg = 'OK' | |
color = 'good' | |
elsif state == 1 | |
state_msg = 'WARNING' | |
color = 'warning' | |
elsif state == 2 | |
state_msg = 'CRITICAL' | |
color = 'danger' | |
else | |
state_msg = 'UNKNOWN' | |
color = '#6600CC' | |
end | |
[state_msg, color] | |
end | |
# run() is passed a copy of the event_data hash | |
def run(event_data) | |
event = event_data | |
# Is this event a resolution? | |
resolved = event[:action].eql?(:resolve) | |
# Is this event a keepalive? | |
# Adding extra config on every client is annoying. Just make some extra settings for it. | |
keepalive = @settings['slack']['keepalive'] || {} | |
if event[:check][:name] == 'keepalive' | |
event[:check][:occurrences] = keepalive['occurrences'] || 6 | |
event[:check][:slack_room] = keepalive['room'] || @settings['slack']['room'] | |
event[:check][:slack_from] = keepalive['from'] || @settings['slack']['from'] || 'Sensu' | |
end | |
# If this event is resolved, or in one of the 'bad' states, and it passes all the filters, | |
# send the message to slack | |
if (resolved || [1, 2, 3].include?(event[:check][:status])) && filters(event) | |
check = event[:check] | |
room = check[:slack_room] || @settings['slack']['room'] | |
from = check[:slack_from] || @settings['slack']['from'] || 'Sensu' | |
state = check[:status] | |
state_msg, color = clarify_state(state, check) | |
status_msg = "#{event[:action].to_s.upcase}:" | |
slack_message = build_slack_message(event, state_msg, status_msg, color) | |
operation = proc { send_slack(room, from, slack_message) } | |
callback = proc {|result| yield "Slack message: #{result}", 0 } | |
EM.defer(operation, callback) | |
else | |
yield 'Slack not handling', 0 | |
end | |
end | |
# Called when Sensu begins to shutdown. | |
def stop | |
true | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment