log-courier 1.0.21.ga82ca4c

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: 480d6e6a5958b6f307727dfa1713e8a9c0a10d29
4
+ data.tar.gz: 2f2de7aeedd2e539a16412dd40f8321ecc9cff20
5
+ SHA512:
6
+ metadata.gz: 3d2d449239c58e05a7654ad0f77148b2fd8ac5ebf0094f50b85c7311cb904650cc426a55aa2918a9b9334c70c283ca1d7aa47cb47c94b0999011bfc0949270b7
7
+ data.tar.gz: 8e51f6507f0b12a7ed56a17fb28213ea9a7f71b5257544c8191ad19bb70e91f1c6a8e940d37203fab587f3c564deb0551426fc8996081a7101faaf707d014748
@@ -0,0 +1,352 @@
1
+ # encoding: utf-8
2
+
3
+ # Copyright 2014 Jason Woods.
4
+ #
5
+ # This file is a modification of code from Logstash Forwarder.
6
+ # Copyright 2012-2013 Jordan Sissel and contributors.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require 'log-courier/event_queue'
21
+ require 'multi_json'
22
+ require 'thread'
23
+ require 'zlib'
24
+
25
+ module LogCourier
26
+ # TODO: Make these shared
27
+ class ClientShutdownSignal < StandardError; end
28
+ class ClientProtocolError < StandardError; end
29
+
30
+ # Describes a pending payload
31
+ class PendingPayload
32
+ attr_accessor :ack_events
33
+ attr_accessor :events
34
+ attr_accessor :nonce
35
+ attr_accessor :data
36
+
37
+ attr_accessor :previous
38
+ attr_accessor :next
39
+
40
+ def initialize(options = {})
41
+ @ack_events = 0
42
+
43
+ options.each do |k, v|
44
+ raise ArgumentError unless self.respond_to?(k)
45
+ instance_variable_set "@#{k}", v
46
+ end
47
+ end
48
+ end
49
+
50
+ # Implementation of a single client connection
51
+ class Client
52
+ def initialize(options = {})
53
+ @options = {
54
+ logger: nil,
55
+ spool_size: 1024,
56
+ idle_timeout: 5
57
+ }.merge!(options)
58
+
59
+ @logger = @options[:logger]
60
+
61
+ require 'log-courier/client_tls'
62
+ @client = ClientTls.new(@options)
63
+
64
+ # Load the json adapter
65
+ @json_adapter = MultiJson.adapter.instance
66
+
67
+ @event_queue = EventQueue.new @options[:spool_size]
68
+ @pending_payloads = {}
69
+ @first_payload = nil
70
+ @last_payload = nil
71
+
72
+ # Start the spooler which will collect events into chunks
73
+ @send_ready = false
74
+ @send_mutex = Mutex.new
75
+ @send_cond = ConditionVariable.new
76
+ @spooler_thread = Thread.new do
77
+ run_spooler
78
+ end
79
+
80
+ @pending_ping = false
81
+
82
+ # Start the IO thread
83
+ @io_control = EventQueue.new 1
84
+ @io_thread = Thread.new do
85
+ run_io
86
+ end
87
+ end
88
+
89
+ def publish(event)
90
+ # Pass the event into the spooler
91
+ @event_queue << event
92
+ end
93
+
94
+ def shutdown
95
+ # Raise a shutdown signal in the spooler and wait for it
96
+ @spooler_thread.raise ClientShutdownSignal
97
+ @io_thread.raise ClientShutdownSignal
98
+ @spooler_thread.join
99
+ @io_thread.join
100
+ end
101
+
102
+ def run_spooler
103
+ loop do
104
+ spooled = []
105
+ next_flush = Time.now.to_i + @options[:idle_timeout]
106
+
107
+ # The spooler loop
108
+ begin
109
+ loop do
110
+ event = @event_queue.pop next_flush - Time.now.to_i
111
+ spooled.push(event)
112
+ break if spooled.length >= @options[:spool_size]
113
+ end
114
+ rescue TimeoutError
115
+ # Hit timeout but no events, keep waiting
116
+ next if spooled.length == 0
117
+ end
118
+
119
+ # Pass through to io_control but only if we're ready to send
120
+ @send_mutex.synchronize do
121
+ @send_cond.wait(@send_mutex) unless @send_ready
122
+ @send_ready = false
123
+ @io_control << ['E', spooled]
124
+ end
125
+ end
126
+ rescue ClientShutdownSignal
127
+ # Just shutdown
128
+ 0
129
+ end
130
+
131
+ def run_io
132
+ # TODO: Make keepalive configurable?
133
+ @keepalive_timeout = 1800
134
+
135
+ # TODO: Make pending payload max configurable?
136
+ max_pending_payloads = 100
137
+
138
+ retry_payload = nil
139
+
140
+ can_send = true
141
+
142
+ loop do
143
+ # Reconnect loop
144
+ @client.connect @io_control
145
+
146
+ reset_keepalive
147
+
148
+ # Capture send exceptions
149
+ begin
150
+ # IO loop
151
+ loop do
152
+ catch :keepalive do
153
+ begin
154
+ action = @io_control.pop @keepalive_next - Time.now.to_i
155
+
156
+ # Process the action
157
+ case action[0]
158
+ when 'S'
159
+ # If we're flushing through the pending, pick from there
160
+ unless retry_payload.nil?
161
+ # Regenerate data if we need to
162
+ retry_payload.data = buffer_jdat_data(retry_payload.events, retry_payload.nonce) if retry_payload.data == nil
163
+
164
+ # Send and move onto next
165
+ @client.send 'JDAT', retry_payload.data
166
+
167
+ retry_payload = retry_payload.next
168
+ throw :keepalive
169
+ end
170
+
171
+ # Ready to send, allow spooler to pass us something
172
+ @send_mutex.synchronize do
173
+ @send_ready = true
174
+ @send_cond.signal
175
+ end
176
+
177
+ can_send = true
178
+ when 'E'
179
+ # If we have too many pending payloads, pause the IO
180
+ if @pending_payloads.length + 1 >= max_pending_payloads
181
+ @client.pause_send
182
+ end
183
+
184
+ # Received some events - send them
185
+ send_jdat action[1]
186
+
187
+ # The send action will trigger another "S" if we have more send buffer
188
+ can_send = false
189
+ when 'R'
190
+ # Received a message
191
+ signature, message = action[1..2]
192
+ case signature
193
+ when 'PONG'
194
+ process_pong message
195
+ when 'ACKN'
196
+ process_ackn message
197
+ else
198
+ # Unknown message - only listener is allowed to respond with a "????" message
199
+ # TODO: What should we do? Just ignore for now and let timeouts conquer
200
+ end
201
+ when 'F'
202
+ # Reconnect, an error occurred
203
+ break
204
+ end
205
+ rescue TimeoutError
206
+ # Keepalive timeout hit, send a PING unless we were awaiting a PONG
207
+ if @pending_ping
208
+ # Timed out, break into reconnect
209
+ raise TimeoutError
210
+ end
211
+
212
+ # Is send full? can_send will be false if so
213
+ # We should've started receiving ACK by now so time out
214
+ raise TimeoutError unless can_send
215
+
216
+ # Send PING
217
+ send_ping
218
+
219
+ # We may have filled send buffer
220
+ can_send = false
221
+ end
222
+ end
223
+
224
+ # Reset keepalive timeout
225
+ reset_keepalive
226
+ end
227
+ rescue ClientProtocolError => e
228
+ # Reconnect required due to a protocol error
229
+ @logger.warn("[LogCourierClient] Protocol error: #{e}") unless @logger.nil?
230
+ rescue TimeoutError
231
+ # Reconnect due to timeout
232
+ @logger.warn('[LogCourierClient] Timeout occurred') unless @logger.nil?
233
+ rescue ClientShutdownSignal
234
+ # Shutdown, break out
235
+ break
236
+ rescue => e
237
+ # Unknown error occurred
238
+ @logger.warn("[LogCourierClient] Unknown error: #{e}") unless @logger.nil?
239
+ @logger.warn("[LogCourierClient] #{e.backtrace}: #{e.message} (#{e.class})") unless @logger.nil?
240
+ end
241
+
242
+ # Disconnect and retry payloads
243
+ @client.disconnect
244
+ retry_payload = @first_payload
245
+
246
+ # TODO: Make reconnect time configurable?
247
+ sleep 5
248
+ end
249
+
250
+ @client.disconnect
251
+ end
252
+
253
+ def reset_keepalive
254
+ @keepalive_next = Time.now.to_i + @keepalive_timeout
255
+ end
256
+
257
+ def generate_nonce
258
+ (0...16).map { rand(256).chr }.join("")
259
+ end
260
+
261
+ def send_ping
262
+ # Send it
263
+ @client.send 'PING', ''
264
+ end
265
+
266
+ def send_jdat(events)
267
+ # Generate the JSON payload and compress it
268
+ nonce = generate_nonce
269
+ data = buffer_jdat_data(events, nonce)
270
+
271
+ # Save the pending payload
272
+ payload = PendingPayload.new(
273
+ :events => events,
274
+ :nonce => nonce,
275
+ :data => data
276
+ )
277
+
278
+ @pending_payloads[nonce] = payload
279
+
280
+ if @first_payload.nil?
281
+ @first_payload = payload
282
+ @last_payload = payload
283
+ else
284
+ @last_payload.next = payload
285
+ @last_payload = payload
286
+ end
287
+
288
+ # Send it
289
+ @client.send 'JDAT', payload.data
290
+ end
291
+
292
+ def buffer_jdat_data(events, nonce)
293
+ buffer = Zlib::Deflate.new
294
+
295
+ # Write each event in JSON format
296
+ events.each do |event|
297
+ buffer_jdat_data_event(buffer, event)
298
+ end
299
+
300
+ # Generate and return the message
301
+ nonce + buffer.flush(Zlib::FINISH)
302
+ end
303
+
304
+ def buffer_jdat_data_event(buffer, event)
305
+ json_data = @json_adapter.dump(event)
306
+
307
+ # Add length and then the data
308
+ buffer << [json_data.length].pack('N') << json_data
309
+ end
310
+
311
+ def process_pong(message)
312
+ # Sanity
313
+ if message.length != 0
314
+ raise ClientProtocolError, "Unexpected data attached to pong message (#{message.length})"
315
+ end
316
+
317
+ # No longer pending a PONG
318
+ @ping_pending = false
319
+ end
320
+
321
+ def process_ackn(message)
322
+ # Sanity
323
+ if message.length != 20
324
+ raise ClientProtocolError, "ACKN message size invalid (#{message.length})"
325
+ end
326
+
327
+ # Grab nonce
328
+ sequence, nonce = message[0...4].unpack('N').first, message[4..-1]
329
+
330
+ # Find the payload - skip if we couldn't as it will just a duplicated ACK
331
+ return unless @pending_payloads.key?(nonce)
332
+
333
+ payload = @pending_payloads[nonce]
334
+
335
+ # Full ACK?
336
+ # TODO: protocol error if sequence too large?
337
+ if sequence >= payload.events.length
338
+ @client.resume_send if @client.send_paused?
339
+
340
+ @pending_payloads.delete nonce
341
+ payload.previous.next = payload.next
342
+ else
343
+ # Partial ACK - only process if something was actually processed
344
+ if sequence > payload.ack_events
345
+ payload.ack_events = sequence
346
+ payload.events = payload.events[0...sequence]
347
+ payload.data = nil
348
+ end
349
+ end
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,217 @@
1
+ # encoding: utf-8
2
+
3
+ # Copyright 2014 Jason Woods.
4
+ #
5
+ # This file is a modification of code from Logstash Forwarder.
6
+ # Copyright 2012-2013 Jordan Sissel and contributors.
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+
20
+ require 'openssl'
21
+ require 'socket'
22
+ require 'thread'
23
+
24
+ module LogCourier
25
+ # TLS transport implementation
26
+ class ClientTls
27
+ def initialize(options = {})
28
+ @options = {
29
+ logger: nil,
30
+ port: nil,
31
+ addresses: [],
32
+ ssl_ca: nil,
33
+ ssl_certificate: nil,
34
+ ssl_key: nil,
35
+ ssl_key_passphrase: nil
36
+ }.merge!(options)
37
+
38
+ @logger = @options[:logger]
39
+
40
+ [:port, :ssl_ca].each do |k|
41
+ raise "[LogCourierClient] '#{k}' is required" if @options[k].nil?
42
+ end
43
+
44
+ raise '[LogCourierClient] \'addresses\' must contain at least one address' if @options[:addresses].empty?
45
+
46
+ c = 0
47
+ [:ssl_certificate, :ssl_key].each do
48
+ c += 1
49
+ end
50
+
51
+ raise '[LogCourierClient] \'ssl_certificate\' and \'ssl_key\' must be specified together' if c == 1
52
+ end
53
+
54
+ def connect(io_control)
55
+ begin
56
+ tls_connect
57
+ rescue ClientShutdownSignal
58
+ raise
59
+ rescue
60
+ # TODO: Make this configurable
61
+ sleep 5
62
+ retry
63
+ end
64
+
65
+ @send_q = SizedQueue.new 1
66
+ @send_paused = false
67
+
68
+ @send_thread = Thread.new do
69
+ run_send io_control
70
+ end
71
+ @recv_thread = Thread.new do
72
+ run_recv io_control
73
+ end
74
+ end
75
+
76
+ def disconnect
77
+ @send_thread.raise ClientShutdownSignal
78
+ @send_thread.join
79
+ @recv_thread.raise ClientShutdownSignal
80
+ @recv_thread.join
81
+ end
82
+
83
+ def run_send(io_control)
84
+ # Ask for something to send
85
+ io_control << ['S']
86
+
87
+ # If paused, we still accept message to send, but we don't release "S" to ask for more
88
+ # As soon as we resume we then release "S" to ask for more
89
+ paused = false
90
+
91
+ loop do
92
+ # Wait for data and send when we get it
93
+ message = @send_q.pop
94
+
95
+ # A nil is a pause/resume
96
+ if message.nil?
97
+ if paused
98
+ paused = false
99
+ io_control << ['S']
100
+ else
101
+ paused = true
102
+ next
103
+ end
104
+ else
105
+ # Ask for more to send while we send this one
106
+ io_control << ['S'] unless paused
107
+
108
+ @ssl_client.write message
109
+ end
110
+ end
111
+ rescue OpenSSL::SSL::SSLError, IOError, Errno::ECONNRESET => e
112
+ @logger.warn("[LogCourierClient] SSL write error: #{e}") unless @logger.nil?
113
+ io_control << ['F']
114
+ rescue ClientShutdownSignal
115
+ # Just shutdown
116
+ rescue => e
117
+ @logger.warn("[LogCourierClient] Unknown SSL write error: #{e}") unless @logger.nil?
118
+ @logger.warn("[LogCourierClient] #{e.backtrace}: #{e.message} (#{e.class})") unless @logger.nil?
119
+ io_control << ['F']
120
+ end
121
+
122
+ def run_recv(io_control)
123
+ loop do
124
+ # Grab a header
125
+ header = @ssl_client.read(8)
126
+ raise EOFError if header.nil?
127
+
128
+ # Decode signature and length
129
+ signature, length = header.unpack('A4N')
130
+
131
+ if length > 1048576
132
+ # Too big raise error
133
+ @logger.warn("[LogCourierClient] Invalid message: data too big (#{length})") unless @logger.nil?
134
+ io_control << ['F']
135
+ break
136
+ end
137
+
138
+ # Read remainder
139
+ message = @ssl_client.read(length)
140
+
141
+ # Pass through to receive
142
+ io_control << ['R', signature, message]
143
+ end
144
+ rescue OpenSSL::SSL::SSLError, IOError, Errno::ECONNRESET => e
145
+ @logger.warn("[LogCourierClient] SSL read error: #{e}") unless @logger.nil?
146
+ io_control << ['F']
147
+ rescue EOFError
148
+ @logger.warn("[LogCourierClient] Connection closed by server") unless @logger.nil?
149
+ io_control << ['F']
150
+ rescue ClientShutdownSignal
151
+ # Just shutdown
152
+ rescue => e
153
+ @logger.warn("[LogCourierClient] Unknown SSL read error: #{e}") unless @logger.nil?
154
+ @logger.warn("[LogCourierClient] #{e.backtrace}: #{e.message} (#{e.class})") unless @logger.nil?
155
+ io_control << ['F']
156
+ end
157
+
158
+ def send(signature, message)
159
+ # Add to send queue
160
+ @send_q << [signature, message.length].pack('A4N') + message
161
+ end
162
+
163
+ def pause_send
164
+ return if @send_paused
165
+ @send_paused = true
166
+ @send_q << nil
167
+ end
168
+
169
+ def send_paused
170
+ @send_paused
171
+ end
172
+
173
+ def resume_send
174
+ if @send_paused
175
+ @send_paused = false
176
+ @send_q << nil
177
+ end
178
+ end
179
+
180
+ def tls_connect
181
+ # TODO: Implement random selection - and don't use separate :port - remember to update post_connection_check too
182
+ @logger.info("[LogCourierClient] Connecting to #{@options[:addresses][0]}:#{@options[:port]}") unless @logger.nil?
183
+ tcp_socket = TCPSocket.new(@options[:addresses][0], @options[:port])
184
+
185
+ ssl = OpenSSL::SSL::SSLContext.new
186
+
187
+ unless @options[:ssl_certificate].nil?
188
+ ssl.cert = OpenSSL::X509::Certificate.new(File.read(@options[:ssl_certificate]))
189
+ ssl.key = OpenSSL::PKey::RSA.new(File.read(@options[:ssl_key]), @options[:ssl_key_passphrase])
190
+ end
191
+
192
+ cert_store = OpenSSL::X509::Store.new
193
+ cert_store.add_file(@options[:ssl_ca])
194
+ #ssl.cert_store = cert_store
195
+ ssl.verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
196
+
197
+ @ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_socket)
198
+
199
+ socket = @ssl_client.connect
200
+
201
+ # Verify certificate
202
+ socket.post_connection_check(@options[:addresses][0])
203
+
204
+ @logger.info("[LogCourierClient] Connected successfully") unless @logger.nil?
205
+
206
+ socket
207
+ rescue OpenSSL::SSL::SSLError, IOError, Errno::ECONNRESET => e
208
+ @logger.warn("[LogCourierClient] Connection to #{@options[:addresses][0]}:#{@options[:port]} failed: #{e}") unless @logger.nil?
209
+ rescue ClientShutdownSignal
210
+ # Just shutdown
211
+ 0
212
+ rescue => e
213
+ @logger.warn("[LogCourierClient] Unknown connection failure to #{@options[:addresses][0]}:#{@options[:port]}: #{e}") unless @logger.nil?
214
+ @logger.warn("[LogCourierClient] #{e.backtrace}: #{e.message} (#{e.class})") unless @logger.nil?
215
+ end
216
+ end
217
+ end