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 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
@@ -0,0 +1,3 @@
1
+ Pry.commands.alias_command 'c', 'continue'
2
+ Pry.commands.alias_command 's', 'step'
3
+ Pry.commands.alias_command 'n', 'next'
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/README.md CHANGED
@@ -1,43 +1,124 @@
1
1
  # fluent-plugin-groupcounter
2
2
 
3
- ## Component
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
- ## GroupCounterOutput
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
- <source>
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 below
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
- groupcounter.apache.access: {"200_GET_/index.html_count":1,"200_GET_/index.html_rate":0.2,"200_GET_/index.html_percentage":100.0}
112
+ Default is `_min`. Should be used with `min_key` option.
32
113
 
33
- ## TODO
114
+ * avg\_suffix
34
115
 
35
- * tests
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.1.0"
6
+ s.version = "0.2.0"
7
7
  s.authors = ["Ryosuke IWANAGA", "Naotoshi SEO"]
8
- s.email = ["@riywo", "@sonots"]
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 :output_messages, :bool, :default => false
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 :tick
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
- @tick = @count_interval.to_i
42
+ @count_interval = @count_interval.to_i
23
43
  else
24
- @tick = case @unit
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 countups(tag, counts)
74
- if @aggregate == :all
75
- tag = 'all'
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
- target_counts.each do |key, count|
104
- output[attr_prefix + key + '_count'] = count
105
- output[attr_prefix + key + '_rate'] = ((count * 100.0) / (1.00 * step)).floor / 100.0
106
- output[attr_prefix + key + '_percentage'] = count * 100.0 / (1.00 * sum) if sum > 0
107
- if @output_messages
108
- output[attr_prefix + 'messages'] = messages
109
- end
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, step)
116
- if @aggregate == :all
117
- return generate_fields(step, counts['all'], '', {})
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
- output = {}
121
- counts.keys.each do |tag|
122
- generate_fields(step, counts[tag], stripped_tag(tag) + '_', output)
123
- end
124
- output
125
- end
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
- output_pairs = {}
133
- counts.keys.each do |tag|
134
- output_pairs[stripped_tag(tag)] = generate_fields(step, counts[tag], '', {})
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 flush_per_tags(step) # returns map of tag - message
145
- flushed,@counts = @counts,count_initialized()
146
- generate_output_per_tags(flushed, step)
137
+ def flush
138
+ flushed, @counts = @counts, count_initialized()
139
+ generate_output(flushed)
147
140
  end
148
141
 
149
- def flush_emit(step)
142
+ # this method emits messages (periodically called)
143
+ def flush_emit
144
+ time = Fluent::Engine.now
150
145
  if @output_per_tag
151
- # tag - message maps
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(step)
158
- if message.keys.size > 0
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 = Fluent::Engine.now
162
+ @last_checked ||= Fluent::Engine.now
172
163
  while true
173
164
  sleep 0.5
174
- if Fluent::Engine.now - @last_checked >= @tick
175
- now = Fluent::Engine.now
176
- flush_emit(now - @last_checked)
177
- @last_checked = now
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
- c = {}
184
-
185
- es.each do |time,record|
186
- values = []
187
- @group_by_keys.each { |key|
188
- v = record[key] || 'undef'
189
- values.push(v)
190
- }
191
- value = values.join('_')
192
-
193
- value = value.to_s.force_encoding('ASCII-8BIT')
194
- c[value] ||= 0
195
- c[value] += 1
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
- countups(tag, c)
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
+
@@ -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.1.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-02-06 00:00:00.000000000 Z
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: fluentd
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: rake
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
- - ! '@riywo'
66
- - ! '@sonots'
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: 1.8.23
121
+ rubygems_version: 2.0.2
104
122
  signing_key:
105
- specification_version: 3
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: