jls-lumberjack-logzio 0.0.26

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
+ SHA256:
3
+ metadata.gz: a1e528d32753cc4cf001f41c1b2a8333d6404c73fb6d46fd9958dbd530cb233d
4
+ data.tar.gz: d9bf8d7e486345f5ea41f70bb9f4f4f202d6b5026f7888f5c556dbe82eda560a
5
+ SHA512:
6
+ metadata.gz: acb9f7b86989d76046fe8d886c972975eaa9a8510e27edc069ef8bdaafd8ed311927c1853fa58493edd5c486a7c55a6a6ffd977399c2943f04ee803711de9eb4
7
+ data.tar.gz: 26e5c8b67f70640144cf1c4859d9eb4d661eaa9bf684bee4df7e8ae688a3af42a1b033368693e3de4a9cc1fc30d299281306b81f189215a84c7161dc8eb6cec3
@@ -0,0 +1,217 @@
1
+ # encoding: utf-8
2
+ require "lumberjack"
3
+ require "socket"
4
+ require "thread"
5
+ require "openssl"
6
+ require "zlib"
7
+
8
+ module Lumberjack
9
+ class Client
10
+ def initialize(opts={})
11
+ @opts = {
12
+ :port => 0,
13
+ :addresses => [],
14
+ :ssl_certificate => nil,
15
+ :ssl => true,
16
+ :json => false,
17
+ }.merge(opts)
18
+
19
+ @opts[:addresses] = [@opts[:addresses]] if @opts[:addresses].class == String
20
+ raise "Must set a port." if @opts[:port] == 0
21
+ raise "Must set atleast one address" if @opts[:addresses].empty? == 0
22
+ raise "Must set a ssl certificate or path" if @opts[:ssl_certificate].nil? && @opts[:ssl]
23
+
24
+ @socket = connect
25
+ end
26
+
27
+ private
28
+ def connect
29
+ addrs = @opts[:addresses].shuffle
30
+ begin
31
+ raise "Could not connect to any hosts" if addrs.empty?
32
+ opts = @opts
33
+ opts[:address] = addrs.pop
34
+ Lumberjack::Socket.new(opts)
35
+ rescue *[Errno::ECONNREFUSED,SocketError]
36
+ retry
37
+ end
38
+ end
39
+
40
+ public
41
+ def write(elements, opts={})
42
+ @socket.write_sync(elements, opts)
43
+ end
44
+
45
+ public
46
+ def host
47
+ @socket.host
48
+ end
49
+ end
50
+
51
+ class Socket
52
+ # Create a new Lumberjack Socket.
53
+ #
54
+ # - options is a hash. Valid options are:
55
+ #
56
+ # * :port - the port to listen on
57
+ # * :address - the host/address to bind to
58
+ # * :ssl - enable/disable ssl support
59
+ # * :ssl_certificate - the path to the ssl cert to use.
60
+ # If ssl_certificate is not set, a plain tcp connection
61
+ # will be used.
62
+ attr_reader :sequence
63
+ attr_reader :host
64
+ def initialize(opts={})
65
+ @sequence = 0
66
+ @last_ack = 0
67
+ @opts = {
68
+ :port => 0,
69
+ :address => "127.0.0.1",
70
+ :ssl_certificate => nil,
71
+ :ssl => true,
72
+ :json => false,
73
+ }.merge(opts)
74
+ @host = @opts[:address]
75
+
76
+ connection_start(opts)
77
+ end
78
+
79
+ private
80
+ def connection_start(opts)
81
+ tcp_socket = TCPSocket.new(opts[:address], opts[:port])
82
+ if !opts[:ssl]
83
+ @socket = tcp_socket
84
+ else
85
+ certificate_store = OpenSSL::X509::Store.new
86
+ certificate_store.add_file(opts[:ssl_certificate])
87
+
88
+ ssl_context = OpenSSL::SSL::SSLContext.new
89
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
90
+ ssl_context.cert_store = certificate_store
91
+
92
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
93
+ @socket.connect
94
+ end
95
+ end
96
+
97
+ private
98
+ def inc
99
+ @sequence = 0 if @sequence + 1 > Lumberjack::SEQUENCE_MAX
100
+ @sequence = @sequence + 1
101
+ end
102
+
103
+ private
104
+ def send_window_size(size)
105
+ @socket.syswrite(["1", "W", size].pack("AAN"))
106
+ end
107
+
108
+ private
109
+ def send_payload(payload)
110
+ # SSLSocket has a limit of 16k per message
111
+ # execute multiple writes if needed
112
+ bytes_written = 0
113
+ while bytes_written < payload.bytesize
114
+ bytes_written += @socket.syswrite(payload.byteslice(bytes_written..-1))
115
+ end
116
+ end
117
+
118
+ public
119
+ def write_sync(elements, opts={})
120
+ options = {
121
+ :json => @opts[:json],
122
+ }.merge(opts)
123
+
124
+ elements = [elements] if elements.is_a?(Hash)
125
+ send_window_size(elements.size)
126
+
127
+ encoder = options[:json] ? JsonEncoder : FrameEncoder
128
+ payload = elements.map { |element| encoder.to_frame(element, inc) }.join
129
+ compress = compress_payload(payload)
130
+ send_payload(compress)
131
+
132
+ ack(elements.size)
133
+ end
134
+
135
+ private
136
+ def compress_payload(payload)
137
+ compress = Zlib::Deflate.deflate(payload)
138
+ ["1", "C", compress.bytesize, compress].pack("AANA*")
139
+ end
140
+
141
+ private
142
+ def ack(size)
143
+ _, type = read_version_and_type
144
+ raise "Whoa we shouldn't get this frame: #{type}" if type != "A"
145
+ @last_ack = read_last_ack
146
+ end
147
+
148
+ private
149
+ def unacked_sequence_size
150
+ sequence - (@last_ack + 1)
151
+ end
152
+
153
+ private
154
+ def read_version_and_type
155
+ version = @socket.read(1)
156
+ type = @socket.read(1)
157
+ [version, type]
158
+ end
159
+
160
+ private
161
+ def read_last_ack
162
+ @socket.read(4).unpack("N").first
163
+ end
164
+ end
165
+
166
+ module JsonEncoder
167
+ def self.to_frame(hash, sequence)
168
+ json = Lumberjack::json.dump(hash)
169
+ json_length = json.bytesize
170
+ pack = "AANNA#{json_length}"
171
+ frame = ["1", "J", sequence, json_length, json]
172
+ frame.pack(pack)
173
+ end
174
+ end # JsonEncoder
175
+
176
+ module FrameEncoder
177
+ def self.to_frame(hash, sequence)
178
+ frame = ["1", "D", sequence]
179
+ pack = "AAN"
180
+ keys = deep_keys(hash)
181
+ frame << keys.length
182
+ pack << "N"
183
+ keys.each do |k|
184
+ val = deep_get(hash,k)
185
+ key_length = k.bytesize
186
+ val_length = val.bytesize
187
+ frame << key_length
188
+ pack << "N"
189
+ frame << k
190
+ pack << "A#{key_length}"
191
+ frame << val_length
192
+ pack << "N"
193
+ frame << val
194
+ pack << "A#{val_length}"
195
+ end
196
+ frame.pack(pack)
197
+ end
198
+
199
+ private
200
+ def self.deep_get(hash, key="")
201
+ return hash if key.nil?
202
+ deep_get(
203
+ hash[key.split('.').first],
204
+ key[key.split('.').first.length+1..key.length]
205
+ )
206
+ end
207
+ private
208
+ def self.deep_keys(hash, prefix="")
209
+ keys = []
210
+ hash.each do |k,v|
211
+ keys << "#{prefix}#{k}" if v.class == String
212
+ keys << deep_keys(hash[k], "#{k}.") if v.class == Hash
213
+ end
214
+ keys.flatten
215
+ end
216
+ end # module Encoder
217
+ end
@@ -0,0 +1,433 @@
1
+ # encoding: utf-8
2
+ require "lumberjack"
3
+ require "socket"
4
+ require "thread"
5
+ require "openssl"
6
+ require "zlib"
7
+ require "json"
8
+ require "concurrent"
9
+
10
+ module Lumberjack
11
+ class Server
12
+ SOCKET_TIMEOUT = 1 # seconds
13
+
14
+ attr_reader :port
15
+
16
+ # Create a new Lumberjack server.
17
+ #
18
+ # - options is a hash. Valid options are:
19
+ #
20
+ # * :port - the port to listen on
21
+ # * :address - the host/address to bind to
22
+ # * :ssl_certificate - the path to the ssl cert to use
23
+ # * :ssl_key - the path to the ssl key to use
24
+ # * :ssl_key_passphrase - the key passphrase (optional)
25
+ def initialize(options={})
26
+ @options = {
27
+ :port => 0,
28
+ :address => "0.0.0.0",
29
+ :ssl => true,
30
+ :ssl_certificate => nil,
31
+ :ssl_key => nil,
32
+ :ssl_key_passphrase => nil
33
+ }.merge(options)
34
+
35
+ if @options[:ssl]
36
+ [:ssl_certificate, :ssl_key].each do |k|
37
+ if @options[k].nil?
38
+ raise "You must specify #{k} in Lumberjack::Server.new(...)"
39
+ end
40
+ end
41
+ end
42
+
43
+ @server = TCPServer.new(@options[:address], @options[:port])
44
+
45
+ @close = Concurrent::AtomicBoolean.new
46
+
47
+ # Query the port in case the port number is '0'
48
+ # TCPServer#addr == [ address_family, port, address, address ]
49
+ @port = @server.addr[1]
50
+
51
+ if @options[:ssl]
52
+ # load SSL certificate
53
+ @ssl = OpenSSL::SSL::SSLContext.new
54
+ @ssl.cert = OpenSSL::X509::Certificate.new(File.read(@options[:ssl_certificate]))
55
+ @ssl.key = OpenSSL::PKey::RSA.new(File.read(@options[:ssl_key]),
56
+ @options[:ssl_key_passphrase])
57
+ end
58
+ end # def initialize
59
+
60
+ def run(&block)
61
+ while !closed?
62
+ connection = accept
63
+
64
+ # Some exception may occur in the accept loop
65
+ # we will try again in the next iteration
66
+ # unless the server is closing
67
+ next unless connection
68
+
69
+ Thread.new(connection) do |connection|
70
+ connection.run(&block)
71
+ end
72
+ end
73
+ end # def run
74
+
75
+ def ssl?
76
+ @ssl
77
+ end
78
+
79
+ def accept(&block)
80
+ begin
81
+ socket = @server.accept_nonblock
82
+ # update the socket with a SSL layer
83
+ socket = accept_ssl(socket) if ssl?
84
+
85
+ if block_given?
86
+ block.call(socket, self)
87
+ else
88
+ return Connection.new(socket, self)
89
+ end
90
+ rescue OpenSSL::SSL::SSLError, IOError, EOFError, Errno::EBADF
91
+ socket.close rescue nil
92
+ retry unless closed?
93
+ rescue IO::WaitReadable, Errno::EAGAIN # Resource not ready yet, so lets try again
94
+ begin
95
+ IO.select([@server], nil, nil, SOCKET_TIMEOUT)
96
+ retry unless closed?
97
+ rescue IOError, Errno::EBADF => e # we currently closing
98
+ raise e unless closed?
99
+ end
100
+ end
101
+ end
102
+
103
+ def accept_ssl(tcp_socket)
104
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, @ssl)
105
+ ssl_socket.sync_close
106
+
107
+ begin
108
+ ssl_socket.accept_nonblock
109
+
110
+ return ssl_socket
111
+ rescue IO::WaitReadable # handshake
112
+ IO.select([ssl_socket], nil, nil, SOCKET_TIMEOUT)
113
+ retry unless closed?
114
+ rescue IO::WaitWritable # handshake
115
+ IO.select(nil, [ssl_socket], nil, SOCKET_TIMEOUT)
116
+ retry unless closed?
117
+ end
118
+ end
119
+
120
+ def closed?
121
+ @close.value
122
+ end
123
+
124
+ def close
125
+ @close.make_true
126
+ @server.close unless @server.closed?
127
+ end
128
+ end # class Server
129
+
130
+ class Parser
131
+ PROTOCOL_VERSION_1 = "1".ord
132
+ PROTOCOL_VERSION_2 = "2".ord
133
+
134
+ SUPPORTED_PROTOCOLS = [PROTOCOL_VERSION_1, PROTOCOL_VERSION_2]
135
+
136
+ def initialize
137
+ @buffer_offset = 0
138
+ @buffer = ""
139
+ @buffer.force_encoding("BINARY")
140
+ transition(:header, 2)
141
+ end # def initialize
142
+
143
+ def transition(state, next_length)
144
+ @state = state
145
+ #puts :transition => state
146
+ # TODO(sissel): Assert this self.respond_to?(state)
147
+ # TODO(sissel): Assert state is in STATES
148
+ # TODO(sissel): Assert next_length is a number
149
+ need(next_length)
150
+ end # def transition
151
+
152
+ # Feed data to this parser.
153
+ #
154
+ # Currently, it will return the raw payload of websocket messages.
155
+ # Otherwise, it returns nil if no complete message has yet been consumed.
156
+ #
157
+ # @param [String] the string data to feed into the parser.
158
+ # @return [String, nil] the websocket message payload, if any, nil otherwise.
159
+ def feed(data, &block)
160
+ @buffer << data
161
+ #p :need => @need
162
+ while have?(@need)
163
+ send(@state, &block)
164
+ #case @state
165
+ #when :header; header(&block)
166
+ #when :window_size; window_size(&block)
167
+ #when :data_lead; data_lead(&block)
168
+ #when :data_field_key_len; data_field_key_len(&block)
169
+ #when :data_field_key; data_field_key(&block)
170
+ #when :data_field_value_len; data_field_value_len(&block)
171
+ #when :data_field_value; data_field_value(&block)
172
+ #when :data_field_value; data_field_value(&block)
173
+ #when :compressed_lead; compressed_lead(&block)
174
+ #when :compressed_payload; compressed_payload(&block)
175
+ #end # case @state
176
+ end
177
+ return nil
178
+ end # def <<
179
+
180
+ # Do we have at least 'length' bytes in the buffer?
181
+ def have?(length)
182
+ return length <= (@buffer.size - @buffer_offset)
183
+ end # def have?
184
+
185
+ # Get 'length' string from the buffer.
186
+ def get(length=nil)
187
+ length = @need if length.nil?
188
+ data = @buffer[@buffer_offset ... @buffer_offset + length]
189
+ @buffer_offset += length
190
+ if @buffer_offset > 16384
191
+ @buffer = @buffer[@buffer_offset .. -1]
192
+ @buffer_offset = 0
193
+ end
194
+ return data
195
+ end # def get
196
+
197
+ # Set the minimum number of bytes we need in the buffer for the next read.
198
+ def need(length)
199
+ @need = length
200
+ end # def need
201
+
202
+ FRAME_WINDOW = "W".ord
203
+ FRAME_DATA = "D".ord
204
+ FRAME_JSON_DATA = "J".ord
205
+ FRAME_COMPRESSED = "C".ord
206
+ def header(&block)
207
+ version, frame_type = get.bytes.to_a[0..1]
208
+ version ||= PROTOCOL_VERSION_1
209
+
210
+ handle_version(version, &block)
211
+
212
+ case frame_type
213
+ when FRAME_WINDOW; transition(:window_size, 4)
214
+ when FRAME_DATA; transition(:data_lead, 8)
215
+ when FRAME_JSON_DATA; transition(:json_data_lead, 8)
216
+ when FRAME_COMPRESSED; transition(:compressed_lead, 4)
217
+ else; raise "Unknown frame type: `#{frame_type}`"
218
+ end
219
+ end
220
+
221
+ def handle_version(version, &block)
222
+ if supported_protocol?(version)
223
+ yield :version, version
224
+ else
225
+ raise "unsupported protocol #{version}"
226
+ end
227
+ end
228
+
229
+ def supported_protocol?(version)
230
+ SUPPORTED_PROTOCOLS.include?(version)
231
+ end
232
+
233
+ def window_size(&block)
234
+ @window_size = get.unpack("N").first
235
+ transition(:header, 2)
236
+ yield :window_size, @window_size
237
+ end # def window_size
238
+
239
+ def json_data_lead(&block)
240
+ @sequence, payload_size = get.unpack("NN")
241
+ transition(:json_data_payload, payload_size)
242
+ end
243
+
244
+ def json_data_payload(&block)
245
+ payload = get
246
+ yield :json, @sequence, Lumberjack::json.load(payload)
247
+ transition(:header, 2)
248
+ end
249
+
250
+ def data_lead(&block)
251
+ @sequence, @data_count = get.unpack("NN")
252
+ @data = {}
253
+ transition(:data_field_key_len, 4)
254
+ end
255
+
256
+ def data_field_key_len(&block)
257
+ key_len = get.unpack("N").first
258
+ transition(:data_field_key, key_len)
259
+ end
260
+
261
+ def data_field_key(&block)
262
+ @key = get
263
+ transition(:data_field_value_len, 4)
264
+ end
265
+
266
+ def data_field_value_len(&block)
267
+ transition(:data_field_value, get.unpack("N").first)
268
+ end
269
+
270
+ def data_field_value(&block)
271
+ @value = get
272
+
273
+ @data_count -= 1
274
+ @data[@key] = @value
275
+
276
+ if @data_count > 0
277
+ transition(:data_field_key_len, 4)
278
+ else
279
+ # emit the whole map now that we found the end of the data fields list.
280
+ yield :data, @sequence, @data
281
+ transition(:header, 2)
282
+ end
283
+
284
+ end # def data_field_value
285
+
286
+ def compressed_lead(&block)
287
+ length = get.unpack("N").first
288
+ transition(:compressed_payload, length)
289
+ end
290
+
291
+ def compressed_payload(&block)
292
+ original = Zlib::Inflate.inflate(get)
293
+ transition(:header, 2)
294
+
295
+ # Parse the uncompressed payload.
296
+ feed(original, &block)
297
+ end
298
+ end # class Parser
299
+
300
+ class Connection
301
+ READ_SIZE = 16384
302
+
303
+ attr_accessor :server
304
+
305
+ def initialize(fd, server)
306
+ @parser = Parser.new
307
+ @fd = fd
308
+
309
+ @server = server
310
+ @ack_handler = nil
311
+ end
312
+
313
+ def run(&block)
314
+ while !server.closed?
315
+ read_socket(&block)
316
+ end
317
+ rescue EOFError, OpenSSL::SSL::SSLError, IOError, Errno::ECONNRESET
318
+ # EOF or other read errors, only action is to shutdown which we'll do in
319
+ # 'ensure'
320
+ ensure
321
+ close rescue 'Already closed stream'
322
+ end # def run
323
+
324
+ def read_socket(&block)
325
+ # TODO(sissel): Ack on idle.
326
+ # X: - if any unacked, IO.select
327
+ # X: - on timeout, ack all.
328
+ # X: Doing so will prevent slow streams from retransmitting
329
+ # X: too many events after errors.
330
+ @parser.feed(@fd.sysread(READ_SIZE)) do |event, *args|
331
+ case event
332
+ when :version
333
+ version(*args)
334
+ when :window_size
335
+ reset_next_ack(*args)
336
+ when :data
337
+ sequence, map = args
338
+ ack_if_needed(sequence) { data(map, &block) }
339
+ when :json
340
+ # If the payload is an array of items we will emit multiple events
341
+ # this behavior was moved from the plugin to the library.
342
+ # see this commit: https://github.com/logstash-plugins/logstash-input-lumberjack/pull/57/files#diff-1b9590423b15f04f215635164e7376ecR158
343
+ sequence, map = args
344
+
345
+ ack_if_needed(sequence) do
346
+ if map.is_a?(Array)
347
+ map.each { |e| data(e, &block) }
348
+ else
349
+ data(map, &block)
350
+ end
351
+ end
352
+ end
353
+ end
354
+ end
355
+
356
+ def version(version)
357
+ @version = version
358
+ end
359
+
360
+ def ack_if_needed(sequence, &block)
361
+ block.call
362
+ send_ack(sequence) if @ack_handler.ack?(sequence)
363
+ end
364
+
365
+ def close
366
+ @fd.close unless @fd.closed?
367
+ end
368
+
369
+ def data(map, &block)
370
+ block.call(map) if block_given?
371
+ end
372
+
373
+ def reset_next_ack(window_size)
374
+ klass = (@version == Parser::PROTOCOL_VERSION_1) ? AckingProtocolV1 : AckingProtocolV2
375
+ @ack_handler = klass.new(window_size)
376
+ end
377
+
378
+ def send_ack(sequence)
379
+ @fd.syswrite(@ack_handler.ack_frame(sequence))
380
+ end
381
+ end # class Connection
382
+
383
+ class AckingProtocolV1
384
+ def initialize(window_size)
385
+ @next_ack = nil
386
+ @window_size = window_size
387
+ end
388
+
389
+ def ack?(sequence)
390
+ # The first encoded event will contain the sequence number
391
+ # this is needed to know when we should ack.
392
+ @next_ack = compute_next_ack(sequence) if @next_ack.nil?
393
+ sequence == @next_ack
394
+ end
395
+
396
+ def ack_frame(sequence)
397
+ ["1A", sequence].pack("A*N")
398
+ end
399
+
400
+ private
401
+ def compute_next_ack(sequence)
402
+ (sequence + @window_size - 1) % SEQUENCE_MAX
403
+ end
404
+ end
405
+
406
+ # Allow lumberjack to send partial ack back to the producer
407
+ # only V2 client support partial Acks
408
+ #
409
+ # Send Ack on every 20% of the data, so with default settings every 200 events
410
+ # This should reduce the congestion on retransmit.
411
+ class AckingProtocolV2
412
+ ACK_RATIO = 5
413
+
414
+ def initialize(window_size)
415
+ @window_size = window_size
416
+ @every = (window_size / ACK_RATIO).round
417
+ end
418
+
419
+ def ack?(sequence)
420
+ if @window_size == sequence
421
+ true
422
+ elsif sequence % @every == 0
423
+ true
424
+ else
425
+ false
426
+ end
427
+ end
428
+
429
+ def ack_frame(sequence)
430
+ ["2A", sequence].pack("A*N")
431
+ end
432
+ end
433
+ end # module Lumberjack
data/lib/lumberjack.rb ADDED
@@ -0,0 +1,22 @@
1
+ require "json"
2
+
3
+ module Lumberjack
4
+ SEQUENCE_MAX = (2**32-1).freeze
5
+
6
+ @@json = Class.new do
7
+ def self.load(blob)
8
+ JSON.parse(blob)
9
+ end
10
+ def self.dump(v)
11
+ v.to_json
12
+ end
13
+ end
14
+
15
+ def self.json
16
+ @@json
17
+ end
18
+
19
+ def self.json=(j)
20
+ @@json = j
21
+ end
22
+ end
@@ -0,0 +1,210 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/client"
3
+ require "lumberjack/server"
4
+ require "stud/temporary"
5
+ require "flores/pki"
6
+ require "fileutils"
7
+ require "thread"
8
+ require "spec_helper"
9
+
10
+ Thread.abort_on_exception = true
11
+ describe "A client" do
12
+ let(:certificate) { Flores::PKI.generate }
13
+ let(:certificate_file_crt) { "certificate.crt" }
14
+ let(:certificate_file_key) { "certificate.key" }
15
+ let(:port) { Flores::Random.integer(1024..65335) }
16
+ let(:tcp_port) { port + 1 }
17
+ let(:host) { "127.0.0.1" }
18
+ let(:queue) { [] }
19
+
20
+ before do
21
+ expect(File).to receive(:read).at_least(1).with(certificate_file_crt) { certificate.first.to_s }
22
+ expect(File).to receive(:read).at_least(1).with(certificate_file_key) { certificate.last.to_s }
23
+
24
+ tcp_server = Lumberjack::Server.new(:port => tcp_port, :address => host, :ssl => false)
25
+
26
+ ssl_server = Lumberjack::Server.new(:port => port,
27
+ :address => host,
28
+ :ssl_certificate => certificate_file_crt,
29
+ :ssl_key => certificate_file_key)
30
+
31
+ @tcp_server = Thread.new do
32
+ while true
33
+ tcp_server.accept do |socket|
34
+ con = Lumberjack::Connection.new(socket, tcp_server)
35
+ begin
36
+ con.run { |data| queue << data }
37
+ rescue
38
+ # Close connection on failure. For example SSL client will make
39
+ # parser for TCP based server trip.
40
+ # Connection is closed by Server connection object
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ @ssl_server = Thread.new do
47
+ ssl_server.run { |data| queue << data }
48
+ end
49
+ end
50
+
51
+ shared_examples "send payload" do
52
+ it "supports single element" do
53
+ (1..random_number_of_events).each do |n|
54
+ expect(client.write(payload)).to eq(sequence_start + n)
55
+ end
56
+ sleep(0.5) # give time to the server to read the events
57
+ expect(queue.size).to eq(random_number_of_events)
58
+ end
59
+
60
+ it "support sending multiple elements in one payload" do
61
+ expect(client.write(batch_payload)).to eq(sequence_start + batch_size)
62
+ sleep(0.5)
63
+
64
+ expect(queue.size).to eq(batch_size)
65
+ expect(queue).to match_array(batch_payload)
66
+ end
67
+ end
68
+
69
+ shared_examples "transmit payloads" do
70
+ let(:random_number_of_events) { Flores::Random.integer(2..10) }
71
+ let(:payload) { { "line" => "foobar" } }
72
+ let(:batch_size) { Flores::Random.integer(1..1024) }
73
+ let(:batch_payload) do
74
+ batch = []
75
+ batch_size.times do |n|
76
+ batch << { "line" => "foobar #{n}" }
77
+ end
78
+ batch
79
+ end
80
+
81
+ context "when sequence start at 0" do
82
+ let(:sequence_start) { 0 }
83
+
84
+ include_examples "send payload"
85
+ end
86
+
87
+ context "when sequence doesn't start at zero" do
88
+ let(:sequence_start) { Flores::Random.integer(1..2000) }
89
+
90
+ before do
91
+ client.instance_variable_get(:@socket).instance_variable_set(:@sequence, sequence_start)
92
+ end
93
+
94
+ include_examples "send payload"
95
+ end
96
+
97
+ context "when the sequence rollover" do
98
+ let(:batch_size) { 100 }
99
+ let(:sequence_start) { Lumberjack::SEQUENCE_MAX - batch_size / 2 }
100
+
101
+ before do
102
+ client.instance_variable_get(:@socket).instance_variable_set(:@sequence, sequence_start)
103
+ end
104
+
105
+ it "adjusts the ack" do
106
+ expect(client.write(batch_payload)).to eq(batch_size / 2)
107
+ sleep(0.5)
108
+ expect(queue.size).to eq(batch_size)
109
+ expect(queue).to match_array(batch_payload)
110
+ end
111
+ end
112
+ end
113
+
114
+ context "using plain tcp connection" do
115
+ it "should successfully connect to tcp server if ssl explicitely disabled" do
116
+ expect {
117
+ Lumberjack::Client.new(:port => tcp_port, :host => host, :addresses => host, :ssl => false)
118
+ }.not_to raise_error
119
+ end
120
+
121
+ it "should fail to connect to tcp server if ssl not explicitely disabled" do
122
+ expect {
123
+ Lumberjack::Client.new(:port => tcp_port, :host => host, :addresses => host)
124
+ }.to raise_error(RuntimeError, /Must set a ssl certificate/)
125
+ end
126
+
127
+ it "should fail to communicate to ssl based server" do
128
+ expect {
129
+ client = Lumberjack::Client.new(:port => port,
130
+ :host => host,
131
+ :addresses => host,
132
+ :ssl => false)
133
+ client.write({ "line" => "foobar" })
134
+ }.to raise_error(RuntimeError)
135
+ end
136
+
137
+ context "When transmitting a payload" do
138
+ let(:options) { {:port => tcp_port, :host => host, :addresses => host, :ssl => false } }
139
+ let(:client) { Lumberjack::Client.new(options) }
140
+
141
+ context "json" do
142
+ let(:options) { super.merge({ :json => true }) }
143
+ include_examples "transmit payloads"
144
+ end
145
+
146
+ context "v1 frame" do
147
+ include_examples "transmit payloads"
148
+ end
149
+ end
150
+ end
151
+
152
+ context "using ssl encrypted connection" do
153
+ context "with a valid certificate" do
154
+ it "successfully connect to the server" do
155
+ expect {
156
+ Lumberjack::Client.new(:port => port,
157
+ :host => host,
158
+ :addresses => host,
159
+ :ssl_certificate => certificate_file_crt)
160
+ }.not_to raise_error
161
+ end
162
+
163
+ it "should fail connecting to plain tcp server" do
164
+ expect {
165
+ Lumberjack::Client.new(:port => tcp_port,
166
+ :host => host,
167
+ :addresses => host,
168
+ :ssl_certificate => certificate_file_crt)
169
+ }.to raise_error(OpenSSL::SSL::SSLError)
170
+ end
171
+ end
172
+
173
+ context "with an invalid certificate" do
174
+ let(:invalid_certificate) { Flores::PKI.generate }
175
+ let(:invalid_certificate_file) { "invalid.crt" }
176
+
177
+ before do
178
+ expect(File).to receive(:read).with(invalid_certificate_file) { invalid_certificate.first.to_s }
179
+ end
180
+
181
+ it "should refuse to connect" do
182
+ expect {
183
+ Lumberjack::Client.new(:port => port,
184
+ :host => host,
185
+ :addresses => host,
186
+ :ssl_certificate => invalid_certificate_file)
187
+
188
+ }.to raise_error(OpenSSL::SSL::SSLError, /certificate verify failed/)
189
+ end
190
+ end
191
+
192
+ context "When transmitting a payload" do
193
+ let(:client) do
194
+ Lumberjack::Client.new(:port => port,
195
+ :host => host,
196
+ :addresses => host,
197
+ :ssl_certificate => certificate_file_crt)
198
+ end
199
+
200
+ context "json" do
201
+ let(:options) { super.merge({ :json => true }) }
202
+ include_examples "transmit payloads"
203
+ end
204
+
205
+ context "v1 frame" do
206
+ include_examples "transmit payloads"
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/server"
3
+ require "flores/random"
4
+
5
+ describe Lumberjack::AckingProtocolV1 do
6
+ let(:number_of_events) { Flores::Random.integer(100..1024) }
7
+
8
+ subject { Lumberjack::AckingProtocolV1.new(number_of_events) }
9
+
10
+ it "should return true only once" do
11
+ results = []
12
+ number_of_events.times { |n| results << subject.ack?(n) }
13
+ expect(results.size).to eq(number_of_events)
14
+ expect(results.count(true)).to eq(1)
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/server"
3
+ require "flores/random"
4
+
5
+ describe Lumberjack::AckingProtocolV2 do
6
+ let(:results) { [] }
7
+ subject { Lumberjack::AckingProtocolV2.new(number_of_events) }
8
+ before { 1.upto(number_of_events) { |n| results << subject.ack?(n) } }
9
+
10
+ context "with multiples events" do
11
+ let(:number_of_events) { Flores::Random.integer(100..1024) }
12
+
13
+ it "should return multiples partial acks" do
14
+ expect(results.size).to eq(number_of_events)
15
+ expect(results.count(true)).to be_within(1).of((number_of_events / number_of_events * Lumberjack::AckingProtocolV2::ACK_RATIO).ceil)
16
+ end
17
+
18
+ it "last ack should be true" do
19
+ expect(results.last).to be_truthy
20
+ end
21
+ end
22
+
23
+ context "with only one event" do
24
+ let(:number_of_events) { 1 }
25
+
26
+ it "should return true only once" do
27
+ expect(results.size).to eq(number_of_events)
28
+ expect(results.count(true)).to eq(1)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,106 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+ require 'lumberjack/client'
4
+ require 'lumberjack/server'
5
+ require "socket"
6
+ require "thread"
7
+ require "openssl"
8
+ require "zlib"
9
+
10
+ describe "Lumberjack::Client" do
11
+ describe "Lumberjack::Socket" do
12
+ let(:port) { 5000 }
13
+
14
+ subject(:socket) { Lumberjack::Socket.new(:port => port, :ssl_certificate => "" ) }
15
+
16
+ before do
17
+ allow_any_instance_of(Lumberjack::Socket).to receive(:connection_start).and_return(true)
18
+ # mock any network call
19
+ allow(socket).to receive(:send_window_size).with(kind_of(Integer)).and_return(true)
20
+ allow(socket).to receive(:send_payload).with(kind_of(String)).and_return(true)
21
+ end
22
+
23
+ context "sequence" do
24
+ let(:hash) { {:a => 1, :b => 2}}
25
+ let(:max_unsigned_int) { (2**32)-1 }
26
+
27
+ before(:each) do
28
+ allow(socket).to receive(:ack).and_return(true)
29
+ end
30
+
31
+ it "force sequence to be an unsigned 32 bits int" do
32
+ socket.instance_variable_set(:@sequence, max_unsigned_int)
33
+ socket.write_sync(hash)
34
+ expect(socket.sequence).to eq(1)
35
+ end
36
+ end
37
+ end
38
+
39
+ describe Lumberjack::FrameEncoder do
40
+ it 'should creates frames without truncating accentued characters' do
41
+ content = {
42
+ "message" => "Le Canadien de Montréal est la meilleure équipe au monde!",
43
+ "other" => "éléphant"
44
+ }
45
+ parser = Lumberjack::Parser.new
46
+ parser.feed(Lumberjack::FrameEncoder.to_frame(content, 0)) do |code, sequence, data|
47
+ if code == :data
48
+ expect(data["message"].force_encoding('UTF-8')).to eq(content["message"])
49
+ expect(data["other"].force_encoding('UTF-8')).to eq(content["other"])
50
+ end
51
+ end
52
+ end
53
+
54
+ it 'should creates frames without dropping multibytes characters' do
55
+ content = {
56
+ "message" => "国際ホッケー連盟" # International Hockey Federation
57
+ }
58
+ parser = Lumberjack::Parser.new
59
+ parser.feed(Lumberjack::FrameEncoder.to_frame(content, 0)) do |code, sequence, data|
60
+ expect(data["message"].force_encoding('UTF-8')).to eq(content["message"]) if code == :data
61
+ end
62
+ end
63
+ end
64
+
65
+ describe Lumberjack::JsonEncoder do
66
+ it 'should create frames from nested hash' do
67
+ content = {
68
+ "number" => 1,
69
+ "string" => "hello world",
70
+ "array" => [1,2,3],
71
+ "sub" => {
72
+ "a" => 1
73
+ }
74
+ }
75
+ parser = Lumberjack::Parser.new
76
+ frame = Lumberjack::JsonEncoder.to_frame(content, 0)
77
+ parser.feed(frame) do |code, sequence, data|
78
+ expect(data).to eq(content) if code == :json
79
+ end
80
+ end
81
+
82
+ it 'should creates frames without truncating accentued characters' do
83
+ content = {
84
+ "message" => "Le Canadien de Montréal est la meilleure équipe au monde!",
85
+ "other" => "éléphant"
86
+ }
87
+ parser = Lumberjack::Parser.new
88
+ parser.feed(Lumberjack::JsonEncoder.to_frame(content, 0)) do |code, sequence, data|
89
+ if code == :json
90
+ expect(data["message"]).to eq(content["message"])
91
+ expect(data["other"]).to eq(content["other"])
92
+ end
93
+ end
94
+ end
95
+
96
+ it 'should creates frames without dropping multibytes characters' do
97
+ content = {
98
+ "message" => "国際ホッケー連盟" # International Hockey Federation
99
+ }
100
+ parser = Lumberjack::Parser.new
101
+ parser.feed(Lumberjack::JsonEncoder.to_frame(content, 0)) do |code, sequence, data|
102
+ expect(data["message"]).to eq(content["message"]) if code == :json
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,47 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/server"
3
+ require "spec_helper"
4
+ require "flores/random"
5
+
6
+ describe "Connnection" do
7
+ let(:server) { double("server", :closed? => false) }
8
+ let(:socket) { double("socket", :closed? => false) }
9
+ let(:connection) { Lumberjack::Connection.new(socket, server) }
10
+ let(:payload) { {"line" => "foobar" } }
11
+ let(:start_sequence) { Flores::Random.integer(0..2000) }
12
+ let(:random_number_of_events) { Flores::Random.integer(2..200) }
13
+
14
+ context "when the server is running" do
15
+ before do
16
+ expect(socket).to receive(:sysread).at_least(:once).with(Lumberjack::Connection::READ_SIZE).and_return("")
17
+ allow(socket).to receive(:syswrite).with(anything).and_return(true)
18
+ allow(socket).to receive(:close)
19
+
20
+
21
+ expectation = receive(:feed)
22
+ .with("")
23
+ .and_yield(:version, Lumberjack::Parser::PROTOCOL_VERSION_1)
24
+ .and_yield(:window_size, random_number_of_events)
25
+
26
+ random_number_of_events.times { |n| expectation.and_yield(:data, start_sequence + n + 1, payload) }
27
+
28
+ expect_any_instance_of(Lumberjack::Parser).to expectation
29
+ end
30
+
31
+ it "should ack the end of a sequence" do
32
+ expect(socket).to receive(:syswrite).with(["1A", random_number_of_events + start_sequence].pack("A*N"))
33
+ connection.read_socket
34
+ end
35
+ end
36
+
37
+ context "when the server stop" do
38
+ let(:server) { double("server", :closed? => true) }
39
+ before do
40
+ expect(socket).to receive(:close).and_return(true)
41
+ end
42
+
43
+ it "stop reading from the socket" do
44
+ expect { |b| connection.run(&b) }.not_to yield_control
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+ require "lumberjack/client"
3
+ require "lumberjack/server"
4
+ require "flores/random"
5
+ require "flores/pki"
6
+ require "spec_helper"
7
+
8
+ Thread.abort_on_exception = true
9
+
10
+ describe "Server" do
11
+ let(:certificate) { Flores::PKI.generate }
12
+ let(:certificate_file_crt) { "certificate.crt" }
13
+ let(:certificate_file_key) { "certificate.key" }
14
+ let(:port) { Flores::Random.integer(1024..65335) }
15
+ let(:tcp_port) { port + 1 }
16
+ let(:host) { "127.0.0.1" }
17
+ let(:queue) { [] }
18
+
19
+ before do
20
+ expect(File).to receive(:read).at_least(1).with(certificate_file_crt) { certificate.first.to_s }
21
+ expect(File).to receive(:read).at_least(1).with(certificate_file_key) { certificate.last.to_s }
22
+ end
23
+
24
+ subject do
25
+ Lumberjack::Server.new(:port => port,
26
+ :address => host,
27
+ :ssl_certificate => certificate_file_crt,
28
+ :ssl_key => certificate_file_key)
29
+ end
30
+
31
+ it "should not block when closing the server" do
32
+ thread = Thread.new do
33
+ subject.run do |event|
34
+ queue << event
35
+ end
36
+ end
37
+
38
+ sleep(1) while thread.status != "run"
39
+ subject.close
40
+ wait_for { thread.status }.to be_falsey
41
+ end
42
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ require 'rspec'
3
+ require 'rspec/mocks'
4
+ require 'rspec/wait'
5
+
6
+ $: << File.realpath(File.join(File.dirname(__FILE__), "..", "lib"))
7
+
8
+ RSpec.configure do |config|
9
+ config.order = :rand
10
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jls-lumberjack-logzio
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.26
5
+ platform: ruby
6
+ authors:
7
+ - Jordan Sissel
8
+ - Roi Rav-Hon
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2020-05-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: concurrent-ruby
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: flores
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: 0.0.6
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: 0.0.6
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: stud
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: pry
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rspec-wait
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ description: lumberjack log transport library
99
+ email:
100
+ - jls@semicomplete.com
101
+ - roi@logz.io
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - lib/lumberjack.rb
107
+ - lib/lumberjack/client.rb
108
+ - lib/lumberjack/server.rb
109
+ - spec/integration_spec.rb
110
+ - spec/lumberjack/acking_protocol_v1_spec.rb
111
+ - spec/lumberjack/acking_protocol_v2_spec.rb
112
+ - spec/lumberjack/client_spec.rb
113
+ - spec/lumberjack/connection_spec.rb
114
+ - spec/lumberjack/server_spec.rb
115
+ - spec/spec_helper.rb
116
+ homepage: https://github.com/jordansissel/lumberjack
117
+ licenses: []
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubygems_version: 3.0.3
135
+ signing_key:
136
+ specification_version: 4
137
+ summary: lumberjack log transport library
138
+ test_files:
139
+ - spec/spec_helper.rb
140
+ - spec/integration_spec.rb
141
+ - spec/lumberjack/acking_protocol_v1_spec.rb
142
+ - spec/lumberjack/client_spec.rb
143
+ - spec/lumberjack/server_spec.rb
144
+ - spec/lumberjack/acking_protocol_v2_spec.rb
145
+ - spec/lumberjack/connection_spec.rb