fluent-plugin-stats 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5bc9f72a5a727bc1397f45b015193d8762b10533
4
+ data.tar.gz: a2ba051b18070f6cf5518416c50330495fa56829
5
+ SHA512:
6
+ metadata.gz: 212b0f23da7de8e80edfa0a01de810730d6f2e23adc83b694a63214197f04b9ae58a0b1091754e0745cf396567a4e60b3f9ef1e0ebfd464be013816bd80d69dc
7
+ data.tar.gz: 577089bb6d526f10d4911891e0fb8284073cc97d6a91a9439dfaedf66af3e4ea41a34c772508df120f5e03ccf4e8aa8ca837b195ef89ea391ec07e3fbc53b899
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ repo_token: i4DJCtdksuIwhBck1tukIjzKoMCxWIIvQ
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /*.gem
2
+ ~*
3
+ #*
4
+ *~
5
+ .bundle
6
+ Gemfile.lock
7
+ .rbenv-version
8
+ vendor
9
+ doc/*
10
+ tmp/*
11
+ coverage
12
+ .yardoc
13
+ pkg/*
data/.rdebugrc ADDED
@@ -0,0 +1,4 @@
1
+ set autolist
2
+ set autoeval
3
+ set autoreload
4
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format documentation
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ - 2.0.0
5
+ gemfile:
6
+ - Gemfile
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ ## 0.3.0 (2013/12/27)
2
+
3
+ Changes
4
+
5
+ - Rename fluent-plugin-stats from fluent-plugin-calc
6
+
7
+ ## 0.2.0 (2013/12/12)
8
+
9
+ Enhancement:
10
+
11
+ - Add `remove_tag_prefix` option
12
+
13
+ ## 0.1.2 (2013/10/10)
14
+
15
+ Enhancement:
16
+
17
+ - Add `zero_emit` option to emit 0 on the next interval like datacounter plugin
18
+
19
+ ## 0.1.1 (2013/09/02)
20
+
21
+ Fixes
22
+
23
+ - Fix the case when not record is found.
24
+
25
+ ## 0.1.0 (2013/09/02)
26
+
27
+ Changes
28
+
29
+ - Accept string data by `to_f`.
30
+
31
+ ## 0.0.5 (2013/09/02)
32
+
33
+ Enhancement:
34
+
35
+ - add `sum_keys`, `max_keys`, `min_keys`, `avg_keys`.
36
+
37
+ ## 0.0.4 (2013/08/30)
38
+
39
+ Enhancement:
40
+
41
+ - add `sum_suffix`, `max_suffix`, `min_suffix`, `avg_suffix`.
42
+
43
+ ## 0.0.2 (2013/05/06)
44
+
45
+ Bugfixes:
46
+
47
+ - Fix so that @avg can be an option
48
+
49
+ ## 0.0.1 (2013/05/05)
50
+
51
+ First version
52
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Naotoshi SEO
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # fluent-plugin-stats [![Build Status](https://secure.travis-ci.org/sonots/fluent-plugin-stats.png?branch=master)](http://travis-ci.org/sonots/fluent-plugin-stats)
2
+
3
+ Fluentd plugin to calculate statistics such as sum, max, min, avg.
4
+
5
+ ## Configuration
6
+
7
+ ### Example 1
8
+
9
+ Get sum for xxx\_count, max for xxx\_max, min for xxx\_min, avg for xxx\_avg
10
+
11
+ <match foo.**>
12
+ type stats
13
+ interval 5s
14
+ add_tag_prefix stats
15
+
16
+ sum .*_count$
17
+ max .*_max$
18
+ min .*_min$
19
+ avg .*_avg$
20
+ </match>
21
+
22
+ Assuming following inputs are coming:
23
+
24
+ foo.bar: {"4xx_count":1,"5xx_count":2","reqtime_max":12083,"reqtime_min":10,"reqtime_avg":240.46}
25
+ foo.bar: {"4xx_count":4,"5xx_count":2","reqtime_max":24831,"reqtime_min":82,"reqtime_avg":300.46}
26
+
27
+ then output bocomes as belows:
28
+
29
+ stats.foo.bar: {"4xx_count":5,"5xx_count":4","reqtime_max":24831,"reqtime_min":10,"reqtime_avg":270.46}
30
+
31
+ ### Example 2
32
+
33
+ Get sum, max, min, avg for the same key
34
+
35
+ <match foo.**>
36
+ type stats
37
+ interval 5s
38
+ add_tag_prefix stats
39
+
40
+ sum ^reqtime$
41
+ max ^reqtime$
42
+ min ^reqtime$
43
+ avg ^reqtime$
44
+ sum_suffix _sum
45
+ max_suffix _max
46
+ min_suffix _min
47
+ avg_suffix _avg
48
+ </match>
49
+
50
+ Assuming following inputs are coming:
51
+
52
+ foo.bar: {"reqtime":1.000}
53
+ foo.bar: {"reqtime":2.000}
54
+
55
+ then output bocomes as belows:
56
+
57
+ stats.foo.bar: {"reqtime_sum":3.000,"reqtime_max":2.000,"reqtime_min":1.000,"reqtime_avg":1.500}
58
+
59
+ ## Parameters
60
+
61
+ - sum, min, max, avg
62
+
63
+ Target of calculation. Specify input keys by a regular expression
64
+
65
+ - sum\_keys, min\_keys, max\_keys, avg\_keys
66
+
67
+ Target of calculation. Specify input keys by a string separated by , (comma) such as
68
+
69
+ sum_keys 4xx_count,5xx_count
70
+
71
+ - sum\_suffix, min\_suffix, max\_suffix, avg\_suffix
72
+
73
+ Add a suffix to keys of the output record
74
+
75
+ - interval
76
+
77
+ The interval to calculate in seconds. Default is 5s.
78
+
79
+ - tag
80
+
81
+ The output tag name. Required for aggregate `all`.
82
+
83
+ - add_tag_prefix
84
+
85
+ Add tag prefix for output message. Default: 'stats'
86
+
87
+ - remove_tag_prefix
88
+
89
+ Remove tag prefix for output message.
90
+
91
+ - aggragate
92
+
93
+ Statsulate by each `tag` or `all`. The default value is `tag`.
94
+
95
+ - store_file
96
+
97
+ Store internal data into a file of the given path on shutdown, and load on starting.
98
+
99
+ - zero_emit
100
+
101
+ Emit 0 on the next interval. This is useful for some software which requires to reset data such as [GrowthForecast](http://kazeburo.github.io/GrowthForecast/) .
102
+
103
+ stats.foo.bar: {"4xx_count":5,"5xx_count":4","reqtime_max":24831,"reqtime_min":10,"reqtime_avg":270.46}
104
+ # after @interval later
105
+ stats.foo.bar: {"4xx_count":0,"5xx_count":0","reqtime_max":0,"reqtime_min":0,"reqtime_avg":0}
106
+
107
+ ## ChangeLog
108
+
109
+ See [CHANGELOG.md](CHANGELOG.md) for details.
110
+
111
+ ## ToDo
112
+
113
+ Get the number of denominator to calculate `avg` from input json field.
114
+
115
+ ## Contributing
116
+
117
+ 1. Fork it
118
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
119
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
120
+ 4. Push to the branch (`git push origin my-new-feature`)
121
+ 5. Create new [Pull Request](../../pull/new/master)
122
+
123
+ ## Copyright
124
+
125
+ Copyright (c) 2013 Naotoshi Seo. See [LICENSE](LICENSE) for details.
126
+
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec) do |spec|
7
+ spec.pattern = FileList['spec/**/*_spec.rb']
8
+ end
9
+ task :default => :spec
10
+
11
+ desc 'Open an irb session preloaded with the gem library'
12
+ task :console do
13
+ sh 'irb -rubygems -I lib'
14
+ end
15
+ task :c => :console
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "fluent-plugin-stats"
6
+ s.version = "0.3.0"
7
+ s.authors = ["Naotoshi Seo"]
8
+ s.email = ["sonots@gmail.com"]
9
+ s.homepage = "https://github.com/sonots/fluent-plugin-stats"
10
+ s.summary = "Fluentd plugin to calculate statistics such as sum, max, min, avg"
11
+ s.description = s.summary
12
+ s.licenses = ["MIT"]
13
+
14
+ s.rubyforge_project = "fluent-plugin-stats"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_runtime_dependency "fluentd"
22
+ s.add_development_dependency "rake"
23
+ s.add_development_dependency "rspec"
24
+ s.add_development_dependency "pry"
25
+ s.add_development_dependency "pry-nav"
26
+ end
@@ -0,0 +1,312 @@
1
+ # encoding: UTF-8
2
+ class Fluent::StatsOutput < Fluent::Output
3
+ Fluent::Plugin.register_output('stats', self)
4
+
5
+ def initialize
6
+ super
7
+ require 'pathname'
8
+ end
9
+
10
+ config_param :sum, :string, :default => nil
11
+ config_param :max, :string, :default => nil
12
+ config_param :min, :string, :default => nil
13
+ config_param :avg, :string, :default => nil
14
+ config_param :sum_keys, :string, :default => nil
15
+ config_param :max_keys, :string, :default => nil
16
+ config_param :min_keys, :string, :default => nil
17
+ config_param :avg_keys, :string, :default => nil
18
+ config_param :sum_suffix, :string, :default => ""
19
+ config_param :max_suffix, :string, :default => ""
20
+ config_param :min_suffix, :string, :default => ""
21
+ config_param :avg_suffix, :string, :default => ""
22
+ config_param :interval, :time, :default => 5
23
+ config_param :tag, :string, :default => nil
24
+ config_param :add_tag_prefix, :string, :default => nil
25
+ config_param :remove_tag_prefix, :string, :default => nil
26
+ config_param :aggregate, :string, :default => 'tag'
27
+ config_param :store_file, :string, :default => nil
28
+ config_param :zero_emit, :bool, :default => false
29
+
30
+ attr_accessor :matches
31
+ attr_accessor :saved_duration
32
+ attr_accessor :saved_at
33
+ attr_accessor :last_checked
34
+
35
+ def configure(conf)
36
+ super
37
+
38
+ @interval = @interval.to_i
39
+ @sum = Regexp.new(@sum) if @sum
40
+ @max = Regexp.new(@max) if @max
41
+ @min = Regexp.new(@min) if @min
42
+ @avg = Regexp.new(@avg) if @avg
43
+ @sum_keys = @sum_keys ? @sum_keys.split(',') : []
44
+ @max_keys = @max_keys ? @max_keys.split(',') : []
45
+ @min_keys = @min_keys ? @min_keys.split(',') : []
46
+ @avg_keys = @avg_keys ? @avg_keys.split(',') : []
47
+
48
+ unless ['tag', 'all'].include?(@aggregate)
49
+ raise Fluent::ConfigError, "aggregate allows tag/all"
50
+ end
51
+
52
+ case @aggregate
53
+ when 'all'
54
+ raise Fluent::ConfigError, "tag must be specified for aggregate all" if @tag.nil?
55
+ end
56
+
57
+ if @tag.nil? and @add_tag_prefix.nil? and @remove_tag_prefix.nil?
58
+ @add_tag_prefix = 'stats' # not ConfigError for lower version compatibility
59
+ end
60
+
61
+ @tag_prefix = "#{@add_tag_prefix}." if @add_tag_prefix
62
+ @tag_prefix_match = "#{@remove_tag_prefix}." if @remove_tag_prefix
63
+ @tag_proc =
64
+ if @tag
65
+ Proc.new {|tag| @tag }
66
+ elsif @tag_prefix and @tag_prefix_match
67
+ Proc.new {|tag| "#{@tag_prefix}#{lstrip(tag, @tag_prefix_match)}" }
68
+ elsif @tag_prefix_match
69
+ Proc.new {|tag| lstrip(tag, @tag_prefix_match) }
70
+ elsif @tag_prefix
71
+ Proc.new {|tag| "#{@tag_prefix}#{tag}" }
72
+ else
73
+ Proc.new {|tag| tag }
74
+ end
75
+
76
+ @matches = {}
77
+ @mutex = Mutex.new
78
+ end
79
+
80
+ def initial_matches(prev_matches = nil)
81
+ if @zero_emit && prev_matches
82
+ matches = {}
83
+ prev_matches.keys.each do |tag|
84
+ next unless prev_matches[tag][:count] > 0 # Prohibit to emit anymore
85
+ matches[tag] = { :count => 0, :sum => {}, :max => {}, :min => {}, :avg => {} }
86
+ # ToDo: would want default configuration for :max, :min
87
+ prev_matches[tag][:sum].keys.each {|key| matches[tag][:sum][key] = 0 }
88
+ prev_matches[tag][:max].keys.each {|key| matches[tag][:max][key] = 0 }
89
+ prev_matches[tag][:min].keys.each {|key| matches[tag][:min][key] = 0 }
90
+ prev_matches[tag][:avg].keys.each {|key| matches[tag][:avg][key] = 0 }
91
+ end
92
+ matches
93
+ else
94
+ {}
95
+ end
96
+ end
97
+
98
+ def start
99
+ super
100
+ load_status(@store_file, @interval) if @store_file
101
+ @watcher = Thread.new(&method(:watcher))
102
+ end
103
+
104
+ def shutdown
105
+ super
106
+ @watcher.terminate
107
+ @watcher.join
108
+ save_status(@store_file) if @store_file
109
+ end
110
+
111
+ # Called when new line comes. This method actually does not emit
112
+ def emit(tag, es, chain)
113
+ tag = 'all' if @aggregate == 'all'
114
+ # stats
115
+ matches = { :count => 0, :sum => {}, :max => {}, :min => {}, :avg => {} }
116
+ es.each do |time, record|
117
+ @sum_keys.each do |key|
118
+ next unless record[key] and value = record[key].to_f
119
+ matches[:sum][key] = sum(matches[:sum][key], value)
120
+ end
121
+ @max_keys.each do |key|
122
+ next unless record[key] and value = record[key].to_f
123
+ matches[:max][key] = max(matches[:max][key], value)
124
+ end
125
+ @min_keys.each do |key|
126
+ next unless record[key] and value = record[key].to_f
127
+ matches[:min][key] = min(matches[:min][key], value)
128
+ end
129
+ @avg_keys.each do |key|
130
+ next unless record[key] and value = record[key].to_f
131
+ matches[:avg][key] = sum(matches[:avg][key], value)
132
+ end
133
+ record.keys.each do |key|
134
+ value = record[key].to_f
135
+ if @sum and @sum.match(key)
136
+ matches[:sum][key] = sum(matches[:sum][key], value)
137
+ end
138
+ if @max and @max.match(key)
139
+ matches[:max][key] = max(matches[:max][key], value)
140
+ end
141
+ if @min and @min.match(key)
142
+ matches[:min][key] = min(matches[:min][key], value)
143
+ end
144
+ if @avg and @avg.match(key)
145
+ matches[:avg][key] = sum(matches[:avg][key], value) # sum yet
146
+ end
147
+ end if @sum || @max || @min || @avg
148
+ matches[:count] += 1
149
+ end
150
+
151
+ # thread safe merge
152
+ @matches[tag] ||= { :count => 0, :sum => {}, :max => {}, :min => {}, :avg => {} }
153
+ @mutex.synchronize do
154
+ matches[:sum].keys.each do |key|
155
+ @matches[tag][:sum][key] = sum(@matches[tag][:sum][key], matches[:sum][key])
156
+ end
157
+ matches[:max].keys.each do |key|
158
+ @matches[tag][:max][key] = max(@matches[tag][:max][key], matches[:max][key])
159
+ end
160
+ matches[:min].keys.each do |key|
161
+ @matches[tag][:min][key] = min(@matches[tag][:min][key], matches[:min][key])
162
+ end
163
+ matches[:avg].keys.each do |key|
164
+ @matches[tag][:avg][key] = sum(@matches[tag][:avg][key], matches[:avg][key]) # sum yet
165
+ end
166
+ @matches[tag][:count] += matches[:count]
167
+ end
168
+
169
+ chain.next
170
+ rescue => e
171
+ $log.warn "#{e.class} #{e.message} #{e.backtrace.first}"
172
+ end
173
+
174
+ # thread callback
175
+ def watcher
176
+ # instance variable, and public accessable, for test
177
+ @last_checked ||= Fluent::Engine.now
178
+ while true
179
+ sleep 0.5
180
+ begin
181
+ if Fluent::Engine.now - @last_checked >= @interval
182
+ now = Fluent::Engine.now
183
+ flush_emit(now - @last_checked)
184
+ @last_checked = now
185
+ end
186
+ rescue => e
187
+ $log.warn "#{e.class} #{e.message} #{e.backtrace.first}"
188
+ end
189
+ end
190
+ end
191
+
192
+ # This method is the real one to emit
193
+ def flush_emit(step)
194
+ time = Fluent::Engine.now
195
+ flushed_matches, @matches = @matches, initial_matches(@matches)
196
+
197
+ flushed_matches.keys.each do |tag|
198
+ matches = flushed_matches[tag]
199
+ output = generate_output(matches)
200
+ emit_tag = @tag_proc.call(tag)
201
+ Fluent::Engine.emit(emit_tag, time, output) if output
202
+ end
203
+ end
204
+
205
+ def generate_output(matches)
206
+ return nil if matches.empty?
207
+ output = {}
208
+ matches[:sum].keys.each do |key|
209
+ output[key + @sum_suffix] = matches[:sum][key]
210
+ end
211
+ matches[:max].keys.each do |key|
212
+ output[key + @max_suffix] = matches[:max][key]
213
+ end
214
+ matches[:min].keys.each do |key|
215
+ output[key + @min_suffix] = matches[:min][key]
216
+ end
217
+ matches[:avg].keys.each do |key|
218
+ output[key + @avg_suffix] = matches[:avg][key]
219
+ output[key + @avg_suffix] /= matches[:count].to_f if matches[:count] > 0
220
+ end
221
+ output
222
+ end
223
+
224
+ def sum(a, b)
225
+ [a, b].compact.inject(:+)
226
+ end
227
+
228
+ def max(a, b)
229
+ [a, b].compact.max
230
+ end
231
+
232
+ def min(a, b)
233
+ [a, b].compact.min
234
+ end
235
+
236
+ # Store internal status into a file
237
+ #
238
+ # @param [String] file_path
239
+ def save_status(file_path)
240
+ return unless file_path
241
+
242
+ begin
243
+ Pathname.new(file_path).open('wb') do |f|
244
+ @saved_at = Fluent::Engine.now
245
+ @saved_duration = @saved_at - @last_checked
246
+ Marshal.dump({
247
+ :matches => @matches,
248
+ :saved_at => @saved_at,
249
+ :saved_duration => @saved_duration,
250
+ :aggregate => @aggregate,
251
+ :sum => @sum,
252
+ :max => @max,
253
+ :min => @min,
254
+ :avg => @avg,
255
+ }, f)
256
+ end
257
+ rescue => e
258
+ $log.warn "out_stats: Can't write store_file #{e.class} #{e.message}"
259
+ end
260
+ end
261
+
262
+ # Load internal status from a file
263
+ #
264
+ # @param [String] file_path
265
+ # @param [Interger] interval
266
+ def load_status(file_path, interval)
267
+ return unless (f = Pathname.new(file_path)).exist?
268
+
269
+ begin
270
+ f.open('rb') do |f|
271
+ stored = Marshal.load(f)
272
+ if stored[:aggregate] == @aggregate and
273
+ stored[:sum] == @sum and
274
+ stored[:max] == @max and
275
+ stored[:min] == @min and
276
+ stored[:avg] == @avg
277
+
278
+ if !stored[:matches].empty? and !stored[:matches].first[1].has_key?(:max)
279
+ $log.warn "out_stats: stored data does not have compatibility with the current version. ignore stored data"
280
+ return
281
+ end
282
+
283
+ if Fluent::Engine.now <= stored[:saved_at] + interval
284
+ @matches = stored[:matches]
285
+ @saved_at = stored[:saved_at]
286
+ @saved_duration = stored[:saved_duration]
287
+ # for lower compatibility
288
+ if counts = stored[:counts]
289
+ @matches.keys.each {|tag| @matches[tag][:count] = counts[tag] }
290
+ end
291
+
292
+ # skip the saved duration to continue counting
293
+ @last_checked = Fluent::Engine.now - @saved_duration
294
+ else
295
+ $log.warn "out_stats: stored data is outdated. ignore stored data"
296
+ end
297
+ else
298
+ $log.warn "out_stats: configuration param was changed. ignore stored data"
299
+ end
300
+ end
301
+ rescue => e
302
+ $log.warn "out_stats: Can't load store_file #{e.class} #{e.message}"
303
+ end
304
+ end
305
+
306
+ private
307
+
308
+ def lstrip(string, substring)
309
+ string.index(substring) == 0 ? string[substring.size..-1] : string
310
+ end
311
+
312
+ end
@@ -0,0 +1,307 @@
1
+ # encoding: UTF-8
2
+ require_relative 'spec_helper'
3
+
4
+ class Fluent::Test::OutputTestDriver
5
+ def emit_with_tag(record, time=Time.now, tag = nil)
6
+ @tag = tag if tag
7
+ emit(record, time)
8
+ end
9
+ end
10
+
11
+ describe Fluent::StatsOutput do
12
+ before { Fluent::Test.setup }
13
+ CONFIG = %[]
14
+ let(:tag) { 'foo.bar' }
15
+ let(:driver) { Fluent::Test::OutputTestDriver.new(Fluent::StatsOutput, tag).configure(config) }
16
+
17
+ describe 'test configure' do
18
+ describe 'bad configuration' do
19
+ context 'invalid aggregate' do
20
+ let(:config) do
21
+ CONFIG + %[
22
+ aggregate foo
23
+ ]
24
+ end
25
+ it { expect { driver }.to raise_error(Fluent::ConfigError) }
26
+ end
27
+
28
+ context 'no tag for aggregate all' do
29
+ let(:config) do
30
+ CONFIG + %[
31
+ aggregate all
32
+ ]
33
+ end
34
+ it { expect { driver }.to raise_error(Fluent::ConfigError) }
35
+ end
36
+ end
37
+
38
+ describe 'good configuration' do
39
+ context "nothing" do
40
+ let(:config) { '' }
41
+ it { expect { driver }.to_not raise_error }
42
+ end
43
+
44
+ context 'sum/max/min/avg' do
45
+ let(:config) do
46
+ CONFIG + %[
47
+ sum _count$
48
+ max _max$
49
+ min _min$
50
+ avg _avg$
51
+ ]
52
+ end
53
+ it { expect { driver }.to_not raise_error }
54
+ end
55
+
56
+ context "check default" do
57
+ subject { driver.instance }
58
+ let(:config) { CONFIG }
59
+ its(:interval) { should == 5 }
60
+ its(:tag) { should be_nil }
61
+ its(:add_tag_prefix) { should == 'stats' }
62
+ its(:aggregate) { should == 'tag' }
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'test emit' do
68
+ let(:time) { Time.now.to_i }
69
+ let(:messages) do
70
+ [
71
+ {"4xx_count"=>"1","5xx_count"=>2,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3},
72
+ {"4xx_count"=>"2","5xx_count"=>2,"reqtime_max"=>5,"reqtime_min"=>2,"reqtime_avg"=>2},
73
+ {"4xx_count"=>"3","5xx_count"=>2,"reqtime_max"=>1,"reqtime_min"=>3,"reqtime_avg"=>4},
74
+ ]
75
+ end
76
+ let(:emit) do
77
+ driver.run { messages.each {|message| driver.emit(message, time) } }
78
+ driver.instance.flush_emit(0)
79
+ end
80
+ let(:empty_emit) do
81
+ driver.instance.flush_emit(0)
82
+ end
83
+
84
+ context 'sum/max/min/avg' do
85
+ let(:config) do
86
+ CONFIG + %[
87
+ sum _count$
88
+ max _max$
89
+ min _min$
90
+ avg _avg$
91
+ ]
92
+ end
93
+ before do
94
+ Fluent::Engine.stub(:now).and_return(time)
95
+ Fluent::Engine.should_receive(:emit).with("stats.#{tag}", time, {
96
+ "4xx_count"=>6,"5xx_count"=>6,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3.0
97
+ })
98
+ end
99
+ it { emit }
100
+ end
101
+
102
+ context 'zero_emit' do
103
+ let(:config) do
104
+ CONFIG + %[
105
+ sum _count$
106
+ max _max$
107
+ min _min$
108
+ avg _avg$
109
+ zero_emit true
110
+ ]
111
+ end
112
+ before do
113
+ Fluent::Engine.stub(:now).and_return(time)
114
+ Fluent::Engine.should_receive(:emit).with("stats.#{tag}", time, {
115
+ "4xx_count"=>6,"5xx_count"=>6,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3.0
116
+ })
117
+ Fluent::Engine.should_receive(:emit).with("stats.#{tag}", time, {
118
+ "4xx_count"=>0,"5xx_count"=>0,"reqtime_max"=>0,"reqtime_min"=>0,"reqtime_avg"=>0.0
119
+ })
120
+ end
121
+ it { emit; empty_emit }
122
+ end
123
+
124
+ context 'sum/max/min/avg_keys' do
125
+ let(:config) do
126
+ CONFIG + %[
127
+ sum_keys 4xx_count,5xx_count
128
+ max_keys reqtime_max
129
+ min_keys reqtime_min
130
+ avg_keys reqtime_avg,not_found
131
+ ]
132
+ end
133
+ before do
134
+ Fluent::Engine.stub(:now).and_return(time)
135
+ Fluent::Engine.should_receive(:emit).with("stats.#{tag}", time, {
136
+ "4xx_count"=>6,"5xx_count"=>6,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3.0
137
+ })
138
+ end
139
+ it { emit }
140
+ end
141
+
142
+ context 'sum/max/min/avg_suffix' do
143
+ let(:config) do
144
+ CONFIG + %[
145
+ sum ^(reqtime|reqsize)$
146
+ max ^reqtime$
147
+ min ^reqtime$
148
+ avg ^reqtime$
149
+ sum_suffix _sum
150
+ max_suffix _max
151
+ min_suffix _min
152
+ avg_suffix _avg
153
+ ]
154
+ end
155
+ let(:messages) do
156
+ [
157
+ {"reqtime"=>1.000,"reqsize"=>10},
158
+ {"reqtime"=>2.000,"reqsize"=>20},
159
+ ]
160
+ end
161
+ before do
162
+ Fluent::Engine.stub(:now).and_return(time)
163
+ Fluent::Engine.should_receive(:emit).with("stats.#{tag}", time, {
164
+ "reqtime_sum"=>3.000,"reqtime_max"=>2.000,"reqtime_min"=>1.000,"reqtime_avg"=>1.500,"reqsize_sum"=>30
165
+ })
166
+ end
167
+ it { emit }
168
+ end
169
+
170
+ context 'tag' do
171
+ let(:config) do
172
+ CONFIG + %[
173
+ tag foo
174
+ sum _count$
175
+ ]
176
+ end
177
+ before do
178
+ Fluent::Engine.stub(:now).and_return(time)
179
+ Fluent::Engine.should_receive(:emit).with("foo", time, {
180
+ "4xx_count"=>6,"5xx_count"=>6
181
+ })
182
+ end
183
+ it { emit }
184
+ end
185
+
186
+ context 'add_tag_prefix' do
187
+ let(:config) do
188
+ CONFIG + %[
189
+ add_tag_prefix foo
190
+ sum _count$
191
+ ]
192
+ end
193
+ before do
194
+ Fluent::Engine.stub(:now).and_return(time)
195
+ Fluent::Engine.should_receive(:emit).with("foo.#{tag}", time, {
196
+ "4xx_count"=>6,"5xx_count"=>6
197
+ })
198
+ end
199
+ it { emit }
200
+ end
201
+
202
+ context 'remove_tag_prefix' do
203
+ let(:config) do
204
+ CONFIG + %[
205
+ remove_tag_prefix foo
206
+ sum _count$
207
+ ]
208
+ end
209
+ before do
210
+ Fluent::Engine.stub(:now).and_return(time)
211
+ Fluent::Engine.should_receive(:emit).with("bar", time, {
212
+ "4xx_count"=>6,"5xx_count"=>6
213
+ })
214
+ end
215
+ it { emit }
216
+ end
217
+
218
+ context 'aggregate' do
219
+ let(:emit) do
220
+ driver.run { messages.each {|message| driver.emit_with_tag(message, time, 'foo.bar') } }
221
+ driver.run { messages.each {|message| driver.emit_with_tag(message, time, 'foo.bar2') } }
222
+ driver.instance.flush_emit(0)
223
+ end
224
+
225
+ context 'aggregate all' do
226
+ let(:config) do
227
+ CONFIG + %[
228
+ aggregate all
229
+ tag foo
230
+ sum _count$
231
+ max _max$
232
+ min _min$
233
+ avg _avg$
234
+ ]
235
+ end
236
+ before do
237
+ Fluent::Engine.stub(:now).and_return(time)
238
+ Fluent::Engine.should_receive(:emit).with("foo", time, {
239
+ "4xx_count"=>12,"5xx_count"=>12,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3.0
240
+ })
241
+ end
242
+ it { emit }
243
+ end
244
+
245
+ context 'aggregate tag' do
246
+ let(:config) do
247
+ CONFIG + %[
248
+ aggregate tag
249
+ add_tag_prefix stats
250
+ sum _count$
251
+ max _max$
252
+ min _min$
253
+ avg _avg$
254
+ ]
255
+ end
256
+ before do
257
+ Fluent::Engine.stub(:now).and_return(time)
258
+ Fluent::Engine.should_receive(:emit).with("stats.foo.bar", time, {
259
+ "4xx_count"=>6,"5xx_count"=>6,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3.0
260
+ })
261
+ Fluent::Engine.should_receive(:emit).with("stats.foo.bar2", time, {
262
+ "4xx_count"=>6,"5xx_count"=>6,"reqtime_max"=>6,"reqtime_min"=>1,"reqtime_avg"=>3.0
263
+ })
264
+ end
265
+ it { emit }
266
+ end
267
+ end
268
+
269
+ describe "store_file" do
270
+ let(:store_file) do
271
+ dirname = "tmp"
272
+ Dir.mkdir dirname unless Dir.exist? dirname
273
+ filename = "#{dirname}/test.dat"
274
+ File.unlink filename if File.exist? filename
275
+ filename
276
+ end
277
+
278
+ let(:config) do
279
+ CONFIG + %[
280
+ sum _count$
281
+ store_file #{store_file}
282
+ ]
283
+ end
284
+
285
+ it 'stored_data and loaded_data should equal' do
286
+ driver.run { messages.each {|message| driver.emit(message, time) } }
287
+ driver.instance.shutdown
288
+ stored_matches = driver.instance.matches
289
+ stored_saved_at = driver.instance.saved_at
290
+ stored_saved_duration = driver.instance.saved_duration
291
+ driver.instance.matches = {}
292
+ driver.instance.saved_at = nil
293
+ driver.instance.saved_duration = nil
294
+
295
+ driver.instance.start
296
+ loaded_matches = driver.instance.matches
297
+ loaded_saved_at = driver.instance.saved_at
298
+ loaded_saved_duration = driver.instance.saved_duration
299
+
300
+ loaded_matches.should == stored_matches
301
+ loaded_saved_at.should == stored_saved_at
302
+ loaded_saved_duration.should == stored_saved_duration
303
+ end
304
+ end
305
+ end
306
+ end
307
+
@@ -0,0 +1,13 @@
1
+ # encoding: UTF-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ Bundler.setup(:default, :test)
5
+ Bundler.require(:default, :test)
6
+
7
+ require 'fluent/test'
8
+ require 'rspec'
9
+ require 'pry'
10
+
11
+ $TESTING=true
12
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
13
+ require 'fluent/plugin/out_stats'
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-stats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Naotoshi Seo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
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: rspec
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'
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-nav
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Fluentd plugin to calculate statistics such as sum, max, min, avg
84
+ email:
85
+ - sonots@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .coveralls.yml
91
+ - .gitignore
92
+ - .rdebugrc
93
+ - .rspec
94
+ - .travis.yml
95
+ - CHANGELOG.md
96
+ - Gemfile
97
+ - LICENSE
98
+ - README.md
99
+ - Rakefile
100
+ - fluent-plugin-stats.gemspec
101
+ - lib/fluent/plugin/out_stats.rb
102
+ - spec/out_stats_spec.rb
103
+ - spec/spec_helper.rb
104
+ homepage: https://github.com/sonots/fluent-plugin-stats
105
+ licenses:
106
+ - MIT
107
+ metadata: {}
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - '>='
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ requirements: []
123
+ rubyforge_project: fluent-plugin-stats
124
+ rubygems_version: 2.0.3
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Fluentd plugin to calculate statistics such as sum, max, min, avg
128
+ test_files:
129
+ - spec/out_stats_spec.rb
130
+ - spec/spec_helper.rb