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
@@ -19,7 +19,7 @@ require 'fluent/plugin/input'
19
19
  require 'fluent/msgpack_factory'
20
20
 
21
21
  require 'cool.io'
22
- require 'yajl'
22
+ require 'json'
23
23
  require 'fileutils'
24
24
  require 'socket'
25
25
 
@@ -158,8 +158,7 @@ module Fluent::Plugin
158
158
  first = data[0]
159
159
  if first == '{'.freeze || first == '['.freeze
160
160
  m = method(:on_read_json)
161
- @parser = Yajl::Parser.new
162
- @parser.on_parse_complete = @on_message
161
+ @parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
163
162
  else
164
163
  m = method(:on_read_msgpack)
165
164
  @parser = Fluent::MessagePackFactory.msgpack_unpacker
@@ -173,6 +172,9 @@ module Fluent::Plugin
173
172
 
174
173
  def on_read_json(data)
175
174
  @parser << data
175
+ while @parser.parse
176
+ @on_message.call(@parser.value)
177
+ end
176
178
  rescue => e
177
179
  @log.error "unexpected error in json payload", error: e.to_s
178
180
  @log.error_backtrace
@@ -17,7 +17,7 @@ require 'fluent/plugin/output'
17
17
  require 'fluent/env'
18
18
  require 'fluent/config/error'
19
19
 
20
- require 'yajl'
20
+ require 'json'
21
21
 
22
22
  module Fluent::Plugin
23
23
  class ExecFilterOutput < Output
@@ -309,7 +309,7 @@ module Fluent::Plugin
309
309
  router.emit(tag, time, record)
310
310
  rescue => e
311
311
  if @suppress_error_log_interval == 0 || Time.now.to_i > @next_log_time
312
- log.error "exec_filter failed to emit", record: Yajl.dump(record), error: e
312
+ log.error "exec_filter failed to emit", record: JSON.generate(record), error: e
313
313
  log.error_backtrace e.backtrace
314
314
  @next_log_time = Time.now.to_i + @suppress_error_log_interval
315
315
  end
@@ -75,9 +75,8 @@ module Fluent::Plugin
75
75
  def metadata(timekey: nil, tag: nil, variables: nil)
76
76
  metadata = super
77
77
 
78
- @latest_metadata ||= new_metadata(timekey: 0)
79
- if metadata.timekey && (metadata.timekey >= @latest_metadata.timekey)
80
- @latest_metadata = metadata
78
+ if newer_timekey?(metadata.timekey)
79
+ synchronize { @latest_timekey = metadata.timekey if newer_timekey?(metadata.timekey) }
81
80
  end
82
81
 
83
82
  metadata
@@ -97,7 +96,8 @@ module Fluent::Plugin
97
96
  # timekey will be appended into that file chunk. On the other side, resumed file chunks might NOT
98
97
  # have timekey, especially in the cases that resumed file chunks are generated by Fluentd v0.12.
99
98
  # These chunks will be enqueued immediately, and will be flushed soon.
100
- if chunk.metadata == @latest_metadata
99
+ # Split chunks (seq > 0) are generated outside the buffer lock, so ln_sf would race between threads.
100
+ if chunk.metadata.timekey && chunk.metadata.seq == 0 && chunk.metadata.timekey == @latest_timekey
101
101
  sym_path = @_output_plugin_for_symlink.extract_placeholders(@_symlink_path, chunk)
102
102
  FileUtils.mkdir_p(File.dirname(sym_path), mode: @_output_plugin_for_symlink.dir_perm)
103
103
  if @_output_plugin_for_symlink.symlink_path_use_relative
@@ -109,6 +109,12 @@ module Fluent::Plugin
109
109
  end
110
110
  chunk
111
111
  end
112
+
113
+ private
114
+
115
+ def newer_timekey?(timekey)
116
+ timekey && (@latest_timekey.nil? || timekey > @latest_timekey)
117
+ end
112
118
  end
113
119
 
114
120
  def configure(conf)
@@ -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)
@@ -119,6 +119,8 @@ module Fluent::Plugin
119
119
  config_param :keepalive, :bool, default: false
120
120
  desc "Expired time of keepalive. Default value is nil, which means to keep connection as long as possible"
121
121
  config_param :keepalive_timeout, :time, default: nil
122
+ desc 'Check the remote connection is still available by sending a keepalive packet if this value is true.'
123
+ config_param :send_keepalive_packet, :bool, default: false
122
124
 
123
125
  config_section :security, required: false, multi: false do
124
126
  desc 'The hostname'
@@ -270,6 +272,10 @@ module Fluent::Plugin
270
272
  log.warn('The value of keepalive_timeout is ignored. if you want to use keepalive, please add `keepalive true` to your conf.')
271
273
  end
272
274
 
275
+ if @send_keepalive_packet && !@keepalive
276
+ raise Fluent::ConfigError, "'send_keepalive_packet' is enabled but 'keepalive' is not. Enable 'keepalive' to use TCP keepalive."
277
+ end
278
+
273
279
  raise Fluent::ConfigError, "ack_response_timeout must be a positive integer" if @ack_response_timeout < 1
274
280
 
275
281
  if @compress == :zstd
@@ -414,6 +420,7 @@ module Fluent::Plugin
414
420
  send_timeout: @send_timeout,
415
421
  recv_timeout: @ack_response_timeout,
416
422
  connect_timeout: @connect_timeout,
423
+ send_keepalive_packet: @send_keepalive_packet,
417
424
  &block
418
425
  )
419
426
  when :tcp
@@ -423,6 +430,7 @@ module Fluent::Plugin
423
430
  send_timeout: @send_timeout,
424
431
  recv_timeout: @ack_response_timeout,
425
432
  connect_timeout: @connect_timeout,
433
+ send_keepalive_packet: @send_keepalive_packet,
426
434
  &block
427
435
  )
428
436
  else
@@ -525,17 +533,28 @@ module Fluent::Plugin
525
533
 
526
534
  def ack_check(select_interval)
527
535
  @ack_handler.collect_response(select_interval) do |chunk_id, node, sock, result|
528
- @connection_manager.close(sock)
529
-
530
536
  case result
531
537
  when AckHandler::Result::SUCCESS
538
+ @connection_manager.close(sock)
532
539
  commit_write(chunk_id)
533
540
  when AckHandler::Result::FAILED
541
+ # The ack timed out or the peer closed the connection. A keepalive
542
+ # socket may still have an in-flight ack for this chunk, so drop it
543
+ # instead of returning it to the reuse pool; otherwise a late ack
544
+ # could later be read against a chunk sent on the reused socket (#4057).
545
+ @connection_manager.discard(sock)
534
546
  node&.disable!
535
547
  rollback_write(chunk_id, update_retry: false) if chunk_id
536
548
  when AckHandler::Result::CHUNKID_UNMATCHED
549
+ # The ack read from this socket belongs to a different chunk than the
550
+ # one tracked for it: the keepalive connection's ack stream is out of
551
+ # sync (e.g. a stale ack left over from before an aggregator restart).
552
+ # Discard the socket so the following acks are not each offset by one,
553
+ # which would otherwise flood the log with mismatch warnings (#4057).
554
+ @connection_manager.discard(sock)
537
555
  rollback_write(chunk_id, update_retry: false)
538
556
  else
557
+ @connection_manager.close(sock)
539
558
  log.warn("BUG: invalid status #{result} #{chunk_id}")
540
559
 
541
560
  if chunk_id
@@ -22,16 +22,6 @@ require 'fluent/tls'
22
22
  require 'fluent/plugin/output'
23
23
  require 'fluent/plugin_helper/socket'
24
24
 
25
- # patch Net::HTTP to support extra_chain_cert which was added in Ruby feature #9758.
26
- # see: https://github.com/ruby/ruby/commit/31af0dafba6d3769d2a39617c0dddedb97883712
27
- unless Net::HTTP::SSL_IVNAMES.include?(:@extra_chain_cert)
28
- class Net::HTTP
29
- SSL_IVNAMES << :@extra_chain_cert
30
- SSL_ATTRIBUTES << :extra_chain_cert
31
- attr_accessor :extra_chain_cert
32
- end
33
- end
34
-
35
25
  module Fluent::Plugin
36
26
  class HTTPOutput < Output
37
27
  Fluent::Plugin.register_output('http', self)
@@ -28,6 +28,7 @@ require 'fluent/unique_id'
28
28
  require 'fluent/clock'
29
29
  require 'fluent/ext_monitor_require'
30
30
 
31
+ require 'json'
31
32
  require 'time'
32
33
 
33
34
  module Fluent
@@ -44,8 +45,21 @@ module Fluent
44
45
  CHUNK_KEY_PLACEHOLDER_PATTERN = /\$\{([-_.@$a-zA-Z0-9]+)\}/
45
46
  CHUNK_TAG_PLACEHOLDER_PATTERN = /\$\{(tag(?:\[-?\d+\])?)\}/
46
47
  CHUNK_ID_PLACEHOLDER_PATTERN = /\$\{chunk_id\}/
47
- INVALID_PATH_COMPONENT_PATTERN = %r{\.\.[/\\]|^[/\\]}
48
- PARENT_DIRECTORY_PATTERN = %r{\.\.[/\\]}
48
+
49
+ # Matches absolute paths, including the POSIX root, Windows drive roots, and UNC paths (e.g., \\host\share).
50
+ # It also accounts for forward slashes used as directory separators in Windows.
51
+ ABSOLUTE_PATH_PATTERN = %r{\A[/\\]}
52
+ # Matches Windows drive specifications.
53
+ # A trailing separator after the colon is not required, because a drive-relative path like C:Windows can still escape a relative path template.
54
+ WINDOWS_DRIVE_PATTERN = %r{\A[a-zA-Z]:}
55
+ # Matches .. only when it acts as an independent path component (e.g., ../etc or foo/..),
56
+ # ignoring dots embedded within strings like app..web or a..b/c.
57
+ # It uses zero-width negative lookarounds to avoid consuming characters.
58
+ # This ensures that adjacent components like /a/../../b are accurately detected and counted without missing any.
59
+ PARENT_DIRECTORY_PATTERN = %r{(?<![^/\\])\.\.(?![^/\\])}
60
+
61
+ INVALID_PATH_COMPONENT_PATTERN =
62
+ Regexp.union(ABSOLUTE_PATH_PATTERN, WINDOWS_DRIVE_PATTERN, PARENT_DIRECTORY_PATTERN)
49
63
 
50
64
  CHUNKING_FIELD_WARN_NUM = 4
51
65
 
@@ -878,12 +892,13 @@ module Fluent
878
892
 
879
893
  replace
880
894
  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
886
- end
895
+ end
896
+
897
+ # Verifies that the number of parent directory traversals (..) has not increased after variable substitution.
898
+ # While each replaced value is validated individually, this step prevents harmless values from concatenating into a dangerous path (e.g., ${tag}${key}/ becoming ../ if both tag and key are .).
899
+ if rvalue.match?(PARENT_DIRECTORY_PATTERN) &&
900
+ rvalue.scan(PARENT_DIRECTORY_PATTERN).size > str.scan(PARENT_DIRECTORY_PATTERN).size
901
+ raise Fluent::UnrecoverableError, "Invalid path component detected, replaced to: #{rvalue}"
887
902
  end
888
903
 
889
904
  if rvalue =~ CHUNK_KEY_PLACEHOLDER_PATTERN
@@ -943,39 +958,47 @@ module Fluent
943
958
  return Struct.new(:timekey, :tag, :variables).new
944
959
  end
945
960
 
961
+ timekey = @chunk_key_time ? calculate_timekey(time) : nil
962
+ @_metadata_cache ||= MetadataCache.new
963
+
946
964
  # timekey is int from epoch, and `timekey - timekey % 60` is assumed to mach with 0s of each minutes.
947
965
  # it's wrong if timezone is configured as one which supports leap second, but it's very rare and
948
966
  # we can ignore it (especially in production systems).
949
967
  if @chunk_keys.empty?
950
- if !@chunk_key_time && !@chunk_key_tag
951
- @buffer.metadata()
952
- elsif @chunk_key_time && @chunk_key_tag
953
- timekey = calculate_timekey(time)
954
- @buffer.metadata(timekey: timekey, tag: tag)
955
- elsif @chunk_key_time
956
- timekey = calculate_timekey(time)
957
- @buffer.metadata(timekey: timekey)
958
- else
959
- @buffer.metadata(tag: tag)
960
- end
968
+ return @_metadata_cache.metadata if @_metadata_cache.cached?(timekey: timekey, tag: tag)
969
+
970
+ meta = if @chunk_key_time && @chunk_key_tag
971
+ @buffer.metadata(timekey: timekey, tag: tag)
972
+ elsif @chunk_key_time
973
+ @buffer.metadata(timekey: timekey)
974
+ elsif @chunk_key_tag
975
+ @buffer.metadata(tag: tag)
976
+ else
977
+ @buffer.metadata()
978
+ end
979
+
980
+ @_metadata_cache.update(timekey: timekey, tag: tag, metadata: meta)
981
+ meta
961
982
  else
962
- timekey = if @chunk_key_time
963
- calculate_timekey(time)
964
- else
965
- nil
966
- end
967
983
  pairs = Hash[@chunk_key_accessors.map { |k, a| [k, a.call(record)] }]
968
- @buffer.metadata(timekey: timekey, tag: (@chunk_key_tag ? tag : nil), variables: pairs)
984
+ return @_metadata_cache.metadata if @_metadata_cache.cached?(timekey: timekey, tag: tag, variables: pairs)
985
+
986
+ meta = @buffer.metadata(timekey: timekey, tag: (@chunk_key_tag ? tag : nil), variables: pairs)
987
+ @_metadata_cache.update(timekey: timekey, tag: tag, variables: pairs, metadata: meta)
988
+ meta
969
989
  end
970
990
  end
971
991
 
972
992
  def calculate_timekey(time)
973
993
  time_int = time.to_i
994
+ return @calculate_timekey_last_timekey if @calculate_timekey_last_time_int == time_int
995
+
996
+ @calculate_timekey_last_time_int = time_int
974
997
  if @timekey_use_utc
975
- (time_int - (time_int % @timekey)).to_i
998
+ @calculate_timekey_last_timekey = (time_int - (time_int % @timekey)).to_i
976
999
  else
977
1000
  offset = @calculate_offset ? @calculate_offset.call(time) : @offset
978
- (time_int - ((time_int + offset)% @timekey)).to_i
1001
+ @calculate_timekey_last_timekey = (time_int - ((time_int + offset) % @timekey)).to_i
979
1002
  end
980
1003
  end
981
1004
 
@@ -1212,7 +1235,7 @@ module Fluent
1212
1235
  end
1213
1236
  end
1214
1237
 
1215
- UNRECOVERABLE_ERRORS = [Fluent::UnrecoverableError, TypeError, ArgumentError, NoMethodError, MessagePack::UnpackError, EncodingError]
1238
+ UNRECOVERABLE_ERRORS = [Fluent::UnrecoverableError, TypeError, ArgumentError, NoMethodError, MessagePack::UnpackError, EncodingError, JSON::GeneratorError]
1216
1239
 
1217
1240
  def try_flush
1218
1241
  chunk = @buffer.dequeue_chunk
@@ -1319,9 +1342,14 @@ module Fluent
1319
1342
  if @buffer.disable_chunk_backup
1320
1343
  log.warn "disable_chunk_backup is true. #{dump_unique_id_hex(chunk.unique_id)} chunk is thrown away"
1321
1344
  else
1322
- @buffer.backup(chunk.unique_id) { |f|
1323
- chunk.write_to(f)
1324
- }
1345
+ begin
1346
+ @buffer.backup(chunk.unique_id) { |f|
1347
+ chunk.write_to(f)
1348
+ }
1349
+ rescue => e
1350
+ log.warn "failed to back up #{dump_unique_id_hex(chunk.unique_id)} chunk. it is thrown away", error: e
1351
+ log.warn_backtrace
1352
+ end
1325
1353
  end
1326
1354
  commit_write(chunk.unique_id, secondary: using_secondary, delayed: delayed_commit)
1327
1355
  end
@@ -1634,5 +1662,27 @@ module Fluent
1634
1662
  { 'output' => stats }
1635
1663
  end
1636
1664
  end
1665
+
1666
+ class MetadataCache
1667
+ attr_reader :metadata
1668
+
1669
+ def initialize
1670
+ @timekey = nil
1671
+ @tag = nil
1672
+ @variables = nil
1673
+ @metadata = nil
1674
+ end
1675
+
1676
+ def cached?(timekey:, tag:, variables: nil)
1677
+ @metadata && @timekey == timekey && @tag == tag && @variables == variables
1678
+ end
1679
+
1680
+ def update(timekey:, tag:, variables: nil, metadata:)
1681
+ @timekey = timekey
1682
+ @tag = tag
1683
+ @variables = variables
1684
+ @metadata = metadata
1685
+ end
1686
+ end
1637
1687
  end
1638
1688
  end
@@ -32,7 +32,7 @@ module Fluent
32
32
 
33
33
  def configure(conf)
34
34
  super
35
-
35
+ @key_num = @keys.length
36
36
 
37
37
  if @parser_engine == :fast
38
38
  @quote_char = '"'
@@ -46,13 +46,18 @@ module Fluent
46
46
  end
47
47
 
48
48
  def parse(text, &block)
49
+ r = {}
50
+ i = 0
49
51
  values = CSV.parse_line(text, col_sep: @delimiter)
50
52
  unless values
51
53
  yield nil, nil
52
54
  return
53
55
  end
54
56
 
55
- r = Hash[@keys.zip(values)]
57
+ while i < @key_num
58
+ r[@keys[i]] = values[i]
59
+ i += 1
60
+ end
56
61
  time, record = convert_values(parse_time(r), r)
57
62
  yield time, record
58
63
  end
@@ -14,11 +14,11 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
+ require 'fluent/env'
17
18
  require 'fluent/plugin/parser'
18
19
  require 'fluent/time'
19
20
  require 'fluent/oj_options'
20
21
 
21
- require 'yajl'
22
22
  require 'json'
23
23
 
24
24
  module Fluent
@@ -28,16 +28,18 @@ module Fluent
28
28
 
29
29
  config_set_default :time_key, 'time'
30
30
  desc 'Set JSON parser'
31
+ # NOTE: Contains yajl for backward compatibility
31
32
  config_param :json_parser, :enum, list: [:oj, :yajl, :json], default: :oj
32
33
 
33
- # The Yajl library defines a default buffer size of 8KiB when parsing
34
- # from IO streams, so maintain this for backwards-compatibility.
35
- # https://www.rubydoc.info/github/brianmario/yajl-ruby/Yajl%2FParser:parse
36
- desc 'Set the buffer size that Yajl will use when parsing streaming input'
34
+ desc 'Set the buffer size that JSON parser will use when parsing streaming input'
37
35
  config_param :stream_buffer_size, :integer, default: 8192
38
36
 
39
37
  config_set_default :time_type, :float
40
38
 
39
+ # Use a shared proc rather than a per-call lambda so that
40
+ # configure_json_parser returns the same object every time.
41
+ JSON_PARSE_PROC = ->(text) { JSON.parse(text, **Fluent::DEFAULT_JSON_PARSE_OPTIONS) }
42
+
41
43
  def configure(conf)
42
44
  if conf.has_key?('time_format')
43
45
  conf['time_type'] ||= 'string'
@@ -54,8 +56,8 @@ module Fluent
54
56
 
55
57
  log&.info "Oj is not installed, and failing back to JSON for json parser"
56
58
  configure_json_parser(:json)
57
- when :json then [JSON.method(:parse), JSON::ParserError]
58
- when :yajl then [Yajl.method(:load), Yajl::ParseError]
59
+ when :yajl, :json # NOTE: Fallback yajl to json for backward compatibility
60
+ [JSON_PARSE_PROC, JSON::ParserError]
59
61
  else
60
62
  raise "BUG: unknown json parser specified: #{name}"
61
63
  end
@@ -94,11 +96,22 @@ module Fluent
94
96
  end
95
97
 
96
98
  def parse_io(io, &block)
97
- y = Yajl::Parser.new
98
- y.on_parse_complete = ->(record){
99
- block.call(parse_time(record), record)
100
- }
101
- y.parse(io, @stream_buffer_size)
99
+ parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
100
+ begin
101
+ chunk = +"".b
102
+ while io.readpartial(@stream_buffer_size, chunk)
103
+ parser << chunk
104
+ while parser.parse
105
+ record = parser.value
106
+ block.call(parse_time(record), record)
107
+ end
108
+ end
109
+ rescue EOFError
110
+ unless parser.empty?
111
+ log&.warn "JSON stream ended in the middle of a document; " \
112
+ "discarding incomplete data"
113
+ end
114
+ end
102
115
  end
103
116
  end
104
117
  end
@@ -38,10 +38,8 @@ module Fluent
38
38
  def parse(text)
39
39
  r = {}
40
40
  text.split(@delimiter).each do |pair|
41
- if pair.include? @label_delimiter
42
- key, value = pair.split(@label_delimiter, 2)
43
- r[key] = value
44
- end
41
+ key, value = pair.split(@label_delimiter, 2)
42
+ r[key] = value if value
45
43
  end
46
44
  time, record = convert_values(parse_time(r), r)
47
45
  yield time, record
@@ -22,21 +22,22 @@ module Fluent
22
22
  class MessagePackParser < Parser
23
23
  Plugin.register_parser('msgpack', self)
24
24
 
25
- def configure(conf)
26
- super
27
- @unpacker = Fluent::MessagePackFactory.engine_factory.unpacker
28
- end
29
-
30
25
  def parser_type
31
26
  :binary
32
27
  end
33
28
 
34
29
  def parse(data, &block)
30
+ Fluent::MessagePackFactory.engine_factory.unpacker.feed_each(data) do |obj|
31
+ parse_unpacked_data(obj, &block)
32
+ end
33
+ end
34
+
35
+ def parse_partial_data(data, &block)
36
+ @unpacker ||= Fluent::MessagePackFactory.engine_factory.unpacker
35
37
  @unpacker.feed_each(data) do |obj|
36
38
  parse_unpacked_data(obj, &block)
37
39
  end
38
40
  end
39
- alias parse_partial_data parse
40
41
 
41
42
  def parse_io(io, &block)
42
43
  u = Fluent::MessagePackFactory.engine_factory.unpacker(io)
@@ -44,7 +44,7 @@ module Fluent
44
44
  RFC3164_CAPTURES = RFC3164_WITHOUT_TIME_AND_PRI_REGEXP.names.freeze
45
45
  RFC3164_PRI_REGEXP = /^<(?<pri>[0-9]{1,3})>/
46
46
 
47
- RFC5424_WITHOUT_TIME_AND_PRI_REGEXP = /(?<host>[!-~]{1,255}) (?<ident>[!-~]{1,48}) (?<pid>[!-~]{1,128}) (?<msgid>[!-~]{1,32}) (?<extradata>(?:\-|(?:\[.*?(?<!\\)\])+))(?: (?<message>.+))?\z/m
47
+ RFC5424_WITHOUT_TIME_AND_PRI_REGEXP = /(?<host>[!-~]{1,255}) (?<ident>[!-~]{1,48}) (?<pid>[!-~]{1,128}) (?<msgid>[!-~]{1,32}) (?<extradata>(?:\-|(?:\[.*?(?<!\\)\])))(?: (?<message>.+))?\z/m
48
48
  RFC5424_CAPTURES = RFC5424_WITHOUT_TIME_AND_PRI_REGEXP.names.freeze
49
49
  RFC5424_PRI_REGEXP = /^<(?<pri>\d{1,3})>\d\d{0,2}\s/
50
50
 
@@ -71,6 +71,7 @@ module Fluent
71
71
  @time_parser_rfc5424 = nil
72
72
  @space_count_rfc3164 = nil
73
73
  @space_count_rfc5424 = nil
74
+ @time_format_starts_with_space_rfc3164 = false
74
75
  @skip_space_count_rfc3164 = false
75
76
  @skip_space_count_rfc5424 = false
76
77
  @time_parser_rfc5424_without_subseconds = nil
@@ -123,10 +124,12 @@ module Fluent
123
124
 
124
125
  def setup_time_parser_3164(time_fmt)
125
126
  @time_parser_rfc3164 = time_parser_create(format: time_fmt)
127
+ @time_format_starts_with_space_rfc3164 = time_fmt.start_with?(SPLIT_CHAR)
126
128
  if ['%b %d %H:%M:%S', '%b %d %H:%M:%S.%N'].include?(time_fmt)
127
129
  @skip_space_count_rfc3164 = true
128
130
  end
129
131
  @space_count_rfc3164 = time_fmt.squeeze(' ').count(' ') + 1
132
+ @space_count_rfc3164 -= 1 if @time_format_starts_with_space_rfc3164
130
133
  end
131
134
 
132
135
  def setup_time_parser_5424(time_fmt)
@@ -162,6 +165,8 @@ module Fluent
162
165
  end
163
166
 
164
167
  SPLIT_CHAR = ' '.freeze
168
+ DOT_CHAR = '.'.freeze
169
+ RFC3164_PRI_DIGITS_REGEXP = /\A[0-9]{1,3}\z/
165
170
 
166
171
  def parse_rfc3164_regex(text, &block)
167
172
  idx = 0
@@ -180,13 +185,26 @@ module Fluent
180
185
 
181
186
  i = idx - 1
182
187
  sq = false
188
+ first_time_field = true
183
189
  @space_count_rfc3164.times do
184
190
  while text[i + 1] == SPLIT_CHAR
191
+ if first_time_field
192
+ unless @time_format_starts_with_space_rfc3164
193
+ idx += 1
194
+ i += 1
195
+ break
196
+ end
197
+ end
185
198
  sq = true
186
199
  i += 1
187
200
  end
188
201
 
202
+ first_time_field = false
189
203
  i = text.index(SPLIT_CHAR, i + 1)
204
+ unless i
205
+ yield nil, nil
206
+ return
207
+ end
190
208
  end
191
209
 
192
210
  time_str = sq ? text.slice(idx, i - idx).squeeze(SPLIT_CHAR) : text.slice(idx, i - idx)
@@ -287,16 +305,30 @@ module Fluent
287
305
  end
288
306
  end
289
307
 
308
+ if text[cursor] == SPLIT_CHAR
309
+ cursor = rfc3164_space_cursor(text, cursor)
310
+ unless cursor
311
+ yield nil, nil
312
+ return
313
+ end
314
+ end
315
+
290
316
  if @skip_space_count_rfc3164
291
317
  # header part
292
318
  time_size = 15 # skip Mmm dd hh:mm:ss
293
- time_end = text[cursor + time_size]
319
+ time_end_index = cursor + time_size
320
+ time_end = text[time_end_index]
321
+
294
322
  if time_end == SPLIT_CHAR
295
323
  time_str = text.slice(cursor, time_size)
296
324
  cursor += 16 # time + ' '
297
- elsif time_end == '.'.freeze
325
+ elsif time_end == DOT_CHAR
298
326
  # support subsecond time
299
- i = text.index(SPLIT_CHAR, time_size)
327
+ i = text.index(SPLIT_CHAR, time_end_index)
328
+ unless i
329
+ yield nil, nil
330
+ return
331
+ end
300
332
  time_str = text.slice(cursor, i - cursor)
301
333
  cursor = i + 1
302
334
  else
@@ -312,14 +344,18 @@ module Fluent
312
344
  i += 1
313
345
  end
314
346
  i = text.index(SPLIT_CHAR, i + 1)
347
+ unless i
348
+ yield nil, nil
349
+ return
350
+ end
315
351
  end
316
352
 
317
- time_str = sq ? text.slice(idx, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
353
+ time_str = sq ? text.slice(cursor, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
318
354
  cursor = i + 1
319
355
  end
320
356
 
321
357
  i = text.index(SPLIT_CHAR, cursor)
322
- if i.nil?
358
+ unless i
323
359
  yield nil, nil
324
360
  return
325
361
  end
@@ -363,12 +399,23 @@ module Fluent
363
399
  msg.chomp!
364
400
  record['message'] = msg
365
401
 
366
- time = @time_parser_rfc3164.parse(time_str)
402
+ begin
403
+ time = @time_parser_rfc3164.parse(time_str)
404
+ rescue Fluent::TimeParser::TimeParseError
405
+ yield nil, nil
406
+ return
407
+ end
367
408
  record['time'] = time_str if @keep_time_key
368
409
 
369
410
  yield time, record
370
411
  end
371
412
 
413
+ def rfc3164_space_cursor(text, cursor)
414
+ return if @with_priority && !RFC3164_PRI_DIGITS_REGEXP.match?(text.slice(1, cursor - 2))
415
+
416
+ @time_format_starts_with_space_rfc3164 ? cursor : cursor + 1
417
+ end
418
+
372
419
  NILVALUE = '-'.freeze
373
420
 
374
421
  def parse_rfc5424(text, &block)
@@ -406,7 +453,7 @@ module Fluent
406
453
  i = text.index(SPLIT_CHAR, i + 1)
407
454
  end
408
455
 
409
- time_str = sq ? text.slice(idx, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
456
+ time_str = sq ? text.slice(cursor, i - cursor).squeeze(SPLIT_CHAR) : text.slice(cursor, i - cursor)
410
457
  cursor = i + 1
411
458
  end
412
459