mirror of
https://github.com/elastic/logstash.git
synced 2025-04-24 22:57:16 -04:00
Email Notification Plugin - really only SMTP tested and confirmed
working.
This commit is contained in:
parent
6c1c5e3c62
commit
8f48d1ed42
4 changed files with 352 additions and 2 deletions
1
Gemfile
1
Gemfile
|
@ -11,6 +11,7 @@ gem "onstomp" # for stomp protocol, Apache 2.0 License
|
||||||
gem "json" # Ruby license
|
gem "json" # Ruby license
|
||||||
#gem "awesome_print" # MIT License
|
#gem "awesome_print" # MIT License
|
||||||
gem "jruby-openssl", :platforms => :jruby # For enabling SSL support, CPL/GPL 2.0
|
gem "jruby-openssl", :platforms => :jruby # For enabling SSL support, CPL/GPL 2.0
|
||||||
|
gem "mail" #outputs/email, # License: MIT License
|
||||||
|
|
||||||
gem "minitest" # License: Ruby
|
gem "minitest" # License: Ruby
|
||||||
gem "rack" # License: MIT
|
gem "rack" # License: MIT
|
||||||
|
|
14
Gemfile.lock
14
Gemfile.lock
|
@ -33,6 +33,7 @@ GEM
|
||||||
rubyzip
|
rubyzip
|
||||||
http_parser.rb (0.5.3)
|
http_parser.rb (0.5.3)
|
||||||
http_parser.rb (0.5.3-java)
|
http_parser.rb (0.5.3-java)
|
||||||
|
i18n (0.6.0)
|
||||||
jls-grok (0.10.6)
|
jls-grok (0.10.6)
|
||||||
cabin (~> 0.4.0)
|
cabin (~> 0.4.0)
|
||||||
jruby-elasticsearch (0.0.11)
|
jruby-elasticsearch (0.0.11)
|
||||||
|
@ -46,12 +47,18 @@ GEM
|
||||||
addressable (~> 2.2.6)
|
addressable (~> 2.2.6)
|
||||||
ffi (~> 1.0.9)
|
ffi (~> 1.0.9)
|
||||||
spoon (~> 0.0.1)
|
spoon (~> 0.0.1)
|
||||||
|
mail (2.4.4)
|
||||||
|
i18n (>= 0.4.0)
|
||||||
|
mime-types (~> 1.16)
|
||||||
|
treetop (~> 1.4.8)
|
||||||
mime-types (1.18)
|
mime-types (1.18)
|
||||||
minitest (2.11.4)
|
minitest (2.11.4)
|
||||||
mongo (1.6.1)
|
mongo (1.6.1)
|
||||||
bson (~> 1.6.1)
|
bson (~> 1.6.1)
|
||||||
mtrc (0.0.4)
|
mtrc (0.0.4)
|
||||||
netrc (0.7.1)
|
netrc (0.7.1)
|
||||||
|
onstomp (1.0.6)
|
||||||
|
polyglot (0.3.3)
|
||||||
rack (1.4.1)
|
rack (1.4.1)
|
||||||
rack-protection (1.2.0)
|
rack-protection (1.2.0)
|
||||||
rack
|
rack
|
||||||
|
@ -70,8 +77,10 @@ GEM
|
||||||
tilt (~> 1.3, >= 1.3.3)
|
tilt (~> 1.3, >= 1.3.3)
|
||||||
spoon (0.0.1)
|
spoon (0.0.1)
|
||||||
statsd-ruby (0.3.0)
|
statsd-ruby (0.3.0)
|
||||||
stomp (1.2.1)
|
|
||||||
tilt (1.3.3)
|
tilt (1.3.3)
|
||||||
|
treetop (1.4.10)
|
||||||
|
polyglot
|
||||||
|
polyglot (>= 0.3.1)
|
||||||
trollop (1.16.2)
|
trollop (1.16.2)
|
||||||
uuidtools (2.1.2)
|
uuidtools (2.1.2)
|
||||||
xmpp4r (0.5)
|
xmpp4r (0.5)
|
||||||
|
@ -96,14 +105,15 @@ DEPENDENCIES
|
||||||
jruby-elasticsearch (= 0.0.11)
|
jruby-elasticsearch (= 0.0.11)
|
||||||
jruby-openssl
|
jruby-openssl
|
||||||
json
|
json
|
||||||
|
mail
|
||||||
minitest
|
minitest
|
||||||
mongo
|
mongo
|
||||||
|
onstomp
|
||||||
rack
|
rack
|
||||||
redis
|
redis
|
||||||
riemann-client (= 0.0.6)
|
riemann-client (= 0.0.6)
|
||||||
sass
|
sass
|
||||||
sinatra
|
sinatra
|
||||||
statsd-ruby (= 0.3.0)
|
statsd-ruby (= 0.3.0)
|
||||||
stomp
|
|
||||||
uuidtools
|
uuidtools
|
||||||
xmpp4r (= 0.5)
|
xmpp4r (= 0.5)
|
||||||
|
|
338
lib/logstash/outputs/email.rb
Executable file
338
lib/logstash/outputs/email.rb
Executable file
|
@ -0,0 +1,338 @@
|
||||||
|
require "logstash/outputs/base"
|
||||||
|
require "logstash/namespace"
|
||||||
|
|
||||||
|
# https://github.com/mikel/mail
|
||||||
|
# supports equal(default), not equal(!), greater than(>), less than(<), greater than or equal(>=), less than or equal(<=), contains(*), does not contain(!*)
|
||||||
|
# you must provide a matchName - which is the key. Then provide your query values - again in key value pairs, separated by a ',' in the value spot.
|
||||||
|
# You can say this :
|
||||||
|
# [ "response errors", "response,501,,or,response,301" ]
|
||||||
|
# I hate making requirements like this but this is the format that is the most flexible for making fine selections over data.
|
||||||
|
# NOTE: In the above example we are using just an equality test - so the two values must be exact for matches to be made. You must provide an AND/OR block
|
||||||
|
# between conditions so we know how to deal with them. Please see below for an example where you wanted an AND instead of the OR default - this would require both to be valid.
|
||||||
|
# [ "response errors", "response,501,,and,response,301" ]
|
||||||
|
# as you can see you can just seperate the Operator logic with a blank key and the operator of your liking - AND/OR
|
||||||
|
# IMPORTANT : you MUST provide a "matchName". This is so I can easily be able to provide a label of sorts for the alert.
|
||||||
|
# In addition, we break after we find the first valid match.
|
||||||
|
#
|
||||||
|
# email {
|
||||||
|
# tags => [ "sometag" ]
|
||||||
|
# match => [ "response errors", "response,501,,or,response,301",
|
||||||
|
# "multiple response errors", "response,501,,and,response,301" ]
|
||||||
|
# to => "main.contact@domain.com"
|
||||||
|
# from => "alert.account@domain.com" # default: logstash.alert@nowhere.com
|
||||||
|
# cc => "" # provide additional recipients
|
||||||
|
# options => [ "smtpIporHost", "smtp.gmail.com",
|
||||||
|
# "port", "587",
|
||||||
|
# "domain", "yourDomain", # optional
|
||||||
|
# "userName", "yourSMTPUsername",
|
||||||
|
# "password", "PASS",
|
||||||
|
# "starttls", "true",
|
||||||
|
# "authenticationType", "plain",
|
||||||
|
# "debug", "true" # optional
|
||||||
|
# ]
|
||||||
|
# via => "smtp" # or pop or sendmail
|
||||||
|
# subject => "Found '%{matchName}' Alert on %{@source_host}"
|
||||||
|
# body => "Here is the event line %{@message}"
|
||||||
|
# htmlbody => "<h2>%{matchName}</h2><br/><br/><h3>Full Event</h3><br/><br/><div align='center'>%{@message}</div>"
|
||||||
|
# }
|
||||||
|
class LogStash::Outputs::Email < LogStash::Outputs::Base
|
||||||
|
|
||||||
|
config_name "email"
|
||||||
|
plugin_status "beta"
|
||||||
|
|
||||||
|
# the registered fields that we want to monitor
|
||||||
|
# A hash of matches of field => value
|
||||||
|
config :match, :validate => :hash, :required => true
|
||||||
|
|
||||||
|
# the To address setting - fully qualified email address to send to
|
||||||
|
config :to, :validate => :string, :required => true
|
||||||
|
|
||||||
|
# The From setting for email - fully qualified email address for the From:
|
||||||
|
config :from, :validate => :string, :default => "logstash.alert@nowhere.com"
|
||||||
|
|
||||||
|
# cc - send to others
|
||||||
|
config :cc, :validate => :string, :default => ""
|
||||||
|
|
||||||
|
# how to send email: either smtp or sendmail - default to 'smtp'
|
||||||
|
config :via, :validate => :string, :default => "smtp"
|
||||||
|
|
||||||
|
# the options to use:
|
||||||
|
# smtp: address, port, enable_starttls_auto, user_name, password, authentication(bool), domain
|
||||||
|
# sendmail: location, arguments
|
||||||
|
# If you do not specify anything, you will get the following equivalent code set in
|
||||||
|
# every new mail object:
|
||||||
|
#
|
||||||
|
# Mail.defaults do
|
||||||
|
# delivery_method :smtp, { :address => "localhost",
|
||||||
|
# :port => 25,
|
||||||
|
# :domain => 'localhost.localdomain',
|
||||||
|
# :user_name => nil,
|
||||||
|
# :password => nil,
|
||||||
|
# :authentication => nil,(plain, login and cram_md5)
|
||||||
|
# :enable_starttls_auto => true }
|
||||||
|
#
|
||||||
|
# retriever_method :pop3, { :address => "localhost",
|
||||||
|
# :port => 995,
|
||||||
|
# :user_name => nil,
|
||||||
|
# :password => nil,
|
||||||
|
# :enable_ssl => true }
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# Mail.delivery_method.new #=> Mail::SMTP instance
|
||||||
|
# Mail.retriever_method.new #=> Mail::POP3 instance
|
||||||
|
#
|
||||||
|
# Each mail object inherits the default set in Mail.delivery_method, however, on
|
||||||
|
# a per email basis, you can override the method:
|
||||||
|
#
|
||||||
|
# mail.delivery_method :sendmail
|
||||||
|
#
|
||||||
|
# Or you can override the method and pass in settings:
|
||||||
|
#
|
||||||
|
# mail.delivery_method :sendmail, { :address => 'some.host' }
|
||||||
|
#
|
||||||
|
# You can also just modify the settings:
|
||||||
|
#
|
||||||
|
# mail.delivery_settings = { :address => 'some.host' }
|
||||||
|
#
|
||||||
|
# The passed in hash is just merged against the defaults with +merge!+ and the result
|
||||||
|
# assigned the mail object. So the above example will change only the :address value
|
||||||
|
# of the global smtp_settings to be 'some.host', keeping all other values
|
||||||
|
config :options, :validate => :hash, :default => {}
|
||||||
|
|
||||||
|
# subject for email
|
||||||
|
config :subject, :validate => :string, :default => ""
|
||||||
|
|
||||||
|
# body for email - just plain text
|
||||||
|
config :body, :validate => :string, :default => ""
|
||||||
|
|
||||||
|
# body for email - can contain html markup
|
||||||
|
config :htmlbody, :validate => :string, :default => ""
|
||||||
|
|
||||||
|
# attachments - has of name of file and file location
|
||||||
|
config :attachments, :validate => :array, :default => []
|
||||||
|
|
||||||
|
# contenttype : for multipart messages, set the content type and/or charset of the html part
|
||||||
|
config :contenttype, :validate => :string, :default => "text/html; charset=UTF-8"
|
||||||
|
|
||||||
|
public
|
||||||
|
def register
|
||||||
|
require "mail"
|
||||||
|
if @via == "smtp"
|
||||||
|
debug = @options.include?("debug")
|
||||||
|
if !debug
|
||||||
|
debug = false
|
||||||
|
else
|
||||||
|
debug = @options.fetch("debug")
|
||||||
|
end
|
||||||
|
smtpIporHost = @options.include?("smtpIporHost")
|
||||||
|
if !smtpIporHost
|
||||||
|
smtpIporHost = "localhost"
|
||||||
|
else
|
||||||
|
smtpIporHost = @options.fetch("smtpIporHost")
|
||||||
|
end
|
||||||
|
domain = @options.include?("domain")
|
||||||
|
if !domain
|
||||||
|
domain = "localhost"
|
||||||
|
else
|
||||||
|
domain = @options.fetch("domain")
|
||||||
|
end
|
||||||
|
port = @options.include?("port")
|
||||||
|
if !port
|
||||||
|
port = 25
|
||||||
|
else
|
||||||
|
port = @options.fetch("port")
|
||||||
|
end
|
||||||
|
tls = @options.include?("starttls")
|
||||||
|
if !tls
|
||||||
|
tls = false
|
||||||
|
else
|
||||||
|
tls = @options.fetch("starttls")
|
||||||
|
end
|
||||||
|
pass = @options.include?("password")
|
||||||
|
if !pass
|
||||||
|
pass = ""
|
||||||
|
else
|
||||||
|
pass = @options.fetch("password")
|
||||||
|
end
|
||||||
|
userName = @options.include?("userName")
|
||||||
|
if !userName
|
||||||
|
userName = ""
|
||||||
|
else
|
||||||
|
userName = @options.fetch("userName")
|
||||||
|
end
|
||||||
|
authenticationType = @options.include?("authenticationType")
|
||||||
|
if !authenticationType
|
||||||
|
authenticationType = "plain"
|
||||||
|
else
|
||||||
|
authenticationType = @options.fetch("authenticationType")
|
||||||
|
end
|
||||||
|
Mail.defaults do
|
||||||
|
delivery_method :smtp , { :address => smtpIporHost,
|
||||||
|
:port => port,
|
||||||
|
:domain => domain,
|
||||||
|
:user_name => userName,
|
||||||
|
:password => pass,
|
||||||
|
:authentication => authenticationType,
|
||||||
|
:enable_starttls_auto => tls,
|
||||||
|
:debug => debug }
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Mail.defaults do
|
||||||
|
delivery_method :@via, @options
|
||||||
|
end
|
||||||
|
end # @via tests
|
||||||
|
@logger.debug("Email Output Registered!", :config => @config)
|
||||||
|
end # def register
|
||||||
|
|
||||||
|
public
|
||||||
|
def receive(event)
|
||||||
|
return unless output?(event)
|
||||||
|
@logger.debug("Event being tested for Email", :tags => @tags, :event => event)
|
||||||
|
# Set Intersection - returns a new array with the items that are the same between the two
|
||||||
|
if !@tags.empty? && (event.tags & @tags).size == 0
|
||||||
|
# Skip events that have no tags in common with what we were configured
|
||||||
|
@logger.debug("No Tags match for Email Output!")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@logger.debug("Match data for Email - ", :match => @match)
|
||||||
|
successful = false
|
||||||
|
matchName = ""
|
||||||
|
operator = ""
|
||||||
|
@match.each do |name, query|
|
||||||
|
if successful
|
||||||
|
break
|
||||||
|
else
|
||||||
|
matchName = name
|
||||||
|
end
|
||||||
|
# now loop over the csv query
|
||||||
|
queryArray = query.split(',')
|
||||||
|
index = 1
|
||||||
|
while index < queryArray.length
|
||||||
|
field = queryArray.at(index -1)
|
||||||
|
value = queryArray.at(index)
|
||||||
|
index = index + 2
|
||||||
|
if field == ""
|
||||||
|
if value.downcase == "and"
|
||||||
|
operator = "and"
|
||||||
|
elsif value.downcase == "or"
|
||||||
|
operator = "or"
|
||||||
|
else
|
||||||
|
operator = "or"
|
||||||
|
@logger.error("Operator Provided Is Not Found, Currently We Only Support AND/OR Values! - defaulting to OR")
|
||||||
|
end
|
||||||
|
else
|
||||||
|
hasField = event.fields.has_key?(field)
|
||||||
|
@logger.debug("Does Event Contain Field - ", :hasField => hasField)
|
||||||
|
isValid = false
|
||||||
|
# if we have maching field and value is wildcard - we have a success
|
||||||
|
if hasField
|
||||||
|
if value == "*"
|
||||||
|
isValid = true
|
||||||
|
else
|
||||||
|
# we get an array so we need to loop over the values and find if we have a match
|
||||||
|
eventFieldValues = event.fields.fetch(field)
|
||||||
|
@logger.debug("Event Field Values - ", :eventFieldValues => eventFieldValues)
|
||||||
|
eventFieldValues.each do |eventFieldValue|
|
||||||
|
isValid = validateValue(eventFieldValue, value)
|
||||||
|
if isValid # no need to iterate any further
|
||||||
|
@logger.debug("VALID CONDITION FOUND - ", :eventFieldValue => eventFieldValue, :value => value)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end # end eventFieldValues.each do
|
||||||
|
end # end value == "*"
|
||||||
|
end # end hasField
|
||||||
|
# if we have an AND operator and we have a successful == false break
|
||||||
|
if operator == "and" && !isValid
|
||||||
|
successful = false
|
||||||
|
elsif operator == "or" && (isValid || successful)
|
||||||
|
successful = true
|
||||||
|
else
|
||||||
|
successful = isValid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end # @match.each do
|
||||||
|
|
||||||
|
@logger.debug("Email Did we match any alerts for event : ", :successful => successful)
|
||||||
|
|
||||||
|
if successful
|
||||||
|
# first add our custom field - matchName - so we can use it in the sprintf function
|
||||||
|
event["matchName"] = matchName
|
||||||
|
@logger.debug("Sending mail with these settings : ", :via => @via, :options => @options, :from => @from, :to => @to, :cc => @cc, :subject => @subject, :body => @body, :content_type => @contenttype, :htmlbody => @htmlbody, :attachments => @attachments, :to => to, :to => to)
|
||||||
|
formatedSubject = event.sprintf(@subject)
|
||||||
|
formattedBody = event.sprintf(@body)
|
||||||
|
formattedHtmlBody = event.sprintf(@htmlbody)
|
||||||
|
# we have a match(s) - send email
|
||||||
|
mail = Mail.new
|
||||||
|
mail.from = event.sprintf(@from)
|
||||||
|
mail.to = event.sprintf(@to)
|
||||||
|
mail.cc = event.sprintf(@cc)
|
||||||
|
mail.subject = formatedSubject
|
||||||
|
if @htmlbody.empty?
|
||||||
|
mail.body = formattedBody
|
||||||
|
else
|
||||||
|
mail.text_part = Mail::Part.new do
|
||||||
|
content_type "text/plain; charset=UTF-8"
|
||||||
|
body formattedBody
|
||||||
|
end
|
||||||
|
mail.html_part = Mail::Part.new do
|
||||||
|
content_type "text/html; charset=UTF-8"
|
||||||
|
body formattedHtmlBody
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@attachments.each do |fileLocation|
|
||||||
|
mail.add_file(fileLocation)
|
||||||
|
end # end @attachments.each
|
||||||
|
mail.deliver!
|
||||||
|
end # end if successful
|
||||||
|
end # def receive
|
||||||
|
|
||||||
|
private
|
||||||
|
def validateValue(eventFieldValue, value)
|
||||||
|
valid = false
|
||||||
|
# order of this if-else is important - please don't change it
|
||||||
|
if value.start_with?(">=")# greater than or equal
|
||||||
|
value.gsub!(">=","")
|
||||||
|
if eventFieldValue.to_i >= value.to_i
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
elsif value.start_with?("<=")# less than or equal
|
||||||
|
value.gsub!("<=","")
|
||||||
|
if eventFieldValue.to_i <= value.to_i
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
elsif value.start_with?(">")# greater than
|
||||||
|
value.gsub!(">","")
|
||||||
|
if eventFieldValue.to_i > value.to_i
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
elsif value.start_with?("<")# less than
|
||||||
|
value.gsub!("<","")
|
||||||
|
if eventFieldValue.to_i < value.to_i
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
elsif value.start_with?("*")# contains
|
||||||
|
value.gsub!("*","")
|
||||||
|
if eventFieldValue.include?(value)
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
elsif value.start_with?("!*")# does not contain
|
||||||
|
value.gsub!("!*","")
|
||||||
|
if !eventFieldValue.include?(value)
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
elsif value.start_with?("!")# not equal
|
||||||
|
value.gsub!("!","")
|
||||||
|
if eventFieldValue != value
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
else # default equal
|
||||||
|
if eventFieldValue == value
|
||||||
|
valid = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return valid
|
||||||
|
end # end validateValue()
|
||||||
|
|
||||||
|
end # class LogStash::Outputs::Email
|
|
@ -47,6 +47,7 @@ Gem::Specification.new do |spec|
|
||||||
spec.add_dependency "stomp" # for stomp protocol, Apache 2.0 License
|
spec.add_dependency "stomp" # for stomp protocol, Apache 2.0 License
|
||||||
spec.add_dependency "uuidtools" # for naming amqp queues, License ???
|
spec.add_dependency "uuidtools" # for naming amqp queues, License ???
|
||||||
spec.add_dependency "xmpp4r", "~> 0.5" # outputs/xmpp, # License: As-Is
|
spec.add_dependency "xmpp4r", "~> 0.5" # outputs/xmpp, # License: As-Is
|
||||||
|
spec.add_dependency "mail" #outputs/email, # License: MIT License
|
||||||
|
|
||||||
spec.add_dependency("ffi-rzmq")
|
spec.add_dependency("ffi-rzmq")
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue