fluent-plugin-beats 0.1.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: 22b370cd6030f188fc607586558a34341419735b
4
+ data.tar.gz: c1c732fbb50f60bcc503d413a0cc40a9e5a3df08
5
+ SHA512:
6
+ metadata.gz: 2f103a1cecb9002a90978920cef9ee907e0f857cabcfe7e5abc305a7b6174a6520e834333cc13cc00b38a9026bfb52497d94f44bfcd908ae38eed0a03d8b2e3a
7
+ data.tar.gz: d872ee9de6e8b2dc1eb7c6b538664357dbf5c7db5113b3782b7a8ca33bdfc817991de0d068620d2238ac4f6bc647f9b75815b93d678d82e0b10531e72f9d5c82
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ ~*
2
+ #*
3
+ *~
4
+ [._]*.s[a-w][a-z]
5
+ .DS_Store
6
+
7
+ *.gem
8
+ .bundle
9
+ Gemfile.lock
10
+ vendor
11
+ .ruby-version
12
+
13
+ test/tmp/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "{}"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright {yyyy} {name of copyright owner}
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # fluent-plugin-beats
2
+
3
+ [Fluentd](http://fluentd.org) plugin to accept events from [Elastic Beats](https://www.elastic.co/products/beats).
4
+
5
+ This plugin uses lumberjack protocol for communicating with each beat.
6
+
7
+ ## Installation
8
+
9
+ $ gem install fluent-plugin-beats
10
+
11
+ ## Configuration
12
+
13
+ Configuration example:
14
+
15
+ <source>
16
+ @type beats
17
+ metadata_as_tag
18
+ </source>
19
+
20
+ # Forward all events from beats to each index on elasticsearch
21
+ <match *beat>
22
+ @type elasticsearch_dynamic
23
+ logstash_format true
24
+ logstash_prefix ${tag_parts[0]}
25
+ type_name ${record['type']}
26
+ </match>
27
+
28
+ **port**
29
+
30
+ The port to listen to. Default Value is `5044`.
31
+
32
+ **bind**
33
+
34
+ The bind address to listen to. Default Value is 0.0.0.0 (all addresses)
35
+
36
+ **tag**
37
+
38
+ The tag of the event.
39
+
40
+ **metadata_as_tag**
41
+
42
+ Use `record['@metadata']['beat']` value instead of fixed tag. Available values are `filebeat`, `packetbeat` and `topbeat`.
43
+
44
+ **format**
45
+
46
+ The format of the log. This format is used for `message` field of `filebeat` event. See Parser article for more detail: http://docs.fluentd.org/articles/parser-plugin-overview
47
+
48
+ **max_connections**
49
+
50
+ Limit the number of connections from beat instances. Default is unlimited.
51
+
52
+ **use_ssl**, **ssl_certificate**, **ssl_key**, **ssl_key_passphrase**
53
+
54
+ For lumberjack protocol.
55
+
56
+ ## Note
57
+
58
+ * `lumberjack` directory is copied from `logstash-input-beats` and bit modified.
59
+ * Add `Server::Connection#closed?` to check connection is dead or not
60
+ * Remove `id_stream` argument from `Server::Connection#run` block
61
+ * From lumberjack limitation, this plugin launches one thread for each connection. You can mitigate this problem by `max_connections`.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'lib' << 'test'
9
+ test.test_files = FileList['test/test_*.rb']
10
+ test.verbose = true
11
+ end
12
+
13
+ task :default => [:build]
14
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,25 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "fluent-plugin-beats"
6
+ gem.description = "Elastic beats plugin for Fluentd event collector"
7
+ gem.license = "Apache-2.0"
8
+ gem.homepage = "https://github.com/repeatedly/fluent-plugin-beats"
9
+ gem.summary = gem.description
10
+ gem.version = File.read("VERSION").strip
11
+ gem.authors = ["Masahiro Nakagawa"]
12
+ gem.email = "repeatedly@gmail.com"
13
+ gem.has_rdoc = false
14
+ #gem.platform = Gem::Platform::RUBY
15
+ gem.files = `git ls-files`.split("\n")
16
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency "fluentd", [">= 0.10.58", "< 2"]
21
+ gem.add_dependency "concurrent-ruby", "~> 0.9.2"
22
+ gem.add_development_dependency "rake", ">= 0.9.2"
23
+ gem.add_development_dependency "test-unit", ">= 3.0.8"
24
+ gem.add_development_dependency "test-unit-rr", ">= 1.0.3"
25
+ end
@@ -0,0 +1,116 @@
1
+ #
2
+ # Fluent
3
+ #
4
+ # Copyright (C) 2015 Masahiro Nakagawa
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module Fluent
19
+ class BeatsInput < Input
20
+ Plugin.register_input('beats', self)
21
+
22
+ def initialize
23
+ super
24
+
25
+ require "lumberjack/beats"
26
+ require "lumberjack/beats/server"
27
+ require 'concurrent/executor/cached_thread_pool'
28
+ end
29
+
30
+ config_param :port, :integer, :default => 5044
31
+ config_param :bind, :string, :default => '0.0.0.0'
32
+ config_param :tag, :string, :default => nil
33
+ config_param :metadata_as_tag, :bool, :default => nil
34
+ config_param :format, :string, :default => nil
35
+ config_param :max_connections, :integer, :default => nil # CachedThreadPool can't limit the number of threads
36
+ config_param :use_ssl, :string, :default => false
37
+ config_param :ssl_certificate, :string, :default => nil
38
+ config_param :ssl_key, :string, :default => nil
39
+ config_param :ssl_key_passphrase, :string, :default => nil
40
+
41
+ def configure(conf)
42
+ super
43
+
44
+ if !@tag && !@metadata_as_tag
45
+ raise ConfigError, "'tag' or 'metadata_as_tag' parameter is required on beats input"
46
+ end
47
+
48
+ @time_parser = TextParser::TimeParser.new('%Y-%m-%dT%H:%M:%S.%N%z')
49
+ if @format
50
+ @parser = Plugin.new_parser(@format)
51
+ @parser.configure(conf)
52
+ end
53
+ @connections = []
54
+ end
55
+
56
+ def start
57
+ @lumberjack = Lumberjack::Beats::Server.new(
58
+ :address => @bind, :port => @port, :ssl => @use_ssl, :ssl_certificate => @ssl_certificate,
59
+ :ssl_key => @ssl_key, :ssl_key_passphrase => @ssl_key_passphrase)
60
+ # Lumberjack::Beats::Server depends on normal accept so we need to launch thread for each connection.
61
+ # TODO: Re-implement Beats Server with Cool.io for resource control
62
+ @thread_pool = Concurrent::CachedThreadPool.new(:idletime => 15) # idletime setting is based on logstash beats input
63
+ @thread = Thread.new(&method(:run))
64
+ end
65
+
66
+ def shutdown
67
+ @lumberjack.close rescue nil
68
+ @thread_pool.shutdown
69
+ @thread.join
70
+ end
71
+
72
+ FILEBEAT_MESSAGE = 'message'
73
+
74
+ def run
75
+ until @lumberjack.closed?
76
+ conn = @lumberjack.accept
77
+ next if conn.nil?
78
+
79
+ if @max_connections
80
+ @connections.reject! { |c| c.closed? }
81
+ if @connections.size >= @max_connections
82
+ conn.close # close for retry on beats side
83
+ sleep 1
84
+ next
85
+ end
86
+ @connections << conn
87
+ end
88
+
89
+ @thread_pool.post {
90
+ begin
91
+ conn.run { |map|
92
+ tag = @metadata_as_tag ? map['@metadata']['beat'] : @tag
93
+
94
+ if map.has_key?('message') && @format
95
+ message = map.delete('message')
96
+ @parser.parse(message) { |time, record|
97
+ record['@timestamp'] = map['@timestamp']
98
+ map.each { |k, v|
99
+ record[k] = v
100
+ }
101
+ router.emit(tag, time, record)
102
+ }
103
+ next
104
+ end
105
+
106
+ router.emit(tag, @time_parser.parse(map['@timestamp']), map)
107
+ }
108
+ rescue => e
109
+ log.error "unexpected error", :error => e.to_s
110
+ log.error_backtrace
111
+ end
112
+ }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,22 @@
1
+ # encoding: utf-8
2
+ require "json"
3
+ module Lumberjack module Beats
4
+ SEQUENCE_MAX = (2**32-1).freeze
5
+
6
+ @@json = Class.new do
7
+ def self.load(blob)
8
+ JSON.parse(blob)
9
+ end
10
+ def self.dump(v)
11
+ v.to_json
12
+ end
13
+ end
14
+
15
+ def self.json
16
+ @@json
17
+ end
18
+
19
+ def self.json=(j)
20
+ @@json = j
21
+ end
22
+ end; end
@@ -0,0 +1,219 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/beats"
3
+ require "socket"
4
+ require "thread"
5
+ require "openssl"
6
+ require "zlib"
7
+
8
+ module Lumberjack module Beats
9
+ class Client
10
+ def initialize(opts={})
11
+ @opts = {
12
+ :port => 0,
13
+ :addresses => [],
14
+ :ssl_certificate => nil,
15
+ :ssl => true,
16
+ :json => false,
17
+ }.merge(opts)
18
+
19
+ @opts[:addresses] = [@opts[:addresses]] if @opts[:addresses].class == String
20
+ raise "Must set a port." if @opts[:port] == 0
21
+ raise "Must set atleast one address" if @opts[:addresses].empty? == 0
22
+ raise "Must set a ssl certificate or path" if @opts[:ssl_certificate].nil? && @opts[:ssl]
23
+
24
+ @socket = connect
25
+ end
26
+
27
+ private
28
+ def connect
29
+ addrs = @opts[:addresses].shuffle
30
+ begin
31
+ raise "Could not connect to any hosts" if addrs.empty?
32
+ opts = @opts
33
+ opts[:address] = addrs.pop
34
+ Lumberjack::Beats::Socket.new(opts)
35
+ rescue *[Errno::ECONNREFUSED,SocketError]
36
+ retry
37
+ end
38
+ end
39
+
40
+ public
41
+ def write(elements, opts={})
42
+ @socket.write_sync(elements, opts)
43
+ end
44
+
45
+ public
46
+ def host
47
+ @socket.host
48
+ end
49
+ end
50
+
51
+ class Socket
52
+ # Create a new Lumberjack Socket.
53
+ #
54
+ # - options is a hash. Valid options are:
55
+ #
56
+ # * :port - the port to listen on
57
+ # * :address - the host/address to bind to
58
+ # * :ssl - enable/disable ssl support
59
+ # * :ssl_certificate - the path to the ssl cert to use.
60
+ # If ssl_certificate is not set, a plain tcp connection
61
+ # will be used.
62
+ attr_reader :sequence
63
+ attr_reader :host
64
+ def initialize(opts={})
65
+ @sequence = 0
66
+ @last_ack = 0
67
+ @opts = {
68
+ :port => 0,
69
+ :address => "127.0.0.1",
70
+ :ssl_certificate => nil,
71
+ :ssl => true,
72
+ :json => false,
73
+ }.merge(opts)
74
+ @host = @opts[:address]
75
+
76
+ connection_start(opts)
77
+ end
78
+
79
+ private
80
+ def connection_start(opts)
81
+ tcp_socket = TCPSocket.new(opts[:address], opts[:port])
82
+ if !opts[:ssl]
83
+ @socket = tcp_socket
84
+ else
85
+ certificate = OpenSSL::X509::Certificate.new(File.read(opts[:ssl_certificate]))
86
+
87
+ certificate_store = OpenSSL::X509::Store.new
88
+ certificate_store.add_cert(certificate)
89
+
90
+ ssl_context = OpenSSL::SSL::SSLContext.new
91
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
92
+ ssl_context.cert_store = certificate_store
93
+
94
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
95
+ @socket.connect
96
+ end
97
+ end
98
+
99
+ private
100
+ def inc
101
+ @sequence = 0 if @sequence + 1 > Lumberjack::Beats::SEQUENCE_MAX
102
+ @sequence = @sequence + 1
103
+ end
104
+
105
+ private
106
+ def send_window_size(size)
107
+ @socket.syswrite(["1", "W", size].pack("AAN"))
108
+ end
109
+
110
+ private
111
+ def send_payload(payload)
112
+ # SSLSocket has a limit of 16k per message
113
+ # execute multiple writes if needed
114
+ bytes_written = 0
115
+ while bytes_written < payload.bytesize
116
+ bytes_written += @socket.syswrite(payload.byteslice(bytes_written..-1))
117
+ end
118
+ end
119
+
120
+ public
121
+ def write_sync(elements, opts={})
122
+ options = {
123
+ :json => @opts[:json],
124
+ }.merge(opts)
125
+
126
+ elements = [elements] if elements.is_a?(Hash)
127
+ send_window_size(elements.size)
128
+
129
+ encoder = options[:json] ? JsonEncoder : FrameEncoder
130
+ payload = elements.map { |element| encoder.to_frame(element, inc) }.join
131
+ compress = compress_payload(payload)
132
+ send_payload(compress)
133
+
134
+ ack(elements.size)
135
+ end
136
+
137
+ private
138
+ def compress_payload(payload)
139
+ compress = Zlib::Deflate.deflate(payload)
140
+ ["1", "C", compress.bytesize, compress].pack("AANA*")
141
+ end
142
+
143
+ private
144
+ def ack(size)
145
+ _, type = read_version_and_type
146
+ raise "Whoa we shouldn't get this frame: #{type}" if type != "A"
147
+ @last_ack = read_last_ack
148
+ end
149
+
150
+ private
151
+ def unacked_sequence_size
152
+ sequence - (@last_ack + 1)
153
+ end
154
+
155
+ private
156
+ def read_version_and_type
157
+ version = @socket.read(1)
158
+ type = @socket.read(1)
159
+ [version, type]
160
+ end
161
+
162
+ private
163
+ def read_last_ack
164
+ @socket.read(4).unpack("N").first
165
+ end
166
+ end
167
+
168
+ module JsonEncoder
169
+ def self.to_frame(hash, sequence)
170
+ json = Lumberjack::Beats::json.dump(hash)
171
+ json_length = json.bytesize
172
+ pack = "AANNA#{json_length}"
173
+ frame = ["1", "J", sequence, json_length, json]
174
+ frame.pack(pack)
175
+ end
176
+ end # JsonEncoder
177
+
178
+ module FrameEncoder
179
+ def self.to_frame(hash, sequence)
180
+ frame = ["1", "D", sequence]
181
+ pack = "AAN"
182
+ keys = deep_keys(hash)
183
+ frame << keys.length
184
+ pack << "N"
185
+ keys.each do |k|
186
+ val = deep_get(hash,k)
187
+ key_length = k.bytesize
188
+ val_length = val.bytesize
189
+ frame << key_length
190
+ pack << "N"
191
+ frame << k
192
+ pack << "A#{key_length}"
193
+ frame << val_length
194
+ pack << "N"
195
+ frame << val
196
+ pack << "A#{val_length}"
197
+ end
198
+ frame.pack(pack)
199
+ end
200
+
201
+ private
202
+ def self.deep_get(hash, key="")
203
+ return hash if key.nil?
204
+ deep_get(
205
+ hash[key.split('.').first],
206
+ key[key.split('.').first.length+1..key.length]
207
+ )
208
+ end
209
+ private
210
+ def self.deep_keys(hash, prefix="")
211
+ keys = []
212
+ hash.each do |k,v|
213
+ keys << "#{prefix}#{k}" if v.class == String
214
+ keys << deep_keys(hash[k], "#{k}.") if v.class == Hash
215
+ end
216
+ keys.flatten
217
+ end
218
+ end # module Encoder
219
+ end; end
@@ -0,0 +1,466 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/beats"
3
+ require "socket"
4
+ require "thread"
5
+ require "openssl"
6
+ require "zlib"
7
+ require "json"
8
+ require "concurrent"
9
+
10
+ module Lumberjack module Beats
11
+ class Server
12
+ SOCKET_TIMEOUT = 1 # seconds
13
+
14
+ attr_reader :port
15
+
16
+ # Create a new Lumberjack server.
17
+ #
18
+ # - options is a hash. Valid options are:
19
+ #
20
+ # * :port - the port to listen on
21
+ # * :address - the host/address to bind to
22
+ # * :ssl_certificate - the path to the ssl cert to use
23
+ # * :ssl_key - the path to the ssl key to use
24
+ # * :ssl_key_passphrase - the key passphrase (optional)
25
+ def initialize(options={})
26
+ @options = {
27
+ :port => 0,
28
+ :address => "0.0.0.0",
29
+ :ssl => true,
30
+ :ssl_certificate => nil,
31
+ :ssl_key => nil,
32
+ :ssl_key_passphrase => nil
33
+ }.merge(options)
34
+
35
+ if @options[:ssl]
36
+ [:ssl_certificate, :ssl_key].each do |k|
37
+ if @options[k].nil?
38
+ raise "You must specify #{k} in Lumberjack::Server.new(...)"
39
+ end
40
+ end
41
+ end
42
+
43
+ @server = TCPServer.new(@options[:address], @options[:port])
44
+
45
+ @close = Concurrent::AtomicBoolean.new
46
+
47
+ # Query the port in case the port number is '0'
48
+ # TCPServer#addr == [ address_family, port, address, address ]
49
+ @port = @server.addr[1]
50
+
51
+ if @options[:ssl]
52
+ # load SSL certificate
53
+ @ssl = OpenSSL::SSL::SSLContext.new
54
+ @ssl.cert = OpenSSL::X509::Certificate.new(File.read(@options[:ssl_certificate]))
55
+ @ssl.key = OpenSSL::PKey::RSA.new(File.read(@options[:ssl_key]),
56
+ @options[:ssl_key_passphrase])
57
+ end
58
+ end # def initialize
59
+
60
+ def run(&block)
61
+ while !closed?
62
+ connection = accept
63
+
64
+ # Some exception may occur in the accept loop
65
+ # we will try again in the next iteration
66
+ # unless the server is closing
67
+ next unless connection
68
+
69
+ Thread.new(connection) do |connection|
70
+ connection.run(&block)
71
+ end
72
+ end
73
+ end # def run
74
+
75
+ def ssl?
76
+ @ssl
77
+ end
78
+
79
+ def accept(&block)
80
+ begin
81
+ socket = @server.accept_nonblock
82
+ # update the socket with a SSL layer
83
+ socket = accept_ssl(socket) if ssl?
84
+
85
+ if block_given?
86
+ block.call(socket, self)
87
+ else
88
+ return Connection.new(socket, self)
89
+ end
90
+ rescue OpenSSL::SSL::SSLError, IOError, EOFError, Errno::EBADF
91
+ socket.close rescue nil
92
+ retry unless closed?
93
+ rescue IO::WaitReadable, Errno::EAGAIN # Resource not ready yet, so lets try again
94
+ begin
95
+ IO.select([@server], nil, nil, SOCKET_TIMEOUT)
96
+ retry unless closed?
97
+ rescue IOError, Errno::EBADF => e # we currently closing
98
+ raise e unless closed?
99
+ end
100
+ end
101
+ end
102
+
103
+ def accept_ssl(tcp_socket)
104
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl)
105
+ ssl_socket.sync_close
106
+
107
+ begin
108
+ ssl_socket.accept_nonblock
109
+
110
+ return ssl_socket
111
+ rescue IO::WaitReadable # handshake
112
+ IO.select([ssl_socket], nil, nil, SOCKET_TIMEOUT)
113
+ retry unless closed?
114
+ rescue IO::WaitWritable # handshake
115
+ IO.select(nil, [ssl_socket], nil, SOCKET_TIMEOUT)
116
+ retry unless closed?
117
+ end
118
+ end
119
+
120
+ def closed?
121
+ @close.value
122
+ end
123
+
124
+ def close
125
+ @close.make_true
126
+ @server.close unless @server.closed?
127
+ end
128
+ end # class Server
129
+
130
+ class Parser
131
+ PROTOCOL_VERSION_1 = "1".ord
132
+ PROTOCOL_VERSION_2 = "2".ord
133
+
134
+ SUPPORTED_PROTOCOLS = [PROTOCOL_VERSION_1, PROTOCOL_VERSION_2]
135
+
136
+ def initialize
137
+ @buffer_offset = 0
138
+ @buffer = ""
139
+ @buffer.force_encoding("BINARY")
140
+ transition(:header, 2)
141
+ end # def initialize
142
+
143
+ def transition(state, next_length)
144
+ @state = state
145
+ #puts :transition => state
146
+ # TODO(sissel): Assert this self.respond_to?(state)
147
+ # TODO(sissel): Assert state is in STATES
148
+ # TODO(sissel): Assert next_length is a number
149
+ need(next_length)
150
+ end # def transition
151
+
152
+ # Feed data to this parser.
153
+ #
154
+ # Currently, it will return the raw payload of websocket messages.
155
+ # Otherwise, it returns nil if no complete message has yet been consumed.
156
+ #
157
+ # @param [String] the string data to feed into the parser.
158
+ # @return [String, nil] the websocket message payload, if any, nil otherwise.
159
+ def feed(data, &block)
160
+ @buffer << data
161
+ #p :need => @need
162
+ while have?(@need)
163
+ send(@state, &block)
164
+ #case @state
165
+ #when :header; header(&block)
166
+ #when :window_size; window_size(&block)
167
+ #when :data_lead; data_lead(&block)
168
+ #when :data_field_key_len; data_field_key_len(&block)
169
+ #when :data_field_key; data_field_key(&block)
170
+ #when :data_field_value_len; data_field_value_len(&block)
171
+ #when :data_field_value; data_field_value(&block)
172
+ #when :data_field_value; data_field_value(&block)
173
+ #when :compressed_lead; compressed_lead(&block)
174
+ #when :compressed_payload; compressed_payload(&block)
175
+ #end # case @state
176
+ end
177
+ return nil
178
+ end # def <<
179
+
180
+ # Do we have at least 'length' bytes in the buffer?
181
+ def have?(length)
182
+ return length <= (@buffer.size - @buffer_offset)
183
+ end # def have?
184
+
185
+ # Get 'length' string from the buffer.
186
+ def get(length=nil)
187
+ length = @need if length.nil?
188
+ data = @buffer[@buffer_offset ... @buffer_offset + length]
189
+ @buffer_offset += length
190
+ if @buffer_offset > 16384
191
+ @buffer = @buffer[@buffer_offset .. -1]
192
+ @buffer_offset = 0
193
+ end
194
+ return data
195
+ end # def get
196
+
197
+ # Set the minimum number of bytes we need in the buffer for the next read.
198
+ def need(length)
199
+ @need = length
200
+ end # def need
201
+
202
+ FRAME_WINDOW = "W".ord
203
+ FRAME_DATA = "D".ord
204
+ FRAME_JSON_DATA = "J".ord
205
+ FRAME_COMPRESSED = "C".ord
206
+ def header(&block)
207
+ version, frame_type = get.bytes.to_a[0..1]
208
+ version ||= PROTOCOL_VERSION_1
209
+
210
+ handle_version(version, &block)
211
+
212
+ case frame_type
213
+ when FRAME_WINDOW; transition(:window_size, 4)
214
+ when FRAME_DATA; transition(:data_lead, 8)
215
+ when FRAME_JSON_DATA; transition(:json_data_lead, 8)
216
+ when FRAME_COMPRESSED; transition(:compressed_lead, 4)
217
+ else; raise "Unknown frame type: `#{frame_type}`"
218
+ end
219
+ end
220
+
221
+ def handle_version(version, &block)
222
+ if supported_protocol?(version)
223
+ yield :version, version
224
+ else
225
+ raise "unsupported protocol #{version}"
226
+ end
227
+ end
228
+
229
+ def supported_protocol?(version)
230
+ SUPPORTED_PROTOCOLS.include?(version)
231
+ end
232
+
233
+ def window_size(&block)
234
+ @window_size = get.unpack("N").first
235
+ transition(:header, 2)
236
+ yield :window_size, @window_size
237
+ end # def window_size
238
+
239
+ def json_data_lead(&block)
240
+ @sequence, payload_size = get.unpack("NN")
241
+ transition(:json_data_payload, payload_size)
242
+ end
243
+
244
+ def json_data_payload(&block)
245
+ payload = get
246
+ yield :json, @sequence, Lumberjack::Beats::json.load(payload)
247
+ transition(:header, 2)
248
+ end
249
+
250
+ def data_lead(&block)
251
+ @sequence, @data_count = get.unpack("NN")
252
+ @data = {}
253
+ transition(:data_field_key_len, 4)
254
+ end
255
+
256
+ def data_field_key_len(&block)
257
+ key_len = get.unpack("N").first
258
+ transition(:data_field_key, key_len)
259
+ end
260
+
261
+ def data_field_key(&block)
262
+ @key = get
263
+ transition(:data_field_value_len, 4)
264
+ end
265
+
266
+ def data_field_value_len(&block)
267
+ transition(:data_field_value, get.unpack("N").first)
268
+ end
269
+
270
+ def data_field_value(&block)
271
+ @value = get
272
+
273
+ @data_count -= 1
274
+ @data[@key] = @value
275
+
276
+ if @data_count > 0
277
+ transition(:data_field_key_len, 4)
278
+ else
279
+ # emit the whole map now that we found the end of the data fields list.
280
+ yield :data, @sequence, @data
281
+ transition(:header, 2)
282
+ end
283
+
284
+ end # def data_field_value
285
+
286
+ def compressed_lead(&block)
287
+ length = get.unpack("N").first
288
+ transition(:compressed_payload, length)
289
+ end
290
+
291
+ def compressed_payload(&block)
292
+ original = Zlib::Inflate.inflate(get)
293
+ transition(:header, 2)
294
+
295
+ # Parse the uncompressed payload.
296
+ feed(original, &block)
297
+ end
298
+ end # class Parser
299
+
300
+ class Connection
301
+ READ_SIZE = 16384
302
+
303
+ attr_accessor :server
304
+
305
+ def initialize(fd, server)
306
+ @parser = Parser.new
307
+ @fd = fd
308
+
309
+ @server = server
310
+ @ack_handler = nil
311
+ end
312
+
313
+ def peer
314
+ "#{@fd.peeraddr[3]}:#{@fd.peeraddr[1]}"
315
+ end
316
+
317
+ def run(&block)
318
+ while !server.closed?
319
+ read_socket(&block)
320
+ end
321
+ rescue EOFError,
322
+ OpenSSL::SSL::SSLError,
323
+ IOError,
324
+ Errno::ECONNRESET,
325
+ Errno::EPIPE
326
+ # EOF or other read errors, only action is to shutdown which we'll do in
327
+ # 'ensure'
328
+ rescue
329
+ # when the server is shutting down we can safely ignore any exceptions
330
+ # On windows, we can get a `SystemCallErr`
331
+ raise unless server.closed?
332
+ ensure
333
+ close rescue 'Already closed stream'
334
+ end # def run
335
+
336
+ def read_socket(&block)
337
+ # TODO(sissel): Ack on idle.
338
+ # X: - if any unacked, IO.select
339
+ # X: - on timeout, ack all.
340
+ # X: Doing so will prevent slow streams from retransmitting
341
+ # X: too many events after errors.
342
+ @parser.feed(@fd.sysread(READ_SIZE)) do |event, *args|
343
+ case event
344
+ when :version
345
+ version(*args)
346
+ when :window_size
347
+ reset_next_ack(*args)
348
+ when :data
349
+ sequence, map = args
350
+ ack_if_needed(sequence) { data(map, &block) }
351
+ when :json
352
+ # If the payload is an array of items we will emit multiple events
353
+ # this behavior was moved from the plugin to the library.
354
+ # see this commit: https://github.com/logstash-plugins/logstash-input-lumberjack/pull/57/files#diff-1b9590423b15f04f215635164e7376ecR158
355
+ sequence, map = args
356
+
357
+ ack_if_needed(sequence) do
358
+ if map.is_a?(Array)
359
+ map.each { |e| data(e, &block) }
360
+ else
361
+ data(map, &block)
362
+ end
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ def version(version)
369
+ @version = version
370
+ end
371
+
372
+ def ack_if_needed(sequence, &block)
373
+ block.call
374
+ send_ack(sequence) if @ack_handler.ack?(sequence)
375
+ end
376
+
377
+ def close
378
+ @fd.close unless @fd.closed?
379
+ end
380
+
381
+ def closed?
382
+ @fd.closed?
383
+ end
384
+
385
+ def data(map, &block)
386
+ block.call(map) if block_given?
387
+ end
388
+
389
+ def reset_next_ack(window_size)
390
+ klass = version_1? ? AckingProtocolV1 : AckingProtocolV2
391
+ @ack_handler = klass.new(window_size)
392
+ end
393
+
394
+ def send_ack(sequence)
395
+ @fd.syswrite(@ack_handler.ack_frame(sequence))
396
+ end
397
+
398
+ def version_1?
399
+ @version == Parser::PROTOCOL_VERSION_1
400
+ end
401
+
402
+ def identity_stream(map)
403
+ id = map.fetch("beat", {})["id"]
404
+
405
+ if id && map["resource_id"]
406
+ identity_values = [id, map["resource_id"]]
407
+ else
408
+ identity_values = [map.fetch("beat", {})["name"],
409
+ map["source"]]
410
+ end
411
+
412
+ identity_values.compact.join("-")
413
+ end
414
+ end # class Connection
415
+
416
+ class AckingProtocolV1
417
+ def initialize(window_size)
418
+ @next_ack = nil
419
+ @window_size = window_size
420
+ end
421
+
422
+ def ack?(sequence)
423
+ # The first encoded event will contain the sequence number
424
+ # this is needed to know when we should ack.
425
+ @next_ack = compute_next_ack(sequence) if @next_ack.nil?
426
+ sequence == @next_ack
427
+ end
428
+
429
+ def ack_frame(sequence)
430
+ ["1A", sequence].pack("A*N")
431
+ end
432
+
433
+ private
434
+ def compute_next_ack(sequence)
435
+ (sequence + @window_size - 1) % SEQUENCE_MAX
436
+ end
437
+ end
438
+
439
+ # Allow lumberjack to send partial ack back to the producer
440
+ # only V2 client support partial Acks
441
+ #
442
+ # Send Ack on every 20% of the data, so with default settings every 200 events
443
+ # This should reduce the congestion on retransmit.
444
+ class AckingProtocolV2
445
+ ACK_RATIO = 5
446
+
447
+ def initialize(window_size)
448
+ @window_size = window_size
449
+ @every = (window_size / ACK_RATIO).round
450
+ end
451
+
452
+ def ack?(sequence)
453
+ if @window_size == sequence
454
+ true
455
+ elsif @every != 0 && sequence % @every == 0
456
+ true
457
+ else
458
+ false
459
+ end
460
+ end
461
+
462
+ def ack_frame(sequence)
463
+ ["2A", sequence].pack("A*N")
464
+ end
465
+ end
466
+ end; end# module Lumberjack
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-beats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Masahiro Nakagawa
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-17 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.10.58
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.10.58
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: concurrent-ruby
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.9.2
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.9.2
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 0.9.2
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 0.9.2
61
+ - !ruby/object:Gem::Dependency
62
+ name: test-unit
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 3.0.8
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 3.0.8
75
+ - !ruby/object:Gem::Dependency
76
+ name: test-unit-rr
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.0.3
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.0.3
89
+ description: Elastic beats plugin for Fluentd event collector
90
+ email: repeatedly@gmail.com
91
+ executables: []
92
+ extensions: []
93
+ extra_rdoc_files: []
94
+ files:
95
+ - ".gitignore"
96
+ - Gemfile
97
+ - LICENSE
98
+ - README.md
99
+ - Rakefile
100
+ - VERSION
101
+ - fluent-plugin-beats.gemspec
102
+ - lib/fluent/plugin/in_beats.rb
103
+ - lib/lumberjack/beats.rb
104
+ - lib/lumberjack/beats/client.rb
105
+ - lib/lumberjack/beats/server.rb
106
+ homepage: https://github.com/repeatedly/fluent-plugin-beats
107
+ licenses:
108
+ - Apache-2.0
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.2.2
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Elastic beats plugin for Fluentd event collector
130
+ test_files: []