@@ -51,31 +51,44 @@ class LogStash::Outputs::Tcp < LogStash::Outputs::Base
5151 # SSL key passphrase
5252 config :ssl_key_passphrase , :validate => :password , :default => nil
5353
54+ ##
55+ # @param socket [Socket]
56+ # @param logger_context [#log_warn&#log_error&#logger]
5457 class Client
55- public
56- def initialize ( socket , logger )
58+ def initialize ( socket , logger_context )
5759 @socket = socket
58- @logger = logger
60+ @peer_info = socket . peer
61+ @logger_context = logger_context
5962 @queue = Queue . new
6063 end
6164
62- public
6365 def run
6466 loop do
6567 begin
66- @socket . write ( @queue . pop )
68+ payload = @queue . pop
69+
70+ @logger_context . logger . trace ( "transmitting #{ payload . bytesize } bytes" , socket : @peer_info ) if @logger_context . logger . trace? && payload && !payload . empty?
71+ while payload && !payload . empty?
72+ written_bytes_size = @socket . write ( payload )
73+ payload = payload . byteslice ( written_bytes_size ..-1 )
74+ @logger_context . logger . log_trace ( ">transmitted #{ written_bytes_size } bytes; #{ payload . bytesize } bytes remain" , socket : @peer_info ) if @logger_context . logger . trace?
75+ end
6776 rescue => e
68- @logger . warn ( "tcp output exception" , :socket => @socket ,
69- :exception => e )
77+ @logger_context . log_warn ( "tcp output exception: socket write failed" , e , :socket => @peer_info )
7078 break
7179 end
7280 end
7381 end # def run
7482
75- public
7683 def write ( msg )
7784 @queue . push ( msg )
7885 end # def write
86+
87+ def close
88+ @socket . close
89+ rescue => e
90+ @logger_context . log_warn 'socket close failed:' , e , socket : @socket &.to_s
91+ end
7992 end # class Client
8093
8194 private
@@ -113,6 +126,8 @@ def register
113126 if @ssl_enable
114127 setup_ssl
115128 end # @ssl_enable
129+ @closed = Concurrent ::AtomicBoolean . new ( false )
130+ @thread_no = Concurrent ::AtomicFixnum . new ( 0 )
116131
117132 if server?
118133 @logger . info ( "Starting tcp output listener" , :address => "#{ @host } :#{ @port } " )
@@ -129,51 +144,85 @@ def register
129144 @client_threads = [ ]
130145
131146 @accept_thread = Thread . new ( @server_socket ) do |server_socket |
147+ LogStash ::Util . set_thread_name ( "[#{ pipeline_id } ]|output|tcp|server_accept" )
132148 loop do
149+ break if @closed . value
133150 Thread . start ( server_socket . accept ) do |client_socket |
134151 # monkeypatch a 'peer' method onto the socket.
135- client_socket . instance_eval { class << self ; include ::LogStash ::Util ::SocketPeer end }
152+ client_socket . extend ( ::LogStash ::Util ::SocketPeer )
136153 @logger . debug ( "Accepted connection" , :client => client_socket . peer ,
137154 :server => "#{ @host } :#{ @port } " )
138- client = Client . new ( client_socket , @logger )
155+ client = Client . new ( client_socket , self )
139156 Thread . current [ :client ] = client
157+ LogStash ::Util . set_thread_name ( "[#{ pipeline_id } ]|output|tcp|client_socket-#{ @thread_no . increment } " )
140158 @client_threads << Thread . current
141- client . run
159+ client . run unless @closed . value
142160 end
143161 end
144162 end
145163
146164 @codec . on_event do |event , payload |
165+ @client_threads . select! ( &:alive? )
147166 @client_threads . each do |client_thread |
148167 client_thread [ :client ] . write ( payload )
149168 end
150- @client_threads . reject! { |t | !t . alive? }
151169 end
152170 else
153- client_socket = nil
171+ @client_socket = nil
172+ peer_info = nil
154173 @codec . on_event do |event , payload |
155174 begin
156- client_socket = connect unless client_socket
157- r , w , e = IO . select ( [ client_socket ] , [ client_socket ] , [ client_socket ] , nil )
158- # don't expect any reads, but a readable socket might
159- # mean the remote end closed, so read it and throw it away.
160- # we'll get an EOFError if it happens.
161- client_socket . sysread ( 16384 ) if r . any?
175+ # not threadsafe; this is why we require `concurrency: single`
176+ unless @client_socket
177+ @client_socket = connect
178+ peer_info = @client_socket . peer
179+ end
180+
181+ writable_io = nil
182+ while writable_io . nil? || writable_io . any? == false
183+ readable_io , writable_io , _ = IO . select ( [ @client_socket ] , [ @client_socket ] )
184+
185+ # don't expect any reads, but a readable socket might
186+ # mean the remote end closed, so read it and throw it away.
187+ # we'll get an EOFError if it happens.
188+ readable_io . each { |readable | readable . sysread ( 16384 ) }
189+ end
162190
163191 # Now send the payload
164- client_socket . syswrite ( payload ) if w . any?
192+ @logger . trace ( "transmitting #{ payload . bytesize } bytes" , socket : peer_info ) if @logger . trace? && payload && !payload . empty?
193+ while payload && payload . bytesize > 0
194+ written_bytes_size = @client_socket . syswrite ( payload )
195+ payload = payload . byteslice ( written_bytes_size ..-1 )
196+ @logger . trace ( ">transmitted #{ written_bytes_size } bytes; #{ payload . bytesize } bytes remain" , socket : peer_info ) if @logger . trace?
197+ end
165198 rescue => e
166- @logger . warn ( "tcp output exception" , :host => @host , :port => @port ,
167- :exception => e , :backtrace => e . backtrace )
168- client_socket . close rescue nil
169- client_socket = nil
199+ log_warn "client socket failed:" , e , host : @host , port : @port , socket : peer_info
200+ @client_socket . close rescue nil
201+ @client_socket = nil
170202 sleep @reconnect_interval
171203 retry
172204 end
173205 end
174206 end
175207 end # def register
176208
209+ # @overload Base#close
210+ def close
211+ if server?
212+ # server-mode clean-up
213+ @closed . make_true
214+ @server_socket . shutdown rescue nil if @server_socket
215+
216+ @client_threads &.each do |thread |
217+ client = thread [ :client ]
218+ client . close rescue nil if client
219+ end
220+ else
221+ # client-mode clean-up
222+ @client_socket &.close
223+ end
224+ end
225+
177226 private
178227 def connect
179228 begin
@@ -183,17 +232,17 @@ def connect
183232 begin
184233 client_socket . connect
185234 rescue OpenSSL ::SSL ::SSLError => ssle
186- @logger . error ( "SSL Error" , :exception => ssle , : backtrace => ssle . backtrace )
235+ log_error 'connect ssl failure:' , ssle , backtrace : false
187236 # NOTE(mrichar1): Hack to prevent hammering peer
188237 sleep ( 5 )
189238 raise
190239 end
191240 end
192- client_socket . instance_eval { class << self ; include ::LogStash ::Util ::SocketPeer end }
241+ client_socket . extend ( ::LogStash ::Util ::SocketPeer )
193242 @logger . debug ( "Opened connection" , :client => "#{ client_socket . peer } " )
194243 return client_socket
195244 rescue StandardError => e
196- @logger . error ( "Failed to connect: #{ e . message } " , :exception => e . class , :backtrace => e . backtrace )
245+ log_error 'failed to connect:' , e
197246 sleep @reconnect_interval
198247 retry
199248 end
@@ -208,4 +257,20 @@ def server?
208257 def receive ( event )
209258 @codec . encode ( event )
210259 end # def receive
260+
261+ def pipeline_id
262+ execution_context . pipeline_id || 'main'
263+ end
264+
265+ def log_warn ( msg , e , backtrace : @logger . debug? , **details )
266+ details = details . merge message : e . message , exception : e . class
267+ details [ :backtrace ] = e . backtrace if backtrace
268+ @logger . warn ( msg , details )
269+ end
270+
271+ def log_error ( msg , e , backtrace : @logger . info? , **details )
272+ details = details . merge message : e . message , exception : e . class
273+ details [ :backtrace ] = e . backtrace if backtrace
274+ @logger . error ( msg , details )
275+ end
211276end # class LogStash::Outputs::Tcp
0 commit comments