fluent-plugin-anomalydetect 0.1.2 → 0.1.3

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