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 +4 -4
- data/README.md +186 -0
- data/fluent-plugin-anormalydetect.gemspec +4 -2
- data/lib/fluent/plugin/out_anomalydetect.rb +229 -98
- data/test/helper.rb +1 -0
- data/test/plugin/test_out_anomalydetect.rb +155 -33
- metadata +32 -4
- data/README.rdoc +0 -107
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a090405a428a2cbcb4cc192885582f624ecb7daa
|
4
|
+
data.tar.gz: 657abfb5723ee0fe3e339d272b4925c30bf3d5ab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b0c52886fda3615b96aa689503d7e1b877708345b24dd66c4673b8a47259e6df70da29dff177711af15137999f3b06f2b5bed748c00df8d56b27b5dcb701506
|
7
|
+
data.tar.gz: 662ed34868f9eec666af86a9a495bd83f5c4f177a5fafc551ea4d0eeb918b30e503966c6daa834272fd35739657702f8731daa11d596b83235eb42817517770f
|
data/README.md
ADDED
@@ -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.
|
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 =>
|
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
|
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
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
189
|
-
if
|
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 <
|
242
|
+
return nil if val < mu
|
193
243
|
when :down
|
194
|
-
return nil if val >
|
244
|
+
return nil if val > mu
|
195
245
|
end
|
196
|
-
|
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
|
203
|
-
|
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
|
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
|
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
|
-
|
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
|
data/test/helper.rb
CHANGED
@@ -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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
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.
|
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.
|
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:
|
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.
|
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.
|
105
|
+
rubygems_version: 2.0.3
|
78
106
|
signing_key:
|
79
107
|
specification_version: 4
|
80
108
|
summary: detect anomal sequential input casually
|
data/README.rdoc
DELETED
@@ -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
|