fluent-plugin-groupcounter 0.1.0 → 0.2.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/.pryrc +3 -0
- data/.travis.yml +6 -0
- data/README.md +99 -18
- data/Rakefile +7 -2
- data/fluent-plugin-groupcounter.gemspec +7 -5
- data/lib/fluent/plugin/out_groupcounter.rb +229 -98
- data/spec/out_groupcounter_spec.rb +354 -0
- data/spec/spec_helper.rb +13 -0
- metadata +55 -34
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 86658cfcab0fe4d17fcbafd52f8baa4063905316
|
4
|
+
data.tar.gz: bd59e1fe0c665c9c942519d1eb026e9f258113d3
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92bab15c3c80a69a66c131de63a99d3cfcc3e4a61eea8618603413e02cd142cd93c897cd87571db89faa7270eb0e4318d1d4ac97237bc3dd824ecdd05bbddf16
|
7
|
+
data.tar.gz: 99cd15d0539ec3a0d898bdf4f6c8937111e523dca1acade1151c0e200500c1868717bd8196d76276991d671da9ec3f24f7bf977dea378d251e763d2589ea8c1e
|
data/.pryrc
ADDED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,43 +1,124 @@
|
|
1
1
|
# fluent-plugin-groupcounter
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
### GroupCounterOutput
|
6
|
-
|
7
|
-
Fluentd plugin to count like COUNT(\*) GROUP BY
|
3
|
+
Fluentd plugin to count like SELECT COUNT(\*) GROUP BY.
|
8
4
|
|
9
5
|
## Configuration
|
10
6
|
|
11
|
-
|
7
|
+
Assume inputs are coming as followings:
|
8
|
+
|
9
|
+
apache.access: {"code":"200", "method":"GET", "path":"/index.html", "reqtime":"1.001" }
|
10
|
+
apache.access: {"code":"404", "method":"GET", "path":"/foo.html", "reqtime":"2.002" }
|
11
|
+
apache.access: {"code":"200", "method":"GET", "path":"/index.html", "reqtime":"3.003" }
|
12
12
|
|
13
|
-
|
14
|
-
type tail
|
15
|
-
path /var/log/httpd-access.log
|
16
|
-
tag apache.access
|
17
|
-
format apache
|
18
|
-
</source>
|
13
|
+
Think of quering `SELECT COUNT(\*) GROUP BY code,method,path`. Configuration becomes as below:
|
19
14
|
|
20
15
|
<match apache.access>
|
21
16
|
type groupcounter
|
22
|
-
count_interval 5s
|
23
17
|
aggregate tag
|
24
18
|
output_per_tag true
|
25
19
|
tag_prefix groupcounter
|
26
20
|
group_by_keys code,method,path
|
27
21
|
</match>
|
28
22
|
|
29
|
-
Output like
|
23
|
+
Output becomes like
|
24
|
+
|
25
|
+
groupcounter.apache.access: {"200_GET_/index.html_count":2, "404_GET_/foo.html_count":1}
|
26
|
+
|
27
|
+
## Parameters
|
28
|
+
|
29
|
+
* group\_by\_keys (semi-required)
|
30
|
+
|
31
|
+
Specify keys in the event record for grouping. `group_by_keys` or `group_by_expression` is required.
|
32
|
+
|
33
|
+
* delimiter
|
34
|
+
|
35
|
+
Specify the delimiter to join `group_by_keys`. Default is '_'.
|
36
|
+
|
37
|
+
* group\_by\_expression (semi-required)
|
38
|
+
|
39
|
+
Use an expression to group the event record. `group_by_keys` or `group_by_expression` is required.
|
40
|
+
|
41
|
+
For examples, for the exampled input above, the configuration as below
|
42
|
+
|
43
|
+
group_by_expression ${method}${path}/${code}
|
44
|
+
|
45
|
+
gives you an output like
|
46
|
+
|
47
|
+
groupcounter.apache.access: {"GET/index.html/200_count":1, "GET/foo.html/400_count":1}
|
48
|
+
|
49
|
+
SECRET TRICK: You can write a ruby code in the ${} placeholder like
|
50
|
+
|
51
|
+
group_by_expression ${method}${path.split(".")[0]}/${code[0]}xx
|
52
|
+
|
53
|
+
This gives an output like
|
54
|
+
|
55
|
+
groupcounter.apache.access: {"GET/index/2xx_count":1, "GET/foo/4xx_count":1}
|
56
|
+
|
57
|
+
* tag
|
58
|
+
|
59
|
+
The output tag. Default is `groupcount`.
|
60
|
+
|
61
|
+
* tag\_prefix
|
62
|
+
|
63
|
+
The prefix string which will be added to the input tag. `output_per_tag yes` must be specified together.
|
64
|
+
|
65
|
+
* input\_tag\_remove\_prefix
|
66
|
+
|
67
|
+
The prefix string which will be removed from the input tag.
|
68
|
+
|
69
|
+
* count\_interval
|
70
|
+
|
71
|
+
The interval time to count in seconds. Default is `60`.
|
72
|
+
|
73
|
+
* unit
|
74
|
+
|
75
|
+
The interval time to monitor specified an unit (either of `minute`, `hour`, or `day`).
|
76
|
+
Use either of `count_interval` or `unit`.
|
77
|
+
|
78
|
+
* store\_file
|
79
|
+
|
80
|
+
Store internal data into a file of the given path on shutdown, and load on starting.
|
81
|
+
|
82
|
+
* max\_key
|
83
|
+
|
84
|
+
Specify key name in the event record to do `SELECT COUNT(\*),MAX(key_name) GROUP BY`.
|
85
|
+
|
86
|
+
For examples, for the exampled input above, adding the configuration as below
|
87
|
+
|
88
|
+
max_key reqtime
|
89
|
+
|
90
|
+
gives you an output like
|
91
|
+
|
92
|
+
groupcounter.apache.access: {"200_GET_/index.html_reqtime_max":3.003, "404_GET_/foo.html_reqtime_max":2.002}
|
93
|
+
|
94
|
+
* min\_key
|
95
|
+
|
96
|
+
Specify key name in the event record to do `SELECT COUNT(\*),MIN(key_name) GROUP BY`.
|
97
|
+
|
98
|
+
* avg\_key
|
99
|
+
|
100
|
+
Specify key name in the event record to do `SELECT COUNT(\*),AVG(key_name) GROUP BY`.
|
101
|
+
|
102
|
+
* count\_suffix
|
103
|
+
|
104
|
+
Default is `_count`
|
105
|
+
|
106
|
+
* max\_suffix
|
107
|
+
|
108
|
+
Default is `_max`. Should be used with `max_key` option.
|
109
|
+
|
110
|
+
* min\_suffix
|
30
111
|
|
31
|
-
|
112
|
+
Default is `_min`. Should be used with `min_key` option.
|
32
113
|
|
33
|
-
|
114
|
+
* avg\_suffix
|
34
115
|
|
35
|
-
|
36
|
-
* documents
|
116
|
+
Default is `_avg`. Should be used with `avg_key` option.
|
37
117
|
|
38
118
|
## Copyright
|
39
119
|
|
40
120
|
* Copyright
|
41
121
|
* Copyright (c) 2012- Ryosuke IWANAGA (riywo)
|
122
|
+
* Copyright (c) 2013- Naotoshi SEO (sonots)
|
42
123
|
* License
|
43
124
|
* Apache License, Version 2.0
|
data/Rakefile
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "bundler/gem_tasks"
|
3
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
|
+
|
4
11
|
desc 'Open an irb session preloaded with the gem library'
|
5
12
|
task :console do
|
6
13
|
sh 'irb -rubygems -I lib'
|
7
14
|
end
|
8
|
-
|
9
15
|
task :c => :console
|
10
|
-
|
@@ -3,12 +3,12 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "fluent-plugin-groupcounter"
|
6
|
-
s.version = "0.
|
6
|
+
s.version = "0.2.0"
|
7
7
|
s.authors = ["Ryosuke IWANAGA", "Naotoshi SEO"]
|
8
|
-
s.email = ["@riywo", "@
|
8
|
+
s.email = ["@riywo", "sonots@gmail.com"]
|
9
9
|
s.homepage = "https://github.com/riywo/fluent-plugin-groupcounter"
|
10
|
-
s.summary = %q{Fluentd plugin to count like COUNT(\*) GROUP BY}
|
11
|
-
s.description = %q{Fluentd plugin to count like COUNT(\*) GROUP BY}
|
10
|
+
s.summary = %q{Fluentd plugin to count like SELECT COUNT(\*) GROUP BY}
|
11
|
+
s.description = %q{Fluentd plugin to count like SELECT COUNT(\*) GROUP BY}
|
12
12
|
|
13
13
|
s.rubyforge_project = "fluent-plugin-groupcounter"
|
14
14
|
|
@@ -18,6 +18,8 @@ Gem::Specification.new do |s|
|
|
18
18
|
s.require_paths = ["lib"]
|
19
19
|
|
20
20
|
s.add_runtime_dependency "fluentd"
|
21
|
-
s.add_development_dependency "fluentd"
|
22
21
|
s.add_development_dependency "rake"
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "pry"
|
24
|
+
s.add_development_dependency "pry-nav"
|
23
25
|
end
|
@@ -1,6 +1,11 @@
|
|
1
1
|
class Fluent::GroupCounterOutput < Fluent::Output
|
2
2
|
Fluent::Plugin.register_output('groupcounter', self)
|
3
3
|
|
4
|
+
def initialize
|
5
|
+
super
|
6
|
+
require 'pathname'
|
7
|
+
end
|
8
|
+
|
4
9
|
config_param :count_interval, :time, :default => nil
|
5
10
|
config_param :unit, :string, :default => 'minute'
|
6
11
|
config_param :output_per_tag, :bool, :default => false
|
@@ -8,20 +13,35 @@ class Fluent::GroupCounterOutput < Fluent::Output
|
|
8
13
|
config_param :tag, :string, :default => 'groupcount'
|
9
14
|
config_param :tag_prefix, :string, :default => nil
|
10
15
|
config_param :input_tag_remove_prefix, :string, :default => nil
|
11
|
-
config_param :group_by_keys, :string
|
12
|
-
config_param :
|
16
|
+
config_param :group_by_keys, :string, :default => nil
|
17
|
+
config_param :group_by_expression, :string, :default => nil
|
18
|
+
config_param :max_key, :string, :default => nil
|
19
|
+
config_param :min_key, :string, :default => nil
|
20
|
+
config_param :avg_key, :string, :default => nil
|
21
|
+
config_param :delimiter, :string, :default => '_'
|
22
|
+
config_param :count_suffix, :string, :default => '_count'
|
23
|
+
config_param :max_suffix, :string, :default => '_max'
|
24
|
+
config_param :min_suffix, :string, :default => '_min'
|
25
|
+
config_param :avg_suffix, :string, :default => '_avg'
|
26
|
+
config_param :store_file, :string, :default => nil
|
13
27
|
|
14
|
-
attr_accessor :
|
28
|
+
attr_accessor :count_interval
|
15
29
|
attr_accessor :counts
|
30
|
+
attr_accessor :saved_duration
|
31
|
+
attr_accessor :saved_at
|
16
32
|
attr_accessor :last_checked
|
17
33
|
|
18
34
|
def configure(conf)
|
19
35
|
super
|
20
36
|
|
37
|
+
if @group_by_keys.nil? and @group_by_expression.nil?
|
38
|
+
raise Fluent::ConfigError, "Either of group_by_keys or group_by_expression must be specified"
|
39
|
+
end
|
40
|
+
|
21
41
|
if @count_interval
|
22
|
-
@
|
42
|
+
@count_interval = @count_interval.to_i
|
23
43
|
else
|
24
|
-
@
|
44
|
+
@count_interval = case @unit
|
25
45
|
when 'minute' then 60
|
26
46
|
when 'hour' then 3600
|
27
47
|
when 'day' then 86400
|
@@ -47,14 +67,23 @@ class Fluent::GroupCounterOutput < Fluent::Output
|
|
47
67
|
@removed_length = @removed_prefix_string.length
|
48
68
|
end
|
49
69
|
|
50
|
-
@group_by_keys = @group_by_keys.split(',')
|
70
|
+
@group_by_keys = @group_by_keys.split(',') if @group_by_keys
|
71
|
+
|
72
|
+
if @store_file
|
73
|
+
f = Pathname.new(@store_file)
|
74
|
+
if (f.exist? && !f.writable_real?) || (!f.exist? && !f.parent.writable_real?)
|
75
|
+
raise Fluent::ConfigError, "#{@store_file} is not writable"
|
76
|
+
end
|
77
|
+
end
|
51
78
|
|
52
79
|
@counts = count_initialized
|
80
|
+
@hostname = Socket.gethostname
|
53
81
|
@mutex = Mutex.new
|
54
82
|
end
|
55
83
|
|
56
84
|
def start
|
57
85
|
super
|
86
|
+
load_status(@store_file, @count_interval) if @store_file
|
58
87
|
start_watch
|
59
88
|
end
|
60
89
|
|
@@ -62,102 +91,64 @@ class Fluent::GroupCounterOutput < Fluent::Output
|
|
62
91
|
super
|
63
92
|
@watcher.terminate
|
64
93
|
@watcher.join
|
94
|
+
save_status(@store_file) if @store_file
|
65
95
|
end
|
66
96
|
|
67
97
|
def count_initialized
|
68
|
-
# counts['tag'][group_by_keys] = count
|
69
|
-
# counts['tag'][__sum] = sum
|
70
98
|
{}
|
71
99
|
end
|
72
100
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
end
|
77
|
-
@counts[tag] ||= {}
|
78
|
-
|
79
|
-
@mutex.synchronize {
|
80
|
-
sum = 0
|
81
|
-
counts.each do |key, count|
|
82
|
-
sum += count
|
83
|
-
@counts[tag][key] ||= 0
|
84
|
-
@counts[tag][key] += count
|
85
|
-
end
|
86
|
-
@counts[tag]['__sum'] ||= 0
|
87
|
-
@counts[tag]['__sum'] += sum
|
88
|
-
}
|
89
|
-
end
|
90
|
-
|
91
|
-
def stripped_tag(tag)
|
92
|
-
return tag unless @input_tag_remove_prefix
|
93
|
-
return tag[@removed_length..-1] if tag.start_with?(@removed_prefix_string) and tag.length > @removed_length
|
94
|
-
return tag[@removed_length..-1] if tag == @input_tag_remove_prefix
|
95
|
-
tag
|
96
|
-
end
|
97
|
-
|
98
|
-
def generate_fields(step, target_counts, attr_prefix, output)
|
99
|
-
return {} unless target_counts
|
100
|
-
sum = target_counts['__sum']
|
101
|
-
messages = target_counts.delete('__sum')
|
101
|
+
def generate_fields(counts_per_tag, output = {}, key_prefix = '')
|
102
|
+
return {} unless counts_per_tag
|
103
|
+
# total_count = counts_per_tag.delete('__total_count')
|
102
104
|
|
103
|
-
|
104
|
-
output[
|
105
|
-
output[
|
106
|
-
output[
|
107
|
-
if
|
108
|
-
|
109
|
-
|
105
|
+
counts_per_tag.each do |group_key, count|
|
106
|
+
output[key_prefix + group_key + @count_suffix] = count[:count] if count[:count]
|
107
|
+
output[key_prefix + group_key + "#{@delimiter}#{@min_key}#{@min_suffix}"] = count[:min] if count[:min]
|
108
|
+
output[key_prefix + group_key + "#{@delimiter}#{@max_key}#{@max_suffix}"] = count[:max] if count[:max]
|
109
|
+
output[key_prefix + group_key + "#{@delimiter}#{@avg_key}#{@avg_suffix}"] = count[:sum] / (count[:count] * 1.0) if count[:sum] and count[:count] > 0
|
110
|
+
# output[key_prefix + group_key + "#{@delimiter}rate"] = ((count[:count] * 100.0) / (1.00 * step)).floor / 100.0
|
111
|
+
# output[key_prefix + group_key + "#{@delimiter}percentage"] = count[:count] * 100.0 / (1.00 * total_count) if total_count > 0
|
110
112
|
end
|
111
113
|
|
112
114
|
output
|
113
115
|
end
|
114
116
|
|
115
|
-
def generate_output(counts
|
116
|
-
if @
|
117
|
-
return generate_fields(
|
118
|
-
end
|
117
|
+
def generate_output(counts)
|
118
|
+
if @output_per_tag # tag => output
|
119
|
+
return {'all' => generate_fields(counts['all'])} if @aggregate == :all
|
119
120
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
def generate_output_per_tags(counts, step)
|
128
|
-
if @aggregate == :all
|
129
|
-
return {'all' => generate_fields(step, counts['all'], '', {})}
|
130
|
-
end
|
121
|
+
output_pairs = {}
|
122
|
+
counts.keys.each do |tag|
|
123
|
+
output_pairs[stripped_tag(tag)] = generate_fields(counts[tag])
|
124
|
+
end
|
125
|
+
output_pairs
|
126
|
+
else
|
127
|
+
return generate_fields(counts['all']) if @aggregate == :all
|
131
128
|
|
132
|
-
|
133
|
-
|
134
|
-
|
129
|
+
output = {}
|
130
|
+
counts.keys.each do |tag|
|
131
|
+
generate_fields(counts[tag], output, stripped_tag(tag) + '_')
|
132
|
+
end
|
133
|
+
output
|
135
134
|
end
|
136
|
-
output_pairs
|
137
|
-
end
|
138
|
-
|
139
|
-
def flush(step) # returns one message
|
140
|
-
flushed,@counts = @counts,count_initialized()
|
141
|
-
generate_output(flushed, step)
|
142
135
|
end
|
143
136
|
|
144
|
-
def
|
145
|
-
flushed
|
146
|
-
|
137
|
+
def flush
|
138
|
+
flushed, @counts = @counts, count_initialized()
|
139
|
+
generate_output(flushed)
|
147
140
|
end
|
148
141
|
|
149
|
-
|
142
|
+
# this method emits messages (periodically called)
|
143
|
+
def flush_emit
|
144
|
+
time = Fluent::Engine.now
|
150
145
|
if @output_per_tag
|
151
|
-
|
152
|
-
time = Fluent::Engine.now
|
153
|
-
flush_per_tags(step).each do |tag,message|
|
146
|
+
flush.each do |tag, message|
|
154
147
|
Fluent::Engine.emit(@tag_prefix_string + tag, time, message)
|
155
148
|
end
|
156
149
|
else
|
157
|
-
message = flush
|
158
|
-
|
159
|
-
Fluent::Engine.emit(@tag, Fluent::Engine.now, message)
|
160
|
-
end
|
150
|
+
message = flush
|
151
|
+
Fluent::Engine.emit(@tag, time, message) unless message.empty?
|
161
152
|
end
|
162
153
|
end
|
163
154
|
|
@@ -168,34 +159,174 @@ class Fluent::GroupCounterOutput < Fluent::Output
|
|
168
159
|
|
169
160
|
def watch
|
170
161
|
# instance variable, and public accessable, for test
|
171
|
-
@last_checked
|
162
|
+
@last_checked ||= Fluent::Engine.now
|
172
163
|
while true
|
173
164
|
sleep 0.5
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
165
|
+
begin
|
166
|
+
if Fluent::Engine.now - @last_checked >= @count_interval
|
167
|
+
now = Fluent::Engine.now
|
168
|
+
flush_emit
|
169
|
+
@last_checked = now
|
170
|
+
end
|
171
|
+
rescue => e
|
172
|
+
$log.warn "#{e.class} #{e.message} #{e.backtrace.first}"
|
178
173
|
end
|
179
174
|
end
|
180
175
|
end
|
181
176
|
|
177
|
+
# recieve messages at here
|
182
178
|
def emit(tag, es, chain)
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
179
|
+
group_counts = {}
|
180
|
+
|
181
|
+
tags = tag.split('.')
|
182
|
+
es.each do |time, record|
|
183
|
+
count = {}
|
184
|
+
count[:count] = 1
|
185
|
+
count[:sum] = record[@avg_key].to_f if @avg_key and record[@avg_key]
|
186
|
+
count[:max] = record[@max_key].to_f if @max_key and record[@max_key]
|
187
|
+
count[:min] = record[@min_key].to_f if @min_key and record[@min_key]
|
188
|
+
|
189
|
+
group_key = group_key(tag, time, record)
|
190
|
+
|
191
|
+
group_counts[group_key] ||= {}
|
192
|
+
countup(group_counts[group_key], count)
|
196
193
|
end
|
197
|
-
|
194
|
+
summarize_counts(tag, group_counts)
|
198
195
|
|
199
196
|
chain.next
|
197
|
+
rescue => e
|
198
|
+
$log.warn "#{e.class} #{e.message} #{e.backtrace.first}"
|
199
|
+
end
|
200
|
+
|
201
|
+
# Summarize counts for each tag
|
202
|
+
def summarize_counts(tag, group_counts)
|
203
|
+
tag = 'all' if @aggregate == :all
|
204
|
+
@counts[tag] ||= {}
|
205
|
+
|
206
|
+
@mutex.synchronize {
|
207
|
+
group_counts.each do |group_key, count|
|
208
|
+
@counts[tag][group_key] ||= {}
|
209
|
+
countup(@counts[tag][group_key], count)
|
210
|
+
end
|
211
|
+
|
212
|
+
# total_count = group_counts.map {|group_key, count| count[:count] }.inject(:+)
|
213
|
+
# @counts[tag]['__total_count'] = sum(@counts[tag]['__total_count'], total_count)
|
214
|
+
}
|
215
|
+
end
|
216
|
+
|
217
|
+
def countup(counts, count)
|
218
|
+
counts[:count] = sum(counts[:count], count[:count])
|
219
|
+
counts[:sum] = sum(counts[:sum], count[:sum]) if @avg_key and count[:sum]
|
220
|
+
counts[:max] = max(counts[:max], count[:max]) if @max_key and count[:max]
|
221
|
+
counts[:min] = min(counts[:min], count[:min]) if @min_key and count[:min]
|
222
|
+
end
|
223
|
+
|
224
|
+
# Expand record with @group_by_keys, and get a value to be a group_key
|
225
|
+
def group_key(tag, time, record)
|
226
|
+
if @group_by_expression
|
227
|
+
tags = tag.split('.')
|
228
|
+
group_key = expand_placeholder(@group_by_expression, record, tag, tags, Time.at(time))
|
229
|
+
else # @group_by_keys
|
230
|
+
values = @group_by_keys.map {|key| record[key] || 'undef'}
|
231
|
+
group_key = values.join(@delimiter)
|
232
|
+
end
|
233
|
+
group_key = group_key.to_s.force_encoding('ASCII-8BIT')
|
234
|
+
end
|
235
|
+
|
236
|
+
def sum(a, b)
|
237
|
+
a ||= 0
|
238
|
+
b ||= 0
|
239
|
+
a + b
|
240
|
+
end
|
241
|
+
|
242
|
+
def max(a, b)
|
243
|
+
return b if a.nil?
|
244
|
+
return a if b.nil?
|
245
|
+
a > b ? a : b
|
246
|
+
end
|
247
|
+
|
248
|
+
def min(a, b)
|
249
|
+
return b if a.nil?
|
250
|
+
return a if b.nil?
|
251
|
+
a > b ? b : a
|
252
|
+
end
|
253
|
+
|
254
|
+
def stripped_tag(tag)
|
255
|
+
return tag unless @input_tag_remove_prefix
|
256
|
+
return tag[@removed_length..-1] if tag.start_with?(@removed_prefix_string) and tag.length > @removed_length
|
257
|
+
return tag[@removed_length..-1] if tag == @input_tag_remove_prefix
|
258
|
+
tag
|
259
|
+
end
|
260
|
+
|
261
|
+
# Store internal status into a file
|
262
|
+
#
|
263
|
+
# @param [String] file_path
|
264
|
+
def save_status(file_path)
|
265
|
+
|
266
|
+
begin
|
267
|
+
Pathname.new(file_path).open('wb') do |f|
|
268
|
+
@saved_at = Fluent::Engine.now
|
269
|
+
@saved_duration = @saved_at - @last_checked
|
270
|
+
Marshal.dump({
|
271
|
+
:counts => @counts,
|
272
|
+
:saved_at => @saved_at,
|
273
|
+
:saved_duration => @saved_duration,
|
274
|
+
:aggregate => @aggregate,
|
275
|
+
:group_by_keys => @group_by_keys,
|
276
|
+
}, f)
|
277
|
+
end
|
278
|
+
rescue => e
|
279
|
+
$log.warn "out_groupcounter: Can't write store_file #{e.class} #{e.message}"
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Load internal status from a file
|
284
|
+
#
|
285
|
+
# @param [String] file_path
|
286
|
+
# @param [Interger] count_interval
|
287
|
+
def load_status(file_path, count_interval)
|
288
|
+
return unless (f = Pathname.new(file_path)).exist?
|
289
|
+
|
290
|
+
begin
|
291
|
+
f.open('rb') do |f|
|
292
|
+
stored = Marshal.load(f)
|
293
|
+
if stored[:aggregate] == @aggregate and
|
294
|
+
stored[:group_by_keys] == @group_by_keys and
|
295
|
+
|
296
|
+
if Fluent::Engine.now <= stored[:saved_at] + count_interval
|
297
|
+
@counts = stored[:counts]
|
298
|
+
@saved_at = stored[:saved_at]
|
299
|
+
@saved_duration = stored[:saved_duration]
|
300
|
+
|
301
|
+
# skip the saved duration to continue counting
|
302
|
+
@last_checked = Fluent::Engine.now - @saved_duration
|
303
|
+
else
|
304
|
+
$log.warn "out_groupcounter: stored data is outdated. ignore stored data"
|
305
|
+
end
|
306
|
+
else
|
307
|
+
$log.warn "out_groupcounter: configuration param was changed. ignore stored data"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
rescue => e
|
311
|
+
$log.warn "out_groupcounter: Can't load store_file #{e.class} #{e.message}"
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
private
|
316
|
+
|
317
|
+
def expand_placeholder(str, record, tag, tags, time)
|
318
|
+
struct = UndefOpenStruct.new(record)
|
319
|
+
struct.tag = tag
|
320
|
+
struct.tags = tags
|
321
|
+
struct.time = time
|
322
|
+
struct.hostname = @hostname
|
323
|
+
str = str.gsub(/\$\{([^}]+)\}/, '#{\1}') # ${..} => #{..}
|
324
|
+
eval "\"#{str}\"", struct.instance_eval { binding }
|
325
|
+
end
|
326
|
+
|
327
|
+
class UndefOpenStruct < OpenStruct
|
328
|
+
(Object.instance_methods).each do |m|
|
329
|
+
undef_method m unless m.to_s =~ /^__|respond_to_missing\?|object_id|public_methods|instance_eval|method_missing|define_singleton_method|respond_to\?|new_ostruct_member/
|
330
|
+
end
|
200
331
|
end
|
201
332
|
end
|
@@ -0,0 +1,354 @@
|
|
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::GroupCounterOutput do
|
12
|
+
before { Fluent::Test.setup }
|
13
|
+
CONFIG = %[
|
14
|
+
count_interval 5s
|
15
|
+
aggragate tag
|
16
|
+
output_per_tag true
|
17
|
+
tag_prefix count
|
18
|
+
group_by_keys code,method,path
|
19
|
+
]
|
20
|
+
|
21
|
+
let(:tag) { 'test' }
|
22
|
+
let(:driver) { Fluent::Test::OutputTestDriver.new(Fluent::GroupCounterOutput, tag).configure(config) }
|
23
|
+
|
24
|
+
describe 'test configure' do
|
25
|
+
describe 'bad configuration' do
|
26
|
+
context 'test empty configuration' do
|
27
|
+
let(:config) { %[] }
|
28
|
+
it { expect { driver }.to raise_error(Fluent::ConfigError) }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'good configuration' do
|
33
|
+
subject { driver.instance }
|
34
|
+
|
35
|
+
context "test least configuration" do
|
36
|
+
let(:config) { %[group_by_keys foo] }
|
37
|
+
its(:count_interval) { should == 60 }
|
38
|
+
its(:unit) { should == 'minute' }
|
39
|
+
its(:output_per_tag) { should == false }
|
40
|
+
its(:aggregate) { should == :tag }
|
41
|
+
its(:tag) { should == 'groupcount' }
|
42
|
+
its(:tag_prefix) { should be_nil }
|
43
|
+
its(:input_tag_remove_prefix) { should be_nil }
|
44
|
+
its(:group_by_keys) { should == %w[foo] }
|
45
|
+
end
|
46
|
+
|
47
|
+
context "test template configuration" do
|
48
|
+
let(:config) { CONFIG }
|
49
|
+
its(:count_interval) { should == 5 }
|
50
|
+
its(:unit) { should == 'minute' }
|
51
|
+
its(:output_per_tag) { should == true }
|
52
|
+
its(:aggregate) { should == :tag }
|
53
|
+
its(:tag) { should == 'groupcount' }
|
54
|
+
its(:tag_prefix) { should == 'count' }
|
55
|
+
its(:input_tag_remove_prefix) { should be_nil }
|
56
|
+
its(:group_by_keys) { should == %w[code method path] }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'test emit' do
|
62
|
+
let(:time) { Time.now.to_i }
|
63
|
+
let(:emit) do
|
64
|
+
driver.run { messages.each {|message| driver.emit(message, time) } }
|
65
|
+
driver.instance.flush_emit
|
66
|
+
end
|
67
|
+
|
68
|
+
let(:messages) do
|
69
|
+
[
|
70
|
+
{"code" => 200, "method" => "GET", "path" => "/ping", "reqtime" => "0.000" },
|
71
|
+
{"code" => 200, "method" => "POST", "path" => "/auth", "reqtime" => "1.001" },
|
72
|
+
{"code" => 200, "method" => "GET", "path" => "/ping", "reqtime" => "2.002" },
|
73
|
+
{"code" => 400, "method" => "GET", "path" => "/ping", "reqtime" => "3.003" },
|
74
|
+
]
|
75
|
+
end
|
76
|
+
let(:expected) do
|
77
|
+
{
|
78
|
+
"200_GET_/ping_count"=>2,
|
79
|
+
"200_POST_/auth_count"=>1,
|
80
|
+
"400_GET_/ping_count"=>1,
|
81
|
+
}
|
82
|
+
end
|
83
|
+
let(:expected_with_tag) do
|
84
|
+
Hash[*(expected.map {|key, val| next ["test_#{key}", val] }.flatten)]
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'default' do
|
88
|
+
let(:config) { CONFIG }
|
89
|
+
before do
|
90
|
+
Fluent::Engine.stub(:now).and_return(time)
|
91
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
92
|
+
end
|
93
|
+
it { emit }
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'delimiter' do
|
97
|
+
let(:config) { CONFIG + %[delimiter /] }
|
98
|
+
let(:expected) do
|
99
|
+
{
|
100
|
+
"200/GET//ping_count"=>2,
|
101
|
+
"200/POST//auth_count"=>1,
|
102
|
+
"400/GET//ping_count"=>1,
|
103
|
+
}
|
104
|
+
end
|
105
|
+
before do
|
106
|
+
Fluent::Engine.stub(:now).and_return(time)
|
107
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
108
|
+
end
|
109
|
+
it { emit }
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'count_suffix' do
|
113
|
+
let(:config) { CONFIG + %[count_suffix /count] }
|
114
|
+
let(:expected) do
|
115
|
+
{
|
116
|
+
"200_GET_/ping/count"=>2,
|
117
|
+
"200_POST_/auth/count"=>1,
|
118
|
+
"400_GET_/ping/count"=>1,
|
119
|
+
}
|
120
|
+
end
|
121
|
+
before do
|
122
|
+
Fluent::Engine.stub(:now).and_return(time)
|
123
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
124
|
+
end
|
125
|
+
it { emit }
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'max_key' do
|
129
|
+
let(:config) { CONFIG + %[max_key reqtime] }
|
130
|
+
let(:expected) do
|
131
|
+
{
|
132
|
+
"200_GET_/ping_count"=>2, "200_GET_/ping_reqtime_max"=>2.002,
|
133
|
+
"200_POST_/auth_count"=>1, "200_POST_/auth_reqtime_max"=>1.001,
|
134
|
+
"400_GET_/ping_count"=>1, "400_GET_/ping_reqtime_max"=>3.003,
|
135
|
+
}
|
136
|
+
end
|
137
|
+
before do
|
138
|
+
Fluent::Engine.stub(:now).and_return(time)
|
139
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
140
|
+
end
|
141
|
+
it { emit }
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'max_suffix' do
|
145
|
+
let(:config) { CONFIG + %[max_key reqtime \n max_suffix /max] }
|
146
|
+
let(:expected) do
|
147
|
+
{
|
148
|
+
"200_GET_/ping_count"=>2, "200_GET_/ping_reqtime/max"=>2.002,
|
149
|
+
"200_POST_/auth_count"=>1, "200_POST_/auth_reqtime/max"=>1.001,
|
150
|
+
"400_GET_/ping_count"=>1, "400_GET_/ping_reqtime/max"=>3.003,
|
151
|
+
}
|
152
|
+
end
|
153
|
+
before do
|
154
|
+
Fluent::Engine.stub(:now).and_return(time)
|
155
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
156
|
+
end
|
157
|
+
it { emit }
|
158
|
+
end
|
159
|
+
|
160
|
+
context 'min_key' do
|
161
|
+
let(:config) { CONFIG + %[min_key reqtime] }
|
162
|
+
let(:expected) do
|
163
|
+
{
|
164
|
+
"200_GET_/ping_count"=>2, "200_GET_/ping_reqtime_min"=>0.000,
|
165
|
+
"200_POST_/auth_count"=>1, "200_POST_/auth_reqtime_min"=>1.001,
|
166
|
+
"400_GET_/ping_count"=>1, "400_GET_/ping_reqtime_min"=>3.003,
|
167
|
+
}
|
168
|
+
end
|
169
|
+
before do
|
170
|
+
Fluent::Engine.stub(:now).and_return(time)
|
171
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
172
|
+
end
|
173
|
+
it { emit }
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'min_suffix' do
|
177
|
+
let(:config) { CONFIG + %[min_key reqtime \n min_suffix /min] }
|
178
|
+
let(:expected) do
|
179
|
+
{
|
180
|
+
"200_GET_/ping_count"=>2, "200_GET_/ping_reqtime/min"=>0.000,
|
181
|
+
"200_POST_/auth_count"=>1, "200_POST_/auth_reqtime/min"=>1.001,
|
182
|
+
"400_GET_/ping_count"=>1, "400_GET_/ping_reqtime/min"=>3.003,
|
183
|
+
}
|
184
|
+
end
|
185
|
+
before do
|
186
|
+
Fluent::Engine.stub(:now).and_return(time)
|
187
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
188
|
+
end
|
189
|
+
it { emit }
|
190
|
+
end
|
191
|
+
|
192
|
+
context 'avg_key' do
|
193
|
+
let(:config) { CONFIG + %[avg_key reqtime] }
|
194
|
+
let(:expected) do
|
195
|
+
{
|
196
|
+
"200_GET_/ping_count"=>2, "200_GET_/ping_reqtime_avg"=>1.001,
|
197
|
+
"200_POST_/auth_count"=>1, "200_POST_/auth_reqtime_avg"=>1.001,
|
198
|
+
"400_GET_/ping_count"=>1, "400_GET_/ping_reqtime_avg"=>3.003,
|
199
|
+
}
|
200
|
+
end
|
201
|
+
before do
|
202
|
+
Fluent::Engine.stub(:now).and_return(time)
|
203
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
204
|
+
end
|
205
|
+
it { emit }
|
206
|
+
end
|
207
|
+
|
208
|
+
context 'avg_suffix' do
|
209
|
+
let(:config) { CONFIG + %[avg_key reqtime \n avg_suffix /avg] }
|
210
|
+
let(:expected) do
|
211
|
+
{
|
212
|
+
"200_GET_/ping_count"=>2, "200_GET_/ping_reqtime/avg"=>1.001,
|
213
|
+
"200_POST_/auth_count"=>1, "200_POST_/auth_reqtime/avg"=>1.001,
|
214
|
+
"400_GET_/ping_count"=>1, "400_GET_/ping_reqtime/avg"=>3.003,
|
215
|
+
}
|
216
|
+
end
|
217
|
+
before do
|
218
|
+
Fluent::Engine.stub(:now).and_return(time)
|
219
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
220
|
+
end
|
221
|
+
it { emit }
|
222
|
+
end
|
223
|
+
|
224
|
+
context 'tag' do
|
225
|
+
context 'not effective if output_per_tag true' do
|
226
|
+
let(:config) do
|
227
|
+
CONFIG + %[
|
228
|
+
output_per_tag true
|
229
|
+
tag foo
|
230
|
+
]
|
231
|
+
end
|
232
|
+
before do
|
233
|
+
Fluent::Engine.stub(:now).and_return(time)
|
234
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
235
|
+
end
|
236
|
+
it { emit }
|
237
|
+
end
|
238
|
+
|
239
|
+
context 'effective if output_per_tag false' do
|
240
|
+
let(:config) do
|
241
|
+
CONFIG + %[
|
242
|
+
output_per_tag false
|
243
|
+
tag foo
|
244
|
+
]
|
245
|
+
end
|
246
|
+
before do
|
247
|
+
Fluent::Engine.stub(:now).and_return(time)
|
248
|
+
Fluent::Engine.should_receive(:emit).with("foo", time, expected_with_tag)
|
249
|
+
end
|
250
|
+
it { emit }
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
context 'tag_prefix' do
|
255
|
+
let(:config) do
|
256
|
+
CONFIG + %[
|
257
|
+
tag_prefix foo
|
258
|
+
]
|
259
|
+
end
|
260
|
+
before do
|
261
|
+
Fluent::Engine.stub(:now).and_return(time)
|
262
|
+
Fluent::Engine.should_receive(:emit).with("foo.#{tag}", time, expected)
|
263
|
+
end
|
264
|
+
it { emit }
|
265
|
+
end
|
266
|
+
|
267
|
+
context 'aggregate all' do
|
268
|
+
let(:emit) do
|
269
|
+
driver.run { messages.each {|message| driver.emit_with_tag(message, time, 'foo.bar') } }
|
270
|
+
driver.run { messages.each {|message| driver.emit_with_tag(message, time, 'foo.bar2') } }
|
271
|
+
driver.instance.flush_emit
|
272
|
+
end
|
273
|
+
|
274
|
+
let(:config) do
|
275
|
+
CONFIG + %[
|
276
|
+
aggregate all
|
277
|
+
tag_prefix count
|
278
|
+
]
|
279
|
+
end
|
280
|
+
|
281
|
+
before do
|
282
|
+
Fluent::Engine.stub(:now).and_return(time)
|
283
|
+
Fluent::Engine.should_receive(:emit).with("count.all", time, {
|
284
|
+
"200_GET_/ping_count"=>4, "200_POST_/auth_count"=>2, "400_GET_/ping_count"=>2
|
285
|
+
})
|
286
|
+
end
|
287
|
+
it { emit }
|
288
|
+
end
|
289
|
+
|
290
|
+
context "store_file" do
|
291
|
+
let(:store_file) do
|
292
|
+
dirname = "tmp"
|
293
|
+
Dir.mkdir dirname unless Dir.exist? dirname
|
294
|
+
filename = "#{dirname}/test.dat"
|
295
|
+
File.unlink filename if File.exist? filename
|
296
|
+
filename
|
297
|
+
end
|
298
|
+
|
299
|
+
let(:config) do
|
300
|
+
CONFIG + %[
|
301
|
+
store_file #{store_file}
|
302
|
+
]
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'stored_data and loaded_data should equal' do
|
306
|
+
driver.run { messages.each {|message| driver.emit({'message' => message}, time) } }
|
307
|
+
driver.instance.shutdown
|
308
|
+
stored_counts = driver.instance.counts
|
309
|
+
stored_saved_at = driver.instance.saved_at
|
310
|
+
stored_saved_duration = driver.instance.saved_duration
|
311
|
+
driver.instance.counts = {}
|
312
|
+
driver.instance.saved_at = nil
|
313
|
+
driver.instance.saved_duration = nil
|
314
|
+
|
315
|
+
driver.instance.start
|
316
|
+
loaded_counts = driver.instance.counts
|
317
|
+
loaded_saved_at = driver.instance.saved_at
|
318
|
+
loaded_saved_duration = driver.instance.saved_duration
|
319
|
+
|
320
|
+
loaded_counts.should == stored_counts
|
321
|
+
loaded_saved_at.should == stored_saved_at
|
322
|
+
loaded_saved_duration.should == stored_saved_duration
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
context 'group_by_expression' do
|
327
|
+
let(:config) { CONFIG + %[group_by_expression ${method}_${path.split("?")[0].split("/")[2]}/${code[0]}xx] }
|
328
|
+
let(:messages) do
|
329
|
+
[
|
330
|
+
{"code" => "200", "method" => "GET", "path" => "/api/people/@me/@self?count=1", "reqtime" => 0.000 },
|
331
|
+
{"code" => "200", "method" => "POST", "path" => "/api/ngword?_method=check", "reqtime" => 1.001 },
|
332
|
+
{"code" => "400", "method" => "GET", "path" => "/api/messages/@me/@outbox", "reqtime" => 2.002 },
|
333
|
+
{"code" => "201", "method" => "GET", "path" => "/api/people/@me/@self", "reqtime" => 3.003 },
|
334
|
+
]
|
335
|
+
end
|
336
|
+
let(:expected) do
|
337
|
+
{
|
338
|
+
"GET_people/2xx_count"=>2,
|
339
|
+
"POST_ngword/2xx_count"=>1,
|
340
|
+
"GET_messages/4xx_count"=>1,
|
341
|
+
}
|
342
|
+
end
|
343
|
+
before do
|
344
|
+
Fluent::Engine.stub(:now).and_return(time)
|
345
|
+
Fluent::Engine.should_receive(:emit).with("count.#{tag}", time, expected)
|
346
|
+
end
|
347
|
+
it { emit }
|
348
|
+
end
|
349
|
+
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
|
354
|
+
|
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_groupcounter'
|
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fluent-plugin-groupcounter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.2.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Ryosuke IWANAGA
|
@@ -10,98 +9,120 @@ authors:
|
|
10
9
|
autorequire:
|
11
10
|
bindir: bin
|
12
11
|
cert_chain: []
|
13
|
-
date: 2013-
|
12
|
+
date: 2013-08-29 00:00:00.000000000 Z
|
14
13
|
dependencies:
|
15
14
|
- !ruby/object:Gem::Dependency
|
16
15
|
name: fluentd
|
17
16
|
requirement: !ruby/object:Gem::Requirement
|
18
|
-
none: false
|
19
17
|
requirements:
|
20
|
-
- -
|
18
|
+
- - '>='
|
21
19
|
- !ruby/object:Gem::Version
|
22
20
|
version: '0'
|
23
21
|
type: :runtime
|
24
22
|
prerelease: false
|
25
23
|
version_requirements: !ruby/object:Gem::Requirement
|
26
|
-
none: false
|
27
24
|
requirements:
|
28
|
-
- -
|
25
|
+
- - '>='
|
29
26
|
- !ruby/object:Gem::Version
|
30
27
|
version: '0'
|
31
28
|
- !ruby/object:Gem::Dependency
|
32
|
-
name:
|
29
|
+
name: rake
|
33
30
|
requirement: !ruby/object:Gem::Requirement
|
34
|
-
none: false
|
35
31
|
requirements:
|
36
|
-
- -
|
32
|
+
- - '>='
|
37
33
|
- !ruby/object:Gem::Version
|
38
34
|
version: '0'
|
39
35
|
type: :development
|
40
36
|
prerelease: false
|
41
37
|
version_requirements: !ruby/object:Gem::Requirement
|
42
|
-
none: false
|
43
38
|
requirements:
|
44
|
-
- -
|
39
|
+
- - '>='
|
45
40
|
- !ruby/object:Gem::Version
|
46
41
|
version: '0'
|
47
42
|
- !ruby/object:Gem::Dependency
|
48
|
-
name:
|
43
|
+
name: rspec
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: pry
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: pry-nav
|
49
72
|
requirement: !ruby/object:Gem::Requirement
|
50
|
-
none: false
|
51
73
|
requirements:
|
52
|
-
- -
|
74
|
+
- - '>='
|
53
75
|
- !ruby/object:Gem::Version
|
54
76
|
version: '0'
|
55
77
|
type: :development
|
56
78
|
prerelease: false
|
57
79
|
version_requirements: !ruby/object:Gem::Requirement
|
58
|
-
none: false
|
59
80
|
requirements:
|
60
|
-
- -
|
81
|
+
- - '>='
|
61
82
|
- !ruby/object:Gem::Version
|
62
83
|
version: '0'
|
63
|
-
description: Fluentd plugin to count like COUNT(\*) GROUP BY
|
84
|
+
description: Fluentd plugin to count like SELECT COUNT(\*) GROUP BY
|
64
85
|
email:
|
65
|
-
-
|
66
|
-
-
|
86
|
+
- '@riywo'
|
87
|
+
- sonots@gmail.com
|
67
88
|
executables: []
|
68
89
|
extensions: []
|
69
90
|
extra_rdoc_files: []
|
70
91
|
files:
|
71
92
|
- .gitignore
|
93
|
+
- .pryrc
|
94
|
+
- .travis.yml
|
72
95
|
- Gemfile
|
73
96
|
- README.md
|
74
97
|
- Rakefile
|
75
98
|
- fluent-plugin-groupcounter.gemspec
|
76
99
|
- lib/fluent/plugin/out_groupcounter.rb
|
100
|
+
- spec/out_groupcounter_spec.rb
|
101
|
+
- spec/spec_helper.rb
|
77
102
|
homepage: https://github.com/riywo/fluent-plugin-groupcounter
|
78
103
|
licenses: []
|
104
|
+
metadata: {}
|
79
105
|
post_install_message:
|
80
106
|
rdoc_options: []
|
81
107
|
require_paths:
|
82
108
|
- lib
|
83
109
|
required_ruby_version: !ruby/object:Gem::Requirement
|
84
|
-
none: false
|
85
110
|
requirements:
|
86
|
-
- -
|
111
|
+
- - '>='
|
87
112
|
- !ruby/object:Gem::Version
|
88
113
|
version: '0'
|
89
|
-
segments:
|
90
|
-
- 0
|
91
|
-
hash: -1667358742827062990
|
92
114
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
93
|
-
none: false
|
94
115
|
requirements:
|
95
|
-
- -
|
116
|
+
- - '>='
|
96
117
|
- !ruby/object:Gem::Version
|
97
118
|
version: '0'
|
98
|
-
segments:
|
99
|
-
- 0
|
100
|
-
hash: -1667358742827062990
|
101
119
|
requirements: []
|
102
120
|
rubyforge_project: fluent-plugin-groupcounter
|
103
|
-
rubygems_version:
|
121
|
+
rubygems_version: 2.0.2
|
104
122
|
signing_key:
|
105
|
-
specification_version:
|
106
|
-
summary: Fluentd plugin to count like COUNT(\*) GROUP BY
|
107
|
-
test_files:
|
123
|
+
specification_version: 4
|
124
|
+
summary: Fluentd plugin to count like SELECT COUNT(\*) GROUP BY
|
125
|
+
test_files:
|
126
|
+
- spec/out_groupcounter_spec.rb
|
127
|
+
- spec/spec_helper.rb
|
128
|
+
has_rdoc:
|