fluent-plugin-in-kinesis 0.0.1

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: 8ebcfb694d8dbaada79463049e7e433714c030ea
4
+ data.tar.gz: b4a49fa0a0616af62f27c3c8ada4bcb11ffce8f9
5
+ SHA512:
6
+ metadata.gz: c14fa067fccac7214cdf06c5f0fe0e78e53c76a2f432a0246e11dc486f11784fbf42d5774fd0f647ae856ab2fcafd53f6bb5e5388b1279fad5f679b063785311
7
+ data.tar.gz: bed45bcc8a7aa050a6edd31c6711e96d5cb8b5120c0fea398673dc3c14fc26c3c04dc067ddb46b348fa6a2c88d00be21032276b9abaa492129755692d747892c
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ # For TextMate, emacs, vim
6
+ *.tmproj
7
+ tmtags
8
+ *~
9
+ \#*
10
+ .\#*
11
+ *.swp
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-in-kinesis.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 優介 山谷
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Fluent::Plugin::InKinesis
2
+
3
+ ##Overview
4
+ * This plugin retrieves records from Amazon Kinesis.
5
+
6
+ * 1 thread is used for each shard; record retrieval occurs in parallel.
7
+
8
+ * Number of threads is automatically adjusted to match number of shards.
9
+
10
+ * Sequence numbers from each shard are saved.
11
+
12
+ * Conforms to default fluent format.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'fluent-plugin-in-kinesis'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install fluent-plugin-in-kinesis
29
+
30
+
31
+ ##Configuration
32
+ ### `type`
33
+ Use the word 'kinesis'.
34
+
35
+ ### `stream_name`
36
+ Name of the stream to put data.
37
+
38
+ ### `aws_key_id`
39
+ AWS access key id.
40
+
41
+ ### `aws_sec_key`
42
+ AWS secret key.
43
+
44
+ ### `region`
45
+ AWS region of your stream.
46
+ It should be in form like "us-east-1", "us-west-2".
47
+
48
+ Refer to [Regions and Endpoints in AWS General Reference](http://docs.aws.amazon.com/general/latest/gr/rande.html#ak_region)
49
+
50
+ for supported regions.
51
+
52
+ ### `profile` & `credentials_path`
53
+ Set as needed to specify credentials file.
54
+
55
+ ### `stream_name`
56
+ Name of the stream to put data.
57
+
58
+ ### `state_dir_path`
59
+ Directory to save sequence number data.
60
+ Save file will be created in specified directory.
61
+
62
+ ### `use_base64`
63
+ Set if BASE64 decode is necessary.
64
+
65
+ ### `load_records_limit`
66
+ The maximum number of records to return.
67
+
68
+ Valid range: Minimum value of 1. Maximum value of 10000.
69
+
70
+ ### `load_record_interval`
71
+ Frequency of record retrieval.
72
+
73
+ Value is in seconds.
74
+
75
+ ### `load_shard_interval`
76
+ Frequency of shard state checks.
77
+
78
+ Value is in seconds.
79
+
80
+ ### `format`
81
+ Parse strings in log.
82
+ fluentd default parser.
83
+
84
+ ### `describe_shard`
85
+ Set to manually specify target Kinesis shards (see below).
86
+
87
+ ### `describe_use_shards`
88
+ Specify the shards to be used (see below).
89
+
90
+ ##Configuration examples
91
+ <source>
92
+
93
+ type kinesis
94
+
95
+ stream_name YOUR_STREAM_NAME
96
+
97
+ aws_key_id YOUR_AWS_ACCESS_KEY
98
+
99
+ aws_sec_key YOUR_SECRET_KEY
100
+
101
+ region ap-northeast-1
102
+
103
+ load_records_limit 1000
104
+
105
+ load_shard_interval 10
106
+
107
+ load_record_interval 2
108
+
109
+ tag target.log
110
+
111
+ state_dir_path /tmp/kinesis/save_file
112
+
113
+ use_base64 true
114
+
115
+ format json
116
+
117
+ </source>
118
+
119
+ ##Using describe_shard
120
+ When describe_shard is specified, target shards are manually set using describe_use_shards parameter.
121
+
122
+ <source>
123
+
124
+ type kinesis
125
+
126
+ stream_name YOUR_STREAM_NAME
127
+
128
+ aws_key_id YOUR_AWS_ACCESS_KEY
129
+
130
+ aws_sec_key YOUR_SECRET_KEY
131
+
132
+ region ap-northeast-1
133
+
134
+ load_records_limit 1000
135
+
136
+ load_shard_interval 10
137
+
138
+ load_record_interval 2
139
+
140
+ tag target.log
141
+
142
+ state_dir_path /tmp/kinesis/save_file
143
+
144
+ use_base64 true
145
+
146
+ format json
147
+
148
+ describe_shard true
149
+
150
+ describe_use_shards ["shardId-000000000000", "shardId-000000000002"]
151
+
152
+ </source>
153
+
154
+ ## Related Resources
155
+
156
+ * [Amazon Kinesis Developer Guide](http://docs.aws.amazon.com/kinesis/latest/dev/introduction.html)
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+ Gem::Specification.new do |spec|
3
+ spec.name = "fluent-plugin-in-kinesis"
4
+ spec.version = "0.0.1"
5
+ spec.authors = ["yusuke yamatani "]
6
+ spec.homepage = "https://github.com/yusukeyamatani/fluent-plugin-in-kinesis"
7
+ spec.summary = %q{Fluentd plugin to count records with specified regexp patterns}
8
+ spec.description = %q{To count records with string fields by regexps (To count records with numbers, use numeric-counter)}
9
+ spec.license = "MIT"
10
+
11
+ spec.files = `git ls-files`.split("\n")
12
+ spec.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
13
+ spec.require_paths = ["lib"]
14
+
15
+ spec.add_runtime_dependency "fluentd"
16
+
17
+ spec.add_development_dependency "bundler"
18
+ spec.add_development_dependency "rake"
19
+
20
+ end
@@ -0,0 +1,106 @@
1
+ # Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4
+ # may not use this file except in compliance with the License. A copy of
5
+ # the License is located at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # or in the "license" file accompanying this file. This file is
10
+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
+ # ANY KIND, either express or implied. See the License for the specific
12
+ # language governing permissions and limitations under the License.
13
+
14
+ require 'aws-sdk-core'
15
+ require 'multi_json'
16
+ require 'yajl'
17
+ require 'logger'
18
+ require 'securerandom'
19
+ require 'base64'
20
+ require 'fluent/plugin/thread_supervisor'
21
+ require 'fluent/plugin/kinesis_shard'
22
+
23
+
24
+ module FluentPluginKinesis
25
+ class InputFilter < Fluent::Input
26
+ include Fluent::DetachMultiProcessMixin
27
+ include KinesisSupervisor
28
+ include KinesisShard
29
+
30
+ USER_AGENT_NAME = 'fluent-plugin-kinesis-input-filter'
31
+
32
+ Fluent::Plugin.register_input("kinesis", self)
33
+
34
+ config_param :tag, :string, :default => nil
35
+ config_param :state_dir_path, :string, :default => nil
36
+
37
+ config_param :aws_key_id, :string, :default => nil, :secret => true
38
+ config_param :aws_sec_key, :string, :default => nil, :secret => true
39
+ config_param :region, :string, :default => nil
40
+ config_param :profile, :string, :default => nil
41
+ config_param :credentials_path, :string, :default => nil
42
+ config_param :stream_name, :string
43
+
44
+ config_param :use_base64, :bool, :default => false, :secret => true
45
+ config_param :load_records_limit, :integer, :default => 10000, :secret => true
46
+ config_param :load_record_interval, :integer, :default => 1, :secret => true #=> sec
47
+ config_param :load_shard_interval, :integer, :default => 1, :secret => true #=> sec
48
+ config_param :format, :string, :default => 'none', :secret => true
49
+ config_param :describe_shard, :bool, :default => false, :secret => true
50
+ config_param :describe_use_shards, :array, :default => [], :secret => true
51
+
52
+ def configure(conf)
53
+ super
54
+
55
+ unless @state_dir_path
56
+ $log.warn "'state_dir_path PATH' parameter is not set to a 'kinesis' source."
57
+ $log.warn "this parameter is highly recommended to save the last rows to resume tailing."
58
+ end
59
+ @parser = Fluent::Plugin.new_parser(conf['format'])
60
+ @parser.configure(conf)
61
+
62
+ @map = {} #=> Thread Object management
63
+ @thread_stop_map = {} #=> Thread stop flag management
64
+ @dead_thread=[] #=> Dead Thread management
65
+ end
66
+
67
+ def start
68
+ detach_multi_process do
69
+ super
70
+ @stop_flag = false
71
+ load_client
72
+ Thread.new(&method(:supervisor_thread))
73
+ end
74
+ end
75
+
76
+ def shutdown
77
+ @stop_flag = true
78
+ end
79
+
80
+ def load_client
81
+ user_agent_suffix = "#{USER_AGENT_NAME}/#{FluentPluginKinesis::VERSION}"
82
+
83
+ options = {
84
+ user_agent_suffix: user_agent_suffix
85
+ }
86
+
87
+ if @region
88
+ options[:region] = @region
89
+ end
90
+
91
+ if @aws_key_id && @aws_sec_key
92
+ options.update(
93
+ access_key_id: @aws_key_id,
94
+ secret_access_key: @aws_sec_key,
95
+ )
96
+ elsif @profile
97
+ credentials_opts = {:profile_name => @profile}
98
+ credentials_opts[:path] = @credentials_path if @credentials_path
99
+ credentials = Aws::SharedCredentials.new(credentials_opts)
100
+ options[:credentials] = credentials
101
+ end
102
+
103
+ @client = Aws::Kinesis::Client.new(options)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,152 @@
1
+
2
+
3
+ module KinesisShard
4
+
5
+ def load_records_thread(shard_id)
6
+ begin
7
+ state_store = @state_dir_path.nil? ? MemoryStateStore.new : StateStore.new(@state_dir_path, shard_id)
8
+ rescue => e
9
+ $log.warn "does not StateStore !!: #{e.message}"
10
+ state_store = MemoryStateStore.new
11
+ end
12
+
13
+ last_sequence_number = state_store.load_sequence_number
14
+ shard_iterator_info = get_shard_iterator_info(shard_id, last_sequence_number)
15
+ shard_iterator = shard_iterator_info.shard_iterator
16
+
17
+ while !@stop_flag && !@thread_stop_map[shard_id] do
18
+ begin
19
+ records_info = @client.get_records(shard_iterator: shard_iterator, limit: @load_records_limit)
20
+ rescue => e
21
+ $log.error "get record Error: #{e.message}"
22
+ re_shard_iterator_info = get_shard_iterator_info(shard_id, last_sequence_number)
23
+ records_info = @client.get_records(
24
+ shard_iterator: re_shard_iterator_info.shard_iterator, limit: @load_records_limit/10)
25
+ end
26
+
27
+ if records_info.next_shard_iterator.nil?
28
+ @thread_stop_map[shard_id] = true
29
+ break
30
+ end
31
+
32
+ data = records_info.records.map(&:data)
33
+ emit_records(data, shard_id)
34
+ tmp_last_sequence_number = sequence(records_info)
35
+
36
+ unless tmp_last_sequence_number.nil?
37
+ state_store.update(tmp_last_sequence_number)
38
+ last_sequence_number = tmp_last_sequence_number
39
+ end
40
+
41
+ shard_iterator = records_info.next_shard_iterator
42
+ sleep(@load_record_interval)
43
+ end
44
+ end
45
+
46
+ def get_shard_iterator_info(shard_id='', last_sequence_number='')
47
+ if last_sequence_number.empty?
48
+ shard_iterator_info = @client.get_shard_iterator(
49
+ stream_name: @stream_name, shard_id: shard_id, shard_iterator_type: 'TRIM_HORIZON')
50
+ else
51
+ shard_iterator_info = @client.get_shard_iterator(
52
+ stream_name: @stream_name, shard_id: shard_id, shard_iterator_type: 'AFTER_SEQUENCE_NUMBER', starting_sequence_number: last_sequence_number)
53
+ end
54
+ rescue => e
55
+ $log.warn "does not AFTER_SEQUENCE_NUMBER : #{e.message}"
56
+ shard_iterator_info = @client.get_shard_iterator(
57
+ stream_name: @stream_name, shard_id: shard_id, shard_iterator_type: 'TRIM_HORIZON')
58
+ end
59
+
60
+ def emit_records(data, shard_id)
61
+ me = Fluent::MultiEventStream.new
62
+ data.each do |d|
63
+ if @use_base64
64
+ d = Base64.decode64(d)
65
+ end
66
+
67
+ time, record = @parser.parse(d)
68
+ if record.nil? || record.empty?
69
+ $log.warn "format error :=> record #{time} : #{d}"
70
+ else
71
+ me.add(time, record)
72
+ end
73
+ end
74
+
75
+ unless me.empty?
76
+ router.emit_stream(@tag, me)
77
+ end
78
+ end
79
+
80
+ def sequence(records_info)
81
+ sequence_number_list = records_info.records.map(&:sequence_number)
82
+ if sequence_number_list.length > 0
83
+ sequence_number = records_info.records.map(&:sequence_number)[-1]
84
+ else
85
+ sequence_number = nil
86
+ end
87
+ end
88
+
89
+
90
+ class StateStore
91
+ def initialize(dir_path, shard_id)
92
+
93
+ unless Dir.exist?(dir_path)
94
+ begin
95
+ FileUtils.mkdir_p(dir_path)
96
+ rescue => e
97
+ raise "does not make a directory : #{e.message}"
98
+ end
99
+ end
100
+ @path = "#{dir_path}/last_recode_#{shard_id}.json"
101
+
102
+ if File.exists?(@path)
103
+ begin
104
+ load_json_file
105
+ rescue => e
106
+ $log.warn "load_json_file: #{e.message}"
107
+ end
108
+ end
109
+
110
+ if @data.nil?
111
+ @data = {'last_sequence_number' => ''}
112
+ end
113
+
114
+ unless @data.is_a?(Hash)
115
+ raise "state_file on #{@path.inspect} is invalid"
116
+ end
117
+ end
118
+
119
+ def load_json_file()
120
+ open(@path) do |io|
121
+ @data =Yajl.load(io)
122
+ end
123
+ end
124
+
125
+ def load_sequence_number
126
+ @data['last_sequence_number']
127
+ end
128
+
129
+ def update(sequence_number)
130
+ @data['last_sequence_number'] = sequence_number
131
+ open(@path, "w") do |io|
132
+ Yajl.dump(@data, io)
133
+ end
134
+ end
135
+ end
136
+
137
+ class MemoryStateStore
138
+
139
+ def initialize
140
+ @data = {'last_sequence_number' => ''}
141
+ end
142
+
143
+ def load_sequence_number
144
+ @data['last_sequence_number']
145
+ end
146
+
147
+ def update(sequence_number)
148
+ @data['last_sequence_number'] = sequence_number
149
+ end
150
+ end
151
+ end
152
+
@@ -0,0 +1,68 @@
1
+
2
+
3
+ module KinesisSupervisor
4
+
5
+ def supervisor_thread()
6
+ until @stop_flag do
7
+ active_shard_ids = get_shard_ids()
8
+ update_maping(active_shard_ids)
9
+ sleep(@load_shard_interval)
10
+ end
11
+ end
12
+
13
+ def update_maping(active_shard_ids)
14
+ active_shard_ids.each do |shard_id|
15
+ if @map.has_key?(shard_id)
16
+ if @map[shard_id].status.nil?
17
+ $log.error "Thread dead => shard : #{shard_id}"
18
+ thread_kill(shard_id)
19
+ elsif @thread_stop_map[shard_id]
20
+ thread_kill(shard_id)
21
+ else
22
+ next
23
+ end
24
+ else
25
+ @thread_stop_map[shard_id] = false
26
+ t = Thread.new(shard_id, &method(:load_records_thread))
27
+ @map[shard_id] = t
28
+ end
29
+ end
30
+
31
+ map_shard_ids = @map.keys
32
+ map_shard_ids.each do |map_shard_id|
33
+ unless active_shard_ids.include?(map_shard_id)
34
+ @thread_stop_map[shard_id] = true
35
+ thread_kill(map_shard_id)
36
+ end
37
+ end
38
+ end
39
+
40
+ def thread_kill(shard_id)
41
+ $log.info "Thread killing => shard : #{shard_id}"
42
+ @map[shard_id].join
43
+ @dead_thread << shard_id
44
+ @thread_stop_map.delete(shard_id)
45
+ @map.delete(shard_id)
46
+ end
47
+
48
+ def get_shard_ids()
49
+ active_shard_ids = []
50
+ shards = @client.describe_stream(stream_name: @stream_name).stream_description.shards
51
+ shards.each do |shard|
52
+ if @describe_shard & !@describe_use_shards.include?(shard.shard_id)
53
+ next
54
+ end
55
+
56
+ unless @dead_thread.include?(shard.shard_id)
57
+ active_shard_ids << shard.shard_id
58
+ end
59
+ end
60
+
61
+ active_shard_ids
62
+ rescue => e
63
+ $log.error "get_shard_ids : #{e.message}"
64
+ end
65
+ end
66
+
67
+
68
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-in-kinesis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - 'yusuke yamatani '
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: To count records with string fields by regexps (To count records with
56
+ numbers, use numeric-counter)
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - .gitignore
63
+ - CODE_OF_CONDUCT.md
64
+ - Gemfile
65
+ - LICENSE.txt
66
+ - README.md
67
+ - Rakefile
68
+ - fluent-plugin-in-kinesis.gemspec
69
+ - lib/fluent/plugin/in_kinesis.rb
70
+ - lib/fluent/plugin/kinesis_shard.rb
71
+ - lib/fluent/plugin/thread_supervisor.rb
72
+ homepage: https://github.com/yusukeyamatani/fluent-plugin-in-kinesis
73
+ licenses:
74
+ - MIT
75
+ metadata: {}
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - '>='
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 2.0.14
93
+ signing_key:
94
+ specification_version: 4
95
+ summary: Fluentd plugin to count records with specified regexp patterns
96
+ test_files: []