Skip to content

Instantly share code, notes, and snippets.

@johntdyer
Last active August 29, 2015 14:11
Show Gist options
  • Save johntdyer/aec1672ee21aede1f899 to your computer and use it in GitHub Desktop.
Save johntdyer/aec1672ee21aede1f899 to your computer and use it in GitHub Desktop.
Sensu slack extension
#!/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