fluent-plugin-groupcounter 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 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: