fluentd 1.19.2 → 1.19.4

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +81 -0
  3. data/Rakefile +7 -0
  4. data/lib/fluent/command/cat.rb +1 -1
  5. data/lib/fluent/config/basic_parser.rb +0 -1
  6. data/lib/fluent/config/literal_parser.rb +18 -7
  7. data/lib/fluent/config/types.rb +10 -2
  8. data/lib/fluent/configurable.rb +1 -0
  9. data/lib/fluent/daemon.rb +2 -1
  10. data/lib/fluent/engine.rb +1 -1
  11. data/lib/fluent/env.rb +1 -0
  12. data/lib/fluent/event.rb +2 -1
  13. data/lib/fluent/plugin/base.rb +7 -2
  14. data/lib/fluent/plugin/buf_file.rb +4 -4
  15. data/lib/fluent/plugin/buf_file_single.rb +2 -2
  16. data/lib/fluent/plugin/buf_memory.rb +1 -1
  17. data/lib/fluent/plugin/buffer/chunk.rb +16 -7
  18. data/lib/fluent/plugin/buffer/file_chunk.rb +7 -5
  19. data/lib/fluent/plugin/buffer/file_single_chunk.rb +7 -5
  20. data/lib/fluent/plugin/buffer/memory_chunk.rb +1 -1
  21. data/lib/fluent/plugin/buffer.rb +26 -5
  22. data/lib/fluent/plugin/compressable.rb +7 -62
  23. data/lib/fluent/plugin/extractor.rb +132 -0
  24. data/lib/fluent/plugin/filter_record_transformer.rb +2 -1
  25. data/lib/fluent/plugin/in_debug_agent.rb +1 -1
  26. data/lib/fluent/plugin/in_forward.rb +3 -1
  27. data/lib/fluent/plugin/in_http.rb +91 -6
  28. data/lib/fluent/plugin/in_monitor_agent.rb +21 -23
  29. data/lib/fluent/plugin/in_sample.rb +2 -1
  30. data/lib/fluent/plugin/in_syslog.rb +70 -4
  31. data/lib/fluent/plugin/out_file.rb +10 -0
  32. data/lib/fluent/plugin/out_forward/connection_manager.rb +18 -0
  33. data/lib/fluent/plugin/out_forward/socket_cache.rb +45 -10
  34. data/lib/fluent/plugin/out_forward.rb +13 -2
  35. data/lib/fluent/plugin/out_http.rb +31 -1
  36. data/lib/fluent/plugin/output.rb +69 -5
  37. data/lib/fluent/plugin/parser_csv.rb +5 -0
  38. data/lib/fluent/plugin/parser_json.rb +6 -1
  39. data/lib/fluent/plugin/parser_syslog.rb +3 -3
  40. data/lib/fluent/plugin/sd_file.rb +2 -1
  41. data/lib/fluent/plugin/storage_local.rb +4 -4
  42. data/lib/fluent/supervisor.rb +22 -1
  43. data/lib/fluent/test/base.rb +5 -0
  44. data/lib/fluent/version.rb +1 -1
  45. metadata +3 -3
  46. data/.deepsource.toml +0 -13
@@ -0,0 +1,132 @@
1
+ #
2
+ # Fluentd
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'stringio'
18
+ require 'zlib'
19
+ require 'zstd-ruby'
20
+ require 'fluent/error'
21
+
22
+ module Fluent
23
+ module Plugin
24
+ module Extractor
25
+ class SizeLimitError < UnrecoverableError; end
26
+
27
+ BYTES_TO_READ = 64 * 1024
28
+ INFLATE_BYTES_TO_READ = 1024
29
+ # Zstd::StreamReader#read takes a number of compressed bytes, unlike
30
+ # Zlib::GzipReader#read which takes decompressed bytes
31
+ ZSTD_BYTES_TO_READ = 1024
32
+
33
+ def self.decompress_gzip(compressed_data, limit:)
34
+ io = StringIO.new(compressed_data)
35
+ out = ''
36
+ loop do
37
+ reader = Zlib::GzipReader.new(io)
38
+ while (chunk = reader.read(BYTES_TO_READ))
39
+ out << chunk
40
+ if out.bytesize > limit
41
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
42
+ end
43
+ end
44
+
45
+ unused = reader.unused
46
+ reader.finish
47
+ unless unused.nil?
48
+ adjust = unused.length
49
+ io.pos -= adjust
50
+ end
51
+ break if io.eof?
52
+ end
53
+ out
54
+ end
55
+
56
+ def self.decompress_zstd(compressed_data, limit:)
57
+ io = StringIO.new(compressed_data)
58
+ reader = Zstd::StreamReader.new(io)
59
+ out = ''
60
+ loop do
61
+ out << reader.read(ZSTD_BYTES_TO_READ)
62
+ if out.bytesize > limit
63
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
64
+ end
65
+
66
+ # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
67
+ break if io.eof?
68
+ end
69
+ out
70
+ end
71
+
72
+ def self.decompress_deflate(compressed_data, limit:)
73
+ io = StringIO.new(compressed_data)
74
+ out = ''
75
+ begin
76
+ zstream = Zlib::Inflate.new
77
+ while (chunk = io.read(INFLATE_BYTES_TO_READ))
78
+ out << zstream.inflate(chunk)
79
+ if out.bytesize > limit
80
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
81
+ end
82
+ end
83
+ out << zstream.finish
84
+ if out.bytesize > limit
85
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
86
+ end
87
+ ensure
88
+ zstream&.close
89
+ end
90
+ out
91
+ end
92
+
93
+ def self.io_decompress_gzip(input, output, limit:)
94
+ size = 0
95
+ loop do
96
+ reader = Zlib::GzipReader.new(input)
97
+ while (chunk = reader.read(BYTES_TO_READ))
98
+ size += chunk.bytesize
99
+ if size > limit
100
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
101
+ end
102
+ output.write(chunk)
103
+ end
104
+ unused = reader.unused
105
+ reader.finish
106
+ unless unused.nil?
107
+ adjust = unused.length
108
+ input.pos -= adjust
109
+ end
110
+ break if input.eof?
111
+ end
112
+ output
113
+ end
114
+
115
+ def self.io_decompress_zstd(input, output, limit:)
116
+ reader = Zstd::StreamReader.new(input)
117
+ size = 0
118
+ loop do
119
+ chunk = reader.read(ZSTD_BYTES_TO_READ)
120
+ size += chunk.bytesize
121
+ if size > limit
122
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
123
+ end
124
+ output.write(chunk)
125
+ # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
126
+ break if input.eof?
127
+ end
128
+ output
129
+ end
130
+ end
131
+ end
132
+ end
@@ -18,6 +18,7 @@ require 'socket'
18
18
  require 'json'
19
19
  require 'ostruct'
20
20
 
21
+ require 'fluent/env'
21
22
  require 'fluent/plugin/filter'
22
23
  require 'fluent/config/error'
23
24
  require 'fluent/event'
@@ -116,7 +117,7 @@ module Fluent::Plugin
116
117
 
117
118
  def parse_value(value_str)
118
119
  if value_str.start_with?('{', '[')
119
- JSON.parse(value_str)
120
+ JSON.parse(value_str, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
120
121
  else
121
122
  value_str
122
123
  end
@@ -26,7 +26,7 @@ module Fluent::Plugin
26
26
  super
27
27
  end
28
28
 
29
- config_param :bind, :string, default: '0.0.0.0'
29
+ config_param :bind, :string, default: '127.0.0.1'
30
30
  config_param :port, :integer, default: 24230
31
31
  config_param :unix_path, :string, default: nil
32
32
  #config_param :unix_mode # TODO
@@ -54,6 +54,8 @@ module Fluent::Plugin
54
54
  config_param :chunk_size_warn_limit, :size, default: nil
55
55
  desc 'Received chunk is dropped if it is larger than this value.'
56
56
  config_param :chunk_size_limit, :size, default: nil
57
+ desc 'The size limit of the decompressed element.'
58
+ config_param :decompression_size_limit, :size, default: 256*1024*1024
57
59
  desc 'Skip an event if incoming event is invalid.'
58
60
  config_param :skip_invalid_event, :bool, default: true
59
61
 
@@ -311,7 +313,7 @@ module Fluent::Plugin
311
313
  size = option['size'] || 0
312
314
 
313
315
  if option['compressed'] && option['compressed'] != 'text'
314
- es = Fluent::CompressedMessagePackEventStream.new(entries, nil, size.to_i, compress: option['compressed'].to_sym)
316
+ es = Fluent::CompressedMessagePackEventStream.new(entries, nil, size.to_i, compress: option['compressed'].to_sym, decompression_size_limit: @decompression_size_limit)
315
317
  else
316
318
  es = Fluent::MessagePackEventStream.new(entries, nil, size.to_i)
317
319
  end
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'fluent/plugin/extractor'
17
18
  require 'fluent/plugin/input'
18
19
  require 'fluent/plugin/parser'
19
20
  require 'fluent/event'
@@ -23,6 +24,8 @@ require 'webrick/httputils'
23
24
  require 'uri'
24
25
  require 'socket'
25
26
  require 'json'
27
+ require 'ipaddr'
28
+ require 'openssl'
26
29
 
27
30
  module Fluent::Plugin
28
31
  class InHttpParser < Parser
@@ -64,6 +67,8 @@ module Fluent::Plugin
64
67
  config_param :bind, :string, default: '0.0.0.0'
65
68
  desc 'The size limit of the POSTed element. Default is 32MB.'
66
69
  config_param :body_size_limit, :size, default: 32*1024*1024 # TODO default
70
+ desc 'The size limit of the decompressed element.'
71
+ config_param :decompression_size_limit, :size, default: 256*1024*1024 # TODO default
67
72
  desc 'The timeout limit for keeping the connection alive.'
68
73
  config_param :keepalive_timeout, :time, default: 10 # TODO default
69
74
  config_param :backlog, :integer, default: nil
@@ -87,6 +92,22 @@ module Fluent::Plugin
87
92
  desc "Add prefix to incoming tag"
88
93
  config_param :add_tag_prefix, :string, default: nil
89
94
 
95
+ config_section :auth, required: false, multi: false do
96
+ desc 'The method for HTTP authentication'
97
+ config_param :method, :enum, list: [:basic], default: :basic
98
+ desc 'The username for basic authentication'
99
+ config_param :username, :string
100
+ desc 'The password for basic authentication'
101
+ config_param :password, :string, secret: true
102
+ end
103
+
104
+ config_section :security, required: false, multi: false do
105
+ config_section :client, param_name: :clients, required: false, multi: true do
106
+ desc 'Address or network of the client, IPv4 or IPv6. e.g. 192.168.1.10, 192.168.0.0/16, fd00::/8'
107
+ config_param :network, :string
108
+ end
109
+ end
110
+
90
111
  config_section :parse do
91
112
  config_set_default :@type, 'in_http'
92
113
  end
@@ -97,6 +118,7 @@ module Fluent::Plugin
97
118
  super
98
119
 
99
120
  @km = nil
121
+ @nodes = []
100
122
  @format_name = nil
101
123
  @parser_time_key = nil
102
124
 
@@ -124,6 +146,8 @@ module Fluent::Plugin
124
146
 
125
147
  raise Fluent::ConfigError, "'add_tag_prefix' parameter must not be empty" if @add_tag_prefix && @add_tag_prefix.empty?
126
148
 
149
+ configure_security if @security
150
+
127
151
  m = if @parser_configs.first['@type'] == 'in_http'
128
152
  @parser_msgpack = parser_create(usage: 'parser_in_http_msgpack', type: 'msgpack')
129
153
  @parser_msgpack.time_key = nil
@@ -257,11 +281,33 @@ module Fluent::Plugin
257
281
 
258
282
  private
259
283
 
284
+ def configure_security
285
+ @nodes = @security.clients.map do |client|
286
+ begin
287
+ IPAddr.new(client.network)
288
+ rescue ArgumentError
289
+ raise Fluent::ConfigError, "network '#{client.network}' address format is invalid"
290
+ end
291
+ end
292
+ end
293
+
294
+ def allowed_client?(address)
295
+ return true if @nodes.empty?
296
+ @nodes.any? { |node| node.include?(address) rescue false }
297
+ end
298
+
260
299
  def on_server_connect(conn)
300
+ unless allowed_client?(conn.remote_addr)
301
+ log.warn "client address does not match any allowed network", address: conn.remote_addr
302
+ conn.data {|_data| }
303
+ conn.close
304
+ return
305
+ end
306
+
261
307
  handler = Handler.new(conn, @km, method(:on_request),
262
- @body_size_limit, @format_name, log,
308
+ @body_size_limit, @decompression_size_limit, @format_name, log,
263
309
  @cors_allow_origins, @cors_allow_credentials,
264
- @add_query_params)
310
+ @add_query_params, @auth)
265
311
 
266
312
  conn.on(:data) do |data|
267
313
  handler.on_read(data)
@@ -343,12 +389,13 @@ module Fluent::Plugin
343
389
  class Handler
344
390
  attr_reader :content_type
345
391
 
346
- def initialize(io, km, callback, body_size_limit, format_name, log,
347
- cors_allow_origins, cors_allow_credentials, add_query_params)
392
+ def initialize(io, km, callback, body_size_limit, decompression_size_limit, format_name, log,
393
+ cors_allow_origins, cors_allow_credentials, add_query_params, auth = nil)
348
394
  @io = io
349
395
  @km = km
350
396
  @callback = callback
351
397
  @body_size_limit = body_size_limit
398
+ @decompression_size_limit = decompression_size_limit
352
399
  @next_close = false
353
400
  @format_name = format_name
354
401
  @log = log
@@ -356,6 +403,8 @@ module Fluent::Plugin
356
403
  @cors_allow_credentials = cors_allow_credentials
357
404
  @idle = 0
358
405
  @add_query_params = add_query_params
406
+ @auth = auth
407
+ @authorization = nil
359
408
  @km.add(self)
360
409
 
361
410
  @remote_port, @remote_addr = io.remote_port, io.remote_addr
@@ -395,6 +444,7 @@ module Fluent::Plugin
395
444
  @env = {}
396
445
  @content_type = ""
397
446
  @content_encoding = ""
447
+ @authorization = nil
398
448
  headers.each_pair {|k,v|
399
449
  @env["HTTP_#{k.tr('-','_').upcase}"] = v
400
450
  case k
@@ -422,8 +472,22 @@ module Fluent::Plugin
422
472
  @access_control_request_method = v
423
473
  when /\AAccess-Control-Request-Headers\Z/i
424
474
  @access_control_request_headers = v
475
+ when /\AAuthorization\Z/i
476
+ @authorization = v.is_a?(Array) ? v.first : v
425
477
  end
426
478
  }
479
+
480
+ if @auth
481
+ # Never let the credential reach a record built by add_http_headers.
482
+ @env.delete("HTTP_AUTHORIZATION")
483
+
484
+ unless preflight_request? || authenticate
485
+ @log.warn "authentication failed", address: @remote_addr
486
+ send_response_and_close(RES_401_STATUS, {'WWW-Authenticate' => AUTH_CHALLENGE}, "")
487
+ return
488
+ end
489
+ end
490
+
427
491
  if expect
428
492
  if expect == '100-continue'.freeze
429
493
  if !size || size < @body_size_limit
@@ -438,6 +502,8 @@ module Fluent::Plugin
438
502
  end
439
503
 
440
504
  def on_body(chunk)
505
+ return if closing?
506
+
441
507
  if @body.bytesize + chunk.bytesize > @body_size_limit
442
508
  unless closing?
443
509
  send_response_and_close("413 Request Entity Too Large", {}, "Too large")
@@ -448,6 +514,8 @@ module Fluent::Plugin
448
514
  end
449
515
 
450
516
  RES_200_STATUS = "200 OK".freeze
517
+ RES_401_STATUS = "401 Unauthorized".freeze
518
+ AUTH_CHALLENGE = 'Basic realm="fluentd"'.freeze
451
519
  RES_403_STATUS = "403 Forbidden".freeze
452
520
 
453
521
  # Azure App Service sends GET requests for health checking purpose.
@@ -518,9 +586,9 @@ module Fluent::Plugin
518
586
  # For now, we only support 'gzip' and 'deflate'.
519
587
  begin
520
588
  if @content_encoding == 'gzip'.freeze
521
- @body = Zlib::GzipReader.new(StringIO.new(@body)).read
589
+ @body = Extractor.decompress_gzip(@body, limit: @decompression_size_limit)
522
590
  elsif @content_encoding == 'deflate'.freeze
523
- @body = Zlib::Inflate.inflate(@body)
591
+ @body = Extractor.decompress_deflate(@body, limit: @decompression_size_limit)
524
592
  end
525
593
  rescue
526
594
  @log.warn 'fails to decode payload', error: $!.to_s
@@ -647,6 +715,23 @@ module Fluent::Plugin
647
715
  def parse_query(query)
648
716
  query.nil? ? {} : Hash[URI.decode_www_form(query, Encoding::ASCII_8BIT)]
649
717
  end
718
+
719
+ # Browsers never attach Authorization to a CORS preflight.
720
+ def preflight_request?
721
+ @parser.http_method == 'OPTIONS'.freeze
722
+ end
723
+
724
+ def authenticate
725
+ scheme, credentials = @authorization.to_s.split(' ', 2)
726
+ return false unless scheme == 'Basic'.freeze
727
+
728
+ username, password = credentials.to_s.unpack1('m').to_s.split(':', 2)
729
+ username_matched = OpenSSL.secure_compare(username.to_s, @auth.username)
730
+ password_matched = OpenSSL.secure_compare(password.to_s, @auth.password)
731
+ username_matched && password_matched
732
+ rescue ArgumentError
733
+ false
734
+ end
650
735
  end
651
736
  end
652
737
  end
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'json'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/config/types'
20
21
  require 'fluent/plugin/input'
21
22
  require 'fluent/plugin/output'
@@ -37,9 +38,11 @@ module Fluent::Plugin
37
38
  desc 'Determine the rate to emit internal metrics as events.'
38
39
  config_param :emit_interval, :time, default: 60
39
40
  desc 'Determine whether to include the config information.'
40
- config_param :include_config, :bool, default: true
41
+ config_param :include_config, :bool, default: false
41
42
  desc 'Determine whether to include the retry information.'
42
- config_param :include_retry, :bool, default: true
43
+ config_param :include_retry, :bool, default: false
44
+ desc 'Determine whether to include the debug information.'
45
+ config_param :include_debug_info, :bool, default: false
43
46
 
44
47
  class APIHandler
45
48
  def initialize(agent)
@@ -99,7 +102,7 @@ module Fluent::Plugin
99
102
  end
100
103
 
101
104
  def render_ltsv(obj, code: 200)
102
- normalized = JSON.parse(obj.to_json)
105
+ normalized = JSON.parse(obj.to_json, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
103
106
  text = ''
104
107
  normalized.each do |hash|
105
108
  row = []
@@ -151,28 +154,23 @@ module Fluent::Plugin
151
154
  # parse ?=query string
152
155
  qs.merge!(req.query || {})
153
156
 
154
- # if ?debug=1 is set, set :with_debug_info for get_monitor_info
155
- # and :pretty_json for render_json_error
156
- opts = { query: qs }
157
- if qs['debug'.freeze].first
158
- opts[:with_debug_info] = true
159
- opts[:pretty_json] = true
160
- end
161
-
162
- if ivars = qs['with_ivars'.freeze].first
163
- opts[:ivars] = ivars.split(',')
164
- end
157
+ opts = {
158
+ query: qs,
159
+ with_config: @agent.include_config,
160
+ with_retry: @agent.include_retry
161
+ }
165
162
 
166
- if with_config = qs['with_config'.freeze].first
167
- opts[:with_config] = Fluent::Config.bool_value(with_config)
168
- else
169
- opts[:with_config] = @agent.include_config
170
- end
163
+ if @agent.include_debug_info
164
+ # if ?debug=1 is set, set :with_debug_info for get_monitor_info
165
+ # and :pretty_json for render_json_error
166
+ if qs['debug'.freeze].first
167
+ opts[:with_debug_info] = true
168
+ opts[:pretty_json] = true
169
+ end
171
170
 
172
- if with_retry = qs['with_retry'.freeze].first
173
- opts[:with_retry] = Fluent::Config.bool_value(with_retry)
174
- else
175
- opts[:with_retry] = @agent.include_retry
171
+ if ivars = qs['with_ivars'.freeze].first
172
+ opts[:ivars] = ivars.split(',')
173
+ end
176
174
  end
177
175
 
178
176
  opts
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'json'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/plugin/input'
20
21
  require 'fluent/config/error'
21
22
 
@@ -44,7 +45,7 @@ module Fluent::Plugin
44
45
  desc "The sample data to be generated. An array of JSON hashes or a single JSON hash."
45
46
  config_param :sample, alias: :dummy, default: [{"message" => "sample"}] do |val|
46
47
  begin
47
- parsed = JSON.parse(val)
48
+ parsed = JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
48
49
  rescue JSON::ParserError => ex
49
50
  # Fluent::ConfigParseError, "got incomplete JSON" will be raised
50
51
  # at literal_parser.rb with --use-v1-config, but I had to
@@ -98,8 +98,8 @@ module Fluent::Plugin
98
98
  desc 'The field name of the facility.'
99
99
  config_param :facility_key, :string, default: nil
100
100
 
101
- desc "The max bytes of message"
102
- config_param :message_length_limit, :size, default: 2048
101
+ desc "The max bytes of message. The messages exceeding this limit are dropped (UDP, TCP/TLS with traditional frame_type) or the connection is closed (TCP/TLS with octet_count frame_type)."
102
+ config_param :message_length_limit, :size, default: 8192
103
103
 
104
104
  config_param :blocking_timeout, :time, default: 0.5
105
105
 
@@ -190,29 +190,95 @@ module Fluent::Plugin
190
190
  resolve_name: @resolve_hostname,
191
191
  send_keepalive_packet: @send_keepalive_packet
192
192
  ) do |conn|
193
+ discard_till_next_delimiter = false
193
194
  conn.data do |data|
194
195
  buffer = conn.buffer
195
196
  buffer << data
196
197
  pos = 0
197
198
  if octet_count_frame
199
+ # Once an octet-counted stream contains an untrustworthy frame header, the stream
200
+ # can't be resynchronized. So close the connection instead of skipping bytes.
201
+ close_connection = false
198
202
  while idx = buffer.index(delimiter, pos)
199
- num = Integer(buffer[pos..idx])
203
+ begin
204
+ num = Integer(buffer[pos..idx])
205
+ rescue ArgumentError
206
+ log.info "The message length is not a number, closing connection"
207
+ log.on_debug { log.debug "The message length is not a number:", head: buffer[pos, 32].dump }
208
+ close_connection = true
209
+ break
210
+ end
211
+
212
+ if num < 0
213
+ log.info "The message length is negative, closing connection:", size: num
214
+ close_connection = true
215
+ break
216
+ end
217
+
218
+ if num > @message_length_limit
219
+ log.info "The message length exceeds 'message_length_limit', closing connection:", limit: @message_length_limit, size: num
220
+ close_connection = true
221
+ break
222
+ end
223
+
200
224
  msg = buffer[idx + delimiter_size, num]
201
225
  if msg.size != num
226
+ # The frame is not fully received yet.
202
227
  break
203
228
  end
204
229
 
205
230
  pos = idx + delimiter_size + num
206
231
  message_handler(msg, conn)
207
232
  end
233
+ buffer.slice!(0, pos) if pos > 0
234
+
235
+ # If the remaining buffer has no delimiter, no message length can be parsed from it.
236
+ # Since a valid frame header is much shorter than the limit, such a buffer never
237
+ # becomes a valid frame. (Note that a partially received valid frame always has a
238
+ # delimiter after its length, so this doesn't drop it even if it slightly exceeds
239
+ # the limit with its length header.)
240
+ if !close_connection && !buffer.index(delimiter) && buffer.bytesize > @message_length_limit
241
+ log.info "The buffer size exceeds 'message_length_limit', closing connection:", limit: @message_length_limit, size: buffer.bytesize
242
+ log.on_debug { log.debug "The buffer size exceeds 'message_length_limit':", head: buffer[...32] }
243
+ close_connection = true
244
+ end
245
+
246
+ if close_connection
247
+ buffer.clear
248
+ conn.close
249
+ next
250
+ end
208
251
  else
209
252
  while idx = buffer.index(delimiter, pos)
210
253
  msg = buffer[pos...idx]
211
254
  pos = idx + delimiter_size
255
+
256
+ if discard_till_next_delimiter
257
+ discard_till_next_delimiter = false
258
+ next
259
+ end
260
+
261
+ if msg.bytesize > @message_length_limit
262
+ log.info "The received data is larger than 'message_length_limit', dropped:", limit: @message_length_limit, size: msg.bytesize
263
+ log.on_debug { log.debug "The received data is larger than 'message_length_limit':", head: msg[...32] }
264
+ next
265
+ end
266
+
212
267
  message_handler(msg, conn)
213
268
  end
269
+ buffer.slice!(0, pos) if pos > 0
270
+
271
+ # If the buffer size exceeds the limit here, it means that the next message will
272
+ # definitely exceed the limit. So we should clear the buffer here. Otherwise, it will
273
+ # keep storing useless data until the next delimiter comes.
274
+ if buffer.bytesize > @message_length_limit
275
+ log.info "The buffer size exceeds 'message_length_limit', cleared:", limit: @message_length_limit, size: buffer.bytesize
276
+ log.on_debug { log.debug "The buffer size exceeds 'message_length_limit':", head: buffer[...32] }
277
+ buffer.clear
278
+ # We should discard the subsequent data until the next delimiter comes.
279
+ discard_till_next_delimiter = true
280
+ end
214
281
  end
215
- buffer.slice!(0, pos) if pos > 0
216
282
  end
217
283
  end
218
284
  end
@@ -117,7 +117,17 @@ module Fluent::Plugin
117
117
  configured_time_slice_format = conf['time_slice_format']
118
118
 
119
119
  if conf.elements(name: 'buffer').empty?
120
+ # no <buffer> section, default time chunk key and timekey (1d) will be used.
121
+ log.warn "default timekey interval (1d) will be used because of missing <buffer> section. To change the output frequency, please modify the timekey value"
122
+
120
123
  conf.add_element('buffer', 'time')
124
+ else
125
+ unless conf.elements(name: 'buffer').first.has_key?('timekey')
126
+ if conf.elements(name: 'buffer').first.arg != "[]"
127
+ # with <buffer> section (except <buffer []>), and no timekey
128
+ log.warn "default timekey interval (1d) will be used. To change the output frequency, please modify the timekey value"
129
+ end
130
+ end
121
131
  end
122
132
  buffer_conf = conf.elements(name: 'buffer').first
123
133
  # Fluent::PluginId#configure is not called yet, so we can't use #plugin_root_dir here.
@@ -78,6 +78,24 @@ module Fluent::Plugin
78
78
  end
79
79
  end
80
80
 
81
+ # Drop a socket instead of returning it to the keepalive reuse pool.
82
+ #
83
+ # Used when a chunk's ack timed out or came back with a mismatched chunk
84
+ # id: such a connection may still carry an in-flight/stale ack for a
85
+ # previous chunk. Reusing it would let that stale ack be read against a
86
+ # chunk sent on the reused socket, permanently offsetting every following
87
+ # ack and flooding the log with 'ack in response and chunk id in sent data
88
+ # are different' warnings (see #4057). Revoking removes it from the pool so
89
+ # the next chunk uses a fresh connection with a clean ack stream.
90
+ def discard(sock)
91
+ if @socket_cache
92
+ @socket_cache.revoke(sock)
93
+ else
94
+ sock.close_write rescue nil
95
+ sock.close rescue nil
96
+ end
97
+ end
98
+
81
99
  private
82
100
 
83
101
  def connect_keepalive(host:, port:, hostname:, ack: nil)