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 +7 -0
- data/lib/lumberjack/client.rb +217 -0
- data/lib/lumberjack/server.rb +433 -0
- data/lib/lumberjack.rb +22 -0
- data/spec/integration_spec.rb +210 -0
- data/spec/lumberjack/acking_protocol_v1_spec.rb +16 -0
- data/spec/lumberjack/acking_protocol_v2_spec.rb +31 -0
- data/spec/lumberjack/client_spec.rb +106 -0
- data/spec/lumberjack/connection_spec.rb +47 -0
- data/spec/lumberjack/server_spec.rb +42 -0
- data/spec/spec_helper.rb +10 -0
- metadata +145 -0
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
|
data/spec/spec_helper.rb
ADDED
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
|