fluentd 1.19.2 → 1.19.3

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
  SHA256:
3
- metadata.gz: 4873da89c533afd45a6f02a86aa8f92f7502bd08204724290dd34187c7183cd5
4
- data.tar.gz: 1f092049f456daae36077e374670a7a32d044da48b01ecea5410bc116f76c61b
3
+ metadata.gz: 9613f1dacdb60cfccb245e988e67cec002daee2f81cbe38c2b80c30d525ad7b8
4
+ data.tar.gz: 4a85aa6c3bdaebd9cd069d508ac969d8792586247bef1d73d25cc906f6cd7c87
5
5
  SHA512:
6
- metadata.gz: 820db817466aaade115300f5839e62d278a7fbbd19479b939206f4a0b212905046c1717c37cf2e19d90640dc07b570455e9b6f10fa5d21b04b96da4ec04f15b2
7
- data.tar.gz: 581e8ac6c84ae65e8b33758de56a65ca684547f64c2864fcdb630c68e2b58bda6232e98c60dc48b99efe8c2cf584f31aef62a14fd1a72785d1b6d88f41635fea
6
+ metadata.gz: c2d63e0bd8cf51a61d254cfda559f38ed1bcc79ceb1bf1f0d0c80b21b922e67091d2ee8c291a0dc0fcac0110873d0045874df297e0795c0cd604af64f431f845
7
+ data.tar.gz: 5fe9e2450048bacc978389621b0e3f2c8aed0e9136ffe11192945a754173c1ce13ff50c310cfcdf1633d7dc784e14e9a4fc5a6de519da3bc81decb12f6d5b28d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # v1.19
2
2
 
3
+ ## Release v1.19.3 - 2026/06/25
4
+
5
+ ### Bug Fix
6
+
7
+ * out_http: add strict host validation for dynamic endpoints https://github.com/fluent/fluentd/pull/5394
8
+ * buffer, in_http: enforce size limits on decompressed payloads https://github.com/fluent/fluentd/pull/5393
9
+ * in_monitor_agent: change default visibility of config, retry, and debug info https://github.com/fluent/fluentd/pull/5392
10
+ * output: enforce strict path boundary validation for tag https://github.com/fluent/fluentd/pull/5391
11
+ * engine: remove duplicated word in unreloadable plugin error message https://github.com/fluent/fluentd/pull/5389
12
+ * storage_local: fix encoding error when reading non-ASCII characters https://github.com/fluent/fluentd/pull/5382
13
+ * parser_csv: skip empty or unparseable lines https://github.com/fluent/fluentd/pull/5359
14
+ * out_forward: avoid reusing closed keepalive sockets after remote disconnects https://github.com/fluent/fluentd/pull/5343
15
+ * buffer: resume buffer correctly even though path contains [] https://github.com/fluent/fluentd/pull/5305
16
+ * in_debug_agent: accept only from local machine by default https://github.com/fluent/fluentd/pull/5279
17
+
18
+ ### Misc
19
+
20
+ * gem: add win32-registry as runtime dependency for Ruby 4.1 https://github.com/fluent/fluentd/pull/5317
21
+ * output windows: check shorter service timeout on shutdown https://github.com/fluent/fluentd/pull/5306
22
+ * buffer: warn if default timekey (1d) will be used https://github.com/fluent/fluentd/pull/5291
23
+ * warn recommended exclusion path for antivirus https://github.com/fluent/fluentd/pull/5280
24
+ * CI fixes
25
+ * https://github.com/fluent/fluentd/pull/5387
26
+ * https://github.com/fluent/fluentd/pull/5386
27
+ * https://github.com/fluent/fluentd/pull/5366
28
+ * https://github.com/fluent/fluentd/pull/5342
29
+ * https://github.com/fluent/fluentd/pull/5341
30
+ * https://github.com/fluent/fluentd/pull/5340
31
+ * https://github.com/fluent/fluentd/pull/5339
32
+ * https://github.com/fluent/fluentd/pull/5338
33
+ * https://github.com/fluent/fluentd/pull/5337
34
+ * https://github.com/fluent/fluentd/pull/5336
35
+ * https://github.com/fluent/fluentd/pull/5335
36
+ * https://github.com/fluent/fluentd/pull/5334
37
+ * https://github.com/fluent/fluentd/pull/5333
38
+
3
39
  ## Release v1.19.2 - 2026/02/13
4
40
 
5
41
  ### Bug Fix
data/Rakefile CHANGED
@@ -78,4 +78,11 @@ task :coverity do
78
78
  FileUtils.rm_rf(['./cov-int', 'cov-fluentd.tar.gz'])
79
79
  end
80
80
 
81
+ task :check_env do
82
+ unless ENV['GEM_HOST_API_KEY']
83
+ abort "Missing required environment variable: GEM_HOST_API_KEY\nSee https://guides.rubygems.org/api-key-scopes/"
84
+ end
85
+ end
86
+ Rake::Task["release:rubygem_push"].enhance(["check_env"])
87
+
81
88
  task default: [:test, :build]
data/lib/fluent/engine.rb CHANGED
@@ -186,7 +186,7 @@ module Fluent
186
186
 
187
187
  ret.all_plugins.each do |plugin|
188
188
  if plugin.respond_to?(:reloadable_plugin?) && !plugin.reloadable_plugin?
189
- raise Fluent::ConfigError, "Unreloadable plugin plugin: #{Fluent::Plugin.lookup_type_from_class(plugin.class)}, plugin_id: #{plugin.plugin_id}, class_name: #{plugin.class})"
189
+ raise Fluent::ConfigError, "Unreloadable plugin: #{Fluent::Plugin.lookup_type_from_class(plugin.class)}, plugin_id: #{plugin.plugin_id}, class_name: #{plugin.class})"
190
190
  end
191
191
  end
192
192
 
data/lib/fluent/event.rb CHANGED
@@ -268,11 +268,12 @@ module Fluent
268
268
  end
269
269
 
270
270
  class CompressedMessagePackEventStream < MessagePackEventStream
271
- def initialize(data, cached_unpacker = nil, size = 0, unpacked_times: nil, unpacked_records: nil, compress: :gzip)
271
+ def initialize(data, cached_unpacker = nil, size = 0, unpacked_times: nil, unpacked_records: nil, compress: :gzip, decompression_size_limit: Fluent::Plugin::Compressable::DEFAULT_DECOMPRESSION_SIZE_LIMIT)
272
272
  super(data, cached_unpacker, size, unpacked_times: unpacked_times, unpacked_records: unpacked_records)
273
273
  @decompressed_data = nil
274
274
  @compressed_data = data
275
275
  @type = compress
276
+ @decompression_size_limit = decompression_size_limit
276
277
  end
277
278
 
278
279
  def empty?
@@ -205,7 +205,7 @@ module Fluent
205
205
  def generate_chunk(metadata)
206
206
  # FileChunk generates real path with unique_id
207
207
  perm = @file_permission || system_config.file_permission
208
- chunk = Fluent::Plugin::Buffer::FileChunk.new(metadata, @path, :create, perm: perm, compress: @compress)
208
+ chunk = Fluent::Plugin::Buffer::FileChunk.new(metadata, @path, :create, perm: perm, compress: @compress, decompression_size_limit: @decompression_size_limit)
209
209
  log.debug "Created new chunk", chunk_id: dump_unique_id_hex(chunk.unique_id), metadata: metadata
210
210
 
211
211
  return chunk
@@ -247,8 +247,8 @@ module Fluent
247
247
 
248
248
  def escaped_patterns(patterns)
249
249
  patterns.map { |pattern|
250
- # '{' '}' are special character in Dir.glob
251
- pattern.gsub(/[\{\}]/) { |c| "\\#{c}" }
250
+ # '{', '}', '[' and ']' are special character in Dir.glob
251
+ pattern.gsub(/[\{\}\[\]]/) { |c| "\\#{c}" }
252
252
  }
253
253
  end
254
254
  end
@@ -183,7 +183,7 @@ module Fluent
183
183
  end
184
184
 
185
185
  begin
186
- chunk = Fluent::Plugin::Buffer::FileSingleChunk.new(m, path, mode, @key_in_path, compress: @compress)
186
+ chunk = Fluent::Plugin::Buffer::FileSingleChunk.new(m, path, mode, @key_in_path, compress: @compress, decompression_size_limit: @decompression_size_limit)
187
187
  chunk.restore_size(@chunk_format) if @calc_num_records
188
188
  rescue Fluent::Plugin::Buffer::FileSingleChunk::FileChunkError => e
189
189
  exist_broken_file = true
@@ -216,7 +216,7 @@ module Fluent
216
216
  def generate_chunk(metadata)
217
217
  # FileChunk generates real path with unique_id
218
218
  perm = @file_permission || system_config.file_permission
219
- chunk = Fluent::Plugin::Buffer::FileSingleChunk.new(metadata, @path, :create, @key_in_path, perm: perm, compress: @compress)
219
+ chunk = Fluent::Plugin::Buffer::FileSingleChunk.new(metadata, @path, :create, @key_in_path, perm: perm, compress: @compress, decompression_size_limit: @decompression_size_limit)
220
220
 
221
221
  log.debug "Created new chunk", chunk_id: dump_unique_id_hex(chunk.unique_id), metadata: metadata
222
222
 
@@ -27,7 +27,7 @@ module Fluent
27
27
  end
28
28
 
29
29
  def generate_chunk(metadata)
30
- Fluent::Plugin::Buffer::MemoryChunk.new(metadata, compress: @compress)
30
+ Fluent::Plugin::Buffer::MemoryChunk.new(metadata, compress: @compress, decompression_size_limit: @decompression_size_limit)
31
31
  end
32
32
  end
33
33
  end
@@ -48,7 +48,7 @@ module Fluent
48
48
 
49
49
  # TODO: CompressedPackedMessage of forward protocol?
50
50
 
51
- def initialize(metadata, compress: :text)
51
+ def initialize(metadata, compress: :text, decompression_size_limit: Compressable::DEFAULT_DECOMPRESSION_SIZE_LIMIT)
52
52
  super()
53
53
  @unique_id = generate_unique_id
54
54
  @metadata = metadata
@@ -64,6 +64,7 @@ module Fluent
64
64
  elsif compress == :zstd
65
65
  extend ZstdDecompressable
66
66
  end
67
+ @decompression_size_limit = decompression_size_limit
67
68
  end
68
69
 
69
70
  attr_reader :unique_id, :metadata, :state
@@ -39,8 +39,8 @@ module Fluent
39
39
 
40
40
  attr_reader :path, :meta_path, :permission
41
41
 
42
- def initialize(metadata, path, mode, perm: nil, compress: :text)
43
- super(metadata, compress: compress)
42
+ def initialize(metadata, path, mode, perm: nil, compress: :text, decompression_size_limit: Compressable::DEFAULT_DECOMPRESSION_SIZE_LIMIT)
43
+ super(metadata, compress: compress, decompression_size_limit: decompression_size_limit)
44
44
  perm ||= Fluent::DEFAULT_FILE_PERMISSION
45
45
  @permission = perm.is_a?(String) ? perm.to_i(8) : perm
46
46
  @bytesize = @size = @adding_bytes = @adding_size = 0
@@ -35,8 +35,8 @@ module Fluent
35
35
 
36
36
  attr_reader :path, :permission
37
37
 
38
- def initialize(metadata, path, mode, key, perm: Fluent::DEFAULT_FILE_PERMISSION, compress: :text)
39
- super(metadata, compress: compress)
38
+ def initialize(metadata, path, mode, key, perm: Fluent::DEFAULT_FILE_PERMISSION, compress: :text, decompression_size_limit: Compressable::DEFAULT_DECOMPRESSION_SIZE_LIMIT)
39
+ super(metadata, compress: compress, decompression_size_limit: decompression_size_limit)
40
40
  @key = key
41
41
  perm ||= Fluent::DEFAULT_FILE_PERMISSION
42
42
  @permission = perm.is_a?(String) ? perm.to_i(8) : perm
@@ -20,7 +20,7 @@ module Fluent
20
20
  module Plugin
21
21
  class Buffer
22
22
  class MemoryChunk < Chunk
23
- def initialize(metadata, compress: :text)
23
+ def initialize(metadata, compress: :text, decompression_size_limit: Compressable::DEFAULT_DECOMPRESSION_SIZE_LIMIT)
24
24
  super
25
25
  @chunk = ''.force_encoding(Encoding::ASCII_8BIT)
26
26
  @chunk_bytes = 0
@@ -66,6 +66,9 @@ module Fluent
66
66
  desc 'Compress buffered data.'
67
67
  config_param :compress, :enum, list: [:text, :gzip, :zstd], default: :text
68
68
 
69
+ desc 'The size limit of the decompressed element.'
70
+ config_param :decompression_size_limit, :size, default: Compressable::DEFAULT_DECOMPRESSION_SIZE_LIMIT
71
+
69
72
  desc 'If true, chunks are thrown away when unrecoverable error happens'
70
73
  config_param :disable_chunk_backup, :bool, default: false
71
74
 
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'fluent/plugin/extractor'
17
18
  require 'stringio'
18
19
  require 'zlib'
19
20
  require 'zstd-ruby'
@@ -21,6 +22,8 @@ require 'zstd-ruby'
21
22
  module Fluent
22
23
  module Plugin
23
24
  module Compressable
25
+ DEFAULT_DECOMPRESSION_SIZE_LIMIT = 256 * 1024 * 1024
26
+
24
27
  def compress(data, type: :gzip, **kwargs)
25
28
  output_io = kwargs[:output_io]
26
29
  io = output_io || StringIO.new
@@ -60,79 +63,21 @@ module Fluent
60
63
 
61
64
  private
62
65
 
63
- def string_decompress_gzip(compressed_data)
64
- io = StringIO.new(compressed_data)
65
- out = ''
66
- loop do
67
- reader = Zlib::GzipReader.new(io)
68
- out << reader.read
69
- unused = reader.unused
70
- reader.finish
71
- unless unused.nil?
72
- adjust = unused.length
73
- io.pos -= adjust
74
- end
75
- break if io.eof?
76
- end
77
- out
78
- end
79
-
80
- def string_decompress_zstd(compressed_data)
81
- io = StringIO.new(compressed_data)
82
- reader = Zstd::StreamReader.new(io)
83
- out = ''
84
- loop do
85
- # Zstd::StreamReader needs to specify the size of the buffer
86
- out << reader.read(1024)
87
- # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
88
- break if io.eof?
89
- end
90
- out
91
- end
92
-
93
66
  def string_decompress(compressed_data, type = :gzip)
94
67
  if type == :gzip
95
- string_decompress_gzip(compressed_data)
68
+ Extractor.decompress_gzip(compressed_data, limit: @decompression_size_limit || DEFAULT_DECOMPRESSION_SIZE_LIMIT)
96
69
  elsif type == :zstd
97
- string_decompress_zstd(compressed_data)
70
+ Extractor.decompress_zstd(compressed_data, limit: @decompression_size_limit || DEFAULT_DECOMPRESSION_SIZE_LIMIT)
98
71
  else
99
72
  raise ArgumentError, "Unknown compression type: #{type}"
100
73
  end
101
74
  end
102
75
 
103
- def io_decompress_gzip(input, output)
104
- loop do
105
- reader = Zlib::GzipReader.new(input)
106
- v = reader.read
107
- output.write(v)
108
- unused = reader.unused
109
- reader.finish
110
- unless unused.nil?
111
- adjust = unused.length
112
- input.pos -= adjust
113
- end
114
- break if input.eof?
115
- end
116
- output
117
- end
118
-
119
- def io_decompress_zstd(input, output)
120
- reader = Zstd::StreamReader.new(input)
121
- loop do
122
- # Zstd::StreamReader needs to specify the size of the buffer
123
- v = reader.read(1024)
124
- output.write(v)
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
-
131
76
  def io_decompress(input, output, type = :gzip)
132
77
  if type == :gzip
133
- io_decompress_gzip(input, output)
78
+ Extractor.io_decompress_gzip(input, output)
134
79
  elsif type == :zstd
135
- io_decompress_zstd(input, output)
80
+ Extractor.io_decompress_zstd(input, output)
136
81
  else
137
82
  raise ArgumentError, "Unknown compression type: #{type}"
138
83
  end
@@ -0,0 +1,121 @@
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
+
30
+ def self.decompress_gzip(compressed_data, limit:)
31
+ io = StringIO.new(compressed_data)
32
+ out = ''
33
+ loop do
34
+ reader = Zlib::GzipReader.new(io)
35
+ while (chunk = reader.read(BYTES_TO_READ))
36
+ out << chunk
37
+ if out.bytesize > limit
38
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
39
+ end
40
+ end
41
+
42
+ unused = reader.unused
43
+ reader.finish
44
+ unless unused.nil?
45
+ adjust = unused.length
46
+ io.pos -= adjust
47
+ end
48
+ break if io.eof?
49
+ end
50
+ out
51
+ end
52
+
53
+ def self.decompress_zstd(compressed_data, limit:)
54
+ io = StringIO.new(compressed_data)
55
+ reader = Zstd::StreamReader.new(io)
56
+ out = ''
57
+ loop do
58
+ # Zstd::StreamReader needs to specify the size of the buffer
59
+ out << reader.read(BYTES_TO_READ)
60
+ if out.bytesize > limit
61
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
62
+ end
63
+
64
+ # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
65
+ break if io.eof?
66
+ end
67
+ out
68
+ end
69
+
70
+ def self.decompress_deflate(compressed_data, limit:)
71
+ io = StringIO.new(compressed_data)
72
+ out = ''
73
+ begin
74
+ zstream = Zlib::Inflate.new
75
+ while (chunk = io.read(INFLATE_BYTES_TO_READ))
76
+ out << zstream.inflate(chunk)
77
+ if out.bytesize > limit
78
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
79
+ end
80
+ end
81
+ out << zstream.finish
82
+ if out.bytesize > limit
83
+ raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
84
+ end
85
+ ensure
86
+ zstream&.close
87
+ end
88
+ out
89
+ end
90
+
91
+ def self.io_decompress_gzip(input, output)
92
+ loop do
93
+ reader = Zlib::GzipReader.new(input)
94
+ while (chunk = reader.read(BYTES_TO_READ))
95
+ output.write(chunk)
96
+ end
97
+ unused = reader.unused
98
+ reader.finish
99
+ unless unused.nil?
100
+ adjust = unused.length
101
+ input.pos -= adjust
102
+ end
103
+ break if input.eof?
104
+ end
105
+ output
106
+ end
107
+
108
+ def self.io_decompress_zstd(input, output)
109
+ reader = Zstd::StreamReader.new(input)
110
+ loop do
111
+ # Zstd::StreamReader needs to specify the size of the buffer
112
+ chunk = reader.read(BYTES_TO_READ)
113
+ output.write(chunk)
114
+ # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
115
+ break if input.eof?
116
+ end
117
+ output
118
+ end
119
+ end
120
+ end
121
+ 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'
@@ -64,6 +65,8 @@ module Fluent::Plugin
64
65
  config_param :bind, :string, default: '0.0.0.0'
65
66
  desc 'The size limit of the POSTed element. Default is 32MB.'
66
67
  config_param :body_size_limit, :size, default: 32*1024*1024 # TODO default
68
+ desc 'The size limit of the decompressed element.'
69
+ config_param :decompression_size_limit, :size, default: 256*1024*1024 # TODO default
67
70
  desc 'The timeout limit for keeping the connection alive.'
68
71
  config_param :keepalive_timeout, :time, default: 10 # TODO default
69
72
  config_param :backlog, :integer, default: nil
@@ -259,7 +262,7 @@ module Fluent::Plugin
259
262
 
260
263
  def on_server_connect(conn)
261
264
  handler = Handler.new(conn, @km, method(:on_request),
262
- @body_size_limit, @format_name, log,
265
+ @body_size_limit, @decompression_size_limit, @format_name, log,
263
266
  @cors_allow_origins, @cors_allow_credentials,
264
267
  @add_query_params)
265
268
 
@@ -343,12 +346,13 @@ module Fluent::Plugin
343
346
  class Handler
344
347
  attr_reader :content_type
345
348
 
346
- def initialize(io, km, callback, body_size_limit, format_name, log,
349
+ def initialize(io, km, callback, body_size_limit, decompression_size_limit, format_name, log,
347
350
  cors_allow_origins, cors_allow_credentials, add_query_params)
348
351
  @io = io
349
352
  @km = km
350
353
  @callback = callback
351
354
  @body_size_limit = body_size_limit
355
+ @decompression_size_limit = decompression_size_limit
352
356
  @next_close = false
353
357
  @format_name = format_name
354
358
  @log = log
@@ -518,9 +522,9 @@ module Fluent::Plugin
518
522
  # For now, we only support 'gzip' and 'deflate'.
519
523
  begin
520
524
  if @content_encoding == 'gzip'.freeze
521
- @body = Zlib::GzipReader.new(StringIO.new(@body)).read
525
+ @body = Extractor.decompress_gzip(@body, limit: @decompression_size_limit)
522
526
  elsif @content_encoding == 'deflate'.freeze
523
- @body = Zlib::Inflate.inflate(@body)
527
+ @body = Extractor.decompress_deflate(@body, limit: @decompression_size_limit)
524
528
  end
525
529
  rescue
526
530
  @log.warn 'fails to decode payload', error: $!.to_s
@@ -37,9 +37,11 @@ module Fluent::Plugin
37
37
  desc 'Determine the rate to emit internal metrics as events.'
38
38
  config_param :emit_interval, :time, default: 60
39
39
  desc 'Determine whether to include the config information.'
40
- config_param :include_config, :bool, default: true
40
+ config_param :include_config, :bool, default: false
41
41
  desc 'Determine whether to include the retry information.'
42
- config_param :include_retry, :bool, default: true
42
+ config_param :include_retry, :bool, default: false
43
+ desc 'Determine whether to include the debug information.'
44
+ config_param :include_debug_info, :bool, default: false
43
45
 
44
46
  class APIHandler
45
47
  def initialize(agent)
@@ -151,28 +153,23 @@ module Fluent::Plugin
151
153
  # parse ?=query string
152
154
  qs.merge!(req.query || {})
153
155
 
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
156
+ opts = {
157
+ query: qs,
158
+ with_config: @agent.include_config,
159
+ with_retry: @agent.include_retry
160
+ }
165
161
 
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
162
+ if @agent.include_debug_info
163
+ # if ?debug=1 is set, set :with_debug_info for get_monitor_info
164
+ # and :pretty_json for render_json_error
165
+ if qs['debug'.freeze].first
166
+ opts[:with_debug_info] = true
167
+ opts[:pretty_json] = true
168
+ end
171
169
 
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
170
+ if ivars = qs['with_ivars'.freeze].first
171
+ opts[:ivars] = ivars.split(',')
172
+ end
176
173
  end
177
174
 
178
175
  opts
@@ -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.
@@ -31,20 +31,26 @@ module Fluent::Plugin
31
31
  end
32
32
 
33
33
  def checkout_or(key)
34
+ obsolete_sockets = []
35
+
34
36
  @mutex.synchronize do
35
- tsock = pick_socket(key)
37
+ tsock, obsolete_sockets = pick_socket(key)
36
38
 
37
39
  if tsock
38
- tsock.sock
40
+ return tsock.sock
39
41
  else
40
42
  sock = yield
41
43
  new_tsock = TimedSocket.new(timeout, key, sock)
42
44
  @log.debug("connect new socket #{new_tsock}")
43
45
 
44
46
  @inflight_sockets[sock] = new_tsock
45
- new_tsock.sock
47
+ return new_tsock.sock
46
48
  end
47
49
  end
50
+ ensure
51
+ obsolete_sockets.each do |sock|
52
+ sock.sock.close rescue nil
53
+ end
48
54
  end
49
55
 
50
56
  def checkin(sock)
@@ -117,17 +123,32 @@ module Fluent::Plugin
117
123
  # this method is not thread safe
118
124
  def pick_socket(key)
119
125
  if @available_sockets[key].empty?
120
- return nil
126
+ return nil, []
121
127
  end
122
128
 
123
129
  t = Time.now
124
- if (s = @available_sockets[key].find { |sock| !expired_socket?(sock, time: t) })
125
- @inflight_sockets[s.sock] = @available_sockets[key].delete(s)
126
- s.timeout = timeout
127
- s
128
- else
129
- nil
130
+ selected = nil
131
+ remaining = []
132
+ obsolete_sockets = []
133
+
134
+ @available_sockets[key].each do |sock|
135
+ if expired_socket?(sock, time: t) || unavailable_socket?(sock.sock)
136
+ obsolete_sockets << sock
137
+ elsif selected.nil?
138
+ selected = sock
139
+ else
140
+ remaining << sock
141
+ end
142
+ end
143
+
144
+ @available_sockets[key] = remaining
145
+
146
+ if selected
147
+ @inflight_sockets[selected.sock] = selected
148
+ selected.timeout = timeout
130
149
  end
150
+
151
+ [selected, obsolete_sockets]
131
152
  end
132
153
 
133
154
  def timeout
@@ -137,6 +158,20 @@ module Fluent::Plugin
137
158
  def expired_socket?(sock, time: Time.now)
138
159
  sock.timeout ? sock.timeout < time : false
139
160
  end
161
+
162
+ def unavailable_socket?(sock)
163
+ return sock.closed? if sock.respond_to?(:closed?) && sock.closed?
164
+
165
+ io = if sock.respond_to?(:to_io)
166
+ sock.to_io
167
+ elsif sock.is_a?(IO)
168
+ sock
169
+ end
170
+
171
+ io ? !!IO.select([io], nil, nil, 0) : false
172
+ rescue IOError, SystemCallError
173
+ true
174
+ end
140
175
  end
141
176
  end
142
177
  end
@@ -17,6 +17,7 @@
17
17
  require 'net/http'
18
18
  require 'uri'
19
19
  require 'openssl'
20
+ require 'securerandom'
20
21
  require 'fluent/tls'
21
22
  require 'fluent/plugin/output'
22
23
  require 'fluent/plugin_helper/socket'
@@ -58,6 +59,9 @@ module Fluent::Plugin
58
59
  desc 'Compress HTTP request body'
59
60
  config_param :compress, :enum, list: [:text, :gzip], default: :text
60
61
 
62
+ desc 'Allowed hosts list for dynamic endpoints'
63
+ config_param :allowed_hosts, :array, default: []
64
+
61
65
  desc 'The connection open timeout in seconds'
62
66
  config_param :open_timeout, :integer, default: nil
63
67
  desc 'The read timeout in seconds'
@@ -106,6 +110,11 @@ module Fluent::Plugin
106
110
  config_param :aws_role_arn, :string, default: nil
107
111
  end
108
112
 
113
+ # To prevent URI::InvalidURIError, we replace Fluentd placeholders with a dummy string.
114
+ # We use the ".invalid" TLD (RFC 2606) to ensure it is RFC-compliant for URI parsing,
115
+ # while guaranteeing it will never conflict with a real-world hostname.
116
+ REPLACED_ENDPOINT_PLACEHOLDER = "#{SecureRandom.uuid}.invalid".freeze
117
+
109
118
  def connection_cache_id_thread_key
110
119
  "#{plugin_id}_connection_cache_id"
111
120
  end
@@ -146,6 +155,15 @@ module Fluent::Plugin
146
155
  @retryable_response_codes = [503]
147
156
  end
148
157
 
158
+ begin
159
+ # Replace all Fluentd placeholder syntaxes (${...} or %{...})
160
+ endpoint = @endpoint.gsub(%r([$%]{[^}]+}), REPLACED_ENDPOINT_PLACEHOLDER)
161
+ # If @endpoint has placeholder as host name, then, @endpoint_host == REPLACED_ENDPOINT_PLACEHOLDER
162
+ @endpoint_host = URI.parse(endpoint).host
163
+ rescue URI::InvalidURIError => e
164
+ raise Fluent::ConfigError, "Invalid endpoint URI: #{@endpoint} (#{e.message})"
165
+ end
166
+
149
167
  @http_opt = setup_http_option
150
168
  @proxy_uri = URI.parse(@proxy) if @proxy
151
169
  @formatter = formatter_create
@@ -278,7 +296,19 @@ module Fluent::Plugin
278
296
 
279
297
  def parse_endpoint(chunk)
280
298
  endpoint = extract_placeholders(@endpoint, chunk)
281
- URI.parse(endpoint)
299
+ uri = URI.parse(endpoint)
300
+
301
+ if @endpoint_host != uri.host
302
+ if @allowed_hosts.empty?
303
+ raise Fluent::UnrecoverableError, "allowed_hosts is strictly required when using placeholders in the endpoint host"
304
+ end
305
+
306
+ unless @allowed_hosts.include?(uri.host)
307
+ raise Fluent::UnrecoverableError, "Not allowed host: #{uri.host}"
308
+ end
309
+ end
310
+
311
+ uri
282
312
  end
283
313
 
284
314
  def set_headers(req, uri, chunk)
@@ -44,6 +44,8 @@ module Fluent
44
44
  CHUNK_KEY_PLACEHOLDER_PATTERN = /\$\{([-_.@$a-zA-Z0-9]+)\}/
45
45
  CHUNK_TAG_PLACEHOLDER_PATTERN = /\$\{(tag(?:\[-?\d+\])?)\}/
46
46
  CHUNK_ID_PLACEHOLDER_PATTERN = /\$\{chunk_id\}/
47
+ INVALID_PATH_COMPONENT_PATTERN = %r{\.\.[/\\]|^[/\\]}
48
+ PARENT_DIRECTORY_PATTERN = %r{\.\.[/\\]}
47
49
 
48
50
  CHUNKING_FIELD_WARN_NUM = 4
49
51
 
@@ -372,6 +374,13 @@ module Fluent
372
374
  buf_type = Plugin.lookup_type_from_class(@buffer.class)
373
375
  log.warn "'flush_at_shutdown' is false, and buffer plugin '#{buf_type}' is not persistent buffer."
374
376
  log.warn "your configuration will lose buffered data at shutdown. please confirm your configuration again."
377
+ else
378
+ if Fluent.windows? && @buffer.persistent?
379
+ service_timeout = read_wait_to_kill_service_timeout
380
+ if service_timeout && service_timeout <= 5000 # default value might varies on windows client/server
381
+ log.warn "your WaitToKillServiceTimeout=#{service_timeout} registry configuration seems too short. Recommend to extend the value of 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\WaitToKillServiceTimeout' to prevent buffer corruption from a forced shutdown"
382
+ end
383
+ end
375
384
  end
376
385
 
377
386
  if (@flush_mode != :interval) && buffer_conf.has_key?('flush_interval')
@@ -418,6 +427,22 @@ module Fluent
418
427
  self
419
428
  end
420
429
 
430
+ def read_wait_to_kill_service_timeout
431
+ if Fluent.windows?
432
+ begin
433
+ require "win32/registry"
434
+ Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\CurrentControlSet\\Control",
435
+ Win32::Registry::KEY_READ) do |reg|
436
+ reg["WaitToKillServiceTimeout"].to_i
437
+ end
438
+ rescue => e
439
+ log.warn "'flush_at_shutdown' is true, but can't check WaitToKillServiceTimeout registry configuration", error: e
440
+ end
441
+ else
442
+ nil # not supported
443
+ end
444
+ end
445
+
421
446
  def keep_buffer_config_compat
422
447
  # Need this to call `@buffer_config.disable_chunk_backup` just as before,
423
448
  # since some plugins may use this option in this way.
@@ -802,9 +827,17 @@ module Fluent
802
827
  # ${tag}, ${tag[0]}, ${tag[1]}, ... , ${tag[-2]}, ${tag[-1]}
803
828
  if @chunk_key_tag
804
829
  if str.include?('${tag}')
830
+ if metadata.tag.match?(INVALID_PATH_COMPONENT_PATTERN)
831
+ raise Fluent::UnrecoverableError, "Invalid path component detected in tag: #{metadata.tag}"
832
+ end
833
+
805
834
  rvalue = rvalue.gsub('${tag}', metadata.tag)
806
835
  end
807
836
  if CHUNK_TAG_PLACEHOLDER_PATTERN.match?(str)
837
+ if metadata.tag.match?(INVALID_PATH_COMPONENT_PATTERN)
838
+ raise Fluent::UnrecoverableError, "Invalid path component detected in tag: #{metadata.tag}"
839
+ end
840
+
808
841
  hash = {}
809
842
  tag_parts = metadata.tag.split('.')
810
843
  tag_parts.each_with_index do |part, i|
@@ -835,10 +868,21 @@ module Fluent
835
868
  end
836
869
 
837
870
  rvalue = rvalue.gsub(CHUNK_KEY_PLACEHOLDER_PATTERN) do |matched|
838
- hash.fetch(matched) do
871
+ replace = hash.fetch(matched) do
839
872
  log.warn "chunk key placeholder '#{matched[2..-2]}' not replaced. template:#{str}"
840
873
  ''
841
874
  end
875
+ if replace.to_s.match?(INVALID_PATH_COMPONENT_PATTERN)
876
+ raise Fluent::UnrecoverableError, "Invalid path component detected in #{matched}: #{replace}"
877
+ end
878
+
879
+ replace
880
+ end
881
+ # Check if the number of parent directory components (../) has increased due to variable substitution
882
+ if rvalue.match?(PARENT_DIRECTORY_PATTERN)
883
+ if rvalue.scan(PARENT_DIRECTORY_PATTERN).size > str.scan(PARENT_DIRECTORY_PATTERN).size
884
+ raise Fluent::UnrecoverableError, "Invalid path component detected, replaced to: #{rvalue}"
885
+ end
842
886
  end
843
887
  end
844
888
 
@@ -47,6 +47,11 @@ module Fluent
47
47
 
48
48
  def parse(text, &block)
49
49
  values = CSV.parse_line(text, col_sep: @delimiter)
50
+ unless values
51
+ yield nil, nil
52
+ return
53
+ end
54
+
50
55
  r = Hash[@keys.zip(values)]
51
56
  time, record = convert_values(parse_time(r), r)
52
57
  yield time, record
@@ -85,7 +85,7 @@ module Fluent
85
85
  if File.exist?(@path)
86
86
  raise Fluent::ConfigError, "Plugin storage path '#{@path}' is not readable/writable" unless File.readable?(@path) && File.writable?(@path)
87
87
  begin
88
- data = File.open(@path, 'r:utf-8') { |io| io.read }
88
+ data = File.open(@path, 'r:utf-8:utf-8') { |io| io.read }
89
89
  if data.empty?
90
90
  log.warn "detect empty plugin storage file during startup. Ignored: #{@path}"
91
91
  return
@@ -113,7 +113,7 @@ module Fluent
113
113
  return if @on_memory
114
114
  return unless File.exist?(@path)
115
115
  begin
116
- json_string = File.open(@path, 'r:utf-8'){ |io| io.read }
116
+ json_string = File.open(@path, 'r:utf-8:utf-8'){ |io| io.read }
117
117
  json = JSON.parse(json_string)
118
118
  unless json.is_a?(Hash)
119
119
  log.error "broken content for plugin storage (Hash required: ignored)", type: json.class
@@ -814,6 +814,20 @@ module Fluent
814
814
  @conf += additional_conf
815
815
  end
816
816
 
817
+ if Fluent.windows?
818
+ @conf.elements.each do |element|
819
+ next unless element.name == 'source'
820
+ if element['@type'] == 'tail' && element['pos_file']
821
+ $log.warn("Recommend adding #{File.dirname(element['pos_file'])} to the exclusion path of your antivirus software on Windows")
822
+ else
823
+ storage = element.elements.find { |v| v.name == 'storage' and v['@type'] == 'local' }
824
+ if storage
825
+ $log.warn("Recommend adding #{File.dirname(storage['path'])} to the exclusion path of your antivirus software on Windows")
826
+ end
827
+ end
828
+ end
829
+ end
830
+
817
831
  @libs.each do |lib|
818
832
  require lib
819
833
  end
@@ -70,7 +70,12 @@ module Fluent
70
70
  num_waits.times { sleep 0.05 }
71
71
  return yield
72
72
  ensure
73
+ @instance.stop
74
+ @instance.before_shutdown
73
75
  @instance.shutdown
76
+ @instance.after_shutdown
77
+ @instance.close
78
+ @instance.terminate
74
79
  end
75
80
  end
76
81
  end
@@ -16,6 +16,6 @@
16
16
 
17
17
  module Fluent
18
18
 
19
- VERSION = '1.19.2'
19
+ VERSION = '1.19.3'
20
20
 
21
21
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluentd
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.19.2
4
+ version: 1.19.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sadayuki Furuhashi
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-25 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: bundler
@@ -531,7 +532,6 @@ executables:
531
532
  extensions: []
532
533
  extra_rdoc_files: []
533
534
  files:
534
- - ".deepsource.toml"
535
535
  - ".rubocop.yml"
536
536
  - ADOPTERS.md
537
537
  - AUTHORS
@@ -691,6 +691,7 @@ files:
691
691
  - lib/fluent/plugin/buffer/memory_chunk.rb
692
692
  - lib/fluent/plugin/compressable.rb
693
693
  - lib/fluent/plugin/exec_util.rb
694
+ - lib/fluent/plugin/extractor.rb
694
695
  - lib/fluent/plugin/file_util.rb
695
696
  - lib/fluent/plugin/filter.rb
696
697
  - lib/fluent/plugin/filter_grep.rb
@@ -869,6 +870,7 @@ metadata:
869
870
  source_code_uri: https://github.com/fluent/fluentd
870
871
  changelog_uri: https://github.com/fluent/fluentd/blob/master/CHANGELOG.md
871
872
  bug_tracker_uri: https://github.com/fluent/fluentd/issues
873
+ post_install_message:
872
874
  rdoc_options: []
873
875
  require_paths:
874
876
  - lib
@@ -883,7 +885,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
883
885
  - !ruby/object:Gem::Version
884
886
  version: '0'
885
887
  requirements: []
886
- rubygems_version: 3.6.9
888
+ rubygems_version: 3.5.22
889
+ signing_key:
887
890
  specification_version: 4
888
891
  summary: Fluentd event collector
889
892
  test_files: []
data/.deepsource.toml DELETED
@@ -1,13 +0,0 @@
1
- version = 1
2
-
3
- test_patterns = ["test/**/test_*.rb"]
4
-
5
- exclude_patterns = [
6
- "bin/**",
7
- "docs/**",
8
- "example/**"
9
- ]
10
-
11
- [[analyzers]]
12
- name = "ruby"
13
- enabled = true