fluent-plugin-notifier 0.0.1

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.
@@ -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