fluent-plugin-stats 0.3.0

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