rec 1.0.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/lib/rec/alert.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'rubygems'
2
+ require 'net/smtp'
3
+ require 'xmpp4r'
4
+
5
+ module REC
6
+ module Alert
7
+
8
+ # Provides the capability to send alerts
9
+ # --mail--
10
+ # The simplest approach is to use the native +mail+ program (no credentials required)
11
+ # Alert.mail(alert)
12
+ #
13
+ # --email and jabber--
14
+ # You can also send emails and instant messages via servers, but you'll need to provide
15
+ # credentials to do that.
16
+ # - Alert.smtp_credentials(user, password, domain, server, port)
17
+ # - Alert.jabber_credentials(user, password, server)
18
+ #
19
+ # Then you can send messages:
20
+ # - Alert.email(alert)
21
+ # - Alert.jabber(alert)
22
+ # or send messages to another recipient, with another subject
23
+ # - Alert.email(alert, you@example.com, "Serious problem")
24
+ # - Alert.jabber(alert, boss@example.com)
25
+ #
26
+ # --Sleeping--
27
+ # If you want to avoid being sent instant messages during sleeping hours, you can
28
+ # specify a range of working hours during which urgent alerts may be sent by jabber
29
+ # and outside those working hours the alert will be sent by email instead
30
+ # Alert.workHours(9,18) # IMs only between 9am and 6pm
31
+ # Alert.urgent(alert) # sent as instant message if during work hours, else by email
32
+ # Alert.jabber(alert) # sent as instant message regardless of the time
33
+ # Alert.normal(alert) # sent by email
34
+
35
+
36
+ def self.default_subject(subject)
37
+ @@defaultSubject = subject
38
+ end
39
+ @@defaultSubject = "Alert"
40
+
41
+ #load("/home/rec/alert.conf") can contain something like this:
42
+ # Alert.email_credentials("rec@gmail.com", "tricky", "mydomain.com")
43
+ # Alert.jabber_credentials("rec@gmail.com", "tricky")
44
+ #so the rules script need not contain passwords.
45
+ #/home/rec/alert.conf should be readable only by the otherwise unprivileged user (sec)
46
+ # running the script
47
+
48
+ # provides the credentials needed for sending email
49
+ def self.smtp_credentials(user, password, domain, server="smtp.gmail.com", port=587)
50
+ @@emailUser = user
51
+ @@emailPassword = password
52
+ @@emailDomain = domain
53
+ @@emailServer = server
54
+ @@emailPort = port
55
+ end
56
+
57
+ # provides the credentials needed for sending instant messages
58
+ def self.jabber_credentials(user, password, server="talk.google.com")
59
+ @@jabberUser = user
60
+ @@jabberPassword = password
61
+ @@jabberServer = server
62
+ end
63
+
64
+ def self.emailTo=(address)
65
+ @@emailTo = address
66
+ end
67
+
68
+ def self.jabberTo=(address)
69
+ @@jabberTo = address
70
+ end
71
+
72
+ # Send the alert via local mailer
73
+ def self.mail(alert, recipient=@@emailTo, subject=@@defaultSubject)
74
+ `echo "#{alert}" | mail -s #{subject.gsub(/\s/,'\ ')} #{recipient} 1<&2`
75
+ end
76
+
77
+ # Send the alert via an email server
78
+ def self.email(alert, recipient=@@emailTo, subject=@@defaultSubject)
79
+ smtp = Net::SMTP.new(@@emailServer, @@emailPort)
80
+ smtp.enable_starttls()
81
+ smtp.start(@@emailDomain, @@emailUsername, @@emailPassword, :plain)
82
+ smtp.send_message(alert, @@emailUsername, recipient)
83
+ end
84
+
85
+ # Send the alert via google talk (or any other XMPP service)
86
+ def self.jabber(alert, recipient=@@jabberTo, subject=@@defaultSubject)
87
+ client = Jabber::Client::new(Jabber::JID.new(@@JabberUser))
88
+ client.connect(@@jabberServer)
89
+ client.auth(@@jabberPassword)
90
+ message = Jabber::Message::new(recipient, alert).set_type(:normal).set_id('1').set_subject(subject)
91
+ client.send(message)
92
+ end
93
+
94
+ # define the working hours during which instant messages are allowed
95
+ # Note that Alert.work_hours(7,21) means "7am-9pm" as you would think, so from 07:00 to 20:59
96
+ def self.work_hours(start, finish)
97
+ @@workHours = start..finish
98
+ end
99
+ @@workHours = 7...21
100
+
101
+ # Send an instant message during work hours, else send an email
102
+ def self.urgent(alert)
103
+ if @@workhours.include?(Time.now.hour)
104
+ self.jabber(alert)
105
+ else
106
+ self.email(alert)
107
+ end
108
+ end
109
+
110
+ def self.normal(alert)
111
+ self.email(alert)
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,121 @@
1
+ require 'rec/rule'
2
+
3
+ module REC
4
+ class Correlator
5
+
6
+ @@eventsIn = 0
7
+ @@eventsMissed = 0
8
+
9
+ def self.start(opts={})
10
+ $debug = opts[:debug] || false
11
+ self.new().start()
12
+ end
13
+
14
+ def initialize()
15
+ @time = @startupTime = Time.now()
16
+ @year = @startupTime.year
17
+ @running = false
18
+ end
19
+
20
+ def start()
21
+ Signal.trap("INT") { finish() }
22
+ Signal.trap("TERM") { finish() }
23
+ Signal.trap("USR1") {
24
+ stats()
25
+ run()
26
+ }
27
+ $stderr.puts("srec is starting...")
28
+ begin
29
+ $miss = IO.open(3, "a") # for missed events
30
+ rescue
31
+ $miss = nil
32
+ end
33
+ @running = true
34
+ run()
35
+ end
36
+
37
+ def run()
38
+ while @running and !$stdin.eof? do
39
+ logLine = gets()
40
+ next if logLine.nil?
41
+ logLine.strip!()
42
+ next if logLine.empty?
43
+ @@eventsIn += 1
44
+ @time, message = parse(logLine)
45
+ $stderr.puts("< "+message) if $debug
46
+ State.expire_states(@time) # remove expired states before we check the rules
47
+ eventMatched = false
48
+ Rule.each { |rule|
49
+ title = rule.check(message)
50
+ eventMatched = true unless title.nil? # empty match is still a match
51
+ next if title.nil?
52
+ break if title.empty? # match without a message means 'swallow this event'
53
+ state = State[title] || rule.create_state(title, @time)
54
+ rule.react(state, @time, logLine)
55
+ $stderr.puts("breaking after rule #{rule.rid}") unless (!$debug or rule.continue())
56
+ break unless rule.continue()
57
+ }
58
+ if !eventMatched
59
+ @@eventsMissed += 1
60
+ $miss.puts("* "+logLine) unless $miss.nil?
61
+ end
62
+ end
63
+ finish() if $stdin.eof?
64
+ end
65
+
66
+ def finish()
67
+ @running = false
68
+ $miss.close() unless $miss.nil? or $miss.closed?
69
+ # NOTE: some states may have something useful to say, or maybe we could store them
70
+ stats()
71
+ $stderr.puts("srec is finished.")
72
+ exit 0
73
+ end
74
+
75
+ def parse(logLine)
76
+ if logLine =~ /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+)\s(\d\d)\:(\d\d)\:(\d\d)/
77
+ # Apr 22 16:40:18 aqua Firewall[205]: Skype is listening from 0.0.0.0:51304 proto=6
78
+ time = Time.local(@year, $1, $2, $3, $4, $5, 0)
79
+ message = $'
80
+ elsif logLine =~ /^\[\w+\]\s[Sun|Mon|Tue|Wed|Thu|Fri|Sat]\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d+)\s(\d\d)\:(\d\d)\:(\d\d)\s(\d{4})/
81
+ # [err] Fri Dec 30 23:58:56 2011 - scan error: 451 SCAN Engine error 2 ...
82
+ time = Time.local($6, $1, $2, $3, $4, $5, 0)
83
+ message = $'
84
+ elseif logLine =~ /^(\d{4})\-(\d\d)\-(\d\d)\s(\d\d)\:(\d\d)\:(\d\d)\.(\d+)\s(\w+)/
85
+ # 2012-04-22 08:43:22.099 EST - Module: PlistFile ...
86
+ if $7 == "UTC" or $7 == "GMT"
87
+ time = Time.utc($1, $2, $3, $4, $5, $6, $7)
88
+ else
89
+ time = Time.local($1, $2, $3, $4, $5, $6, $7)
90
+ end
91
+ message = $'
92
+ else
93
+ time = @time # time stands still
94
+ message = logLine
95
+ end
96
+ [time, message]
97
+ end
98
+
99
+ def stats()
100
+ # report statistics to stderr
101
+ checked, matched, created, reacted, rules = Rule.stats()
102
+ statesCount, eventsOut = State.stats()
103
+ $stderr.puts("-"*40)
104
+ $stderr.puts("srec has been running for %.1fs" % [Time.now - @startupTime])
105
+ $stderr.puts("events: in %-8d out %-8d missed %-8d" % [@@eventsIn, eventsOut, @@eventsMissed])
106
+ $stderr.puts("states: active %-8d created %-8d " % [statesCount, created.values.reduce(:+)])
107
+ $stderr.puts("rules: checked %-8d matched %-8d reacted %-8d" % [checked.values.reduce(:+),matched.values.reduce(:+), reacted.values.reduce(:+)])
108
+ $stderr.puts("Rule ID checked matched freq % reacted ")
109
+ #checked.keys.sort { |a,b| matched[b] <=> matched[a] }.each {|rid| # descending by matches
110
+ rules.collect { |rule| rule.rid }.each { |rid|
111
+ if checked[rid] > 0
112
+ freq = "%5.2f" % [matched[rid].to_f() / checked[rid].to_f() * 100]
113
+ else
114
+ freq = "never"
115
+ end
116
+ $stderr.puts("%-8d %-8d %-8d %-8s %-8d" % [rid, checked[rid], matched[rid], freq, reacted[rid]])
117
+ }
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,25 @@
1
+ module REC
2
+ # mock the Alert class for testing purposes
3
+ class Alert
4
+
5
+ @@emailsSent = []
6
+ @@jabbersSent = []
7
+
8
+ def self.email(alert, recipient=@@emailTo, subject=@@defaultSubject)
9
+ @@emailsSent << [alert, recipient, subject]
10
+ end
11
+
12
+ def self.jabber(alert, recipient=@@jabberTo, subject=@@defaultSubject)
13
+ @@jabbersSent << [alert, recipient, subject]
14
+ end
15
+
16
+ def emailsSent()
17
+ @@emailsSent
18
+ end
19
+
20
+ def jabbersSent()
21
+ @@jabberSent
22
+ end
23
+
24
+ end
25
+ end
data/lib/rec/rule.rb ADDED
@@ -0,0 +1,85 @@
1
+ require 'rec/state'
2
+
3
+ module REC
4
+ class Rule
5
+
6
+ @@rules = []
7
+ @@index = {} # index of rules to allow lookup of messages etc.
8
+ @@checked = {} # number of times each rule was checked
9
+ @@matched = {} # number of times each rule was matched
10
+ @@created = {} # number of states created by each rule
11
+ @@reacted = {} # number of times #react was called on each rule
12
+
13
+ def self.each(&block)
14
+ @@rules.each(&block)
15
+ end
16
+
17
+ def self.<<(rule)
18
+ @@rules << rule
19
+ @@index[rule.rid] = rule
20
+ @@checked[rule.rid] = 0
21
+ @@matched[rule.rid] = 0
22
+ @@created[rule.rid] = 0
23
+ @@reacted[rule.rid] = 0
24
+ end
25
+
26
+ def self.[](rid)
27
+ @@index[rid]
28
+ end
29
+
30
+ def self.stats()
31
+ [@@checked, @@matched, @@created, @@reacted, @@rules]
32
+ end
33
+
34
+ attr_reader(:rid, :pattern, :message, :lifespan, :alert, :params, :action)
35
+
36
+ def initialize(rid, params={}, &action)
37
+ @rid = rid
38
+ @pattern = params[:pattern] || raise("No pattern specified for rule #{@ruleId}")
39
+ @message = params[:message] || "" # no message means no state created - ie. ignore event
40
+ @lifespan = params[:lifespan] || 0
41
+ @alert = params[:alert] || @message
42
+ @allstates = params[:allstates] || []
43
+ @anystates = params[:anystates] || []
44
+ @notstates = params[:notstates] || []
45
+ @details = params[:details] || []
46
+ @params = params
47
+ @action = action
48
+ @matches = nil
49
+ Rule << self
50
+ end
51
+
52
+ def check(logMessage)
53
+ @@checked[@rid] += 1
54
+ matchData = @pattern.match(logMessage) || return
55
+ @matches = Hash[@details.zip(matchData.to_a()[1..-1])] # map matched values to detail keys
56
+ # if any notstates are specified, make sure none are present
57
+ @notstates.each { |str| return if State[str.sprinth(@matches)] }
58
+ # if any allstates are specified, they all need to be present
59
+ @allstates.each { |str| return unless State[str.sprinth(@matches)] }
60
+ # if anystates are specified, we must find one that matches
61
+ return unless @anystates.empty? or @anystates.detect {|str| State[str.sprinth(@matches)] }
62
+ title = @message.sprinth(@matches)
63
+ @@matched[@rid] += 1
64
+ return(title)
65
+ end
66
+
67
+ def create_state(title, time)
68
+ @@created[@rid] += 1
69
+ $stderr.puts("+ Creating new state #{title}") if $debug
70
+ State.new(title, time, @lifespan, @params)
71
+ end
72
+
73
+ def react(state, time, logLine)
74
+ @@reacted[@rid] += 1
75
+ state.update(time, @rid, @matches, @alert, logLine)
76
+ $stderr.puts("~ Rule #{@rid}, state = #{state.inspect()}") if $debug
77
+ @action.call(state) if @action
78
+ end
79
+
80
+ def continue()
81
+ @params[:continue]
82
+ end
83
+
84
+ end
85
+ end
data/lib/rec/state.rb ADDED
@@ -0,0 +1,102 @@
1
+ require 'time'
2
+
3
+ module REC
4
+ class State
5
+
6
+ @@timeouts = []
7
+ @@states = {}
8
+ @@eventsOut = 0
9
+
10
+ Struct.new("Timeout", :expiry, :key)
11
+
12
+ def self.[](key)
13
+ @@states[key]
14
+ end
15
+
16
+ def self.timeout_at(time, title)
17
+ tnew = Struct::Timeout.new(time, title)
18
+ n = @@timeouts.find_index { |to|
19
+ to.expiry > time
20
+ }
21
+ if n.nil?
22
+ @@timeouts = @@timeouts << tnew
23
+ else
24
+ @@timeouts[n..n] = [tnew, @@timeouts[n]]
25
+ end
26
+ end
27
+
28
+ def self.expire_states(time)
29
+ timeout = @@timeouts.first()
30
+ while @@timeouts.length > 0 and timeout.expiry < time do
31
+ state = State[timeout.key]
32
+ if state.nil?
33
+ @@timeouts.shift
34
+ timeout = @@timeouts.first()
35
+ next
36
+ end
37
+ #$stderr.puts("Releasing state #{state.title} with count of #{state.count}")
38
+ @@states.delete(@@timeouts.shift().key)
39
+ timeout = @@timeouts.first()
40
+ end
41
+ end
42
+
43
+ def self.stats()
44
+ statesCount = @@states.keys.length
45
+ [statesCount, @@eventsOut]
46
+ end
47
+
48
+ attr_reader(:rid, :title, :lifespan, :alert, :params, :count, :created, :updated, :dur, :details)
49
+
50
+ def initialize(title, time, lifespan, params={})
51
+ @title = title
52
+ @lifespan = lifespan.to_f
53
+ @params = params
54
+ @count = 0
55
+ @created = time
56
+ @updated = time
57
+ @dur = 0
58
+ @rid = 0
59
+ @alert = ""
60
+ @logs = [] # array of remembered logLines
61
+ @details = {} # hash of remembered details
62
+ @@states[title] = self
63
+ State.timeout_at(time + @lifespan, @title)
64
+ end
65
+
66
+ def update(time, rid, matches, alert, logLine=nil)
67
+ @count = @count.succ
68
+ @updated = time
69
+ @dur = @updated - @created
70
+ @rid = rid
71
+ @details.merge!(matches)
72
+ @alert = alert
73
+ @logs << logLine if @params[:capture]
74
+ end
75
+
76
+ def release()
77
+ @@states.delete(@title)
78
+ end
79
+
80
+ def stats()
81
+ @details.merge({"count"=>@count, "dur"=>@dur, "created"=>@created, "updated"=>@updated})
82
+ end
83
+
84
+ def generate_alert()
85
+ message = @alert.sprinth(stats())
86
+ event = "%s %s" % [@created.iso8601, message] + @logs.join("\n")
87
+ print("> ") if $debug
88
+ puts(event)
89
+ @@eventsOut = @@eventsOut + 1
90
+ event
91
+ end
92
+
93
+ def alert_first_only()
94
+ generate_alert() if @count == 1
95
+ end
96
+
97
+ def method_missing(symbol, *args)
98
+ @params[symbol]
99
+ end
100
+
101
+ end
102
+ end
data/lib/rec.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Ruby Event Correlation
2
+ require 'string'
3
+ require 'rec/correlator'
4
+ require 'rec/alert'
data/lib/string.rb ADDED
@@ -0,0 +1,11 @@
1
+ class String
2
+ # interpolate hash values into a formatted string
3
+ # The string should have a form like "Stats uid %uid$-5d belongs to %userid$s"
4
+ # '%uid$-5d' is replaced by '%-5d' and '%userid$s' becomes '%s'
5
+ # and the relevant values are pulled from the hash into an array
6
+ # and interpolated using normal sprintf features
7
+ def sprinth(hash={})
8
+ raise ArgumentError.new("sprinth argument must be a Hash") unless hash.is_a?(Hash)
9
+ self.gsub(/\%\w+\$/,"%") % self.scan(/\%\w+\$/).collect { |token| hash[token[1..-2]] }
10
+ end
11
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rec
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 0
9
+ version: 1.0.0
10
+ platform: ruby
11
+ authors:
12
+ - Richard Kernahan
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-09-06 00:00:00 +10:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: "\tSifts through your log files in real time, using stateful intelligence to determine\n\
22
+ \twhat is really important. REC can alert you (by email or IM) or it can simply condense\n\
23
+ \ta large log file into a much shorter and more meaningful log.\n\
24
+ \tREC is inspired by Risto Vaarandi's brilliant *sec* (simple-evcorr.sourceforge.net)\n\
25
+ \tbut is original code and any defects are entirely mine.\n\
26
+ \tWhile event correlation is inherently complex, REC attempts to make common tasks easy\n\
27
+ \twhile preserving plenty of power and flexibility for ambitious tasks.\n"
28
+ email: rec@finalstep.com.au
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - lib/rec.rb
37
+ - lib/rec/rule.rb
38
+ - lib/rec/state.rb
39
+ - lib/rec/correlator.rb
40
+ - lib/rec/alert.rb
41
+ - lib/rec/mock-alert.rb
42
+ - lib/string.rb
43
+ has_rdoc: true
44
+ homepage: http://rubygems.org/gems/rec
45
+ licenses: []
46
+
47
+ post_install_message:
48
+ rdoc_options: []
49
+
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project: rec
69
+ rubygems_version: 1.3.6
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Ruby event correlation
73
+ test_files: []
74
+