logstash-input-relp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MTA3ZmU0ZDgzYzVlOWIxZTRhMzFlODU1NTFlNDgyZTQ5MTU4ZGY0OA==
5
+ data.tar.gz: !binary |-
6
+ MDAxNDczYmU0ZTU0OTE5N2U5OGE2OWY0ZTdjMDBmNzYwM2Q5YzU3Mg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZTdjMmI2ZjRhYWNiZDhjZTBjYWQyNzhjMTRhMWRiODBmNWMxNjc1ODJhZmVk
10
+ OWM5OTZlZWQ4ZWNhYTM4Yzg3MGM3MTFjNGRmMTllMDFlZjQzZjEyYTExZDEz
11
+ ZTQyMjk5ZTBlMDExOGM0YjA1MzAzM2ZkNWZlN2M3MTkxZTMxNzY=
12
+ data.tar.gz: !binary |-
13
+ MjM0NzFlNjNlZWQ5MTU0ODNkZTAxODJlZmMzNzEwZDgyOGQ0MGQ3YzVhOTI5
14
+ MDk5Y2ZhYmU5ZTVkODUyMDE0OTk0YzJkODY4MGE0OTFkZjc5NWI0ZmYxZTli
15
+ MGIwYmNkNWZiNzBjNWQ1OTM1NzhjYzMyZWZjZWZiOGQzN2NkZTI=
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ Gemfile.lock
3
+ .bundle
4
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+ gem 'rake'
3
+ gem 'gem_publisher'
4
+ gem 'archive-tar-minitar'
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2012-2014 Elasticsearch <http://www.elasticsearch.org>
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ @files=[]
2
+
3
+ task :default do
4
+ system("rake -T")
5
+ end
6
+
@@ -0,0 +1,107 @@
1
+ # encoding: utf-8
2
+ require "logstash/inputs/base"
3
+ require "logstash/namespace"
4
+ require "logstash/util/relp"
5
+ require "logstash/util/socket_peer"
6
+
7
+
8
+
9
+ # Read RELP events over a TCP socket.
10
+ #
11
+ # For more information about RELP, see
12
+ # <http://www.rsyslog.com/doc/imrelp.html>
13
+ #
14
+ # This protocol implements application-level acknowledgements to help protect
15
+ # against message loss.
16
+ #
17
+ # Message acks only function as far as messages being put into the queue for
18
+ # filters; anything lost after that point will not be retransmitted
19
+ class LogStash::Inputs::Relp < LogStash::Inputs::Base
20
+ class Interrupted < StandardError; end
21
+
22
+ config_name "relp"
23
+ milestone 1
24
+
25
+ default :codec, "plain"
26
+
27
+ # The address to listen on.
28
+ config :host, :validate => :string, :default => "0.0.0.0"
29
+
30
+ # The port to listen on.
31
+ config :port, :validate => :number, :required => true
32
+
33
+ def initialize(*args)
34
+ super(*args)
35
+ end # def initialize
36
+
37
+ public
38
+ def register
39
+ @logger.info("Starting relp input listener", :address => "#{@host}:#{@port}")
40
+ @relp_server = RelpServer.new(@host, @port,['syslog'])
41
+ end # def register
42
+
43
+ private
44
+ def relp_stream(relpserver,socket,output_queue,client_address)
45
+ loop do
46
+ frame = relpserver.syslog_read(socket)
47
+ @codec.decode(frame["message"]) do |event|
48
+ decorate(event)
49
+ event["host"] = client_address
50
+ output_queue << event
51
+ end
52
+
53
+ #To get this far, the message must have made it into the queue for
54
+ #filtering. I don't think it's possible to wait for output before ack
55
+ #without fundamentally breaking the plugin architecture
56
+ relpserver.ack(socket, frame['txnr'])
57
+ end
58
+ end
59
+
60
+ public
61
+ def run(output_queue)
62
+ @thread = Thread.current
63
+ loop do
64
+ begin
65
+ # Start a new thread for each connection.
66
+ Thread.start(@relp_server.accept) do |client|
67
+ rs = client[0]
68
+ socket = client[1]
69
+ # monkeypatch a 'peer' method onto the socket.
70
+ socket.instance_eval { class << self; include ::LogStash::Util::SocketPeer end }
71
+ peer = socket.peer
72
+ @logger.debug("Relp Connection to #{peer} created")
73
+ begin
74
+ relp_stream(rs,socket, output_queue, peer)
75
+ rescue Relp::ConnectionClosed => e
76
+ @logger.debug("Relp Connection to #{peer} Closed")
77
+ rescue Relp::RelpError => e
78
+ @logger.warn('Relp error: '+e.class.to_s+' '+e.message)
79
+ #TODO: Still not happy with this, are they all warn level?
80
+ #Will this catch everything I want it to?
81
+ #Relp spec says to close connection on error, ensure this is the case
82
+ end
83
+ end # Thread.start
84
+ rescue Relp::InvalidCommand,Relp::InappropriateCommand => e
85
+ @logger.warn('Relp client trying to open connection with something other than open:'+e.message)
86
+ rescue Relp::InsufficientCommands
87
+ @logger.warn('Relp client incapable of syslog')
88
+ rescue IOError, Interrupted
89
+ if @interrupted
90
+ # Intended shutdown, get out of the loop
91
+ @relp_server.shutdown
92
+ break
93
+ else
94
+ # Else it was a genuine IOError caused by something else, so propagate it up..
95
+ raise
96
+ end
97
+ end
98
+ end # loop
99
+ end # def run
100
+
101
+ def teardown
102
+ @interrupted = true
103
+ @thread.raise(Interrupted.new)
104
+ end
105
+ end # class LogStash::Inputs::Relp
106
+
107
+ #TODO: structured error logging
@@ -0,0 +1,326 @@
1
+ # encoding: utf-8
2
+ require "socket"
3
+
4
+ class Relp#This isn't much use on its own, but gives RelpServer and RelpClient things
5
+
6
+ RelpVersion = '0'#TODO: spec says this is experimental, but rsyslog still seems to exclusively use it
7
+ RelpSoftware = 'logstash,1.1.1,http://logstash.net'
8
+
9
+ class RelpError < StandardError; end
10
+ class InvalidCommand < RelpError; end
11
+ class InappropriateCommand < RelpError; end
12
+ class ConnectionClosed < RelpError; end
13
+ class InsufficientCommands < RelpError; end
14
+
15
+ def valid_command?(command)
16
+ valid_commands = Array.new
17
+
18
+ #Allow anything in the basic protocol for both directions
19
+ valid_commands << 'open'
20
+ valid_commands << 'close'
21
+
22
+ #These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.) TODO: would they be invalid or just innapropriate?
23
+ valid_commands += @basic_relp_commands
24
+
25
+ #These are extra commands that we require, otherwise refuse the connection TODO: some of these are only valid on one direction
26
+ valid_commands += @required_relp_commands
27
+
28
+ #TODO: optional_relp_commands
29
+
30
+ #TODO: vague mentions of abort and starttls commands in spec need looking into
31
+ return valid_commands.include?(command)
32
+ end
33
+
34
+ def frame_write(socket, frame)
35
+ unless self.server? #I think we have to trust a server to be using the correct txnr
36
+ #Only allow txnr to be 0 or be determined automatically
37
+ frame['txnr'] = self.nexttxnr() unless frame['txnr']==0
38
+ end
39
+ frame['txnr'] = frame['txnr'].to_s
40
+ frame['message'] = '' if frame['message'].nil?
41
+ frame['datalen'] = frame['message'].length.to_s
42
+ wiredata=[
43
+ frame['txnr'],
44
+ frame['command'],
45
+ frame['datalen'],
46
+ frame['message']
47
+ ].join(' ').strip
48
+ begin
49
+ @logger.debug? and @logger.debug("Writing to socket", :data => wiredata)
50
+ socket.write(wiredata)
51
+ #Ending each frame with a newline is required in the specifications
52
+ #Doing it a separately is useful (but a bit of a bodge) because
53
+ #for some reason it seems to take 2 writes after the server closes the
54
+ #connection before we get an exception
55
+ socket.write("\n")
56
+ rescue Errno::EPIPE,IOError,Errno::ECONNRESET#TODO: is this sufficient to catch all broken connections?
57
+ raise ConnectionClosed
58
+ end
59
+ return frame['txnr'].to_i
60
+ end
61
+
62
+ def frame_read(socket)
63
+ begin
64
+ frame = Hash.new
65
+ frame['txnr'] = socket.readline(' ').strip.to_i
66
+ frame['command'] = socket.readline(' ').strip
67
+
68
+ #Things get a little tricky here because if the length is 0 it is not followed by a space.
69
+ leading_digit=socket.read(1)
70
+ if leading_digit=='0' then
71
+ frame['datalen'] = 0
72
+ frame['message'] = ''
73
+ else
74
+ frame['datalen'] = (leading_digit + socket.readline(' ')).strip.to_i
75
+ frame['message'] = socket.read(frame['datalen'])
76
+ end
77
+ @logger.debug? and @logger.debug("Read frame", :frame => frame)
78
+ rescue EOFError,Errno::ECONNRESET,IOError
79
+ raise ConnectionClosed
80
+ end
81
+ if ! self.valid_command?(frame['command'])#TODO: is this enough to catch framing errors?
82
+ if self.server?
83
+ self.serverclose(socket)
84
+ else
85
+ self.close
86
+ end
87
+ raise InvalidCommand,frame['command']
88
+ end
89
+ return frame
90
+ end
91
+
92
+ def server?
93
+ @server
94
+ end
95
+
96
+ end
97
+
98
+ class RelpServer < Relp
99
+
100
+ def initialize(host,port,required_commands=[])
101
+ @logger = Cabin::Channel.get(LogStash)
102
+
103
+ @server=true
104
+
105
+ #These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.)
106
+ @basic_relp_commands = ['close']#TODO: check for others
107
+
108
+ #These are extra commands that we require, otherwise refuse the connection
109
+ @required_relp_commands = required_commands
110
+
111
+ begin
112
+ @server = TCPServer.new(host, port)
113
+ rescue Errno::EADDRINUSE
114
+ @logger.error("Could not start RELP server: Address in use",
115
+ :host => host, :port => port)
116
+ raise
117
+ end
118
+ @logger.info? and @logger.info("Started RELP Server", :host => host, :port => port)
119
+ end
120
+
121
+ def accept
122
+ socket = @server.accept
123
+ frame=self.frame_read(socket)
124
+ if frame['command'] == 'open'
125
+ offer=Hash[*frame['message'].scan(/^(.*)=(.*)$/).flatten]
126
+ if offer['relp_version'].nil?
127
+ @logger.warn("No relp version specified")
128
+ #if no version specified, relp spec says we must close connection
129
+ self.serverclose(socket)
130
+ raise RelpError, 'No relp_version specified'
131
+ #subtracting one array from the other checks to see if all elements in @required_relp_commands are present in the offer
132
+ elsif ! (@required_relp_commands - offer['commands'].split(',')).empty?
133
+ @logger.warn("Not all required commands are available", :required => @required_relp_commands, :offer => offer['commands'])
134
+ #Tell them why we're closing the connection:
135
+ response_frame = Hash.new
136
+ response_frame['txnr'] = frame['txnr']
137
+ response_frame['command'] = 'rsp'
138
+ response_frame['message'] = '500 Required command(s) '
139
+ + (@required_relp_commands - offer['commands'].split(',')).join(',')
140
+ + ' not offered'
141
+ self.frame_write(socket,response_frame)
142
+ self.serverclose(socket)
143
+ raise InsufficientCommands, offer['commands']
144
+ + ' offered, require ' + @required_relp_commands.join(',')
145
+ else
146
+ #attempt to set up connection
147
+ response_frame = Hash.new
148
+ response_frame['txnr'] = frame['txnr']
149
+ response_frame['command'] = 'rsp'
150
+
151
+ response_frame['message'] = '200 OK '
152
+ response_frame['message'] += 'relp_version=' + RelpVersion + "\n"
153
+ response_frame['message'] += 'relp_software=' + RelpSoftware + "\n"
154
+ response_frame['message'] += 'commands=' + @required_relp_commands.join(',')#TODO: optional ones
155
+ self.frame_write(socket, response_frame)
156
+ return self, socket
157
+ end
158
+ else
159
+ self.serverclose(socket)
160
+ raise InappropriateCommand, frame['command'] + ' expecting open'
161
+ end
162
+ end
163
+
164
+ #This does not ack the frame, just reads it
165
+ def syslog_read(socket)
166
+ frame = self.frame_read(socket)
167
+ if frame['command'] == 'syslog'
168
+ return frame
169
+ elsif frame['command'] == 'close'
170
+ #the client is closing the connection, acknowledge the close and act on it
171
+ response_frame = Hash.new
172
+ response_frame['txnr'] = frame['txnr']
173
+ response_frame['command'] = 'rsp'
174
+ self.frame_write(socket,response_frame)
175
+ self.serverclose(socket)
176
+ raise ConnectionClosed
177
+ else
178
+ #the client is trying to do something unexpected
179
+ self.serverclose(socket)
180
+ raise InappropriateCommand, frame['command'] + ' expecting syslog'
181
+ end
182
+ end
183
+
184
+ def serverclose(socket)
185
+ frame = Hash.new
186
+ frame['txnr'] = 0
187
+ frame['command'] = 'serverclose'
188
+ begin
189
+ self.frame_write(socket,frame)
190
+ socket.close
191
+ rescue ConnectionClosed
192
+ end
193
+ end
194
+
195
+ def shutdown
196
+ @server.close
197
+ rescue Exception#@server might already be down
198
+ end
199
+
200
+ def ack(socket, txnr)
201
+ frame = Hash.new
202
+ frame['txnr'] = txnr
203
+ frame['command'] = 'rsp'
204
+ frame['message'] = '200 OK'
205
+ self.frame_write(socket, frame)
206
+ end
207
+
208
+ end
209
+
210
+ #This is only used by the tests; any problems here are not as important as elsewhere
211
+ class RelpClient < Relp
212
+
213
+ def initialize(host,port,required_commands = [],buffer_size = 128,
214
+ retransmission_timeout=10)
215
+ @logger = Cabin::Channel.get(LogStash)
216
+ @logger.info? and @logger.info("Starting RELP client", :host => host, :port => port)
217
+ @server = false
218
+ @buffer = Hash.new
219
+
220
+ @buffer_size = buffer_size
221
+ @retransmission_timeout = retransmission_timeout
222
+
223
+ #These are things that are part of the basic protocol, but only valid in one direction (rsp, close etc.)
224
+ @basic_relp_commands = ['serverclose','rsp']#TODO: check for others
225
+
226
+ #These are extra commands that we require, otherwise refuse the connection
227
+ @required_relp_commands = required_commands
228
+
229
+ @socket=TCPSocket.new(host,port)
230
+
231
+ #This'll start the automatic frame numbering
232
+ @lasttxnr = 0
233
+
234
+ offer=Hash.new
235
+ offer['command'] = 'open'
236
+ offer['message'] = 'relp_version=' + RelpVersion + "\n"
237
+ offer['message'] += 'relp_software=' + RelpSoftware + "\n"
238
+ offer['message'] += 'commands=' + @required_relp_commands.join(',')#TODO: add optional ones
239
+ self.frame_write(@socket, offer)
240
+ response_frame = self.frame_read(@socket)
241
+ if response_frame['message'][0,3] != '200'
242
+ raise RelpError,response_frame['message']
243
+ end
244
+
245
+ response=Hash[*response_frame['message'][7..-1].scan(/^(.*)=(.*)$/).flatten]
246
+ if response['relp_version'].nil?
247
+ #if no version specified, relp spec says we must close connection
248
+ self.close()
249
+ raise RelpError, 'No relp_version specified; offer: '
250
+ + response_frame['message'][6..-1].scan(/^(.*)=(.*)$/).flatten
251
+
252
+ #subtracting one array from the other checks to see if all elements in @required_relp_commands are present in the offer
253
+ elsif ! (@required_relp_commands - response['commands'].split(',')).empty?
254
+ #if it can't receive syslog it's useless to us; close the connection
255
+ self.close()
256
+ raise InsufficientCommands, response['commands'] + ' offered, require '
257
+ + @required_relp_commands.join(',')
258
+ end
259
+ #If we've got this far with no problems, we're good to go
260
+ @logger.info? and @logger.info("Connection establish with server")
261
+
262
+ #This thread deals with responses that come back
263
+ reader = Thread.start do
264
+ loop do
265
+ f = self.frame_read(@socket)
266
+ if f['command'] == 'rsp' && f['message'] == '200 OK'
267
+ @buffer.delete(f['txnr'])
268
+ elsif f['command'] == 'rsp' && f['message'][0,1] == '5'
269
+ #TODO: What if we get an error for something we're already retransmitted due to timeout?
270
+ new_txnr = self.frame_write(@socket, @buffer[f['txnr']])
271
+ @buffer[new_txnr] = @buffer[f['txnr']]
272
+ @buffer.delete(f['txnr'])
273
+ elsif f['command'] == 'serverclose' || f['txnr'] == @close_txnr
274
+ break
275
+ else
276
+ #Don't know what's going on if we get here, but it can't be good
277
+ raise RelpError#TODO: raising errors like this makes no sense
278
+ end
279
+ end
280
+ end
281
+
282
+ #While this one deals with frames for which we get no reply
283
+ Thread.start do
284
+ old_buffer = Hash.new
285
+ loop do
286
+ #This returns old txnrs that are still present
287
+ (@buffer.keys & old_buffer.keys).each do |txnr|
288
+ new_txnr = self.frame_write(@socket, @buffer[txnr])
289
+ @buffer[new_txnr] = @buffer[txnr]
290
+ @buffer.delete(txnr)
291
+ end
292
+ old_buffer = @buffer
293
+ sleep @retransmission_timeout
294
+ end
295
+ end
296
+ end
297
+
298
+ #TODO: have a way to get back unacked messages on close
299
+ def close
300
+ frame = Hash.new
301
+ frame['command'] = 'close'
302
+ @close_txnr=self.frame_write(@socket, frame)
303
+ #TODO: ought to properly wait for a reply etc. The serverclose will make it work though
304
+ sleep @retransmission_timeout
305
+ @socket.close#TODO: shutdown?
306
+ return @buffer
307
+ end
308
+
309
+ def syslog_write(logline)
310
+
311
+ #If the buffer is already full, wait until a gap opens up
312
+ sleep 0.1 until @buffer.length<@buffer_size
313
+
314
+ frame = Hash.new
315
+ frame['command'] = 'syslog'
316
+ frame['message'] = logline
317
+
318
+ txnr = self.frame_write(@socket, frame)
319
+ @buffer[txnr] = frame
320
+ end
321
+
322
+ def nexttxnr
323
+ @lasttxnr += 1
324
+ end
325
+
326
+ end
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+
3
+ s.name = 'logstash-input-relp'
4
+ s.version = '0.1.0'
5
+ s.licenses = ['Apache License (2.0)']
6
+ s.summary = "Read RELP events over a TCP socket."
7
+ s.description = "Read RELP events over a TCP socket."
8
+ s.authors = ["Elasticsearch"]
9
+ s.email = 'richard.pijnenburg@elasticsearch.com'
10
+ s.homepage = "http://logstash.net/"
11
+ s.require_paths = ["lib"]
12
+
13
+ # Files
14
+ s.files = `git ls-files`.split($\)+::Dir.glob('vendor/*')
15
+
16
+ # Tests
17
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
18
+
19
+ # Special flag to let us know this is actually a logstash plugin
20
+ s.metadata = { "logstash_plugin" => "true", "group" => "input" }
21
+
22
+ # Gem dependencies
23
+ s.add_runtime_dependency 'logstash', '>= 1.4.0', '< 2.0.0'
24
+
25
+ s.add_runtime_dependency 'logstash-codec-plain'
26
+ end
27
+
@@ -0,0 +1,9 @@
1
+ require "gem_publisher"
2
+
3
+ desc "Publish gem to RubyGems.org"
4
+ task :publish_gem do |t|
5
+ gem_file = Dir.glob(File.expand_path('../*.gemspec',File.dirname(__FILE__))).first
6
+ gem = GemPublisher.publish_if_updated(gem_file, :rubygems)
7
+ puts "Published #{gem}" if gem
8
+ end
9
+
@@ -0,0 +1,169 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "digest/sha1"
4
+
5
+ def vendor(*args)
6
+ return File.join("vendor", *args)
7
+ end
8
+
9
+ directory "vendor/" => ["vendor"] do |task, args|
10
+ mkdir task.name
11
+ end
12
+
13
+ def fetch(url, sha1, output)
14
+
15
+ puts "Downloading #{url}"
16
+ actual_sha1 = download(url, output)
17
+
18
+ if actual_sha1 != sha1
19
+ fail "SHA1 does not match (expected '#{sha1}' but got '#{actual_sha1}')"
20
+ end
21
+ end # def fetch
22
+
23
+ def file_fetch(url, sha1)
24
+ filename = File.basename( URI(url).path )
25
+ output = "vendor/#{filename}"
26
+ task output => [ "vendor/" ] do
27
+ begin
28
+ actual_sha1 = file_sha1(output)
29
+ if actual_sha1 != sha1
30
+ fetch(url, sha1, output)
31
+ end
32
+ rescue Errno::ENOENT
33
+ fetch(url, sha1, output)
34
+ end
35
+ end.invoke
36
+
37
+ return output
38
+ end
39
+
40
+ def file_sha1(path)
41
+ digest = Digest::SHA1.new
42
+ fd = File.new(path, "r")
43
+ while true
44
+ begin
45
+ digest << fd.sysread(16384)
46
+ rescue EOFError
47
+ break
48
+ end
49
+ end
50
+ return digest.hexdigest
51
+ ensure
52
+ fd.close if fd
53
+ end
54
+
55
+ def download(url, output)
56
+ uri = URI(url)
57
+ digest = Digest::SHA1.new
58
+ tmp = "#{output}.tmp"
59
+ Net::HTTP.start(uri.host, uri.port, :use_ssl => (uri.scheme == "https")) do |http|
60
+ request = Net::HTTP::Get.new(uri.path)
61
+ http.request(request) do |response|
62
+ fail "HTTP fetch failed for #{url}. #{response}" if [200, 301].include?(response.code)
63
+ size = (response["content-length"].to_i || -1).to_f
64
+ count = 0
65
+ File.open(tmp, "w") do |fd|
66
+ response.read_body do |chunk|
67
+ fd.write(chunk)
68
+ digest << chunk
69
+ if size > 0 && $stdout.tty?
70
+ count += chunk.bytesize
71
+ $stdout.write(sprintf("\r%0.2f%%", count/size * 100))
72
+ end
73
+ end
74
+ end
75
+ $stdout.write("\r \r") if $stdout.tty?
76
+ end
77
+ end
78
+
79
+ File.rename(tmp, output)
80
+
81
+ return digest.hexdigest
82
+ rescue SocketError => e
83
+ puts "Failure while downloading #{url}: #{e}"
84
+ raise
85
+ ensure
86
+ File.unlink(tmp) if File.exist?(tmp)
87
+ end # def download
88
+
89
+ def untar(tarball, &block)
90
+ require "archive/tar/minitar"
91
+ tgz = Zlib::GzipReader.new(File.open(tarball))
92
+ # Pull out typesdb
93
+ tar = Archive::Tar::Minitar::Input.open(tgz)
94
+ tar.each do |entry|
95
+ path = block.call(entry)
96
+ next if path.nil?
97
+ parent = File.dirname(path)
98
+
99
+ mkdir_p parent unless File.directory?(parent)
100
+
101
+ # Skip this file if the output file is the same size
102
+ if entry.directory?
103
+ mkdir path unless File.directory?(path)
104
+ else
105
+ entry_mode = entry.instance_eval { @mode } & 0777
106
+ if File.exists?(path)
107
+ stat = File.stat(path)
108
+ # TODO(sissel): Submit a patch to archive-tar-minitar upstream to
109
+ # expose headers in the entry.
110
+ entry_size = entry.instance_eval { @size }
111
+ # If file sizes are same, skip writing.
112
+ next if stat.size == entry_size && (stat.mode & 0777) == entry_mode
113
+ end
114
+ puts "Extracting #{entry.full_name} from #{tarball} #{entry_mode.to_s(8)}"
115
+ File.open(path, "w") do |fd|
116
+ # eof? check lets us skip empty files. Necessary because the API provided by
117
+ # Archive::Tar::Minitar::Reader::EntryStream only mostly acts like an
118
+ # IO object. Something about empty files in this EntryStream causes
119
+ # IO.copy_stream to throw "can't convert nil into String" on JRuby
120
+ # TODO(sissel): File a bug about this.
121
+ while !entry.eof?
122
+ chunk = entry.read(16384)
123
+ fd.write(chunk)
124
+ end
125
+ #IO.copy_stream(entry, fd)
126
+ end
127
+ File.chmod(entry_mode, path)
128
+ end
129
+ end
130
+ tar.close
131
+ File.unlink(tarball) if File.file?(tarball)
132
+ end # def untar
133
+
134
+ def ungz(file)
135
+
136
+ outpath = file.gsub('.gz', '')
137
+ tgz = Zlib::GzipReader.new(File.open(file))
138
+ begin
139
+ File.open(outpath, "w") do |out|
140
+ IO::copy_stream(tgz, out)
141
+ end
142
+ File.unlink(file)
143
+ rescue
144
+ File.unlink(outpath) if File.file?(outpath)
145
+ raise
146
+ end
147
+ tgz.close
148
+ end
149
+
150
+ desc "Process any vendor files required for this plugin"
151
+ task "vendor" do |task, args|
152
+
153
+ @files.each do |file|
154
+ download = file_fetch(file['url'], file['sha1'])
155
+ if download =~ /.tar.gz/
156
+ prefix = download.gsub('.tar.gz', '').gsub('vendor/', '')
157
+ untar(download) do |entry|
158
+ if !file['files'].nil?
159
+ next unless file['files'].include?(entry.full_name.gsub(prefix, ''))
160
+ out = entry.full_name.split("/").last
161
+ end
162
+ File.join('vendor', out)
163
+ end
164
+ elsif download =~ /.gz/
165
+ ungz(download)
166
+ end
167
+ end
168
+
169
+ end
@@ -0,0 +1,69 @@
1
+ # coding: utf-8
2
+ require "spec_helper"
3
+ require "socket"
4
+ require "logstash/util/relp"
5
+
6
+ describe "inputs/relp", :socket => true do
7
+
8
+ describe "Single client connection" do
9
+ event_count = 10
10
+ port = 5511
11
+ config <<-CONFIG
12
+ input {
13
+ relp {
14
+ type => "blah"
15
+ port => #{port}
16
+ }
17
+ }
18
+ CONFIG
19
+
20
+ input do |pipeline, queue|
21
+ th = Thread.new { pipeline.run }
22
+ sleep 0.1 while !pipeline.ready?
23
+
24
+ #Send events from clients
25
+ client = RelpClient.new("0.0.0.0", port, ["syslog"])
26
+ event_count.times do |value|
27
+ client.syslog_write("Hello #{value}")
28
+ end
29
+
30
+ events = event_count.times.collect { queue.pop }
31
+ event_count.times do |i|
32
+ insist { events[i]["message"] } == "Hello #{i}"
33
+ end
34
+
35
+ pipeline.shutdown
36
+ th.join
37
+ end # input
38
+ end
39
+ describe "Two client connection" do
40
+ event_count = 100
41
+ port = 5512
42
+ config <<-CONFIG
43
+ input {
44
+ relp {
45
+ type => "blah"
46
+ port => #{port}
47
+ }
48
+ }
49
+ CONFIG
50
+
51
+ input do |pipeline, queue|
52
+ Thread.new { pipeline.run }
53
+ sleep 0.1 while !pipeline.ready?
54
+
55
+ #Send events from clients sockets
56
+ client = RelpClient.new("0.0.0.0", port, ["syslog"])
57
+ client2 = RelpClient.new("0.0.0.0", port, ["syslog"])
58
+
59
+ event_count.times do |value|
60
+ client.syslog_write("Hello from client")
61
+ client2.syslog_write("Hello from client 2")
62
+ end
63
+
64
+ events = (event_count*2).times.collect { queue.pop }
65
+ insist { events.select{|event| event["message"]=="Hello from client" }.size } == event_count
66
+ insist { events.select{|event| event["message"]=="Hello from client 2" }.size } == event_count
67
+ end # input
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: logstash-input-relp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Elasticsearch
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: logstash
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 1.4.0
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: 2.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.4.0
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: 2.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: logstash-codec-plain
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ! '>='
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ description: Read RELP events over a TCP socket.
48
+ email: richard.pijnenburg@elasticsearch.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - Rakefile
57
+ - lib/logstash/inputs/relp.rb
58
+ - lib/logstash/util/relp.rb
59
+ - logstash-input-relp.gemspec
60
+ - rakelib/publish.rake
61
+ - rakelib/vendor.rake
62
+ - spec/inputs/relp_spec.rb
63
+ homepage: http://logstash.net/
64
+ licenses:
65
+ - Apache License (2.0)
66
+ metadata:
67
+ logstash_plugin: 'true'
68
+ group: input
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ! '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.4.1
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Read RELP events over a TCP socket.
89
+ test_files:
90
+ - spec/inputs/relp_spec.rb