fluent-plugin-notifier 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ test.conf
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-notifier.gemspec
4
+ gemspec
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012- TAGOMORI Satoshi
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,63 @@
1
+ # fluent-plugin-notifier
2
+
3
+ ## Component
4
+
5
+ ### NotifierOutput
6
+
7
+ Plugin to emit notifications for messages, with numbers over/under threshold, or specified pattern strings.
8
+
9
+ ## Configuration
10
+
11
+ ### NotifierOutput
12
+
13
+ To notify apache logs with over 1000000 (microseconds) duration for CRITICAL , or status '500' by string pattern match:
14
+
15
+ <match apache.log.**>
16
+ type notifier
17
+ <def>
18
+ pattern apache_duration
19
+ check numeric_upward
20
+ warn_threshold 800000
21
+ crit_threshold 1000000
22
+ target_keys duration
23
+ </def>
24
+ <def>
25
+ pattern status_500
26
+ check string_find
27
+ warn_regexp 5\d\d
28
+ crit_regexp 500
29
+ target_key_pattern ^status.*$
30
+ </def>
31
+ </match>
32
+
33
+ With this configuration, you will get notification message like this:
34
+
35
+ 2012-05-15 19:44:29 +0900 notification: {"pattern":"apache_duration","target_tag":"apache.log.xxx","target_key":"duration","check_type":"numeric_upward","level":"crit","threshold":1000000,"value":"1057231","message_time":"2012-05-15 19:44:27 +0900"}
36
+ 2012-05-15 19:44:29 +0900 notification: {"pattern":"status_500","target_tag":"apache.log.xxx","target_key":"status","check_type":"string_find","level":"crit","regexp":"/500/","value":"500","message_time":"2012-05-15 19:44:27 +0900"}
37
+
38
+ Available 'check' types: 'numeric\_upward', 'numeric\_downward' and 'string\_find'
39
+
40
+ Default configurations:
41
+
42
+ * tag: 'notification'
43
+ * in <match> top level, 'default\_tag', 'default\_tag\_warn,' and 'default\_tag\_crit' available
44
+ * in each <def> section, 'tag', 'tag\_warn' and 'tag\_crit' available
45
+ * notification suppression
46
+ * at first, notified once in 1 minute, 5 times
47
+ * next, notified once in 5 minutes, 5 times
48
+ * last, notified once in 30 minutes
49
+ * in <match> top level, 'default\_interval\_1st', 'default\_interval\_2nd', 'default\_interval\_3rd', 'default\_repetitions\_1st' and 'default\_repetitions\_2nd' available
50
+ * in each <def> section, 'interval\_1st', 'interval\_2nd', 'interval\_3rd', 'repetitions\_1st' and 'repetitions\_2nd' available
51
+
52
+ If you want to get every 5 minutes notifications (after 1 minutes notifications), specify '0' for 'repetitions\_2nd'.
53
+
54
+ ## TODO
55
+
56
+ * long run
57
+
58
+ ## Copyright
59
+
60
+ * Copyright
61
+ * Copyright (c) 2012- TAGOMORI Satoshi (tagomoris)
62
+ * License
63
+ * Apache License, Version 2.0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.name = "fluent-plugin-notifier"
4
+ gem.version = "0.0.1"
5
+ gem.authors = ["TAGOMORI Satoshi"]
6
+ gem.email = ["tagomoris@gmail.com"]
7
+ gem.summary = %q{check matched messages and emit alert message}
8
+ gem.description = %q{check matched messages and emit alert message with throttling by conditions...}
9
+ gem.homepage = "https://github.com/tagomoris/fluent-plugin-notifier"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_development_dependency "fluentd"
17
+ gem.add_runtime_dependency "fluentd"
18
+ end
@@ -0,0 +1,350 @@
1
+ class Fluent::NotifierOutput < Fluent::Output
2
+ Fluent::Plugin.register_output('notifier', self)
3
+
4
+ NOTIFICATION_LEVELS = ['OK', 'WARN', 'CRIT', 'LOST'].freeze
5
+
6
+ STATES_CLEAN_INTERVAL = 3600 # 1hours
7
+ STATES_EXPIRE_SECONDS = 14400 # 4hours
8
+
9
+ config_param :default_tag, :string, :default => 'notification'
10
+ config_param :default_tag_warn, :string, :default => nil
11
+ config_param :default_tag_crit, :string, :default => nil
12
+
13
+ config_param :default_interval_1st, :time, :default => 60
14
+ config_param :default_repetitions_1st, :integer, :default => 5
15
+ config_param :default_interval_2nd, :time, :default => 300
16
+ config_param :default_repetitions_2nd, :integer, :default => 5
17
+ config_param :default_interval_3rd, :time, :default => 1800
18
+
19
+ attr_accessor :defs, :states, :match_cache, :negative_cache
20
+
21
+ ### output
22
+ # {
23
+ # 'pattern' => 'http_status_errors',
24
+ # 'target_tag' => 'httpstatus.blog',
25
+ # 'target_key' => 'blog_5xx_percentage',
26
+ # 'check_type' => 'numeric_upward'
27
+ # 'level' => 'warn',
28
+ # 'threshold' => 25, # or 'regexp' => ....,
29
+ # 'value' => 49, # or 'value' => 'matched some string...',
30
+ # 'message_time' => Time.instance
31
+ # }
32
+
33
+ # <match httpstatus.blog>
34
+ # type notifier
35
+ # default_tag notification
36
+ # default_interval_1st 1m
37
+ # default_repetitions_1st 5
38
+ # default_interval_2nd 5m
39
+ # default_repetitions_2nd 5
40
+ # default_interval_3rd 30m
41
+ # <def>
42
+ # pattern http_status_errors
43
+ # check numeric_upward
44
+ # warn_threshold 25
45
+ # crit_threshold 50
46
+ # tag alert
47
+ # # tag_warn alert.warn
48
+ # # tag_crit alert.crit
49
+ # # target_keys blog_5xx_percentage
50
+ # target_key_pattern ^.*_5xx_percentage$
51
+ # </def>
52
+ # <def>
53
+ # pattern log_checker
54
+ # check string_find
55
+ # crit_pattern 'ERROR'
56
+ # warn_pattern 'WARNING'
57
+ # tag alert
58
+ # # target_keys message
59
+ # target_key_pattern ^.*_message$
60
+ # </def>
61
+ # </match>
62
+
63
+ def configure(conf)
64
+ super
65
+
66
+ @match_cache = {} # cache which has map (fieldname => definition(s))
67
+ @negative_cache = {}
68
+ @defs = []
69
+ @states = {} # key: tag+field ?
70
+
71
+ defaults = {
72
+ :tag => @default_tag, :tag_warn => @default_tag_warn, :tag_crit => @default_tag_crit,
73
+ :interval_1st => @default_interval_1st, :repetitions_1st => @default_repetitions_1st,
74
+ :interval_2nd => @default_interval_2nd, :repetitions_2nd => @default_repetitions_2nd,
75
+ :interval_3rd => @default_interval_3rd,
76
+ }
77
+
78
+ conf.elements.each do |element|
79
+ if element.name != 'def'
80
+ raise Fluent::ConfigError, "invalid section name for out_notifier: #{d.name}"
81
+ end
82
+ defs.push(Definition.new(element, defaults))
83
+ end
84
+ end
85
+
86
+ def start
87
+ super
88
+ @mutex = Mutex.new
89
+ @last_status_cleaned = Fluent::Engine.now
90
+ end
91
+
92
+ # def shutdown
93
+ # end
94
+
95
+ def suppressed_emit(notifications)
96
+ notifications.each do |n|
97
+ hashkey = n.delete(:hashkey)
98
+ definition = n.delete(:match_def)
99
+ tag = n.delete(:emit_tag)
100
+
101
+ state = @states[hashkey]
102
+ if state
103
+ unless state.suppress?(definition, n)
104
+ Fluent::Engine.emit(tag, Fluent::Engine.now, n)
105
+ state.update_notified(definition, n)
106
+ end
107
+ else
108
+ Fluent::Engine.emit(tag, Fluent::Engine.now, n)
109
+ @states[hashkey] = State.new(n)
110
+ end
111
+ end
112
+ end
113
+
114
+ def states_cleanup
115
+ now = Fluent::Engine.now
116
+ @states.keys.each do |key|
117
+ if now - @states[key].last_notified > STATES_EXPIRE_SECONDS
118
+ @states.delete(key)
119
+ end
120
+ end
121
+ end
122
+
123
+ def emit(tag, es, chain)
124
+ notifications = []
125
+
126
+ es.each do |time,record|
127
+ record.keys.each do |key|
128
+
129
+ next if @negative_cache[key]
130
+
131
+ defs = @match_cache[key]
132
+ unless defs
133
+ defs = []
134
+ @defs.each do |d|
135
+ defs.push(d) if d.match?(key)
136
+ end
137
+ if defs.size < 1
138
+ @negative_cache[key] = true
139
+ end
140
+ end
141
+
142
+ defs.each do |d|
143
+ alert = d.check(tag, time, record, key)
144
+ if alert
145
+ notifications.push(alert)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ if notifications.size > 0
152
+ @mutex.synchronize do
153
+ suppressed_emit(notifications)
154
+ end
155
+ end
156
+
157
+ if Fluent::Engine.now - @last_status_cleaned > STATES_CLEAN_INTERVAL
158
+ @mutex.synchronize do
159
+ states_cleanup
160
+ @last_status_cleaned = Fluent::Engine.now
161
+ end
162
+ end
163
+
164
+ chain.next
165
+ end
166
+
167
+ class Definition
168
+ attr_accessor :tag, :tag_warn, :tag_crit
169
+ attr_accessor :intervals, :repetitions
170
+ attr_accessor :pattern, :check, :target_keys, :target_key_pattern
171
+ attr_accessor :crit_threshold, :warn_threshold # for 'numeric_upward', 'numeric_downward'
172
+ attr_accessor :crit_regexp, :warn_regexp # for 'string_find'
173
+
174
+ def initialize(element, defaults)
175
+ element.keys.each do |k|
176
+ case k
177
+ when 'pattern'
178
+ @pattern = element[k]
179
+ when 'check'
180
+ case element[k]
181
+ when 'numeric_upward'
182
+ @check = :upward
183
+ @crit_threshold = element['crit_threshold'].to_f
184
+ @warn_threshold = element['warn_threshold'].to_f
185
+ when 'numeric_downward'
186
+ @check = :downward
187
+ @crit_threshold = element['crit_threshold'].to_f
188
+ @warn_threshold = element['warn_threshold'].to_f
189
+ when 'string_find'
190
+ @check = :find
191
+ @crit_regexp = Regexp.compile(element['crit_regexp'].to_s)
192
+ @warn_regexp = Regexp.compile(element['warn_regexp'].to_s)
193
+ else
194
+ raise Fluent::ConfigError, "invalid check type: #{element[k]}"
195
+ end
196
+ when 'target_keys'
197
+ @target_keys = element['target_keys'].split(',')
198
+ when 'target_key_pattern'
199
+ @target_key_pattern = Regexp.compile(element['target_key_pattern'])
200
+ end
201
+ end
202
+ if @pattern.nil? or @pattern.length < 1
203
+ raise Fluent::ConfigError, "pattern must be set"
204
+ end
205
+ if @target_keys.nil? and @target_key_pattern.nil?
206
+ raise Fluent::ConfigError, "out_notifier needs one of target_keys or target_key_pattern"
207
+ end
208
+ @tag = element['tag'] || defaults[:tag]
209
+ @tag_warn = element['tag_warn'] || defaults[:tag_warn]
210
+ @tag_crit = element['tag_crit'] || defaults[:tag_crit]
211
+ @intervals = [
212
+ (element['interval_1st'] || defaults[:interval_1st]),
213
+ (element['interval_2nd'] || defaults[:interval_2nd]),
214
+ (element['interval_3rd'] || defaults[:interval_3rd])
215
+ ]
216
+ @repetitions = [
217
+ (element['repetitions_1st'] || defaults[:repetitions_1st]),
218
+ (element['repetitions_2nd'] || defaults[:repetitions_2nd])
219
+ ]
220
+ end
221
+
222
+ def match?(key)
223
+ (@target_keys and @target_keys.include?(key)) or (@target_key_pattern and @target_key_pattern.match(key))
224
+ end
225
+
226
+ # {
227
+ # 'pattern' => 'http_status_errors',
228
+ # 'target_tag' => 'httpstatus.blog',
229
+ # 'target_key' => 'blog_5xx_percentage',
230
+ # 'check_type' => 'numeric_upward'
231
+ # 'level' => 'warn', # 'regexp' => '[WARN] .* MUST BE CHECKED!$'
232
+ # 'threshold' => 25,
233
+ # 'value' => 49, # 'value' => '2012/05/15 18:01:59 [WARN] wooooops, MUST BE CHECKED!'
234
+ # 'message_time' => Time.instance
235
+ # }
236
+ def check(tag, time, record, key)
237
+ case @check
238
+ when :upward
239
+ if @crit_threshold and record[key].to_f >= @crit_threshold
240
+ {
241
+ :hashkey => @pattern + "\t" + tag + "\t" + key,
242
+ :match_def => self,
243
+ :emit_tag => (@tag_crit || @tag),
244
+ 'pattern' => @pattern, 'target_tag' => tag, 'target_key' => key, 'check_type' => 'numeric_upward', 'level' => 'crit',
245
+ 'threshold' => @crit_threshold, 'value' => record[key], 'message_time' => Time.at(time).to_s
246
+ }
247
+ elsif @warn_threshold and record[key].to_f >= @warn_threshold
248
+ {
249
+ :hashkey => @pattern + "\t" + tag + "\t" + key,
250
+ :match_def => self,
251
+ :emit_tag => (@tag_warn || @tag),
252
+ 'pattern' => @pattern, 'target_tag' => tag, 'target_key' => key, 'check_type' => 'numeric_upward', 'level' => 'warn',
253
+ 'threshold' => @warn_threshold, 'value' => record[key], 'message_time' => Time.at(time).to_s
254
+ }
255
+ else
256
+ nil
257
+ end
258
+ when :downward
259
+ if @crit_threshold and record[key].to_f <= @crit_threshold
260
+ {
261
+ :hashkey => @pattern + "\t" + tag + "\t" + key,
262
+ :match_def => self,
263
+ :emit_tag => (@tag_crit || @tag),
264
+ 'pattern' => @pattern, 'target_tag' => tag, 'target_key' => key, 'check_type' => 'numeric_downward', 'level' => 'crit',
265
+ 'threshold' => @crit_threshold, 'value' => record[key], 'message_time' => Time.at(time).to_s
266
+ }
267
+ elsif @warn_threshold and record[key].to_f <= @warn_threshold
268
+ {
269
+ :hashkey => @pattern + "\t" + tag + "\t" + key,
270
+ :match_def => self,
271
+ :emit_tag => (@tag_warn || @tag),
272
+ 'pattern' => @pattern, 'target_tag' => tag, 'target_key' => key, 'check_type' => 'numeric_downward', 'level' => 'warn',
273
+ 'threshold' => @warn_threshold, 'value' => record[key], 'message_time' => Time.at(time).to_s
274
+ }
275
+ else
276
+ nil
277
+ end
278
+ when :find
279
+ if @crit_regexp and @crit_regexp.match(record[key].to_s)
280
+ {
281
+ :hashkey => @pattern + "\t" + tag + "\t" + key,
282
+ :match_def => self,
283
+ :emit_tag => (@tag_crit || @tag),
284
+ 'pattern' => @pattern, 'target_tag' => tag, 'target_key' => key, 'check_type' => 'string_find', 'level' => 'crit',
285
+ 'regexp' => @crit_regexp.inspect, 'value' => record[key], 'message_time' => Time.at(time).to_s
286
+ }
287
+ elsif @warn_regexp and @warn_regexp.match(record[key].to_s)
288
+ {
289
+ :hashkey => @pattern + "\t" + tag + "\t" + key,
290
+ :match_def => self,
291
+ :emit_tag => (@tag_warn || @tag),
292
+ 'pattern' => @pattern, 'target_tag' => tag, 'target_key' => key, 'check_type' => 'string_find', 'level' => 'warn',
293
+ 'regexp' => @warn_regexp.inspect, 'value' => record[key], 'message_time' => Time.at(time).to_s
294
+ }
295
+ else
296
+ nil
297
+ end
298
+ else
299
+ raise ArgumentError, "unknown check type (maybe bug): #{@check}"
300
+ end
301
+ end
302
+ end
303
+
304
+ class State
305
+ # level: :warn, :crit
306
+ # stage: 0(1st)/1(2nd)/2(3rd)
307
+ attr_accessor :pattern, :target_tag, :target_key, :level, :stage, :counter, :first_notified, :last_notified
308
+
309
+ def initialize(notification)
310
+ @pattern = notification[:pattern]
311
+ @target_tag = notification[:target_tag]
312
+ @target_key = notification[:target_key]
313
+ @level = notification['level']
314
+ @stage = 0
315
+ @counter = 1
316
+ t = Fluent::Engine.now
317
+ @first_notified = t
318
+ @last_notified = t
319
+ end
320
+
321
+ def suppress?(definition, notification)
322
+ if @level == notification['level']
323
+ (Fluent::Engine.now - @last_notified) <= definition.intervals[@stage]
324
+ else
325
+ true
326
+ end
327
+ end
328
+
329
+ def update_notified(definition, notification)
330
+ t = Fluent::Engine.now
331
+
332
+ if @level == notification['level']
333
+ rep = definition.repetitions[@stage]
334
+ if rep and rep > 0
335
+ @counter += 1
336
+ if @counter > rep
337
+ @stage += 1
338
+ @counter = 0
339
+ end
340
+ end
341
+ else
342
+ @level = notification['level']
343
+ @stage = 0
344
+ @counter = 1
345
+ @first_notified = t
346
+ end
347
+ @last_notified = t
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,28 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'fluent/test'
15
+ unless ENV.has_key?('VERBOSE')
16
+ nulllogger = Object.new
17
+ nulllogger.instance_eval {|obj|
18
+ def method_missing(method, *args)
19
+ # pass
20
+ end
21
+ }
22
+ $log = nulllogger
23
+ end
24
+
25
+ require 'fluent/plugin/out_notifier'
26
+
27
+ class Test::Unit::TestCase
28
+ end
File without changes
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-notifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - TAGOMORI Satoshi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fluentd
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: fluentd
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: check matched messages and emit alert message with throttling by conditions...
47
+ email:
48
+ - tagomoris@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE.txt
56
+ - README.md
57
+ - Rakefile
58
+ - fluent-plugin-notifier.gemspec
59
+ - lib/fluent/plugin/out_notifier.rb
60
+ - test/helper.rb
61
+ - test/plugin/test_out_notifier.rb
62
+ homepage: https://github.com/tagomoris/fluent-plugin-notifier
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 1.8.21
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: check matched messages and emit alert message
86
+ test_files:
87
+ - test/helper.rb
88
+ - test/plugin/test_out_notifier.rb