log-courier 1.0.21.ga82ca4c

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,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