jls-lumberjack-logzio 0.0.26

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