-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsyslog-alert.tcl
executable file
·318 lines (235 loc) · 9.24 KB
/
syslog-alert.tcl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"
## Copyright (C) 2020 [email protected]
# https://github.com/nabbi/syslog-alert
oo::class create SQLite {
constructor {} {
package require sqlite3
sqlite3 Db :memory:
next
}
destructor {
Db close
}
}
oo::class create Contacts {
constructor {} {
variable debug
variable trace
set debug 0
set trace 0
# create and populate table for looking up email and pager addresses for group contacts
my ImportContacts
if {$debug} { puts "## Database ## contacts imported" }
next
}
method ImportContacts {} {
# read contacts list and into the database table
##variable trace
#{name} {group} {email} {page}
set conf [open /etc/syslog-ng/alert-contacts.conf {r}]
set lines [split [read $conf] "\n"]
close $conf
Db eval {CREATE TABLE contacts(name text, "group" text, email text, page text)}
foreach l $lines {
if { [string index $l 0] == "#" || [string length $l] == 0 } {
continue
}
lassign $l name group email page
##if {$trace} { puts "#trace contacts_import# name: $name group: $group email: $email page: $page" }
Db eval {INSERT INTO contacts VALUES(:name,:group,:email,:page)}
}
}
method Group {groups a} {
#groups to lookup
#address, email or mobile-pager
foreach g $groups {
# TODO SELECT :a or $a resulted in a literal return of that var value
switch -glob -- $a {
"email" { append results { } [Db eval {SELECT "email" FROM contacts WHERE "group"=:g}] }
"page" { append results { } [Db eval {SELECT "page" FROM contacts WHERE "group"=:g}] }
default { return }
}
}
#format results for csv sendmail recipient
return [join [lsearch -all -inline -not -exact $results {}] ", "]
}
method page {g s b} {
#group contact to page
#subject
#body
set to [my Group $g "page"]
#silently fail as we do not want to exit. check configs for valid entry
if { [string length $to] > 0 } {
my Sendmail "$to" $s $b
}
}
method email {g s b} {
#groups to email
#subject
#body
set to [my Group $g "email"]
#silently fail as we do not want to exit. check configs for valid entry
if { [string length $to] > 0 } {
my Sendmail $to "Subject: $s" $b
}
}
method Sendmail {to subject body} {
variable debug
set msg "From: syslog@[info hostname]"
append msg \n "To: $to" \n
append msg $subject \n\n
append msg $body \n
if {$debug} { puts "## msg: $msg" }
#background to not wait as this blocks further message processing
exec sendmail -oi -t << $msg &
}
}
oo::class create Alert {
mixin SQLite Contacts
constructor {} {
variable debug
variable trace
# create table for tracking which alerts
Db eval {CREATE TABLE alert(time int, hash text primary key)}
if {$debug} { puts "## Database ## alert table created" }
my CreatePatterns
if {$debug} { puts "## Config imported" }
}
method Recent {delta hash} {
##variable trace
set now [clock seconds]
set time [Db eval {SELECT time FROM alert WHERE hash=:hash}]
## check if this is new hash to throttle
if { [string length $time] <= 0} {
##if ($trace) { puts "## alert - first time event" }
Db eval {INSERT INTO alert VALUES(:now,:hash)}
return 100
} elseif { [expr {$now-$time}] > $delta } {
##if ($trace) { "## alert - previous event aged out" }
Db eval {UPDATE alert SET time=:now WHERE hash=:hash}
return 200
} else {
##if ($trace) { puts "## suppressed - last alert occurred within $delta" }
return 0
}
}
method purge {} {
# the sql table can grow in memory if we do not purge old events
##variable trace
# ideally this should be greater than your largest throttle delta
# 3 days
set historic [expr {259200 - [clock seconds]}]
Db eval {DELETE FROM alert WHERE time<=:historic}
##if {$trace} { puts "#trace# purged records [clock seconds] $historic" }
}
method CreatePatterns {} {
#assemble the patterns method from user configuration file
variable trace
append method "oo::define Alert method patterns \{line\} \{\n\n"
# "{${ISODATE}} {${HOST}} {${FACILITY}} {${LEVEL}} {${MSGHDR}} {${MSG}}"
append method "lassign \$line log(isodate) log(host) log(facility) log(level) log(msghdr) log(msg)\n"
append method "set log(all) \"\$log(isodate) \$log(host) \$log(facility).\$log(level) \$log(msghdr)\$log(msg)\"\n"
# TODO This isn't perfect as some vendors don't encode messages consitently
# consider using syslog-ng PROGRAM var and adjust the input templates.
# consider replacing trailing ": " for when pid was not include in MSGHDR
set split "\\\["
append method "set log(program) \[lindex \[split \$log(msghdr) \"$split\"\] 0\]\n"
append method "\nswitch -glob -nocase -- \$log(all) \{\n[my ImportAlert] \}\n"
append method "\}\n"
eval $method
if {$trace} { puts "## method patterns\n[info class definition Alert patterns]" }
}
method ImportAlert {} {
# read configuration file to generate switch condition body.
##variable trace
set conf [open /etc/syslog-ng/alert.conf {r}]
set lines [split [read $conf] "\n"]
close $conf
foreach l $lines {
if { [string index $l 0] == "#" || [string length $l] == 0 } {
continue
}
#TODO validate inputs
if { [llength $l] != 8 } {
puts "#ignore bad config line: $l"
continue
}
#{{pattern1} {pattern2}} {{exclude1} {exclude2}} {hash} {delay} {email} {page} {ignore} {custom tcl code}
lassign $l pattern exclude hash delay email page ignore custom
##if {$trace} { puts "#trace events_import# p:$pattern e:$exclude h:$hash d:$delay e:$email p:$page i:$ignore c:$custom" }
# Generates the inner switch body from the provided configurations
#match patterns
set i 1
foreach p $pattern {
#check if this is the last pattern in the list
#shares same body
if { [llength $pattern] > $i } {
incr i
append sw "$p -\n"
continue
}
#global ignore pattens
if { $ignore == 1 } {
append sw "$p \{ return \}\n"
continue
} else {
append sw "$p \{ \n"
#sub exclude patterns
foreach e $exclude {
# split the key=value (ie host="test*")
lassign [split $e "="] ek ev
append sw "\tif \{ \[string match -nocase $ev \$log($ek)\] \} \{ return \}\n"
}
#check if we throttle or alert
append sw "\tif \{ \[my Recent $delay $hash\] \} \{\n"
# this section was added to tweak the subject lines form custom config scripts
# overrides the default of using the hash
append sw "\t\tset subject $hash\n"
if { [string length $custom] > 0 } {
append sw "$custom\n"
}
#email groups
if { [string length $email] > 0 } {
append sw "\t\tmy email \"$email\" \"\$subject\" \$log(all)\n"
}
#page groups
if { [string length $page] > 0 } {
append sw "\t\t\my page \"$page\" \"\$subject\" \$log(msg)\n"
}
# close this switch condition
append sw "\t\}\n\}\n"
continue
}
}
}
## end foreach line
if { ! [info exists sw] } {
puts "fatal: no switch conditions compiled."
exit 1
}
##if {$trace} { puts "## Imported alerts.conf\n$sw" }
return $sw
}
}
set syslog [Alert new]
###
# read from standard input
while { [gets stdin line] >= 0 } {
# skip line if we did not get expected list length
# userful while debugging, avoids null pointer issues as a result
if { [llength $line] != 6 } { continue }
#call our dynamically created method
$syslog patterns $line
# periodically clean out the database of old alerts to free memory
# TODO suspect there is a better approach
incr counter
if {$counter > 100} {
set counter 0
$syslog purge
}
}
###
$syslog destroy
exit 0