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 +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +13 -0
- data/.rdebugrc +4 -0
- data/.rspec +2 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +126 -0
- data/Rakefile +15 -0
- data/fluent-plugin-stats.gemspec +26 -0
- data/lib/fluent/plugin/out_stats.rb +312 -0
- data/spec/out_stats_spec.rb +307 -0
- data/spec/spec_helper.rb +13 -0
- metadata +130 -0
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
data/.rdebugrc
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
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
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 [](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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|