jls-lumberjack 0.0.24 → 0.0.25.beta1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d58912ce6afda9045d016510618a04363349628d
4
- data.tar.gz: faeaba187853930185d78673f4b69c16c5b107bb
3
+ metadata.gz: e2b2959f02e7f417cf30eb4d840508950edf0808
4
+ data.tar.gz: a1f37209be258dbcc97617b8eed7e5c61ca66db5
5
5
  SHA512:
6
- metadata.gz: e40270de1f12efc47375be7a5a9cd9c83fae6ce2b615fe50885037746ecf074e1a26bf2dd4b2bf8a2ebdd1816c95f6d36b56e84de32e3fc5f73a225be7bc6aa7
7
- data.tar.gz: 6224d5a7b5475195b0afd5168eea3c37a8c704cef432cf90b01cb8fcd520ea3293f1503e89b6bd4e0eaf2e84f24afeab78b7681ab9be4999e2720b2987291dd8
6
+ metadata.gz: a87220c6b45a3c53f4bf685c2309c37cdd953c06d489f56cb442eb9ebc482357067b3fcad526b7e44c28455117be4c189226815184d65049bb6b429326ef5d42
7
+ data.tar.gz: 8d326476bf7fe3d76873fc998dae45fbaf94cbe776de1a9e276fe8d4fadcfe6243a3ac2db7c90568056998c8296dabe44424fc3b03dfb1a7f91f7d87a5df41ae
data/lib/lumberjack.rb CHANGED
@@ -1,3 +1,22 @@
1
+ require "json"
2
+
1
3
  module Lumberjack
2
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
3
22
  end
@@ -12,15 +12,16 @@ module Lumberjack
12
12
  :port => 0,
13
13
  :addresses => [],
14
14
  :ssl_certificate => nil,
15
+ :ssl => true,
16
+ :json => false,
15
17
  }.merge(opts)
16
18
 
17
19
  @opts[:addresses] = [@opts[:addresses]] if @opts[:addresses].class == String
18
20
  raise "Must set a port." if @opts[:port] == 0
19
21
  raise "Must set atleast one address" if @opts[:addresses].empty? == 0
20
- raise "Must set a ssl certificate or path" if @opts[:ssl_certificate].nil?
22
+ raise "Must set a ssl certificate or path" if @opts[:ssl_certificate].nil? && @opts[:ssl]
21
23
 
22
24
  @socket = connect
23
-
24
25
  end
25
26
 
26
27
  private
@@ -37,8 +38,8 @@ module Lumberjack
37
38
  end
38
39
 
39
40
  public
40
- def write(elements)
41
- @socket.write_sync(elements)
41
+ def write(elements, opts={})
42
+ @socket.write_sync(elements, opts)
42
43
  end
43
44
 
44
45
  public
@@ -54,7 +55,10 @@ module Lumberjack
54
55
  #
55
56
  # * :port - the port to listen on
56
57
  # * :address - the host/address to bind to
57
- # * :ssl_certificate - the path to the ssl cert to use
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.
58
62
  attr_reader :sequence
59
63
  attr_reader :host
60
64
  def initialize(opts={})
@@ -64,6 +68,8 @@ module Lumberjack
64
68
  :port => 0,
65
69
  :address => "127.0.0.1",
66
70
  :ssl_certificate => nil,
71
+ :ssl => true,
72
+ :json => false,
67
73
  }.merge(opts)
68
74
  @host = @opts[:address]
69
75
 
@@ -73,18 +79,21 @@ module Lumberjack
73
79
  private
74
80
  def connection_start(opts)
75
81
  tcp_socket = TCPSocket.new(opts[:address], opts[:port])
82
+ if !opts[:ssl]
83
+ @socket = tcp_socket
84
+ else
85
+ certificate = OpenSSL::X509::Certificate.new(File.read(opts[:ssl_certificate]))
76
86
 
77
- certificate = OpenSSL::X509::Certificate.new(File.read(opts[:ssl_certificate]))
78
-
79
- certificate_store = OpenSSL::X509::Store.new
80
- certificate_store.add_cert(certificate)
87
+ certificate_store = OpenSSL::X509::Store.new
88
+ certificate_store.add_cert(certificate)
81
89
 
82
- ssl_context = OpenSSL::SSL::SSLContext.new
83
- ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
84
- ssl_context.cert_store = certificate_store
90
+ ssl_context = OpenSSL::SSL::SSLContext.new
91
+ ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
92
+ ssl_context.cert_store = certificate_store
85
93
 
86
- @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
87
- @socket.connect
94
+ @socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
95
+ @socket.connect
96
+ end
88
97
  end
89
98
 
90
99
  private
@@ -109,11 +118,16 @@ module Lumberjack
109
118
  end
110
119
 
111
120
  public
112
- def write_sync(elements)
121
+ def write_sync(elements, opts={})
122
+ options = {
123
+ :json => @opts[:json],
124
+ }.merge(opts)
125
+
113
126
  elements = [elements] if elements.is_a?(Hash)
114
127
  send_window_size(elements.size)
115
128
 
116
- payload = elements.map { |element| Encoder.to_frame(element, inc) }.join
129
+ encoder = options[:json] ? JsonEncoder : FrameEncoder
130
+ payload = elements.map { |element| encoder.to_frame(element, inc) }.join
117
131
  compress = compress_payload(payload)
118
132
  send_payload(compress)
119
133
 
@@ -151,7 +165,17 @@ module Lumberjack
151
165
  end
152
166
  end
153
167
 
154
- module Encoder
168
+ module JsonEncoder
169
+ def self.to_frame(hash, sequence)
170
+ json = Lumberjack::json.dump(hash)
171
+ json_length = json.bytesize
172
+ pack = "AANNA#{json_length}"
173
+ frame = ["1", "J", sequence, json_length, json]
174
+ frame.pack(pack)
175
+ end
176
+ end # JsonEncoder
177
+
178
+ module FrameEncoder
155
179
  def self.to_frame(hash, sequence)
156
180
  frame = ["1", "D", sequence]
157
181
  pack = "AAN"
@@ -4,9 +4,13 @@ require "socket"
4
4
  require "thread"
5
5
  require "openssl"
6
6
  require "zlib"
7
+ require "json"
8
+ require "concurrent"
7
9
 
8
10
  module Lumberjack
9
11
  class Server
12
+ SOCKET_TIMEOUT = 1 # seconds
13
+
10
14
  attr_reader :port
11
15
 
12
16
  # Create a new Lumberjack server.
@@ -22,55 +26,113 @@ module Lumberjack
22
26
  @options = {
23
27
  :port => 0,
24
28
  :address => "0.0.0.0",
29
+ :ssl => true,
25
30
  :ssl_certificate => nil,
26
31
  :ssl_key => nil,
27
32
  :ssl_key_passphrase => nil
28
33
  }.merge(options)
29
34
 
30
- [:ssl_certificate, :ssl_key].each do |k|
31
- if @options[k].nil?
32
- raise "You must specify #{k} in Lumberjack::Server.new(...)"
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
33
40
  end
34
41
  end
35
42
 
36
- @tcp_server = TCPServer.new(@options[:address], @options[:port])
43
+ @server = TCPServer.new(@options[:address], @options[:port])
44
+
45
+ @close = Concurrent::AtomicBoolean.new
37
46
 
38
47
  # Query the port in case the port number is '0'
39
48
  # TCPServer#addr == [ address_family, port, address, address ]
40
- @port = @tcp_server.addr[1]
41
- @ssl = OpenSSL::SSL::SSLContext.new
42
- @ssl.cert = OpenSSL::X509::Certificate.new(File.read(@options[:ssl_certificate]))
43
- @ssl.key = OpenSSL::PKey::RSA.new(File.read(@options[:ssl_key]),
44
- @options[:ssl_key_passphrase])
45
- @ssl_server = OpenSSL::SSL::SSLServer.new(@tcp_server, @ssl)
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
46
58
  end # def initialize
47
59
 
48
60
  def run(&block)
49
- while true
61
+ while !closed?
50
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
+
51
69
  Thread.new(connection) do |connection|
52
70
  connection.run(&block)
53
71
  end
54
72
  end
55
73
  end # def run
56
74
 
75
+ def ssl?
76
+ @ssl
77
+ end
78
+
57
79
  def accept(&block)
58
80
  begin
59
- fd = @ssl_server.accept
60
- rescue EOFError, OpenSSL::SSL::SSLError, IOError
61
- # ssl handshake or other accept-related failure.
62
- # TODO(sissel): Make it possible to log this.
63
- retry
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
64
100
  end
65
- if block_given?
66
- block.call(fd)
67
- else
68
- Connection.new(fd)
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?
69
117
  end
70
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
71
128
  end # class Server
72
129
 
73
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
+
74
136
  def initialize
75
137
  @buffer_offset = 0
76
138
  @buffer = ""
@@ -139,24 +201,52 @@ module Lumberjack
139
201
 
140
202
  FRAME_WINDOW = "W".ord
141
203
  FRAME_DATA = "D".ord
204
+ FRAME_JSON_DATA = "J".ord
142
205
  FRAME_COMPRESSED = "C".ord
143
206
  def header(&block)
144
207
  version, frame_type = get.bytes.to_a[0..1]
208
+ version ||= PROTOCOL_VERSION_1
209
+
210
+ handle_version(version, &block)
145
211
 
146
212
  case frame_type
147
213
  when FRAME_WINDOW; transition(:window_size, 4)
148
214
  when FRAME_DATA; transition(:data_lead, 8)
215
+ when FRAME_JSON_DATA; transition(:json_data_lead, 8)
149
216
  when FRAME_COMPRESSED; transition(:compressed_lead, 4)
150
- else; raise "Unknown frame type: #{frame_type}"
217
+ else; raise "Unknown frame type: `#{frame_type}`"
151
218
  end
152
219
  end
153
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
+
154
233
  def window_size(&block)
155
234
  @window_size = get.unpack("N").first
156
235
  transition(:header, 2)
157
236
  yield :window_size, @window_size
158
237
  end # def window_size
159
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
+
160
250
  def data_lead(&block)
161
251
  @sequence, @data_count = get.unpack("NN")
162
252
  @data = {}
@@ -210,17 +300,18 @@ module Lumberjack
210
300
  class Connection
211
301
  READ_SIZE = 16384
212
302
 
213
- def initialize(fd)
214
- super()
303
+ attr_accessor :server
304
+
305
+ def initialize(fd, server)
215
306
  @parser = Parser.new
216
307
  @fd = fd
217
308
 
218
- # a safe default until we are told by the client what window size to use
219
- @window_size = 1
309
+ @server = server
310
+ @ack_handler = nil
220
311
  end
221
312
 
222
313
  def run(&block)
223
- while true
314
+ while !server.closed?
224
315
  read_socket(&block)
225
316
  end
226
317
  rescue EOFError, OpenSSL::SSL::SSLError, IOError, Errno::ECONNRESET
@@ -238,42 +329,97 @@ module Lumberjack
238
329
  # X: too many events after errors.
239
330
  @parser.feed(@fd.sysread(READ_SIZE)) do |event, *args|
240
331
  case event
332
+ when :version
333
+ version(*args)
241
334
  when :window_size
242
- # We receive a new payload
243
- window_size(*args)
244
- reset_next_ack
335
+ reset_next_ack(*args)
245
336
  when :data
246
- data(*args, &block)
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
247
352
  end
248
353
  end
249
354
  end
250
355
 
251
- def close
252
- @fd.close
356
+ def version(version)
357
+ @version = version
253
358
  end
254
359
 
255
- def window_size(size)
256
- @window_size = size
360
+ def ack_if_needed(sequence, &block)
361
+ block.call
362
+ send_ack(sequence) if @ack_handler.ack?(sequence)
257
363
  end
258
364
 
259
- def reset_next_ack
260
- @next_ack = nil
365
+ def close
366
+ @fd.close unless @fd.closed?
261
367
  end
262
368
 
263
- def data(sequence, map, &block)
369
+ def data(map, &block)
264
370
  block.call(map) if block_given?
265
- ack_if_needed(sequence)
266
371
  end
267
-
268
- def compute_next_ack(sequence)
269
- (sequence + @window_size - 1) % SEQUENCE_MAX
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(["1A", sequence].pack("A*N"))
380
+ end
381
+ end # class Connection
382
+
383
+ class AckingProtocolV1
384
+ def initialize(window_size)
385
+ @next_ack = nil
386
+ @window_size = window_size
270
387
  end
271
388
 
272
- def ack_if_needed(sequence)
389
+ def ack?(sequence)
273
390
  # The first encoded event will contain the sequence number
274
391
  # this is needed to know when we should ack.
275
392
  @next_ack = compute_next_ack(sequence) if @next_ack.nil?
276
- @fd.syswrite(["1A", sequence].pack("A*N")) if sequence == @next_ack
393
+ sequence == @next_ack
277
394
  end
278
- end # class Connection
395
+
396
+ private
397
+ def compute_next_ack(sequence)
398
+ (sequence + @window_size - 1) % SEQUENCE_MAX
399
+ end
400
+ end
401
+
402
+ # Allow lumberjack to send partial ack back to the producer
403
+ # only V2 client support partial Acks
404
+ #
405
+ # Send Ack on every 20% of the data, so with default settings every 200 events
406
+ # This should reduce the congestion on retransmit.
407
+ class AckingProtocolV2
408
+ ACK_RATIO = 5
409
+
410
+ def initialize(window_size)
411
+ @window_size = window_size
412
+ @every = (window_size / ACK_RATIO).round
413
+ end
414
+
415
+ def ack?(sequence)
416
+ if @window_size == sequence
417
+ true
418
+ elsif sequence % @every == 0
419
+ true
420
+ else
421
+ false
422
+ end
423
+ end
424
+ end
279
425
  end # module Lumberjack
@@ -8,60 +8,46 @@ require "thread"
8
8
  require "spec_helper"
9
9
 
10
10
  Thread.abort_on_exception = true
11
-
12
11
  describe "A client" do
13
12
  let(:certificate) { Flores::PKI.generate }
14
13
  let(:certificate_file_crt) { "certificate.crt" }
15
14
  let(:certificate_file_key) { "certificate.key" }
16
15
  let(:port) { Flores::Random.integer(1024..65335) }
16
+ let(:tcp_port) { port + 1 }
17
17
  let(:host) { "127.0.0.1" }
18
18
  let(:queue) { [] }
19
19
 
20
20
  before do
21
21
  expect(File).to receive(:read).at_least(1).with(certificate_file_crt) { certificate.first.to_s }
22
22
  expect(File).to receive(:read).at_least(1).with(certificate_file_key) { certificate.last.to_s }
23
-
24
- server = Lumberjack::Server.new(:port => port,
25
- :address => host,
26
- :ssl_certificate => certificate_file_crt,
27
- :ssl_key => certificate_file_key)
28
-
29
- @server = Thread.new do
30
- server.run { |data| queue << data }
31
- end
32
- end
33
-
34
- context "with a valid certificate" do
35
- it "successfully connect to the server" do
36
- expect {
37
- Lumberjack::Client.new(:port => port,
38
- :host => host,
39
- :addresses => host,
40
- :ssl_certificate => certificate_file_crt)
41
23
 
42
- }.not_to raise_error
43
- end
44
- end
45
-
46
- context "with an invalid certificate" do
47
- let(:invalid_certificate) { Flores::PKI.generate }
48
- let(:invalid_certificate_file) { "invalid.crt" }
49
-
50
- before do
51
- expect(File).to receive(:read).with(invalid_certificate_file) { invalid_certificate.first.to_s }
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
52
44
  end
53
45
 
54
- it "should refuse to connect" do
55
- expect {
56
- Lumberjack::Client.new(:port => port,
57
- :host => host,
58
- :addresses => host,
59
- :ssl_certificate => invalid_certificate_file)
60
-
61
- }.to raise_error(OpenSSL::SSL::SSLError, /certificate verify failed/)
46
+ @ssl_server = Thread.new do
47
+ ssl_server.run { |data| queue << data }
62
48
  end
63
49
  end
64
-
50
+
65
51
  shared_examples "send payload" do
66
52
  it "supports single element" do
67
53
  (1..random_number_of_events).each do |n|
@@ -80,15 +66,9 @@ describe "A client" do
80
66
  end
81
67
  end
82
68
 
83
- context "When transmitting a payload" do
69
+ shared_examples "transmit payloads" do
84
70
  let(:random_number_of_events) { Flores::Random.integer(2..10) }
85
71
  let(:payload) { { "line" => "foobar" } }
86
- let(:client) do
87
- Lumberjack::Client.new(:port => port,
88
- :host => host,
89
- :addresses => host,
90
- :ssl_certificate => certificate_file_crt)
91
- end
92
72
  let(:batch_size) { Flores::Random.integer(1..1024) }
93
73
  let(:batch_payload) do
94
74
  batch = []
@@ -130,4 +110,101 @@ describe "A client" do
130
110
  end
131
111
  end
132
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
133
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 eq((number_of_events / number_of_events * Lumberjack::AckingProtocolV2::ACK_RATIO).ceil + 1)
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
@@ -8,9 +8,7 @@ require "openssl"
8
8
  require "zlib"
9
9
 
10
10
  describe "Lumberjack::Client" do
11
-
12
11
  describe "Lumberjack::Socket" do
13
-
14
12
  let(:port) { 5000 }
15
13
 
16
14
  subject(:socket) { Lumberjack::Socket.new(:port => port, :ssl_certificate => "" ) }
@@ -38,16 +36,60 @@ describe "Lumberjack::Client" do
38
36
  end
39
37
  end
40
38
 
41
- describe Lumberjack::Encoder do
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
+
42
82
  it 'should creates frames without truncating accentued characters' do
43
83
  content = {
44
84
  "message" => "Le Canadien de Montréal est la meilleure équipe au monde!",
45
85
  "other" => "éléphant"
46
86
  }
47
87
  parser = Lumberjack::Parser.new
48
- parser.feed(Lumberjack::Encoder.to_frame(content, 0)) do |code, sequence, data|
49
- expect(data["message"].force_encoding('UTF-8')).to eq(content["message"])
50
- expect(data["other"].force_encoding('UTF-8')).to eq(content["other"])
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
51
93
  end
52
94
  end
53
95
 
@@ -56,8 +98,8 @@ describe "Lumberjack::Client" do
56
98
  "message" => "国際ホッケー連盟" # International Hockey Federation
57
99
  }
58
100
  parser = Lumberjack::Parser.new
59
- parser.feed(Lumberjack::Encoder.to_frame(content, 0)) do |code, sequence, data|
60
- expect(data["message"].force_encoding('UTF-8')).to eq(content["message"])
101
+ parser.feed(Lumberjack::JsonEncoder.to_frame(content, 0)) do |code, sequence, data|
102
+ expect(data["message"]).to eq(content["message"]) if code == :json
61
103
  end
62
104
  end
63
105
  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
@@ -1,33 +1,42 @@
1
1
  # encoding: utf-8
2
+ require "lumberjack/client"
2
3
  require "lumberjack/server"
3
- require "spec_helper"
4
4
  require "flores/random"
5
+ require "flores/pki"
6
+ require "spec_helper"
7
+
8
+ Thread.abort_on_exception = true
5
9
 
6
10
  describe "Server" do
7
- let(:socket) { double("socket") }
8
- let(:connection) { Lumberjack::Connection.new(socket) }
9
- let(:payload) { {"line" => "foobar" } }
10
- let(:start_sequence) { Flores::Random.integer(0..2000) }
11
- let(:random_number_of_events) { Flores::Random.integer(2..200) }
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) { [] }
12
18
 
13
19
  before do
14
- expect(socket).to receive(:sysread).at_least(:once).with(Lumberjack::Connection::READ_SIZE).and_return("")
15
- allow(socket).to receive(:syswrite).with(anything).and_return(true)
16
- allow(socket).to receive(:close)
17
-
18
- expectation = receive(:feed)
19
- .with("")
20
- .and_yield(:window_size, random_number_of_events)
21
-
22
- random_number_of_events.times { |n| expectation.and_yield(:data, start_sequence + n + 1, payload) }
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
23
 
24
- expect_any_instance_of(Lumberjack::Parser).to expectation
24
+ subject do
25
+ Lumberjack::Server.new(:port => port,
26
+ :address => host,
27
+ :ssl_certificate => certificate_file_crt,
28
+ :ssl_key => certificate_file_key)
25
29
  end
26
30
 
27
- describe "Connnection" do
28
- it "should ack the end of a sequence" do
29
- expect(socket).to receive(:syswrite).with(["1A", random_number_of_events + start_sequence].pack("A*N"))
30
- connection.read_socket
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
31
36
  end
37
+
38
+ sleep(1) while thread.status != "run"
39
+ subject.close
40
+ wait_for { thread.status }.to be_falsey
32
41
  end
33
42
  end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # encoding: utf-8
2
2
  require 'rspec'
3
3
  require 'rspec/mocks'
4
+ require 'rspec/wait'
5
+
4
6
  $: << File.realpath(File.join(File.dirname(__FILE__), "..", "lib"))
5
7
 
6
8
  RSpec.configure do |config|
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jls-lumberjack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.24
4
+ version: 0.0.25.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Sissel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-08-17 00:00:00.000000000 Z
11
+ date: 2015-10-17 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '>='
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ name: concurrent-ruby
20
+ prerelease: false
21
+ type: :runtime
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  requirement: !ruby/object:Gem::Requirement
15
29
  requirements:
@@ -66,6 +80,20 @@ dependencies:
66
80
  - - '>='
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ name: rspec-wait
90
+ prerelease: false
91
+ type: :development
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
69
97
  description: lumberjack log transport library
70
98
  email:
71
99
  - jls@semicomplete.com
@@ -77,7 +105,10 @@ files:
77
105
  - lib/lumberjack/client.rb
78
106
  - lib/lumberjack/server.rb
79
107
  - spec/integration_spec.rb
108
+ - spec/lumberjack/acking_protocol_v1_spec.rb
109
+ - spec/lumberjack/acking_protocol_v2_spec.rb
80
110
  - spec/lumberjack/client_spec.rb
111
+ - spec/lumberjack/connection_spec.rb
81
112
  - spec/lumberjack/server_spec.rb
82
113
  - spec/spec_helper.rb
83
114
  homepage: https://github.com/jordansissel/lumberjack
@@ -94,9 +125,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
94
125
  version: '0'
95
126
  required_rubygems_version: !ruby/object:Gem::Requirement
96
127
  requirements:
97
- - - '>='
128
+ - - '>'
98
129
  - !ruby/object:Gem::Version
99
- version: '0'
130
+ version: 1.3.1
100
131
  requirements: []
101
132
  rubyforge_project:
102
133
  rubygems_version: 2.4.8
@@ -106,5 +137,8 @@ summary: lumberjack log transport library
106
137
  test_files:
107
138
  - spec/integration_spec.rb
108
139
  - spec/spec_helper.rb
140
+ - spec/lumberjack/acking_protocol_v1_spec.rb
141
+ - spec/lumberjack/acking_protocol_v2_spec.rb
109
142
  - spec/lumberjack/client_spec.rb
143
+ - spec/lumberjack/connection_spec.rb
110
144
  - spec/lumberjack/server_spec.rb