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 +15 -0
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +13 -0
- data/Rakefile +6 -0
- data/lib/logstash/inputs/relp.rb +107 -0
- data/lib/logstash/util/relp.rb +326 -0
- data/logstash-input-relp.gemspec +27 -0
- data/rakelib/publish.rake +9 -0
- data/rakelib/vendor.rake +169 -0
- data/spec/inputs/relp_spec.rb +69 -0
- metadata +90 -0
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
data/Gemfile
ADDED
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,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
|
+
|
data/rakelib/vendor.rake
ADDED
@@ -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
|