fluent-plugin-anomalydetect 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5dc986d4d340cf2115aaf591724f1f7215dbbbd8
4
- data.tar.gz: 9cb4fcb75b4500f544a1d81e1159e60ffdc956d3
3
+ metadata.gz: a090405a428a2cbcb4cc192885582f624ecb7daa
4
+ data.tar.gz: 657abfb5723ee0fe3e339d272b4925c30bf3d5ab
5
5
  SHA512:
6
- metadata.gz: d414c7d4fc72620c9b35ec135145b67d7042315e3e0280339c0a5e08526c0da9ec104432b60321ff761c00fae1db63e56fb75dd4f758979a604910b41ba74067
7
- data.tar.gz: f48841a679ffe543fbba58bdf27443653f50ed0e144f94055562edff663f9e30aa5782f1cc7ad62ace7b9cb922f13c5c509ce622ac669e44cb55d5ab9e919b61
6
+ metadata.gz: 6b0c52886fda3615b96aa689503d7e1b877708345b24dd66c4673b8a47259e6df70da29dff177711af15137999f3b06f2b5bed748c00df8d56b27b5dcb701506
7
+ data.tar.gz: 662ed34868f9eec666af86a9a495bd83f5c4f177a5fafc551ea4d0eeb918b30e503966c6daa834272fd35739657702f8731daa11d596b83235eb42817517770f
@@ -0,0 +1,186 @@
1
+ # Fluent::Plugin::Anomalydetect
2
+
3
+ To detect anomaly for log stream, use this plugin.
4
+ Then you can find changes in logs casually.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ gem 'fluent-plugin-anomalydetect'
11
+
12
+ And then execute:
13
+
14
+ $ bundle
15
+
16
+ Or install it yourself as:
17
+
18
+ $ gem install fluent-plugin-anomalydetect
19
+
20
+ ## Usage
21
+
22
+ <source>
23
+ type file
24
+ ...
25
+ tag access.log
26
+ </source>
27
+
28
+ <match access.**>
29
+ type anomalydetect
30
+ tag anomaly.access
31
+ tick 86400
32
+ </match>
33
+
34
+ <match anomaly.access>
35
+ type file
36
+ ...
37
+ </match>
38
+
39
+ Then the plugin output anomaly log counts in each day.
40
+
41
+ This plugin watches a value of input record number in the interval set with `tick`.
42
+
43
+ If you want to watch a value for a target field <fieldname> in data, write below:
44
+
45
+ <match access.**>
46
+ type anomalydetect
47
+ tag anomaly.access
48
+ tick 86400
49
+ target fieldname
50
+ </match>
51
+
52
+ ## more configuration
53
+
54
+ <match access.**>
55
+ type anomalydetect
56
+ tag anomaly.access
57
+ tick 86400
58
+ target fieldname
59
+ outlier_term 7
60
+ outlier_discount 0.5
61
+ smooth_term 7
62
+ score_term 28
63
+ score_discount 0.01
64
+ </match>
65
+
66
+ If you want to know detail of these parameters, see "Theory".
67
+
68
+ <match access.**>
69
+ type anomalydetect
70
+ ...
71
+ store_file /path/to/anomalydetect.dat
72
+ </match>
73
+
74
+ If "store_file" option was specified, a historical stat will be stored to the file at shutdown, and it will be restored on started.
75
+
76
+
77
+ <match access.**>
78
+ type anomalydetect
79
+ ...
80
+ threshold 3
81
+ </match>
82
+
83
+ If "threshold" option was specified, plugin only ouput when the anomalyscore is more than threshold.
84
+
85
+ <match access.**>
86
+ type anomalydetect
87
+ ...
88
+ trend up
89
+ </match>
90
+
91
+ If "trend" option was specified, plugin only ouput when the input data tends to up (or down).
92
+
93
+ ## Parameters
94
+
95
+ - outlier\_term
96
+
97
+ - outlier\_discount
98
+
99
+ - smooth\_term
100
+
101
+ - score\_term
102
+
103
+ - score\_discount
104
+
105
+ - tick
106
+
107
+ The time interval to watch in seconds.
108
+
109
+ - tag
110
+
111
+ The output tag name. Required for aggregate `all`. Default is `anomaly`.
112
+
113
+ - add_tag_prefix
114
+
115
+ Add tag prefix for output message. Required for aggregate `tag`.
116
+
117
+ - remove_tag_prefix
118
+
119
+ Remove tag prefix for output message.
120
+
121
+ - aggragate
122
+
123
+ Process data for each `tag` or `all`. The default is `all`.
124
+
125
+ - target
126
+
127
+ Watch a value of a target field in data. If not specified, the number of records is watched (default). The output would become like:
128
+
129
+ {"outlier":1.783,"score":4.092,"target":10}
130
+
131
+ - threshold
132
+
133
+ Emit message only if the score is greater than the threshold. Default is `-1.0`.
134
+
135
+ - trend
136
+
137
+ Emit message only if the input data trend is `up` (or `down`). Default is nil.
138
+
139
+ - store\_file
140
+
141
+ Store the learning results into a file, and reload it on restarting.
142
+
143
+ - targets
144
+
145
+ Watch target fields in data. Specify by comma separated value like `x,y`. The output messsages would be like:
146
+
147
+ {"x_outlier":1.783,"x_score":4.092,"x":10,"y_outlier":2.310,"y_score":3.982,"y":3}
148
+
149
+ - thresholds
150
+
151
+ Threahold values for each target. Specify by comma separated value like `1.0,2.0`. Use with `targets` option.
152
+
153
+ - outlier\_suffix
154
+
155
+ Change the suffix of emitted messages of `targets` option. Default is `_outlier`.
156
+
157
+ - score\_suffix
158
+
159
+ Change the suffix of emitted messages of `targets` option. Default is `_score`.
160
+
161
+ - target\_suffix
162
+
163
+ Change the suffix of emitted messages of `targets` option. Default is `` (empty).
164
+
165
+ - suppress\_tick
166
+
167
+ Suppress to emit output messsages during specified seconds after starting up.
168
+
169
+
170
+ ## Theory
171
+ "データマイニングによる異常検知" http://amzn.to/XHXNun
172
+
173
+ # ToDo
174
+
175
+ ## FFT algorithms
176
+
177
+ # Copyright
178
+
179
+ * Copyright
180
+
181
+ * Copyright (c) 2013- Muddy Dixon
182
+ * Copyright (c) 2013- Naotoshi Seo
183
+
184
+ * License
185
+
186
+ * Apache License, Version 2.0
@@ -3,7 +3,7 @@ lib = File.expand_path('../lib', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
5
  gem.name = "fluent-plugin-anomalydetect"
6
- gem.version = "0.1.2"
6
+ gem.version = "0.1.3"
7
7
  gem.authors = ["Muddy Dixon"]
8
8
  gem.email = ["muddydixon@gmail.com"]
9
9
  gem.description = %q{detect anomal sequential input casually}
@@ -15,7 +15,9 @@ Gem::Specification.new do |gem|
15
15
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
16
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
17
  gem.require_paths = ["lib"]
18
-
18
+
19
19
  gem.add_development_dependency "rake"
20
+ gem.add_development_dependency "pry"
21
+ gem.add_development_dependency "pry-nav"
20
22
  gem.add_runtime_dependency "fluentd"
21
23
  end
@@ -1,7 +1,7 @@
1
1
  module Fluent
2
2
  class AnomalyDetectOutput < Output
3
3
  Fluent::Plugin.register_output('anomalydetect', self)
4
-
4
+
5
5
  require_relative 'change_finder'
6
6
  require 'pathname'
7
7
 
@@ -11,10 +11,19 @@ module Fluent
11
11
  config_param :score_term, :integer, :default => 14
12
12
  config_param :score_discount, :float, :default => 0.1
13
13
  config_param :tick, :integer, :default => 60 * 5
14
+ config_param :suppress_tick, :integer, :default => 0
14
15
  config_param :tag, :string, :default => "anomaly"
16
+ config_param :add_tag_prefix, :string, :default => nil
17
+ config_param :remove_tag_prefix, :string, :default => nil
18
+ config_param :aggregate, :string, :default => 'all'
15
19
  config_param :target, :string, :default => nil
20
+ config_param :targets, :string, :default => nil
21
+ config_param :outlier_suffix, :string, :default => '_outlier'
22
+ config_param :score_suffix, :string, :default => '_score'
23
+ config_param :target_suffix, :string, :default => ''
16
24
  config_param :store_file, :string, :default => nil
17
- config_param :threshold, :float, :default => -1.0
25
+ config_param :threshold, :float, :default => nil
26
+ config_param :thresholds, :string, :default => nil
18
27
  config_param :trend, :default => nil do |val|
19
28
  case val.downcase
20
29
  when 'up'
@@ -22,24 +31,16 @@ module Fluent
22
31
  when 'down'
23
32
  :down
24
33
  else
25
- raise ConfigError, "out_anomaly treand should be 'up' or 'down'"
34
+ raise ConfigError, "out_anomaly trend should be 'up' or 'down'"
26
35
  end
27
36
  end
28
37
 
29
- attr_accessor :outlier
30
- attr_accessor :score
31
- attr_accessor :record_count
32
-
33
- attr_accessor :outlier_buf
34
-
35
- attr_accessor :records
36
-
37
38
  def configure (conf)
38
39
  super
39
- unless 0 < @outlier_discount and @outlier_discount < 1
40
- raise Fluent::ConfigError, "discount ratio should be between (0, 1)"
40
+ unless 0 < @outlier_discount and @outlier_discount < 1
41
+ raise Fluent::ConfigError, "discount ratio should be between (0, 1)"
41
42
  end
42
- unless 0 < @score_discount and @score_discount < 1
43
+ unless 0 < @score_discount and @score_discount < 1
43
44
  raise Fluent::ConfigError, "discount ratio should be between (0, 1)"
44
45
  end
45
46
  if @outlier_term < 1
@@ -54,25 +55,113 @@ module Fluent
54
55
  if @tick < 1
55
56
  raise Fluent::ConfigError, "tick timer should be greater than 1 sec"
56
57
  end
58
+ if @suppress_tick < 0
59
+ raise Fluent::ConfigError, "`suppress_tick` must be greater or equal to 0 sec"
60
+ end
57
61
  if @store_file
58
62
  f = Pathname.new(@store_file)
59
63
  if (f.exist? && !f.writable_real?) || (!f.exist? && !f.parent.writable_real?)
60
64
  raise Fluent::ConfigError, "#{@store_file} is not writable"
61
65
  end
62
66
  end
63
- @outlier_buf = []
64
- @outlier = ChangeFinder.new(@outlier_term, @outlier_discount)
65
- @score = ChangeFinder.new(@score_term, @score_discount)
67
+
68
+ case @aggregate
69
+ when 'all'
70
+ raise Fluent::ConfigError, "anomalydetect: `tag` must be specified with aggregate all" if @tag.nil?
71
+ when 'tag'
72
+ raise Fluent::ConfigError, "anomalydetect: `add_tag_prefix` must be specified with aggregate tag" if @add_tag_prefix.nil?
73
+ else
74
+ raise Fluent::ConfigError, "anomalydetect: aggregate allows tag/all"
75
+ end
76
+
77
+ @tag_prefix = "#{@add_tag_prefix}." if @add_tag_prefix
78
+ @tag_prefix_match = "#{@remove_tag_prefix}." if @remove_tag_prefix
79
+ @tag_proc =
80
+ if @tag_prefix and @tag_prefix_match
81
+ Proc.new {|tag| "#{@tag_prefix}#{lstrip(tag, @tag_prefix_match)}" }
82
+ elsif @tag_prefix_match
83
+ Proc.new {|tag| lstrip(tag, @tag_prefix_match) }
84
+ elsif @tag_prefix
85
+ Proc.new {|tag| "#{@tag_prefix}#{tag}" }
86
+ elsif @tag
87
+ Proc.new {|tag| @tag }
88
+ else
89
+ Proc.new {|tag| tag }
90
+ end
91
+
92
+ if @target and @targets
93
+ raise Fluent::ConfigError, "anomalydetect: Either of `target` or `targets` can be specified"
94
+ end
95
+ if @targets
96
+ @targets = @targets.split(',')
97
+ end
98
+ @output_each_proc =
99
+ if @targets
100
+ Proc.new {|outlier, score, val, target| {"#{target}#{@outlier_suffix}" => outlier, "#{target}#{@score_suffix}" => score, "#{target}#{@target_suffix}" => val } }
101
+ else
102
+ Proc.new {|outlier, score, val, target| {"outlier" => outlier, "score" => score, "target" => val} }
103
+ end
104
+
105
+ if @threshold and @thresholds
106
+ raise Fluent::ConfigError, "anomalydetect: Either of `threshold` or `thresholds` can be specified"
107
+ end
108
+ if thresholds = @thresholds
109
+ if @targets.nil?
110
+ raise Fluent::ConfigError, "anomalydetect: `thresholds` must be specified together with `targets`"
111
+ end
112
+ @thresholds = {}
113
+ thresholds.split(',').map.with_index {|threshold, idx| @thresholds[@targets[idx]]= threshold.to_f }
114
+ if @thresholds.size != @targets.size
115
+ raise Fluent::ConfigError, "anomalydetect: The size of `thresholds` must be same with the size of `targets`"
116
+ end
117
+ else
118
+ @threshold = -1.0 if @threshold.nil? # for lower compatibility
119
+ end
120
+ @threshold_proc =
121
+ if @thresholds
122
+ Proc.new {|target| @thresholds[target] }
123
+ else
124
+ Proc.new {|target| @threshold }
125
+ end
126
+
127
+ @records = {}
128
+ @outliers = {}
129
+ @outlier_bufs = {}
130
+ @scores = {}
66
131
 
67
132
  @mutex = Mutex.new
133
+ end
134
+
135
+ # for test
136
+ attr_reader :thresholds
137
+ attr_reader :threshold_proc
68
138
 
69
- @record_count = @target.nil?
139
+ def outlier_bufs(tag, target = nil)
140
+ @outlier_bufs[tag] ||= {}
141
+ @outlier_bufs[tag][target] ||= []
142
+ end
143
+
144
+ def outliers(tag, target = nil)
145
+ @outliers[tag] ||= {}
146
+ @outliers[tag][target] ||= ChangeFinder.new(@outlier_term, @outlier_discount)
147
+ end
148
+
149
+ def scores(tag, target = nil)
150
+ @scores[tag] ||= {}
151
+ @scores[tag][target] ||= ChangeFinder.new(@score_term, @score_discount)
152
+ end
153
+
154
+ def init_records(tags)
155
+ records = {}
156
+ tags.each do |tag|
157
+ records[tag] = []
158
+ end
159
+ records
70
160
  end
71
161
 
72
162
  def start
73
163
  super
74
164
  load_from_file
75
- init_records
76
165
  start_watch
77
166
  rescue => e
78
167
  $log.warn "anomalydetect: #{e.class} #{e.message} #{e.backtrace.first}"
@@ -89,62 +178,20 @@ module Fluent
89
178
  $log.warn "anomalydetect: #{e.class} #{e.message} #{e.backtrace.first}"
90
179
  end
91
180
 
92
- def load_from_file
93
- return unless @store_file
94
- f = Pathname.new(@store_file)
95
- return unless f.exist?
96
-
97
- begin
98
- f.open('rb') do |f|
99
- stored = Marshal.load(f)
100
- if (( stored[:outlier_term] == @outlier_term ) &&
101
- ( stored[:outlier_discount] == @outlier_discount ) &&
102
- ( stored[:score_term] == @score_term ) &&
103
- ( stored[:score_discount] == @score_discount ) &&
104
- ( stored[:smooth_term] == @smooth_term ))
105
- then
106
- @outlier = stored[:outlier]
107
- @outlier_buf = stored[:outlier_buf]
108
- @score = stored[:score]
109
- else
110
- $log.warn "anomalydetect: configuration param was changed. ignore stored data"
111
- end
112
- end
113
- rescue => e
114
- $log.warn "anomalydetect: Can't load store_file #{e}"
115
- end
116
- end
117
-
118
- def store_to_file
119
- return unless @store_file
120
- begin
121
- Pathname.new(@store_file).open('wb') do |f|
122
- Marshal.dump({
123
- :outlier => @outlier,
124
- :outlier_buf => @outlier_buf,
125
- :score => @score,
126
- :outlier_term => @outlier_term,
127
- :outlier_discount => @outlier_discount,
128
- :score_term => @score_term,
129
- :score_discount => @score_discount,
130
- :smooth_term => @smooth_term,
131
- }, f)
132
- end
133
- rescue => e
134
- $log.warn "anomalydetect: Can't write store_file #{e}"
135
- end
136
- end
137
-
138
181
  def start_watch
139
182
  @watcher = Thread.new(&method(:watch))
140
183
  end
141
184
 
142
185
  def watch
143
- @last_checked = Fluent::Engine.now
186
+ @started = @last_checked = Fluent::Engine.now
187
+ @suppress = true
144
188
  loop do
145
189
  begin
146
190
  sleep 0.5
147
191
  now = Fluent::Engine.now
192
+ if @suppress and (now - @started >= @suppress_tick)
193
+ @suppress = false
194
+ end
148
195
  if now - @last_checked >= @tick
149
196
  flush_emit(now - @last_checked)
150
197
  @last_checked = now
@@ -155,67 +202,151 @@ module Fluent
155
202
  end
156
203
  end
157
204
 
158
- def init_records
159
- @records = []
160
- end
161
-
162
205
  def flush_emit(step)
163
- output = flush
164
- if output
165
- Fluent::Engine.emit(@tag, Fluent::Engine.now, output)
206
+ outputs = flush
207
+ outputs.each do |tag, output|
208
+ emit_tag = @tag_proc.call(tag)
209
+ Fluent::Engine.emit(emit_tag, Fluent::Engine.now, output) if output and !output.empty?
166
210
  end
167
211
  end
168
212
 
169
213
  def flush
170
- flushed, @records = @records, init_records
171
-
172
- val = if @record_count
173
- flushed.size
174
- else
175
- filtered = flushed.map {|record| record[@target] }.compact
176
- return nil if filtered.empty?
177
- filtered.inject(:+).to_f / filtered.size
214
+ flushed_records, @records = @records, init_records(tags = @records.keys)
215
+ outputs = {}
216
+ flushed_records.each do |tag, records|
217
+ output =
218
+ if @targets
219
+ @targets.each_with_object({}) do |target, output|
220
+ output_each = flush_each(records, tag, target)
221
+ output.merge!(output_each) if output_each
178
222
  end
223
+ elsif @target
224
+ flush_each(records, tag, @target)
225
+ else
226
+ flush_each(records, tag)
227
+ end
228
+ outputs[tag] = output if output
229
+ end
230
+ outputs
231
+ end
179
232
 
180
- outlier = @outlier.next(val)
181
-
182
- @outlier_buf.push outlier
183
- @outlier_buf.shift if @outlier_buf.size > @smooth_term
184
- outlier_avg = @outlier_buf.empty? ? 0.0 : @outlier_buf.inject(:+).to_f / @outlier_buf.size
185
-
186
- score = @score.next(outlier_avg)
233
+ def flush_each(records, tag, target = nil)
234
+ val = get_value(records, target)
235
+ outlier, score, mu = get_score(val, tag, target) if val
236
+ threshold = @threshold_proc.call(target)
187
237
 
188
- $log.debug "out_anomalydetect:#{Thread.current.object_id} flushed:#{flushed} val:#{val} outlier:#{outlier} outlier_buf:#{@outlier_buf} score:#{score}"
189
- if @threshold < 0 or (@threshold >= 0 and score > @threshold)
238
+ return nil if @suppress
239
+ if score and threshold < 0 or (threshold >= 0 and score > threshold)
190
240
  case @trend
191
241
  when :up
192
- return nil if val < @outlier.mu
242
+ return nil if val < mu
193
243
  when :down
194
- return nil if val > @outlier.mu
244
+ return nil if val > mu
195
245
  end
196
- {"outlier" => outlier, "score" => score, "target" => val}
246
+ @output_each_proc.call(outlier, score, val, target)
197
247
  else
198
248
  nil
199
249
  end
200
250
  end
201
251
 
202
- def tick_time(time)
203
- (time - time % @tick).to_s
252
+ def get_value(records, target = nil)
253
+ if target
254
+ compacted_records = records.map {|record| record[target] }.compact
255
+ return nil if compacted_records.empty?
256
+ compacted_records.inject(:+).to_f / compacted_records.size # average
257
+ else
258
+ records.size.to_f # num of records
259
+ end
204
260
  end
205
261
 
206
- def push_records(records)
262
+ def get_score(val, tag, target = nil)
263
+ outlier = outliers(tag, target).next(val)
264
+ mu = outliers(tag, target).mu
265
+
266
+ outlier_buf = outlier_bufs(tag, target)
267
+ outlier_buf.push outlier
268
+ outlier_buf.shift if outlier_buf.size > @smooth_term
269
+ outlier_avg = outlier_buf.empty? ? 0.0 : outlier_buf.inject(:+).to_f / outlier_buf.size
270
+
271
+ score = scores(tag, target).next(outlier_avg)
272
+
273
+ $log.debug "out_anomalydetect:#{Thread.current.object_id} tag:#{tag} val:#{val} outlier:#{outlier} outlier_buf:#{outlier_buf} score:#{score} mu:#{mu}"
274
+
275
+ [outlier, score, mu]
276
+ end
277
+
278
+ def push_records(tag, records)
207
279
  @mutex.synchronize do
208
- @records.concat(records)
280
+ @records[tag] ||= []
281
+ @records[tag].concat(records)
209
282
  end
210
283
  end
211
284
 
212
285
  def emit(tag, es, chain)
213
286
  records = es.map { |time, record| record }
214
- push_records records
287
+ if @aggregate == 'all'
288
+ push_records(:all, records)
289
+ else
290
+ push_records(tag, records)
291
+ end
215
292
 
216
293
  chain.next
217
294
  rescue => e
218
295
  $log.warn "anomalydetect: #{e.class} #{e.message} #{e.backtrace.first}"
219
296
  end
297
+
298
+ def load_from_file
299
+ return unless @store_file
300
+ f = Pathname.new(@store_file)
301
+ return unless f.exist?
302
+
303
+ begin
304
+ f.open('rb') do |f|
305
+ stored = Marshal.load(f)
306
+ if (( stored[:outlier_term] == @outlier_term ) &&
307
+ ( stored[:outlier_discount] == @outlier_discount ) &&
308
+ ( stored[:score_term] == @score_term ) &&
309
+ ( stored[:score_discount] == @score_discount ) &&
310
+ ( stored[:smooth_term] == @smooth_term ) &&
311
+ ( stored[:aggregate] == @aggregate ))
312
+ then
313
+ @outliers = stored[:outliers]
314
+ @outlier_bufs = stored[:outlier_bufs]
315
+ @scores = stored[:scores]
316
+ else
317
+ $log.warn "anomalydetect: configuration param was changed. ignore stored data"
318
+ end
319
+ end
320
+ rescue => e
321
+ $log.warn "anomalydetect: Can't load store_file #{e}"
322
+ end
323
+ end
324
+
325
+ def store_to_file
326
+ return unless @store_file
327
+ begin
328
+ Pathname.new(@store_file).open('wb') do |f|
329
+ Marshal.dump({
330
+ :outliers => @outliers,
331
+ :outlier_bufs => @outlier_bufs,
332
+ :scores => @scores,
333
+ :outlier_term => @outlier_term,
334
+ :outlier_discount => @outlier_discount,
335
+ :score_term => @score_term,
336
+ :score_discount => @score_discount,
337
+ :smooth_term => @smooth_term,
338
+ :aggregate => @aggregate,
339
+ }, f)
340
+ end
341
+ rescue => e
342
+ $log.warn "anomalydetect: Can't write store_file #{e}"
343
+ end
344
+ end
345
+
346
+ private
347
+
348
+ def lstrip(string, substring)
349
+ string.index(substring) == 0 ? string[substring.size..-1] : string
350
+ end
220
351
  end
221
352
  end
@@ -8,6 +8,7 @@ rescue Bundler::BundlerError => e
8
8
  exit e.status_code
9
9
  end
10
10
  require 'test/unit'
11
+ require 'pry'
11
12
 
12
13
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
14
  $LOAD_PATH.unshift(File.dirname(__FILE__))
@@ -15,7 +15,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
15
15
  smooth_term 3
16
16
  target y
17
17
  ]
18
-
18
+
19
19
  def create_driver (conf=CONFIG, tag="debug.anomaly")
20
20
  Fluent::Test::OutputTestDriver.new(Fluent::AnomalyDetectOutput, tag).configure(conf)
21
21
  end
@@ -30,7 +30,6 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
30
30
  assert_equal 300, d.instance.tick
31
31
  assert_nil d.instance.target
32
32
  assert_equal 'anomaly', d.instance.tag
33
- assert d.instance.record_count
34
33
 
35
34
  d = create_driver
36
35
  assert_equal 28, d.instance.outlier_term
@@ -41,7 +40,6 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
41
40
  assert_equal 10, d.instance.tick
42
41
  assert_equal "y", d.instance.target
43
42
  assert_equal 'test.anomaly', d.instance.tag
44
- assert !d.instance.record_count
45
43
 
46
44
  assert_raise(Fluent::ConfigError) {
47
45
  d = create_driver %[
@@ -83,18 +81,29 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
83
81
  tick 0
84
82
  ]
85
83
  }
86
- end
87
-
88
- def test_array_init
89
- d = create_driver
90
- assert_equal [], d.instance.outlier_buf
91
- assert_nil d.instance.records # @records is initialized at start, not configure
92
- end
93
-
94
- def test_sdar
95
- d = create_driver
96
- assert_instance_of Fluent::ChangeFinder, d.instance.outlier
97
- assert_instance_of Fluent::ChangeFinder, d.instance.score
84
+ assert_raise(Fluent::ConfigError) {
85
+ d = create_driver %[
86
+ target y
87
+ targets x,y,z
88
+ ]
89
+ }
90
+ assert_raise(Fluent::ConfigError) {
91
+ d = create_driver %[
92
+ threshold 1.0
93
+ thresholds 1.0,2.0
94
+ ]
95
+ }
96
+ assert_raise(Fluent::ConfigError) {
97
+ d = create_driver %[
98
+ thresholds 1,2
99
+ ]
100
+ }
101
+ assert_raise(Fluent::ConfigError) {
102
+ d = create_driver %[
103
+ targets x,y,z
104
+ thresholds 1
105
+ ]
106
+ }
98
107
  end
99
108
 
100
109
  def test_emit_record_count
@@ -109,12 +118,12 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
109
118
  ]
110
119
 
111
120
  data = 10.times.map { (rand * 100).to_i } + [0]
112
- d.run do
121
+ d.run do
113
122
  data.each do |val|
114
123
  (0..val - 1).each do ||
115
124
  d.emit({'y' => 1})
116
125
  end
117
- r = d.instance.flush
126
+ r = d.instance.flush[:all]
118
127
  assert_equal val, r['target']
119
128
  end
120
129
  end
@@ -136,7 +145,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
136
145
  d.run do
137
146
  data.each do |val|
138
147
  d.emit({'y' => val})
139
- r = d.instance.flush
148
+ r = d.instance.flush[:all]
140
149
  assert_equal val, r['target']
141
150
  end
142
151
  end
@@ -157,7 +166,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
157
166
  d.run do
158
167
  10.times do
159
168
  d.emit({'foobar' => 999.99})
160
- r = d.instance.flush
169
+ r = d.instance.flush[:all]
161
170
  assert_equal nil, r
162
171
  end
163
172
  end
@@ -168,11 +177,11 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
168
177
  reader = CSV.open("test/stock.2432.csv", "r")
169
178
  header = reader.take(1)[0]
170
179
  d = create_driver
171
- d.run do
180
+ d.run do
172
181
  reader.each_with_index do |row, idx|
173
182
  break if idx > 5
174
183
  d.emit({'y' => row[4].to_i})
175
- r = d.instance.flush
184
+ r = d.instance.flush[:all]
176
185
  assert r['target']
177
186
  assert r['outlier']
178
187
  assert r['score']
@@ -191,15 +200,15 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
191
200
  ]
192
201
 
193
202
  d.run do
194
- assert_equal [], d.instance.outlier_buf
203
+ assert_equal([], d.instance.outlier_bufs(:all))
195
204
  d.emit({'x' => 1})
196
205
  d.emit({'x' => 1})
197
206
  d.emit({'x' => 1})
198
- d.instance.flush
207
+ d.instance.flush[:all]
199
208
  d.emit({'x' => 1})
200
209
  d.emit({'x' => 1})
201
210
  d.emit({'x' => 1})
202
- d.instance.flush
211
+ d.instance.flush[:all]
203
212
  end
204
213
  assert File.exist? file
205
214
 
@@ -207,7 +216,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
207
216
  store_file #{file}
208
217
  ]
209
218
  d2.run do
210
- assert_equal 2, d2.instance.outlier_buf.size
219
+ assert_equal 2, d2.instance.outlier_bufs(:all).size
211
220
  end
212
221
 
213
222
  File.unlink file
@@ -220,11 +229,11 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
220
229
  d = create_driver %[
221
230
  threshold 1000
222
231
  ]
223
- d.run do
232
+ d.run do
224
233
  reader.each_with_index do |row, idx|
225
234
  break if idx > 5
226
235
  d.emit({'y' => row[4].to_i})
227
- r = d.instance.flush
236
+ r = d.instance.flush[:all]
228
237
  assert_equal nil, r
229
238
  end
230
239
  end
@@ -237,11 +246,11 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
237
246
  d = create_driver %[
238
247
  threshold 1
239
248
  ]
240
- d.run do
249
+ d.run do
241
250
  reader.each_with_index do |row, idx|
242
251
  break if idx > 5
243
252
  d.emit({'y' => row[4].to_i})
244
- r = d.instance.flush
253
+ r = d.instance.flush[:all]
245
254
  assert_not_equal nil, r
246
255
  end
247
256
  end
@@ -258,7 +267,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
258
267
  d.emit({'y' => 0.0}); d.instance.flush
259
268
  d.emit({'y' => 0.0}); d.instance.flush
260
269
  d.emit({'y' => 0.0}); d.instance.flush
261
- d.emit({'y' => -1.0}); r = d.instance.flush
270
+ d.emit({'y' => -1.0}); r = d.instance.flush[:all]
262
271
  assert_equal nil, r
263
272
  end
264
273
 
@@ -267,7 +276,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
267
276
  d.emit({'y' => -1.0}); d.instance.flush
268
277
  d.emit({'y' => -1.0}); d.instance.flush
269
278
  d.emit({'y' => -1.0}); d.instance.flush
270
- d.emit({'y' => 0.0}); r = d.instance.flush
279
+ d.emit({'y' => 0.0}); r = d.instance.flush[:all]
271
280
  assert_not_equal nil, r
272
281
  end
273
282
  end
@@ -282,7 +291,7 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
282
291
  d.emit({'y' => 0.0}); d.instance.flush
283
292
  d.emit({'y' => 0.0}); d.instance.flush
284
293
  d.emit({'y' => 0.0}); d.instance.flush
285
- d.emit({'y' => -1.0}); r = d.instance.flush
294
+ d.emit({'y' => -1.0}); r = d.instance.flush[:all]
286
295
  assert_not_equal nil, r
287
296
  end
288
297
 
@@ -292,8 +301,121 @@ class AnomalyDetectOutputTest < Test::Unit::TestCase
292
301
  d.emit({'y' => -1.0}); d.instance.flush
293
302
  d.emit({'y' => -1.0}); d.instance.flush
294
303
  d.emit({'y' => 0.0})
295
- r = d.instance.flush
304
+ r = d.instance.flush[:all]
296
305
  assert_equal nil, r
297
306
  end
298
307
  end
308
+
309
+ def test_aggregate_tag
310
+ d = create_driver %[
311
+ outlier_term 28
312
+ outlier_discount 0.05
313
+ score_term 28
314
+ score_discount 0.05
315
+ tick 10
316
+ smooth_term 3
317
+ aggregate tag
318
+ add_tag_prefix test
319
+ ]
320
+
321
+ data = 10.times.map { (rand * 100).to_i } + [0]
322
+ d.run do
323
+ data.each do |val|
324
+ (0..val - 1).each do ||
325
+ d.emit({'y' => 1})
326
+ end
327
+ r = d.instance.flush['debug.anomaly']
328
+ assert_equal val, r['target']
329
+ end
330
+ end
331
+ end
332
+
333
+ def test_targets
334
+ d = create_driver %[
335
+ targets x,y
336
+ ]
337
+ data = 10.times.map { (rand * 100).to_i } + [0]
338
+ d.run do
339
+ data.each do |val|
340
+ d.emit({'x' => val, 'y' => val})
341
+ r = d.instance.flush[:all]
342
+ assert_equal val, r['x']
343
+ assert_equal val, r['y']
344
+ end
345
+ end
346
+ end
347
+
348
+ def test_targets_default_suffix
349
+ d = create_driver %[
350
+ targets x,y
351
+ ]
352
+ data = 1.times.map { (rand * 100).to_i } + [0]
353
+ d.run do
354
+ data.each do |val|
355
+ d.emit({'x' => val, 'y' => val})
356
+ r = d.instance.flush[:all]
357
+ assert r.has_key?('x')
358
+ assert r.has_key?('y')
359
+ assert r.has_key?('x_outlier')
360
+ assert r.has_key?('x_score')
361
+ assert r.has_key?('y_outlier')
362
+ assert r.has_key?('y_score')
363
+ end
364
+ end
365
+ end
366
+
367
+ def test_targets_suffix
368
+ d = create_driver %[
369
+ targets x,y
370
+ outlier_suffix
371
+ score_suffix _anomaly
372
+ target_suffix _target
373
+ ]
374
+ data = 1.times.map { (rand * 100).to_i } + [0]
375
+ d.run do
376
+ data.each do |val|
377
+ d.emit({'x' => val, 'y' => val})
378
+ r = d.instance.flush[:all]
379
+ assert r.has_key?('x_target')
380
+ assert r.has_key?('y_target')
381
+ assert r.has_key?('x')
382
+ assert r.has_key?('x_anomaly')
383
+ assert r.has_key?('y')
384
+ assert r.has_key?('y_anomaly')
385
+ end
386
+ end
387
+ end
388
+
389
+ def test_targets_thresholds
390
+ d = create_driver %[
391
+ targets x,y
392
+ thresholds 1,2
393
+ ]
394
+ d.run do
395
+ thresholds = d.instance.thresholds
396
+ assert_equal 1, thresholds['x']
397
+ assert_equal 2, thresholds['y']
398
+
399
+ threshold_proc = d.instance.threshold_proc
400
+ assert_equal 1, threshold_proc.call('x')
401
+ assert_equal 2, threshold_proc.call('y')
402
+ end
403
+ end
404
+
405
+ def test_suppress_tick
406
+ d = create_driver %[
407
+ tick 10
408
+ suppress_tick 30
409
+ target y
410
+ ]
411
+
412
+ data = 10.times.map { (rand * 100).to_i } + [0]
413
+ d.run do
414
+ data.each do |val|
415
+ d.emit({'y' => val})
416
+ r = d.instance.flush[:all]
417
+ assert_equal nil, r
418
+ end
419
+ end
420
+ end
299
421
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-anomalydetect
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muddy Dixon
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-08-20 00:00:00.000000000 Z
11
+ date: 2014-01-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry-nav
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: fluentd
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -47,7 +75,7 @@ extra_rdoc_files: []
47
75
  files:
48
76
  - .gitignore
49
77
  - Gemfile
50
- - README.rdoc
78
+ - README.md
51
79
  - Rakefile
52
80
  - fluent-plugin-anormalydetect.gemspec
53
81
  - lib/fluent/plugin/change_finder.rb
@@ -74,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
102
  version: '0'
75
103
  requirements: []
76
104
  rubyforge_project:
77
- rubygems_version: 2.0.2
105
+ rubygems_version: 2.0.3
78
106
  signing_key:
79
107
  specification_version: 4
80
108
  summary: detect anomal sequential input casually
@@ -1,107 +0,0 @@
1
- = Fluent::Plugin::Anomalydetect
2
-
3
- To detect anomaly for log stream, use this plugin.
4
- Then you can find changes in logs casually.
5
-
6
- = Installation
7
-
8
- Add this line to your application's Gemfile:
9
-
10
- gem 'fluent-plugin-anomalydetect'
11
-
12
- And then execute:
13
-
14
- $ bundle
15
-
16
- Or install it yourself as:
17
-
18
- $ gem install fluent-plugin-anomalydetect
19
-
20
- == Usage
21
-
22
- <source>
23
- type file
24
- ...
25
- tag access.log
26
- </source>
27
-
28
- <match access.**>
29
- type anomalydetect
30
- tag anomaly.access
31
- tick 86400
32
- </match>
33
-
34
- <match anomaly.access>
35
- type file
36
- ...
37
- </match>
38
-
39
- Then the plugin output anomaly log counts in each day.
40
-
41
- This plugin watches a value of input record number in the interval set with `tick`.
42
-
43
- If you want to watch a value for a target field <fieldname> in data, write below:
44
-
45
- <match access.**>
46
- type anomalydetect
47
- tag anomaly.access
48
- tick 86400
49
- target fieldname
50
- </match>
51
-
52
- == more configuration
53
-
54
- <match access.**>
55
- type anomalydetect
56
- tag anomaly.access
57
- tick 86400
58
- target fieldname
59
- outlier_term 7
60
- outlier_discount 0.5
61
- smooth_term 7
62
- score_term 28
63
- score_discount 0.01
64
- </match>
65
-
66
- If you want to know detail of these parameters, see "Theory".
67
-
68
- <match access.**>
69
- type anomalydetect
70
- ...
71
- store_file /path/to/anomalydetect.dat
72
- </match>
73
-
74
- If "store_file" option was specified, a historical stat will be stored to the file at shutdown, and it will be restored on started.
75
-
76
-
77
- <match access.**>
78
- type anomalydetect
79
- ...
80
- threshold 3
81
- </match>
82
-
83
- If "threshold" option was specified, plugin only ouput when the anomalyscore is more than threshold.
84
-
85
- <match access.**>
86
- type anomalydetect
87
- ...
88
- trend up
89
- </match>
90
-
91
- If "trend" option was specified, plugin only ouput when the input data tends to up (or down).
92
-
93
- == Theory
94
- "データマイニングによる異常検知" http://amzn.to/XHXNun
95
-
96
- = TODO
97
-
98
- == threshold
99
-
100
- fluentd outputs value when the outlier value over threshold
101
-
102
- == FFT algorithms
103
-
104
- = Copyright
105
-
106
- Copyright:: Copyright (c) 2013- Muddy Dixon
107
- License:: Apache License, Version 2.0