fluentd 1.19.3 → 1.20.0

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +46 -0
  3. data/CHANGELOG.md +120 -0
  4. data/GithubWorkflow.md +1 -1
  5. data/MAINTAINERS.md +2 -2
  6. data/README.md +3 -2
  7. data/Rakefile +2 -1
  8. data/SECURITY.md +26 -1
  9. data/lib/fluent/command/cat.rb +3 -3
  10. data/lib/fluent/compat/exec_util.rb +20 -5
  11. data/lib/fluent/config/basic_parser.rb +0 -1
  12. data/lib/fluent/config/literal_parser.rb +22 -10
  13. data/lib/fluent/config/types.rb +10 -2
  14. data/lib/fluent/config/yaml_parser/loader.rb +12 -1
  15. data/lib/fluent/config/yaml_parser/parser.rb +14 -13
  16. data/lib/fluent/configurable.rb +1 -0
  17. data/lib/fluent/daemon.rb +2 -1
  18. data/lib/fluent/env.rb +1 -0
  19. data/lib/fluent/event.rb +37 -0
  20. data/lib/fluent/load.rb +0 -1
  21. data/lib/fluent/log.rb +2 -2
  22. data/lib/fluent/plugin/base.rb +7 -2
  23. data/lib/fluent/plugin/buf_file.rb +3 -3
  24. data/lib/fluent/plugin/buf_file_single.rb +6 -2
  25. data/lib/fluent/plugin/buffer/chunk.rb +14 -6
  26. data/lib/fluent/plugin/buffer/file_chunk.rb +28 -13
  27. data/lib/fluent/plugin/buffer/file_single_chunk.rb +28 -13
  28. data/lib/fluent/plugin/buffer.rb +125 -41
  29. data/lib/fluent/plugin/compressable.rb +2 -2
  30. data/lib/fluent/plugin/extractor.rb +17 -6
  31. data/lib/fluent/plugin/filter_record_transformer.rb +2 -1
  32. data/lib/fluent/plugin/formatter_out_file.rb +3 -4
  33. data/lib/fluent/plugin/in_exec.rb +6 -4
  34. data/lib/fluent/plugin/in_forward.rb +39 -29
  35. data/lib/fluent/plugin/in_http.rb +83 -2
  36. data/lib/fluent/plugin/in_monitor_agent.rb +2 -1
  37. data/lib/fluent/plugin/in_object_space.rb +2 -2
  38. data/lib/fluent/plugin/in_sample.rb +2 -1
  39. data/lib/fluent/plugin/in_syslog.rb +72 -6
  40. data/lib/fluent/plugin/in_tail/compatibility.rb +111 -0
  41. data/lib/fluent/plugin/in_tail/dir_watcher.rb +713 -0
  42. data/lib/fluent/plugin/in_tail/group_watch.rb +13 -13
  43. data/lib/fluent/plugin/in_tail/io_handler.rb +391 -0
  44. data/lib/fluent/plugin/in_tail/line_feeder.rb +230 -0
  45. data/lib/fluent/plugin/in_tail/stat_watcher.rb +52 -0
  46. data/lib/fluent/plugin/in_tail/tail_watcher.rb +276 -0
  47. data/lib/fluent/plugin/in_tail/worker_pool.rb +298 -0
  48. data/lib/fluent/plugin/in_tail.rb +324 -815
  49. data/lib/fluent/plugin/in_unix.rb +5 -3
  50. data/lib/fluent/plugin/out_exec_filter.rb +2 -2
  51. data/lib/fluent/plugin/out_file.rb +10 -4
  52. data/lib/fluent/plugin/out_forward/connection_manager.rb +18 -0
  53. data/lib/fluent/plugin/out_forward.rb +21 -2
  54. data/lib/fluent/plugin/out_http.rb +0 -10
  55. data/lib/fluent/plugin/output.rb +81 -31
  56. data/lib/fluent/plugin/parser_csv.rb +7 -2
  57. data/lib/fluent/plugin/parser_json.rb +25 -12
  58. data/lib/fluent/plugin/parser_ltsv.rb +2 -4
  59. data/lib/fluent/plugin/parser_msgpack.rb +7 -6
  60. data/lib/fluent/plugin/parser_syslog.rb +55 -8
  61. data/lib/fluent/plugin/parser_tsv.rb +6 -1
  62. data/lib/fluent/plugin/sd_file.rb +2 -1
  63. data/lib/fluent/plugin/storage_local.rb +2 -2
  64. data/lib/fluent/plugin_helper/http_server/server.rb +29 -13
  65. data/lib/fluent/plugin_helper/http_server.rb +2 -1
  66. data/lib/fluent/root_agent.rb +1 -1
  67. data/lib/fluent/supervisor.rb +32 -6
  68. data/lib/fluent/system_config.rb +5 -2
  69. data/lib/fluent/time.rb +8 -7
  70. data/lib/fluent/version.rb +1 -1
  71. data/templates/new_gem/.gitignore +8 -0
  72. metadata +41 -21
data/lib/fluent/log.rb CHANGED
@@ -221,7 +221,7 @@ module Fluent
221
221
  if wid = get_worker_id(type)
222
222
  r['worker_id'] = wid
223
223
  end
224
- Yajl.dump(r)
224
+ JSON.generate(r)
225
225
  }
226
226
  end
227
227
 
@@ -554,7 +554,7 @@ module Fluent
554
554
  Thread.current[:last_repeated_stacktrace] = CachedLog.new(backtrace, time) if @suppress_repeated_stacktrace
555
555
  end
556
556
 
557
- puts Yajl.dump(r)
557
+ puts JSON.generate(r)
558
558
  end
559
559
 
560
560
  nil
@@ -14,6 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'zlib'
17
18
  require 'fluent/plugin'
18
19
  require 'fluent/configurable'
19
20
  require 'fluent/system_config'
@@ -69,9 +70,13 @@ module Fluent
69
70
  true
70
71
  end
71
72
 
73
+ LOCK_FILE_BUCKETS = 65536
74
+
72
75
  def get_lock_path(name)
73
- name = name.gsub(/[^a-zA-Z0-9]/, "_")
74
- File.join(@fluentd_lock_dir, "fluentd-#{name}.lock")
76
+ # The mapping from a name to a bucket MUST be identical across worker processes.
77
+ # Ruby's String#hash is randomly seeded per process and MUST NOT be used here.
78
+ bucket = Zlib.crc32(name.to_s) % LOCK_FILE_BUCKETS
79
+ File.join(@fluentd_lock_dir, "fluentd-bucket-#{bucket}.lock")
75
80
  end
76
81
 
77
82
  def acquire_worker_lock(name)
@@ -163,7 +163,7 @@ module Fluent
163
163
  end
164
164
 
165
165
  begin
166
- chunk = Fluent::Plugin::Buffer::FileChunk.new(m, path, mode, compress: @compress) # file chunk resumes contents of metadata
166
+ chunk = Fluent::Plugin::Buffer::FileChunk.new(m, path, mode, compress: @compress, decompression_size_limit: @decompression_size_limit) # file chunk resumes contents of metadata
167
167
  rescue Fluent::Plugin::Buffer::FileChunk::FileChunkError => e
168
168
  exist_broken_file = true
169
169
  handle_broken_files(path, mode, e)
@@ -174,7 +174,7 @@ module Fluent
174
174
  when :staged
175
175
  # unstaged chunk created at Buffer#write_step_by_step is identified as the staged chunk here because FileChunk#assume_chunk_state checks only the file name.
176
176
  # https://github.com/fluent/fluentd/blob/9d113029d4550ce576d8825bfa9612aa3e55bff0/lib/fluent/plugin/buffer.rb#L663
177
- # This case can happen when fluentd process is killed by signal or other reasons between creating unstaged chunks and changing them to staged mode in Buffer#write
177
+ # This case can happen when fluentd process is killed by signal or other reasons between creating unstaged chunks and enqueueing them (which renames them) in Buffer#write
178
178
  # these chunks(unstaged chunks) has shared the same metadata
179
179
  # So perform enqueue step again https://github.com/fluent/fluentd/blob/9d113029d4550ce576d8825bfa9612aa3e55bff0/lib/fluent/plugin/buffer.rb#L364
180
180
  if chunk_size_full?(chunk) || stage.key?(chunk.metadata)
@@ -234,7 +234,7 @@ module Fluent
234
234
  raise ArgumentError, "The chunk must be FileChunk, but it was #{chunk.class}."
235
235
  end
236
236
 
237
- backup_dir = File.join(backup_base_dir, 'buffer', safe_owner_id)
237
+ backup_dir = evacuate_dir
238
238
  FileUtils.mkdir_p(backup_dir, mode: system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION) unless Dir.exist?(backup_dir)
239
239
 
240
240
  FileUtils.copy([chunk.path, chunk.meta_path], backup_dir)
@@ -193,7 +193,11 @@ module Fluent
193
193
 
194
194
  case chunk.state
195
195
  when :staged
196
- stage[chunk.metadata] = chunk
196
+ if chunk_size_full?(chunk) || stage.key?(chunk.metadata)
197
+ queue << chunk.enqueued!
198
+ else
199
+ stage[chunk.metadata] = chunk
200
+ end
197
201
  when :queued
198
202
  queue << chunk
199
203
  end
@@ -246,7 +250,7 @@ module Fluent
246
250
  raise ArgumentError, "The chunk must be FileSingleChunk, but it was #{chunk.class}."
247
251
  end
248
252
 
249
- backup_dir = File.join(backup_base_dir, 'buffer', safe_owner_id)
253
+ backup_dir = evacuate_dir
250
254
  FileUtils.mkdir_p(backup_dir, mode: system_config.dir_permission || Fluent::DEFAULT_DIR_PERMISSION) unless Dir.exist?(backup_dir)
251
255
 
252
256
  FileUtils.copy(chunk.path, backup_dir)
@@ -220,9 +220,13 @@ module Fluent
220
220
  Tempfile.new('decompressed-data')
221
221
  end
222
222
  output_io.binmode if output_io.is_a?(Tempfile)
223
- decompress(input_io: chunk_io, output_io: output_io)
224
- output_io.seek(0, IO::SEEK_SET)
225
- yield output_io
223
+ begin
224
+ decompress(input_io: chunk_io, output_io: output_io)
225
+ output_io.seek(0, IO::SEEK_SET)
226
+ yield output_io
227
+ ensure
228
+ output_io.close! if output_io.is_a?(Tempfile)
229
+ end
226
230
  end
227
231
  end
228
232
  end
@@ -274,9 +278,13 @@ module Fluent
274
278
  Tempfile.new('decompressed-data')
275
279
  end
276
280
  output_io.binmode if output_io.is_a?(Tempfile)
277
- decompress(input_io: chunk_io, output_io: output_io, type: :zstd)
278
- output_io.seek(0, IO::SEEK_SET)
279
- yield output_io
281
+ begin
282
+ decompress(input_io: chunk_io, output_io: output_io, type: :zstd)
283
+ output_io.seek(0, IO::SEEK_SET)
284
+ yield output_io
285
+ ensure
286
+ output_io.close! if output_io.is_a?(Tempfile)
287
+ end
280
288
  end
281
289
  end
282
290
  end
@@ -100,8 +100,14 @@ module Fluent
100
100
  end
101
101
 
102
102
  def enqueued!
103
- return unless self.staged?
103
+ return self unless self.writable?
104
104
 
105
+ super
106
+ rename_to_queued_path
107
+ self
108
+ end
109
+
110
+ def rename_to_queued_path
105
111
  new_chunk_path = self.class.generate_queued_chunk_path(@path, @unique_id)
106
112
  new_meta_path = new_chunk_path + '.meta'
107
113
 
@@ -139,9 +145,8 @@ module Fluent
139
145
 
140
146
  @path = new_chunk_path
141
147
  @meta_path = new_meta_path
142
-
143
- super
144
148
  end
149
+ private :rename_to_queued_path
145
150
 
146
151
  def close
147
152
  super
@@ -168,9 +173,11 @@ module Fluent
168
173
 
169
174
  def open(**kwargs, &block)
170
175
  @chunk.seek(0, IO::SEEK_SET)
171
- val = yield @chunk
172
- @chunk.seek(0, IO::SEEK_END) if self.staged?
173
- val
176
+ begin
177
+ yield @chunk
178
+ ensure
179
+ @chunk.seek(0, IO::SEEK_END) if self.staged?
180
+ end
174
181
  end
175
182
 
176
183
  def self.assume_chunk_state(path)
@@ -264,19 +271,27 @@ module Fluent
264
271
 
265
272
  def file_rename(file, old_path, new_path, callback=nil)
266
273
  pos = file.pos
274
+ setup = ->(f) {
275
+ f.set_encoding(Encoding::ASCII_8BIT)
276
+ f.sync = true
277
+ f.binmode
278
+ f.pos = pos
279
+ callback.call(f) if callback
280
+ }
267
281
  if Fluent.windows?
268
282
  file.close
269
- File.rename(old_path, new_path)
270
- file = File.open(new_path, 'rb', @permission)
283
+ renamed = false
284
+ begin
285
+ File.rename(old_path, new_path)
286
+ renamed = true
287
+ ensure
288
+ setup.call(File.open(renamed ? new_path : old_path, 'rb', @permission))
289
+ end
271
290
  else
272
291
  File.rename(old_path, new_path)
273
292
  file.reopen(new_path, 'rb')
293
+ setup.call(file)
274
294
  end
275
- file.set_encoding(Encoding::ASCII_8BIT)
276
- file.sync = true
277
- file.binmode
278
- file.pos = pos
279
- callback.call(file) if callback
280
295
  end
281
296
 
282
297
  def create_new_chunk(path, perm)
@@ -93,8 +93,14 @@ module Fluent
93
93
  end
94
94
 
95
95
  def enqueued!
96
- return unless self.staged?
96
+ return self unless self.writable?
97
97
 
98
+ super
99
+ rename_to_queued_path
100
+ self
101
+ end
102
+
103
+ def rename_to_queued_path
98
104
  new_chunk_path = self.class.generate_queued_chunk_path(@path, @unique_id)
99
105
 
100
106
  begin
@@ -114,9 +120,8 @@ module Fluent
114
120
  end
115
121
 
116
122
  @path = new_chunk_path
117
-
118
- super
119
123
  end
124
+ private :rename_to_queued_path
120
125
 
121
126
  def close
122
127
  super
@@ -141,9 +146,11 @@ module Fluent
141
146
 
142
147
  def open(**kwargs, &block)
143
148
  @chunk.seek(0, IO::SEEK_SET)
144
- val = yield @chunk
145
- @chunk.seek(0, IO::SEEK_END) if self.staged?
146
- val
149
+ begin
150
+ yield @chunk
151
+ ensure
152
+ @chunk.seek(0, IO::SEEK_END) if self.staged?
153
+ end
147
154
  end
148
155
 
149
156
  def self.assume_chunk_state(path)
@@ -223,19 +230,27 @@ module Fluent
223
230
 
224
231
  def file_rename(file, old_path, new_path, callback = nil)
225
232
  pos = file.pos
233
+ setup = ->(f) {
234
+ f.set_encoding(Encoding::ASCII_8BIT)
235
+ f.sync = true
236
+ f.binmode
237
+ f.pos = pos
238
+ callback.call(f) if callback
239
+ }
226
240
  if Fluent.windows?
227
241
  file.close
228
- File.rename(old_path, new_path)
229
- file = File.open(new_path, 'rb', @permission)
242
+ renamed = false
243
+ begin
244
+ File.rename(old_path, new_path)
245
+ renamed = true
246
+ ensure
247
+ setup.call(File.open(renamed ? new_path : old_path, 'rb', @permission))
248
+ end
230
249
  else
231
250
  File.rename(old_path, new_path)
232
251
  file.reopen(new_path, 'rb')
252
+ setup.call(file)
233
253
  end
234
- file.set_encoding(Encoding::ASCII_8BIT)
235
- file.sync = true
236
- file.binmode
237
- file.pos = pos
238
- callback.call(file) if callback
239
254
  end
240
255
 
241
256
  ESCAPE_REGEXP = /[^-_.a-zA-Z0-9]/n
@@ -72,6 +72,9 @@ module Fluent
72
72
  desc 'If true, chunks are thrown away when unrecoverable error happens'
73
73
  config_param :disable_chunk_backup, :bool, default: false
74
74
 
75
+ desc 'The approximate size limit of the evacuated chunk files in the evacuation directory. When the retry limit is reached, queued chunks that fit within the remaining capacity are evacuated, and the others are purged without being evacuated. The total size may exceed this limit due to chunk metadata or concurrent evacuation by multiple workers. Set to 0 to disable evacuation. nil (the default) means no limit.'
76
+ config_param :evacuate_limit_size, :size, default: nil
77
+
75
78
  Metadata = Struct.new(:timekey, :tag, :variables, :seq) do
76
79
  def initialize(timekey, tag, variables)
77
80
  super(timekey, tag, variables, 0)
@@ -227,6 +230,20 @@ module Fluent
227
230
  unless @queue_limit_length.nil?
228
231
  @total_limit_size = @chunk_limit_size * @queue_limit_length
229
232
  end
233
+ if @evacuate_limit_size
234
+ raise Fluent::ConfigError, "evacuate_limit_size must be 0 or larger" if @evacuate_limit_size < 0
235
+ if !persistent?
236
+ log.warn "evacuate_limit_size is ignored because this buffer does not evacuate chunks"
237
+ @evacuate_limit_size = nil
238
+ elsif @evacuate_limit_size > 0
239
+ unless owner.plugin_id_configured?
240
+ log.warn "evacuate_limit_size should be used with @id, because the evacuation directory without @id can change when the configuration is changed or reloaded"
241
+ end
242
+ if @evacuate_limit_size < @chunk_limit_size
243
+ log.warn "evacuate_limit_size is smaller than chunk_limit_size, so full chunks may not be evacuated", evacuate_limit_size: @evacuate_limit_size, chunk_limit_size: @chunk_limit_size
244
+ end
245
+ end
246
+ end
230
247
  @stage_length_metrics = metrics_create(namespace: "fluentd", subsystem: "buffer", name: "stage_length",
231
248
  help_text: 'Length of stage buffers', prefer_gauge: true)
232
249
  @stage_length_metrics.set(0)
@@ -383,6 +400,12 @@ module Fluent
383
400
  if enqueue || first_chunk.unstaged? || chunk_size_full?(first_chunk)
384
401
  chunks_to_enqueue << first_chunk
385
402
  end
403
+ if (bytesize = staged_bytesizes_by_chunk[first_chunk])
404
+ # Account while the chunk lock is still held so enqueue_chunk
405
+ # cannot remove the chunk and subtract its bytes first.
406
+ @stage_size_metrics.add(bytesize)
407
+ log.on_trace { log.trace { "chunk #{first_chunk.path} size_added: #{bytesize} new_size: #{first_chunk.bytesize}" } }
408
+ end
386
409
  first_chunk.mon_exit
387
410
  rescue
388
411
  operated_chunks.unshift(first_chunk)
@@ -397,6 +420,10 @@ module Fluent
397
420
  if enqueue || chunk.unstaged? || chunk_size_full?(chunk)
398
421
  chunks_to_enqueue << chunk
399
422
  end
423
+ if (bytesize = staged_bytesizes_by_chunk[chunk])
424
+ @stage_size_metrics.add(bytesize)
425
+ log.on_trace { log.trace { "chunk #{chunk.path} size_added: #{bytesize} new_size: #{chunk.bytesize}" } }
426
+ end
400
427
  chunk.mon_exit
401
428
  rescue => e
402
429
  chunk.rollback
@@ -405,44 +432,46 @@ module Fluent
405
432
  end
406
433
  end
407
434
 
408
- # All locks about chunks are released.
409
-
410
- #
411
- # Now update the stage, stage_size with proper locking
412
- # FIX FOR stage_size miscomputation - https://github.com/fluent/fluentd/issues/2712
413
- #
414
- staged_bytesizes_by_chunk.each do |chunk, bytesize|
415
- chunk.synchronize do
416
- synchronize { @stage_size_metrics.add(bytesize) }
417
- log.on_trace { log.trace { "chunk #{chunk.path} size_added: #{bytesize} new_size: #{chunk.bytesize}" } }
418
- end
419
- end
435
+ # All locks about chunks are released. Stage-size accounting for the
436
+ # staged chunks happened inside each chunk monitor above, so a
437
+ # concurrent enqueue_chunk cannot observe a transiently negative
438
+ # stage_size (issue #2712 race).
420
439
 
440
+ enqueue_errors = []
421
441
  chunks_to_enqueue.each do |c|
422
- if c.staged? && (enqueue || chunk_size_full?(c))
423
- m = c.metadata
424
- enqueue_chunk(m)
425
- if unstaged_chunks[m] && !unstaged_chunks[m].empty?
426
- u = unstaged_chunks[m].pop
427
- u.synchronize do
428
- if u.unstaged? && !chunk_size_full?(u)
429
- # `u.metadata.seq` and `m.seq` can be different but Buffer#enqueue_chunk expect them to be the same value
430
- u.metadata.seq = 0
431
- synchronize {
432
- @stage[m] = u.staged!
433
- @stage_size_metrics.add(u.bytesize)
434
- }
442
+ begin
443
+ if c.staged? && (enqueue || chunk_size_full?(c))
444
+ m = c.metadata
445
+ enqueue_chunk(m)
446
+ if unstaged_chunks[m] && !unstaged_chunks[m].empty?
447
+ u = unstaged_chunks[m].pop
448
+ u.synchronize do
449
+ if u.unstaged? && !chunk_size_full?(u)
450
+ # `u.metadata.seq` and `m.seq` can be different but Buffer#enqueue_chunk expect them to be the same value
451
+ u.metadata.seq = 0
452
+ synchronize {
453
+ @stage[m] = u.staged!
454
+ @stage_size_metrics.add(u.bytesize)
455
+ }
456
+ end
435
457
  end
436
458
  end
459
+ elsif c.unstaged?
460
+ enqueue_unstaged_chunk(c)
461
+ else
462
+ # previously staged chunk is already enqueued, closed or purged.
463
+ # no problem.
437
464
  end
438
- elsif c.unstaged?
439
- enqueue_unstaged_chunk(c)
440
- else
441
- # previously staged chunk is already enqueued, closed or purged.
442
- # no problem.
465
+ rescue => e
466
+ enqueue_errors << e
443
467
  end
444
468
  end
445
469
 
470
+ enqueue_errors.each do |e|
471
+ log.warn "error occurs in enqueueing a chunk", error: e
472
+ log.warn_backtrace e.backtrace
473
+ end
474
+
446
475
  operated_chunks.clear if errors.empty?
447
476
 
448
477
  if errors.size > 0
@@ -492,6 +521,9 @@ module Fluent
492
521
 
493
522
  chunk.synchronize do
494
523
  synchronize do
524
+ bytesize = chunk.bytesize
525
+ @stage_size_metrics.sub(bytesize)
526
+ @queue_size_metrics.add(bytesize)
495
527
  if chunk.empty?
496
528
  chunk.close
497
529
  else
@@ -500,9 +532,6 @@ module Fluent
500
532
  @queued_num[metadata] = @queued_num.fetch(metadata, 0) + 1
501
533
  chunk.enqueued!
502
534
  end
503
- bytesize = chunk.bytesize
504
- @stage_size_metrics.sub(bytesize)
505
- @queue_size_metrics.add(bytesize)
506
535
  end
507
536
  end
508
537
  nil
@@ -517,9 +546,9 @@ module Fluent
517
546
  metadata.seq = 0 # metadata.seq should be 0 for counting @queued_num
518
547
  @queue << chunk
519
548
  @queued_num[metadata] = @queued_num.fetch(metadata, 0) + 1
549
+ @queue_size_metrics.add(chunk.bytesize)
520
550
  chunk.enqueued!
521
551
  end
522
- @queue_size_metrics.add(chunk.bytesize)
523
552
  end
524
553
  end
525
554
 
@@ -601,13 +630,19 @@ module Fluent
601
630
  metadata = chunk.metadata
602
631
  log.on_trace { log.trace "purging a chunk", instance: self.object_id, chunk_id: dump_unique_id_hex(chunk_id), metadata: metadata }
603
632
 
633
+ bytesize = chunk.bytesize
604
634
  begin
605
- bytesize = chunk.bytesize
606
635
  chunk.purge
607
- @queue_size_metrics.sub(bytesize)
608
636
  rescue => e
609
637
  log.error "failed to purge buffer chunk", chunk_id: dump_unique_id_hex(chunk_id), error_class: e.class, error: e
610
638
  log.error_backtrace
639
+ ensure
640
+ # Always release the queued byte counter, even when purge raises
641
+ # (e.g. unlink/close failure on the buffer path). Otherwise
642
+ # @queue_size is never decremented for a dequeued chunk that is
643
+ # gone from both @queue and @dequeued, so the leak ratchets toward
644
+ # total_limit_size and storable? becomes permanently false (#5468).
645
+ @queue_size_metrics.sub(bytesize)
611
646
  end
612
647
 
613
648
  @dequeued_num[chunk.metadata] -= 1
@@ -624,12 +659,27 @@ module Fluent
624
659
  def clear_queue!
625
660
  log.on_trace { log.trace "clearing queue", instance: self.object_id }
626
661
 
662
+ evacuate_budget = nil
663
+ skipped_num = 0
664
+ skipped_bytesize = 0
665
+
627
666
  synchronize do
628
667
  until @queue.empty?
629
668
  begin
630
669
  q = @queue.shift
631
- evacuate_chunk(q)
632
- log.trace("purging a chunk in queue"){ {id: dump_unique_id_hex(chunk.unique_id), bytesize: chunk.bytesize, size: chunk.size} }
670
+ if @evacuate_limit_size.nil?
671
+ evacuate_chunk(q)
672
+ elsif @evacuate_limit_size > 0
673
+ evacuate_budget ||= remaining_evacuate_size
674
+ if q.bytesize <= evacuate_budget
675
+ evacuate_chunk(q)
676
+ evacuate_budget -= q.bytesize
677
+ else
678
+ skipped_num += 1
679
+ skipped_bytesize += q.bytesize
680
+ end
681
+ end
682
+ log.trace("purging a chunk in queue"){ {id: dump_unique_id_hex(q.unique_id), bytesize: q.bytesize, size: q.size} }
633
683
  q.purge
634
684
  rescue => e
635
685
  log.error "unexpected error while clearing buffer queue", error_class: e.class, error: e
@@ -638,6 +688,10 @@ module Fluent
638
688
  end
639
689
  @queue_size_metrics.set(0)
640
690
  end
691
+
692
+ if skipped_num > 0
693
+ log.warn "some chunks are purged without being evacuated to stay within evacuate_limit_size.", evacuate_limit_size: @evacuate_limit_size, chunks: skipped_num, bytesize: skipped_bytesize
694
+ end
641
695
  end
642
696
 
643
697
  def evacuate_chunk(chunk)
@@ -850,7 +904,12 @@ module Fluent
850
904
  # As already processed content is kept after rollback, then unstaged chunk should be queued.
851
905
  # After that, re-process current split again.
852
906
  # New chunk should be allocated, to do it, modify @stage and so on.
853
- synchronize { @stage.delete(modified_metadata) }
907
+ synchronize do
908
+ @stage.delete(modified_metadata)
909
+ # Subtract this chunk's already-counted staged bytes here;
910
+ # otherwise @stage_size leaks upward over the buffer's lifetime.
911
+ @stage_size_metrics.sub(original_bytesize) if chunk.staged?
912
+ end
854
913
  staged_chunk_used = false
855
914
  chunk.unstaged!
856
915
  break
@@ -912,8 +971,15 @@ module Fluent
912
971
  ]
913
972
 
914
973
  def statistics
915
- stage_size, queue_size = @stage_size_metrics.get, @queue_size_metrics.get
916
- buffer_space = 1.0 - ((stage_size + queue_size * 1.0) / @total_limit_size)
974
+ # Export-only clamp: internal gauges may go transiently negative during
975
+ # the deferred stage_size add vs enqueue_chunk sub race (#5303, #2712).
976
+ # Clamping the gauge store itself would turn that into a permanent
977
+ # over-count and break Buffer#storable? -- keep raw gauge semantics.
978
+ stage_size = [@stage_size_metrics.get, 0].max
979
+ queue_size = [@queue_size_metrics.get, 0].max
980
+ denom = @total_limit_size.to_f
981
+ # denom > 0 already excludes 0/0 NaN; stage/queue are floored above.
982
+ buffer_space = denom > 0.0 ? (1.0 - (stage_size + queue_size).to_f / denom).clamp(0.0, 1.0) : 0.0
917
983
  @stage_length_metrics.set(@stage.size)
918
984
  @queue_length_metrics.set(@queue.size)
919
985
  @available_buffer_space_ratios_metrics.set(buffer_space * 100)
@@ -974,6 +1040,24 @@ module Fluent
974
1040
  def backup_base_dir
975
1041
  system_config.root_dir || DEFAULT_BACKUP_DIR
976
1042
  end
1043
+
1044
+ def evacuate_dir
1045
+ File.join(backup_base_dir, 'buffer', safe_owner_id)
1046
+ end
1047
+
1048
+ def evacuated_bytesize
1049
+ dir = evacuate_dir
1050
+ Dir.children(dir, encoding: dir.encoding).sum { |name| File.size?(File.join(dir, name)).to_i }
1051
+ rescue Errno::ENOENT
1052
+ 0
1053
+ end
1054
+
1055
+ def remaining_evacuate_size
1056
+ @evacuate_limit_size - evacuated_bytesize
1057
+ rescue => e
1058
+ log.warn "failed to get the size of the evacuated chunk files. queued chunks are purged without being evacuated.", error: e
1059
+ 0
1060
+ end
977
1061
  end
978
1062
  end
979
1063
  end
@@ -75,9 +75,9 @@ module Fluent
75
75
 
76
76
  def io_decompress(input, output, type = :gzip)
77
77
  if type == :gzip
78
- Extractor.io_decompress_gzip(input, output)
78
+ Extractor.io_decompress_gzip(input, output, limit: @decompression_size_limit || DEFAULT_DECOMPRESSION_SIZE_LIMIT)
79
79
  elsif type == :zstd
80
- Extractor.io_decompress_zstd(input, output)
80
+ Extractor.io_decompress_zstd(input, output, limit: @decompression_size_limit || DEFAULT_DECOMPRESSION_SIZE_LIMIT)
81
81
  else
82
82
  raise ArgumentError, "Unknown compression type: #{type}"
83
83
  end
@@ -26,6 +26,9 @@ module Fluent
26
26
 
27
27
  BYTES_TO_READ = 64 * 1024
28
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
29
32
 
30
33
  def self.decompress_gzip(compressed_data, limit:)
31
34
  io = StringIO.new(compressed_data)
@@ -55,8 +58,7 @@ module Fluent
55
58
  reader = Zstd::StreamReader.new(io)
56
59
  out = ''
57
60
  loop do
58
- # Zstd::StreamReader needs to specify the size of the buffer
59
- out << reader.read(BYTES_TO_READ)
61
+ out << reader.read(ZSTD_BYTES_TO_READ)
60
62
  if out.bytesize > limit
61
63
  raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
62
64
  end
@@ -88,10 +90,15 @@ module Fluent
88
90
  out
89
91
  end
90
92
 
91
- def self.io_decompress_gzip(input, output)
93
+ def self.io_decompress_gzip(input, output, limit:)
94
+ size = 0
92
95
  loop do
93
96
  reader = Zlib::GzipReader.new(input)
94
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
95
102
  output.write(chunk)
96
103
  end
97
104
  unused = reader.unused
@@ -105,11 +112,15 @@ module Fluent
105
112
  output
106
113
  end
107
114
 
108
- def self.io_decompress_zstd(input, output)
115
+ def self.io_decompress_zstd(input, output, limit:)
109
116
  reader = Zstd::StreamReader.new(input)
117
+ size = 0
110
118
  loop do
111
- # Zstd::StreamReader needs to specify the size of the buffer
112
- chunk = reader.read(BYTES_TO_READ)
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
113
124
  output.write(chunk)
114
125
  # Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
115
126
  break if input.eof?
@@ -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
@@ -44,10 +44,9 @@ module Fluent
44
44
 
45
45
  def format(tag, time, record)
46
46
  json_str = JSON.generate(record)
47
- header = ''
48
- header << "#{@timef.format(time)}#{@delimiter}" if @output_time
49
- header << "#{tag}#{@delimiter}" if @output_tag
50
- "#{header}#{json_str}#{@newline}"
47
+ time_str = @output_time ? "#{@timef.format(time)}#{@delimiter}" : ''
48
+ tag_str = @output_tag ? "#{tag}#{@delimiter}" : ''
49
+ "#{time_str}#{tag_str}#{json_str}#{@newline}"
51
50
  ensure
52
51
  json_str&.clear
53
52
  end