@@ -51,31 +51,44 @@ class LogStash::Outputs::Tcp < LogStash::Outputs::Base
51
51
# SSL key passphrase
52
52
config :ssl_key_passphrase , :validate => :password , :default => nil
53
53
54
+ ##
55
+ # @param socket [Socket]
56
+ # @param logger_context [#log_warn&#log_error&#logger]
54
57
class Client
55
- public
56
- def initialize ( socket , logger )
58
+ def initialize ( socket , logger_context )
57
59
@socket = socket
58
- @logger = logger
60
+ @peer_info = socket . peer
61
+ @logger_context = logger_context
59
62
@queue = Queue . new
60
63
end
61
64
62
- public
63
65
def run
64
66
loop do
65
67
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
67
76
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 )
70
78
break
71
79
end
72
80
end
73
81
end # def run
74
82
75
- public
76
83
def write ( msg )
77
84
@queue . push ( msg )
78
85
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
79
92
end # class Client
80
93
81
94
private
@@ -113,6 +126,8 @@ def register
113
126
if @ssl_enable
114
127
setup_ssl
115
128
end # @ssl_enable
129
+ @closed = Concurrent ::AtomicBoolean . new ( false )
130
+ @thread_no = Concurrent ::AtomicFixnum . new ( 0 )
116
131
117
132
if server?
118
133
@logger . info ( "Starting tcp output listener" , :address => "#{ @host } :#{ @port } " )
@@ -129,51 +144,85 @@ def register
129
144
@client_threads = [ ]
130
145
131
146
@accept_thread = Thread . new ( @server_socket ) do |server_socket |
147
+ LogStash ::Util . set_thread_name ( "[#{ pipeline_id } ]|output|tcp|server_accept" )
132
148
loop do
149
+ break if @closed . value
133
150
Thread . start ( server_socket . accept ) do |client_socket |
134
151
# 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 )
136
153
@logger . debug ( "Accepted connection" , :client => client_socket . peer ,
137
154
:server => "#{ @host } :#{ @port } " )
138
- client = Client . new ( client_socket , @logger )
155
+ client = Client . new ( client_socket , self )
139
156
Thread . current [ :client ] = client
157
+ LogStash ::Util . set_thread_name ( "[#{ pipeline_id } ]|output|tcp|client_socket-#{ @thread_no . increment } " )
140
158
@client_threads << Thread . current
141
- client . run
159
+ client . run unless @closed . value
142
160
end
143
161
end
144
162
end
145
163
146
164
@codec . on_event do |event , payload |
165
+ @client_threads . select! ( &:alive? )
147
166
@client_threads . each do |client_thread |
148
167
client_thread [ :client ] . write ( payload )
149
168
end
150
- @client_threads . reject! { |t | !t . alive? }
151
169
end
152
170
else
153
- client_socket = nil
171
+ @client_socket = nil
172
+ peer_info = nil
154
173
@codec . on_event do |event , payload |
155
174
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
162
190
163
191
# 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
165
198
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
170
202
sleep @reconnect_interval
171
203
retry
172
204
end
173
205
end
174
206
end
175
207
end # def register
176
208
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
+
177
226
private
178
227
def connect
179
228
begin
@@ -183,17 +232,17 @@ def connect
183
232
begin
184
233
client_socket . connect
185
234
rescue OpenSSL ::SSL ::SSLError => ssle
186
- @logger . error ( "SSL Error" , :exception => ssle , : backtrace => ssle . backtrace )
235
+ log_error 'connect ssl failure:' , ssle , backtrace : false
187
236
# NOTE(mrichar1): Hack to prevent hammering peer
188
237
sleep ( 5 )
189
238
raise
190
239
end
191
240
end
192
- client_socket . instance_eval { class << self ; include ::LogStash ::Util ::SocketPeer end }
241
+ client_socket . extend ( ::LogStash ::Util ::SocketPeer )
193
242
@logger . debug ( "Opened connection" , :client => "#{ client_socket . peer } " )
194
243
return client_socket
195
244
rescue StandardError => e
196
- @logger . error ( "Failed to connect: #{ e . message } " , :exception => e . class , :backtrace => e . backtrace )
245
+ log_error 'failed to connect:' , e
197
246
sleep @reconnect_interval
198
247
retry
199
248
end
@@ -208,4 +257,20 @@ def server?
208
257
def receive ( event )
209
258
@codec . encode ( event )
210
259
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
211
276
end # class LogStash::Outputs::Tcp
0 commit comments