fluentd 1.14.6 → 1.15.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.
Potentially problematic release.
This version of fluentd might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.github/workflows/linux-test.yaml +1 -1
- data/.github/workflows/windows-test.yaml +4 -1
- data/CHANGELOG.md +53 -1
- data/fluentd.gemspec +2 -1
- data/lib/fluent/command/ctl.rb +4 -1
- data/lib/fluent/command/fluentd.rb +10 -0
- data/lib/fluent/config/literal_parser.rb +2 -2
- data/lib/fluent/config/yaml_parser/fluent_value.rb +47 -0
- data/lib/fluent/config/yaml_parser/loader.rb +91 -0
- data/lib/fluent/config/yaml_parser/parser.rb +166 -0
- data/lib/fluent/config/yaml_parser/section_builder.rb +107 -0
- data/lib/fluent/config/yaml_parser.rb +56 -0
- data/lib/fluent/config.rb +14 -1
- data/lib/fluent/plugin/file_wrapper.rb +52 -107
- data/lib/fluent/plugin/in_tail/group_watch.rb +204 -0
- data/lib/fluent/plugin/in_tail/position_file.rb +1 -15
- data/lib/fluent/plugin/in_tail.rb +66 -47
- data/lib/fluent/plugin/out_forward/socket_cache.rb +2 -0
- data/lib/fluent/plugin/output.rb +2 -1
- data/lib/fluent/plugin/parser_syslog.rb +1 -1
- data/lib/fluent/plugin_helper/server.rb +3 -1
- data/lib/fluent/plugin_helper/service_discovery.rb +2 -2
- data/lib/fluent/supervisor.rb +109 -25
- data/lib/fluent/system_config.rb +2 -1
- data/lib/fluent/version.rb +1 -1
- data/lib/fluent/winsvc.rb +2 -0
- data/test/command/test_ctl.rb +0 -1
- data/test/command/test_fluentd.rb +33 -0
- data/test/config/test_system_config.rb +3 -1
- data/test/config/test_types.rb +1 -1
- data/test/plugin/in_tail/test_io_handler.rb +14 -4
- data/test/plugin/in_tail/test_position_file.rb +0 -63
- data/test/plugin/out_forward/test_socket_cache.rb +26 -1
- data/test/plugin/test_file_wrapper.rb +0 -68
- data/test/plugin/test_in_object_space.rb +9 -3
- data/test/plugin/test_in_syslog.rb +1 -1
- data/test/plugin/test_in_tail.rb +629 -353
- data/test/plugin/test_out_forward.rb +30 -20
- data/test/plugin/test_parser_syslog.rb +1 -1
- data/test/plugin_helper/test_cert_option.rb +1 -1
- data/test/plugin_helper/test_child_process.rb +16 -4
- data/test/test_config.rb +135 -4
- data/test/test_supervisor.rb +155 -0
- metadata +11 -5
| @@ -16,8 +16,8 @@ | |
| 16 16 |  | 
| 17 17 | 
             
            module Fluent
         | 
| 18 18 | 
             
              module FileWrapper
         | 
| 19 | 
            -
                def self.open( | 
| 20 | 
            -
                  io = WindowsFile.new( | 
| 19 | 
            +
                def self.open(path, mode='r')
         | 
| 20 | 
            +
                  io = WindowsFile.new(path, mode).io
         | 
| 21 21 | 
             
                  if block_given?
         | 
| 22 22 | 
             
                    v = yield io
         | 
| 23 23 | 
             
                    io.close
         | 
| @@ -35,116 +35,36 @@ module Fluent | |
| 35 35 | 
             
                end
         | 
| 36 36 | 
             
              end
         | 
| 37 37 |  | 
| 38 | 
            -
              module WindowsFileExtension
         | 
| 39 | 
            -
                attr_reader :path
         | 
| 40 | 
            -
             | 
| 41 | 
            -
                def stat
         | 
| 42 | 
            -
                  s = super
         | 
| 43 | 
            -
                  s.instance_variable_set :@ino, @ino
         | 
| 44 | 
            -
                  def s.ino; @ino; end
         | 
| 45 | 
            -
                  s
         | 
| 46 | 
            -
                end
         | 
| 47 | 
            -
              end
         | 
| 48 | 
            -
             | 
| 49 | 
            -
              class Win32Error < StandardError
         | 
| 50 | 
            -
                require 'windows/error'
         | 
| 51 | 
            -
                include Windows::Error
         | 
| 52 | 
            -
             | 
| 53 | 
            -
                attr_reader :errcode, :msg
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                WSABASEERR = 10000
         | 
| 56 | 
            -
             | 
| 57 | 
            -
                def initialize(errcode, msg = nil)
         | 
| 58 | 
            -
                  @errcode = errcode
         | 
| 59 | 
            -
                  @msg = msg
         | 
| 60 | 
            -
                end
         | 
| 61 | 
            -
             | 
| 62 | 
            -
                def format_english_message(errcode)
         | 
| 63 | 
            -
                  buf = 0.chr * 260
         | 
| 64 | 
            -
                  flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_ARGUMENT_ARRAY
         | 
| 65 | 
            -
                  english_lang_id = 1033 # The result of MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US)
         | 
| 66 | 
            -
                  FormatMessageA.call(flags, 0, errcode, english_lang_id, buf, buf.size, 0)
         | 
| 67 | 
            -
                  buf.force_encoding(Encoding.default_external).strip
         | 
| 68 | 
            -
                end
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                def to_s
         | 
| 71 | 
            -
                  msg = super
         | 
| 72 | 
            -
                  msg << ": code: #{@errcode}, #{format_english_message(@errcode)}"
         | 
| 73 | 
            -
                  msg << " - #{@msg}" if @msg
         | 
| 74 | 
            -
                  msg
         | 
| 75 | 
            -
                end
         | 
| 76 | 
            -
             | 
| 77 | 
            -
                def inspect
         | 
| 78 | 
            -
                  "#<#{to_s}>"
         | 
| 79 | 
            -
                end
         | 
| 80 | 
            -
             | 
| 81 | 
            -
                def ==(other)
         | 
| 82 | 
            -
                  return false if other.class != Win32Error
         | 
| 83 | 
            -
                  @errcode == other.errcode && @msg == other.msg
         | 
| 84 | 
            -
                end
         | 
| 85 | 
            -
             | 
| 86 | 
            -
                def wsaerr?
         | 
| 87 | 
            -
                  @errcode >= WSABASEERR
         | 
| 88 | 
            -
                end
         | 
| 89 | 
            -
              end
         | 
| 90 | 
            -
             | 
| 91 | 
            -
              # To open and get stat with setting FILE_SHARE_DELETE
         | 
| 92 38 | 
             
              class WindowsFile
         | 
| 93 39 | 
             
                require 'windows/file'
         | 
| 94 | 
            -
                require 'windows/error'
         | 
| 95 40 | 
             
                require 'windows/handle'
         | 
| 96 | 
            -
                require 'windows/nio'
         | 
| 97 41 |  | 
| 98 | 
            -
                include  | 
| 42 | 
            +
                include File::Constants
         | 
| 99 43 | 
             
                include Windows::File
         | 
| 100 44 | 
             
                include Windows::Handle
         | 
| 101 | 
            -
                include Windows::NIO
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                def initialize(path, mode='r', sharemode=FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE)
         | 
| 104 | 
            -
                  @path = path
         | 
| 105 | 
            -
                  @file_handle = INVALID_HANDLE_VALUE
         | 
| 106 | 
            -
                  @mode = mode
         | 
| 107 45 |  | 
| 46 | 
            +
                attr_reader :io
         | 
| 108 47 |  | 
| 109 | 
            -
             | 
| 110 | 
            -
                   | 
| 111 | 
            -
                   | 
| 112 | 
            -
                   | 
| 113 | 
            -
                   | 
| 114 | 
            -
                   | 
| 115 | 
            -
             | 
| 116 | 
            -
                  else raise "unknown mode '#{mode}'"
         | 
| 117 | 
            -
                  end
         | 
| 118 | 
            -
             | 
| 119 | 
            -
                  @file_handle = CreateFile.call(@path, access, sharemode,
         | 
| 120 | 
            -
                                 0, creationdisposition, FILE_ATTRIBUTE_NORMAL, 0)
         | 
| 121 | 
            -
                  if @file_handle == INVALID_HANDLE_VALUE
         | 
| 122 | 
            -
                    win32err = Win32Error.new(Win32::API.last_error, path)
         | 
| 123 | 
            -
                    errno = ServerEngine::RbWinSock.rb_w32_map_errno(win32err.errcode)
         | 
| 124 | 
            -
                    if errno == Errno::EINVAL::Errno || win32err.wsaerr?
         | 
| 125 | 
            -
                      # maybe failed to map
         | 
| 126 | 
            -
                      raise win32err
         | 
| 127 | 
            -
                    else
         | 
| 128 | 
            -
                      raise SystemCallError.new(win32err.message, errno)
         | 
| 129 | 
            -
                    end
         | 
| 48 | 
            +
                def initialize(path, mode='r')
         | 
| 49 | 
            +
                  @path = path
         | 
| 50 | 
            +
                  @io = File.open(path, mode2flags(mode))
         | 
| 51 | 
            +
                  @file_handle = _get_osfhandle(@io.to_i)
         | 
| 52 | 
            +
                  @io.instance_variable_set(:@file_index, self.ino)
         | 
| 53 | 
            +
                  def @io.ino
         | 
| 54 | 
            +
                    @file_index
         | 
| 130 55 | 
             
                  end
         | 
| 131 56 | 
             
                end
         | 
| 132 57 |  | 
| 133 58 | 
             
                def close
         | 
| 134 | 
            -
                   | 
| 59 | 
            +
                  @io.close
         | 
| 135 60 | 
             
                  @file_handle = INVALID_HANDLE_VALUE
         | 
| 136 61 | 
             
                end
         | 
| 137 62 |  | 
| 138 | 
            -
                 | 
| 139 | 
            -
             | 
| 140 | 
            -
             | 
| 141 | 
            -
             | 
| 142 | 
            -
             | 
| 143 | 
            -
                  io.instance_variable_set :@path, @path
         | 
| 144 | 
            -
                  io.extend WindowsFileExtension
         | 
| 145 | 
            -
                  io
         | 
| 146 | 
            -
                end
         | 
| 147 | 
            -
             | 
| 63 | 
            +
                # To keep backward compatibility, we continue to use GetFileInformationByHandle()
         | 
| 64 | 
            +
                # to get file id.
         | 
| 65 | 
            +
                # Note that Ruby's File.stat uses GetFileInformationByHandleEx() with FileIdInfo
         | 
| 66 | 
            +
                # and returned value is different with above one, former one is 64 bit while
         | 
| 67 | 
            +
                # later one is 128bit.
         | 
| 148 68 | 
             
                def ino
         | 
| 149 69 | 
             
                  by_handle_file_information = '\0'*(4+8+8+8+4+4+4+4+4+4)   #72bytes
         | 
| 150 70 |  | 
| @@ -155,6 +75,41 @@ module Fluent | |
| 155 75 | 
             
                  by_handle_file_information.unpack("I11Q1")[11] # fileindex
         | 
| 156 76 | 
             
                end
         | 
| 157 77 |  | 
| 78 | 
            +
                def stat
         | 
| 79 | 
            +
                  raise Errno::ENOENT if delete_pending
         | 
| 80 | 
            +
                  s = File.stat(@path)
         | 
| 81 | 
            +
                  s.instance_variable_set :@ino, self.ino
         | 
| 82 | 
            +
                  def s.ino; @ino; end
         | 
| 83 | 
            +
                  s
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                private
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def mode2flags(mode)
         | 
| 89 | 
            +
                  # Always inject File::Constants::SHARE_DELETE
         | 
| 90 | 
            +
                  # https://github.com/fluent/fluentd/pull/3585#issuecomment-1101502617
         | 
| 91 | 
            +
                  # To enable SHARE_DELETE, BINARY is also required.
         | 
| 92 | 
            +
                  # https://bugs.ruby-lang.org/issues/11218
         | 
| 93 | 
            +
                  # https://github.com/ruby/ruby/blob/d6684f063bc53e3cab025bd39526eca3b480b5e7/win32/win32.c#L6332-L6345
         | 
| 94 | 
            +
                  flags = BINARY | SHARE_DELETE
         | 
| 95 | 
            +
                  case mode.delete("b")
         | 
| 96 | 
            +
                  when "r"
         | 
| 97 | 
            +
                    flags |= RDONLY
         | 
| 98 | 
            +
                  when "r+"
         | 
| 99 | 
            +
                    flags |= RDWR
         | 
| 100 | 
            +
                  when "w"
         | 
| 101 | 
            +
                    flags |= WRONLY | CREAT | TRUNC
         | 
| 102 | 
            +
                  when "w+"
         | 
| 103 | 
            +
                    flags |= RDWR | CREAT | TRUNC
         | 
| 104 | 
            +
                  when "a"
         | 
| 105 | 
            +
                    flags |= WRONLY | CREAT | APPEND
         | 
| 106 | 
            +
                  when "a+"
         | 
| 107 | 
            +
                    flags |= RDWR | CREAT | APPEND
         | 
| 108 | 
            +
                  else
         | 
| 109 | 
            +
                    raise Errno::EINVAL.new("Unsupported mode by Fluent::FileWrapper: #{mode}")
         | 
| 110 | 
            +
                  end
         | 
| 111 | 
            +
                end
         | 
| 112 | 
            +
             | 
| 158 113 | 
             
                # DeletePending is a Windows-specific file state that roughly means
         | 
| 159 114 | 
             
                # "this file is queued for deletion, so close any open handlers"
         | 
| 160 115 | 
             
                #
         | 
| @@ -173,15 +128,5 @@ module Fluent | |
| 173 128 |  | 
| 174 129 | 
             
                  return buf.unpack("QQICC")[3] != 0
         | 
| 175 130 | 
             
                end
         | 
| 176 | 
            -
             | 
| 177 | 
            -
                private :delete_pending
         | 
| 178 | 
            -
             | 
| 179 | 
            -
                def stat
         | 
| 180 | 
            -
                  raise Errno::ENOENT if delete_pending
         | 
| 181 | 
            -
                  s = File.stat(@path)
         | 
| 182 | 
            -
                  s.instance_variable_set :@ino, self.ino
         | 
| 183 | 
            -
                  def s.ino; @ino; end
         | 
| 184 | 
            -
                  s
         | 
| 185 | 
            -
                end
         | 
| 186 131 | 
             
              end
         | 
| 187 132 | 
             
            end if Fluent.windows?
         | 
| @@ -0,0 +1,204 @@ | |
| 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 'fluent/plugin/input'
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            module Fluent::Plugin
         | 
| 20 | 
            +
              class TailInput < Fluent::Plugin::Input
         | 
| 21 | 
            +
                module GroupWatchParams
         | 
| 22 | 
            +
                  include Fluent::Configurable
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  DEFAULT_KEY = /.*/
         | 
| 25 | 
            +
                  DEFAULT_LIMIT = -1
         | 
| 26 | 
            +
                  REGEXP_JOIN = "_"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  config_section :group, param_name: :group, required: false, multi: false do
         | 
| 29 | 
            +
                    desc 'Regex for extracting group\'s metadata'
         | 
| 30 | 
            +
                    config_param :pattern,
         | 
| 31 | 
            +
                                 :regexp,
         | 
| 32 | 
            +
                                 default: /^\/var\/log\/containers\/(?<podname>[a-z0-9]([-a-z0-9]*[a-z0-9])?(\/[a-z0-9]([-a-z0-9]*[a-z0-9])?)*)_(?<namespace>[^_]+)_(?<container>.+)-(?<docker_id>[a-z0-9]{64})\.log$/
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                    desc 'Period of time in which the group_line_limit is applied'
         | 
| 35 | 
            +
                    config_param :rate_period, :time, default: 5
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    config_section :rule, param_name: :rule, required: true, multi: true do
         | 
| 38 | 
            +
                      desc 'Key-value pairs for grouping'
         | 
| 39 | 
            +
                      config_param :match, :hash, value_type: :regexp, default: { namespace: [DEFAULT_KEY], podname: [DEFAULT_KEY] }
         | 
| 40 | 
            +
                      desc 'Maximum number of log lines allowed per group over a period of rate_period'
         | 
| 41 | 
            +
                      config_param :limit, :integer, default: DEFAULT_LIMIT
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                module GroupWatch
         | 
| 47 | 
            +
                  def self.included(mod)
         | 
| 48 | 
            +
                    mod.include GroupWatchParams
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  attr_reader :group_watchers, :default_group_key
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def initialize
         | 
| 54 | 
            +
                    super
         | 
| 55 | 
            +
                    @group_watchers = {}
         | 
| 56 | 
            +
                    @group_keys = nil
         | 
| 57 | 
            +
                    @default_group_key = nil
         | 
| 58 | 
            +
                  end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  def configure(conf)
         | 
| 61 | 
            +
                    super
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                    unless @group.nil?
         | 
| 64 | 
            +
                      ## Ensuring correct time period syntax
         | 
| 65 | 
            +
                      @group.rule.each { |rule|
         | 
| 66 | 
            +
                        raise "Metadata Group Limit >= DEFAULT_LIMIT" unless rule.limit >= GroupWatchParams::DEFAULT_LIMIT
         | 
| 67 | 
            +
                      }
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                      @group_keys = Regexp.compile(@group.pattern).named_captures.keys
         | 
| 70 | 
            +
                      @default_group_key = ([GroupWatchParams::DEFAULT_KEY] * @group_keys.length).join(GroupWatchParams::REGEXP_JOIN)
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                      ## Ensures that "specific" rules (with larger number of `rule.match` keys)
         | 
| 73 | 
            +
                      ## have a higher priority against "generic" rules (with less number of `rule.match` keys).
         | 
| 74 | 
            +
                      ## This will be helpful when a file satisfies more than one rule.
         | 
| 75 | 
            +
                      @group.rule.sort_by! { |rule| -rule.match.length() }
         | 
| 76 | 
            +
                      construct_groupwatchers
         | 
| 77 | 
            +
                      @group_watchers[@default_group_key] ||= GroupWatcher.new(@group.rate_period, GroupWatchParams::DEFAULT_LIMIT)
         | 
| 78 | 
            +
                    end
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def add_path_to_group_watcher(path)
         | 
| 82 | 
            +
                    return nil if @group.nil?
         | 
| 83 | 
            +
                    group_watcher = find_group_from_metadata(path)
         | 
| 84 | 
            +
                    group_watcher.add(path) unless group_watcher.include?(path)
         | 
| 85 | 
            +
                    group_watcher
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  def remove_path_from_group_watcher(path)
         | 
| 89 | 
            +
                    return if @group.nil?
         | 
| 90 | 
            +
                    group_watcher = find_group_from_metadata(path)
         | 
| 91 | 
            +
                    group_watcher.delete(path)
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  def construct_group_key(named_captures)
         | 
| 95 | 
            +
                    match_rule = []
         | 
| 96 | 
            +
                    @group_keys.each { |key|
         | 
| 97 | 
            +
                      match_rule.append(named_captures.fetch(key, GroupWatchParams::DEFAULT_KEY))
         | 
| 98 | 
            +
                    }
         | 
| 99 | 
            +
                    match_rule = match_rule.join(GroupWatchParams::REGEXP_JOIN)
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    match_rule
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  def construct_groupwatchers
         | 
| 105 | 
            +
                    @group.rule.each { |rule|
         | 
| 106 | 
            +
                      match_rule = construct_group_key(rule.match)
         | 
| 107 | 
            +
                      @group_watchers[match_rule] ||= GroupWatcher.new(@group.rate_period, rule.limit)
         | 
| 108 | 
            +
                    }
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                  def find_group(metadata)
         | 
| 112 | 
            +
                    metadata_key = construct_group_key(metadata)
         | 
| 113 | 
            +
                    gw_key = @group_watchers.keys.find { |regexp| metadata_key.match?(regexp) && regexp != @default_group_key }
         | 
| 114 | 
            +
                    gw_key ||= @default_group_key
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                    @group_watchers[gw_key]
         | 
| 117 | 
            +
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  def find_group_from_metadata(path)
         | 
| 120 | 
            +
                    begin
         | 
| 121 | 
            +
                      metadata = @group.pattern.match(path).named_captures
         | 
| 122 | 
            +
                      group_watcher = find_group(metadata)
         | 
| 123 | 
            +
                    rescue
         | 
| 124 | 
            +
                      log.warn "Cannot find group from metadata, Adding file in the default group"
         | 
| 125 | 
            +
                      group_watcher = @group_watchers[@default_group_key]
         | 
| 126 | 
            +
                    end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    group_watcher
         | 
| 129 | 
            +
                  end
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                class GroupWatcher
         | 
| 133 | 
            +
                  attr_accessor :current_paths, :limit, :number_lines_read, :start_reading_time, :rate_period
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                  FileCounter = Struct.new(
         | 
| 136 | 
            +
                    :number_lines_read,
         | 
| 137 | 
            +
                    :start_reading_time,
         | 
| 138 | 
            +
                  )
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  def initialize(rate_period = 60, limit = -1)
         | 
| 141 | 
            +
                    @current_paths = {}
         | 
| 142 | 
            +
                    @rate_period = rate_period
         | 
| 143 | 
            +
                    @limit = limit
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  def add(path)
         | 
| 147 | 
            +
                    @current_paths[path] = FileCounter.new(0, nil)
         | 
| 148 | 
            +
                  end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  def include?(path)
         | 
| 151 | 
            +
                    @current_paths.key?(path)
         | 
| 152 | 
            +
                  end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                  def size
         | 
| 155 | 
            +
                    @current_paths.size
         | 
| 156 | 
            +
                  end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                  def delete(path)
         | 
| 159 | 
            +
                    @current_paths.delete(path)
         | 
| 160 | 
            +
                  end
         | 
| 161 | 
            +
             | 
| 162 | 
            +
                  def update_reading_time(path)
         | 
| 163 | 
            +
                    @current_paths[path].start_reading_time ||= Fluent::Clock.now
         | 
| 164 | 
            +
                  end
         | 
| 165 | 
            +
             | 
| 166 | 
            +
                  def update_lines_read(path, value)
         | 
| 167 | 
            +
                    @current_paths[path].number_lines_read += value
         | 
| 168 | 
            +
                  end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                  def reset_counter(path)
         | 
| 171 | 
            +
                    @current_paths[path].start_reading_time = nil
         | 
| 172 | 
            +
                    @current_paths[path].number_lines_read = 0
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
             | 
| 175 | 
            +
                  def time_spent_reading(path)
         | 
| 176 | 
            +
                    Fluent::Clock.now - @current_paths[path].start_reading_time
         | 
| 177 | 
            +
                  end
         | 
| 178 | 
            +
             | 
| 179 | 
            +
                  def limit_time_period_reached?(path)
         | 
| 180 | 
            +
                    time_spent_reading(path) < @rate_period
         | 
| 181 | 
            +
                  end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                  def limit_lines_reached?(path)
         | 
| 184 | 
            +
                    return true unless include?(path)
         | 
| 185 | 
            +
                    return true if @limit == 0
         | 
| 186 | 
            +
             | 
| 187 | 
            +
                    return false if @limit < 0
         | 
| 188 | 
            +
                    return false if @current_paths[path].number_lines_read < @limit / size
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                    # update_reading_time(path)
         | 
| 191 | 
            +
                    if limit_time_period_reached?(path) # Exceeds limit
         | 
| 192 | 
            +
                      true
         | 
| 193 | 
            +
                    else # Does not exceed limit
         | 
| 194 | 
            +
                      reset_counter(path)
         | 
| 195 | 
            +
                      false
         | 
| 196 | 
            +
                    end
         | 
| 197 | 
            +
                  end
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                  def to_s
         | 
| 200 | 
            +
                    super + " current_paths: #{@current_paths} rate_period: #{@rate_period} limit: #{@limit}"
         | 
| 201 | 
            +
                  end
         | 
| 202 | 
            +
                end
         | 
| 203 | 
            +
              end
         | 
| 204 | 
            +
            end
         | 
| @@ -250,20 +250,6 @@ module Fluent::Plugin | |
| 250 250 | 
             
                  end
         | 
| 251 251 | 
             
                end
         | 
| 252 252 |  | 
| 253 | 
            -
                TargetInfo = Struct.new(:path, :ino) | 
| 254 | 
            -
                  def ==(other)
         | 
| 255 | 
            -
                    return false unless other.is_a?(TargetInfo)
         | 
| 256 | 
            -
                    self.path == other.path
         | 
| 257 | 
            -
                  end
         | 
| 258 | 
            -
             | 
| 259 | 
            -
                  def hash
         | 
| 260 | 
            -
                    self.path.hash
         | 
| 261 | 
            -
                  end
         | 
| 262 | 
            -
             | 
| 263 | 
            -
                  def eql?(other)
         | 
| 264 | 
            -
                    return false unless other.is_a?(TargetInfo)
         | 
| 265 | 
            -
                    self.path == other.path
         | 
| 266 | 
            -
                  end
         | 
| 267 | 
            -
                end
         | 
| 253 | 
            +
                TargetInfo = Struct.new(:path, :ino)
         | 
| 268 254 | 
             
              end
         | 
| 269 255 | 
             
            end
         | 
| @@ -24,6 +24,7 @@ require 'fluent/plugin/parser_multiline' | |
| 24 24 | 
             
            require 'fluent/variable_store'
         | 
| 25 25 | 
             
            require 'fluent/capability'
         | 
| 26 26 | 
             
            require 'fluent/plugin/in_tail/position_file'
         | 
| 27 | 
            +
            require 'fluent/plugin/in_tail/group_watch'
         | 
| 27 28 |  | 
| 28 29 | 
             
            if Fluent.windows?
         | 
| 29 30 | 
             
              require_relative 'file_wrapper'
         | 
| @@ -33,6 +34,8 @@ end | |
| 33 34 |  | 
| 34 35 | 
             
            module Fluent::Plugin
         | 
| 35 36 | 
             
              class TailInput < Fluent::Plugin::Input
         | 
| 37 | 
            +
                include GroupWatch
         | 
| 38 | 
            +
             | 
| 36 39 | 
             
                Fluent::Plugin.register_input('tail', self)
         | 
| 37 40 |  | 
| 38 41 | 
             
                helpers :timer, :event_loop, :parser, :compat_parameters
         | 
| @@ -354,11 +357,11 @@ module Fluent::Plugin | |
| 354 357 |  | 
| 355 358 | 
             
                def existence_path
         | 
| 356 359 | 
             
                  hash = {}
         | 
| 357 | 
            -
                  @tails. | 
| 360 | 
            +
                  @tails.each {|path, tw|
         | 
| 358 361 | 
             
                    if @follow_inodes
         | 
| 359 | 
            -
                      hash[ | 
| 362 | 
            +
                      hash[tw.ino] = TargetInfo.new(tw.path, tw.ino)
         | 
| 360 363 | 
             
                    else
         | 
| 361 | 
            -
                      hash[ | 
| 364 | 
            +
                      hash[tw.path] = TargetInfo.new(tw.path, tw.ino)
         | 
| 362 365 | 
             
                    end
         | 
| 363 366 | 
             
                  }
         | 
| 364 367 | 
             
                  hash
         | 
| @@ -406,6 +409,8 @@ module Fluent::Plugin | |
| 406 409 | 
             
                    event_loop_attach(watcher)
         | 
| 407 410 | 
             
                  end
         | 
| 408 411 |  | 
| 412 | 
            +
                  tw.group_watcher = add_path_to_group_watcher(target_info.path)
         | 
| 413 | 
            +
             | 
| 409 414 | 
             
                  tw
         | 
| 410 415 | 
             
                rescue => e
         | 
| 411 416 | 
             
                  if tw
         | 
| @@ -420,36 +425,31 @@ module Fluent::Plugin | |
| 420 425 | 
             
                end
         | 
| 421 426 |  | 
| 422 427 | 
             
                def construct_watcher(target_info)
         | 
| 428 | 
            +
                  path = target_info.path
         | 
| 429 | 
            +
             | 
| 430 | 
            +
                  # The file might be rotated or removed after collecting paths, so check inode again here.
         | 
| 431 | 
            +
                  begin
         | 
| 432 | 
            +
                    target_info.ino = Fluent::FileWrapper.stat(path).ino
         | 
| 433 | 
            +
                  rescue Errno::ENOENT, Errno::EACCES
         | 
| 434 | 
            +
                    $log.warn "stat() for #{path} failed. Continuing without tailing it."
         | 
| 435 | 
            +
                    return
         | 
| 436 | 
            +
                  end
         | 
| 437 | 
            +
             | 
| 423 438 | 
             
                  pe = nil
         | 
| 424 439 | 
             
                  if @pf
         | 
| 425 440 | 
             
                    pe = @pf[target_info]
         | 
| 426 | 
            -
                    if @read_from_head && pe.read_inode.zero?
         | 
| 427 | 
            -
                      begin
         | 
| 428 | 
            -
                        pe.update(Fluent::FileWrapper.stat(target_info.path).ino, 0)
         | 
| 429 | 
            -
                      rescue Errno::ENOENT, Errno::EACCES
         | 
| 430 | 
            -
                        $log.warn "stat() for #{target_info.path} failed. Continuing without tailing it."
         | 
| 431 | 
            -
                      end
         | 
| 432 | 
            -
                    end
         | 
| 441 | 
            +
                    pe.update(target_info.ino, 0) if @read_from_head && pe.read_inode.zero?
         | 
| 433 442 | 
             
                  end
         | 
| 434 443 |  | 
| 435 444 | 
             
                  begin
         | 
| 436 445 | 
             
                    tw = setup_watcher(target_info, pe)
         | 
| 437 446 | 
             
                  rescue WatcherSetupError => e
         | 
| 438 | 
            -
                    log.warn "Skip #{ | 
| 447 | 
            +
                    log.warn "Skip #{path} because unexpected setup error happens: #{e}"
         | 
| 439 448 | 
             
                    return
         | 
| 440 449 | 
             
                  end
         | 
| 441 450 |  | 
| 442 | 
            -
                   | 
| 443 | 
            -
             | 
| 444 | 
            -
                    @tails.delete(target_info)
         | 
| 445 | 
            -
                    @tails[target_info] = tw
         | 
| 446 | 
            -
                    tw.on_notify
         | 
| 447 | 
            -
                  rescue Errno::ENOENT, Errno::EACCES => e
         | 
| 448 | 
            -
                    $log.warn "stat() for #{target_info.path} failed with #{e.class.name}. Drop tail watcher for now."
         | 
| 449 | 
            -
                    # explicitly detach and unwatch watcher `tw`.
         | 
| 450 | 
            -
                    tw.unwatched = true
         | 
| 451 | 
            -
                    detach_watcher(tw, target_info.ino, false)
         | 
| 452 | 
            -
                  end
         | 
| 451 | 
            +
                  @tails[path] = tw
         | 
| 452 | 
            +
                  tw.on_notify
         | 
| 453 453 | 
             
                end
         | 
| 454 454 |  | 
| 455 455 | 
             
                def start_watchers(targets_info)
         | 
| @@ -461,10 +461,12 @@ module Fluent::Plugin | |
| 461 461 |  | 
| 462 462 | 
             
                def stop_watchers(targets_info, immediate: false, unwatched: false, remove_watcher: true)
         | 
| 463 463 | 
             
                  targets_info.each_value { |target_info|
         | 
| 464 | 
            +
                    remove_path_from_group_watcher(target_info.path)
         | 
| 465 | 
            +
             | 
| 464 466 | 
             
                    if remove_watcher
         | 
| 465 | 
            -
                      tw = @tails.delete(target_info)
         | 
| 467 | 
            +
                      tw = @tails.delete(target_info.path)
         | 
| 466 468 | 
             
                    else
         | 
| 467 | 
            -
                      tw = @tails[target_info]
         | 
| 469 | 
            +
                      tw = @tails[target_info.path]
         | 
| 468 470 | 
             
                    end
         | 
| 469 471 | 
             
                    if tw
         | 
| 470 472 | 
             
                      tw.unwatched = unwatched
         | 
| @@ -478,8 +480,8 @@ module Fluent::Plugin | |
| 478 480 | 
             
                end
         | 
| 479 481 |  | 
| 480 482 | 
             
                def close_watcher_handles
         | 
| 481 | 
            -
                  @tails.keys.each do | | 
| 482 | 
            -
                    tw = @tails.delete( | 
| 483 | 
            +
                  @tails.keys.each do |path|
         | 
| 484 | 
            +
                    tw = @tails.delete(path)
         | 
| 483 485 | 
             
                    if tw
         | 
| 484 486 | 
             
                      tw.close
         | 
| 485 487 | 
             
                    end
         | 
| @@ -488,20 +490,20 @@ module Fluent::Plugin | |
| 488 490 |  | 
| 489 491 | 
             
                # refresh_watchers calls @tails.keys so we don't use stop_watcher -> start_watcher sequence for safety.
         | 
| 490 492 | 
             
                def update_watcher(target_info, pe)
         | 
| 491 | 
            -
                   | 
| 493 | 
            +
                  path = target_info.path
         | 
| 494 | 
            +
             | 
| 495 | 
            +
                  log.info("detected rotation of #{path}; waiting #{@rotate_wait} seconds")
         | 
| 492 496 |  | 
| 493 497 | 
             
                  if @pf
         | 
| 494 498 | 
             
                    pe_inode = pe.read_inode
         | 
| 495 | 
            -
                    target_info_from_position_entry = TargetInfo.new( | 
| 499 | 
            +
                    target_info_from_position_entry = TargetInfo.new(path, pe_inode)
         | 
| 496 500 | 
             
                    unless pe_inode == @pf[target_info_from_position_entry].read_inode
         | 
| 497 501 | 
             
                      log.debug "Skip update_watcher because watcher has been already updated by other inotify event"
         | 
| 498 502 | 
             
                      return
         | 
| 499 503 | 
             
                    end
         | 
| 500 504 | 
             
                  end
         | 
| 501 505 |  | 
| 502 | 
            -
                   | 
| 503 | 
            -
                  rotated_tw = @tails[rotated_target_info]
         | 
| 504 | 
            -
                  new_target_info = target_info.dup
         | 
| 506 | 
            +
                  rotated_tw = @tails[path]
         | 
| 505 507 |  | 
| 506 508 | 
             
                  if @follow_inodes
         | 
| 507 509 | 
             
                    new_position_entry = @pf[target_info]
         | 
| @@ -509,17 +511,13 @@ module Fluent::Plugin | |
| 509 511 | 
             
                    if new_position_entry.read_inode == 0
         | 
| 510 512 | 
             
                      # When follow_inodes is true, it's not cleaned up by refresh_watcher.
         | 
| 511 513 | 
             
                      # So it should be unwatched here explicitly.
         | 
| 512 | 
            -
                      rotated_tw.unwatched = true
         | 
| 513 | 
            -
                       | 
| 514 | 
            -
                      @tails. | 
| 515 | 
            -
                      @tails[new_target_info] = setup_watcher(new_target_info, new_position_entry)
         | 
| 516 | 
            -
                      @tails[new_target_info].on_notify
         | 
| 514 | 
            +
                      rotated_tw.unwatched = true if rotated_tw
         | 
| 515 | 
            +
                      @tails[path] = setup_watcher(target_info, new_position_entry)
         | 
| 516 | 
            +
                      @tails[path].on_notify
         | 
| 517 517 | 
             
                    end
         | 
| 518 518 | 
             
                  else
         | 
| 519 | 
            -
                     | 
| 520 | 
            -
                    @tails. | 
| 521 | 
            -
                    @tails[new_target_info] = setup_watcher(new_target_info, pe)
         | 
| 522 | 
            -
                    @tails[new_target_info].on_notify
         | 
| 519 | 
            +
                    @tails[path] = setup_watcher(target_info, pe)
         | 
| 520 | 
            +
                    @tails[path].on_notify
         | 
| 523 521 | 
             
                  end
         | 
| 524 522 | 
             
                  detach_watcher_after_rotate_wait(rotated_tw, pe.read_inode) if rotated_tw
         | 
| 525 523 | 
             
                end
         | 
| @@ -542,18 +540,19 @@ module Fluent::Plugin | |
| 542 540 | 
             
                  end
         | 
| 543 541 | 
             
                end
         | 
| 544 542 |  | 
| 543 | 
            +
                def throttling_is_enabled?(tw)
         | 
| 544 | 
            +
                  return true if @read_bytes_limit_per_second > 0
         | 
| 545 | 
            +
                  return true if tw.group_watcher && tw.group_watcher.limit >= 0
         | 
| 546 | 
            +
                  false
         | 
| 547 | 
            +
                end
         | 
| 548 | 
            +
             | 
| 545 549 | 
             
                def detach_watcher_after_rotate_wait(tw, ino)
         | 
| 546 550 | 
             
                  # Call event_loop_attach/event_loop_detach is high-cost for short-live object.
         | 
| 547 551 | 
             
                  # If this has a problem with large number of files, use @_event_loop directly instead of timer_execute.
         | 
| 548 552 | 
             
                  if @open_on_every_update
         | 
| 549 553 | 
             
                    # Detach now because it's already closed, waiting it doesn't make sense.
         | 
| 550 554 | 
             
                    detach_watcher(tw, ino)
         | 
| 551 | 
            -
                  elsif  | 
| 552 | 
            -
                    # throttling isn't enabled, just wait @rotate_wait
         | 
| 553 | 
            -
                    timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
         | 
| 554 | 
            -
                      detach_watcher(tw, ino)
         | 
| 555 | 
            -
                    end
         | 
| 556 | 
            -
                  else
         | 
| 555 | 
            +
                  elsif throttling_is_enabled?(tw)
         | 
| 557 556 | 
             
                    # When the throttling feature is enabled, it might not reach EOF yet.
         | 
| 558 557 | 
             
                    # Should ensure to read all contents before closing it, with keeping throttling.
         | 
| 559 558 | 
             
                    start_time_to_wait = Fluent::Clock.now
         | 
| @@ -564,6 +563,11 @@ module Fluent::Plugin | |
| 564 563 | 
             
                        detach_watcher(tw, ino)
         | 
| 565 564 | 
             
                      end
         | 
| 566 565 | 
             
                    end
         | 
| 566 | 
            +
                  else
         | 
| 567 | 
            +
                    # when the throttling feature isn't enabled, just wait @rotate_wait
         | 
| 568 | 
            +
                    timer_execute(:in_tail_close_watcher, @rotate_wait, repeat: false) do
         | 
| 569 | 
            +
                      detach_watcher(tw, ino)
         | 
| 570 | 
            +
                    end
         | 
| 567 571 | 
             
                  end
         | 
| 568 572 | 
             
                end
         | 
| 569 573 |  | 
| @@ -775,6 +779,7 @@ module Fluent::Plugin | |
| 775 779 | 
             
                  attr_reader :line_buffer_timer_flusher
         | 
| 776 780 | 
             
                  attr_accessor :unwatched  # This is used for removing position entry from PositionFile
         | 
| 777 781 | 
             
                  attr_reader :watchers
         | 
| 782 | 
            +
                  attr_accessor :group_watcher
         | 
| 778 783 |  | 
| 779 784 | 
             
                  def tag
         | 
| 780 785 | 
             
                    @parsed_tag ||= @path.tr('/', '.').gsub(/\.+/, '.').gsub(/^\./, '')
         | 
| @@ -997,6 +1002,10 @@ module Fluent::Plugin | |
| 997 1002 | 
             
                      @log.info "following tail of #{@path}"
         | 
| 998 1003 | 
             
                    end
         | 
| 999 1004 |  | 
| 1005 | 
            +
                    def group_watcher
         | 
| 1006 | 
            +
                      @watcher.group_watcher
         | 
| 1007 | 
            +
                    end
         | 
| 1008 | 
            +
             | 
| 1000 1009 | 
             
                    def on_notify
         | 
| 1001 1010 | 
             
                      @notify_mutex.synchronize { handle_notify }
         | 
| 1002 1011 | 
             
                    end
         | 
| @@ -1054,6 +1063,7 @@ module Fluent::Plugin | |
| 1054 1063 |  | 
| 1055 1064 | 
             
                    def handle_notify
         | 
| 1056 1065 | 
             
                      return if limit_bytes_per_second_reached?
         | 
| 1066 | 
            +
                      return if group_watcher&.limit_lines_reached?(@path)
         | 
| 1057 1067 |  | 
| 1058 1068 | 
             
                      with_io do |io|
         | 
| 1059 1069 | 
             
                        begin
         | 
| @@ -1063,17 +1073,26 @@ module Fluent::Plugin | |
| 1063 1073 | 
             
                            begin
         | 
| 1064 1074 | 
             
                              while true
         | 
| 1065 1075 | 
             
                                @start_reading_time ||= Fluent::Clock.now
         | 
| 1076 | 
            +
                                group_watcher&.update_reading_time(@path)
         | 
| 1077 | 
            +
             | 
| 1066 1078 | 
             
                                data = io.readpartial(BYTES_TO_READ, @iobuf)
         | 
| 1067 1079 | 
             
                                @eof = false
         | 
| 1068 1080 | 
             
                                @number_bytes_read += data.bytesize
         | 
| 1069 1081 | 
             
                                @fifo << data
         | 
| 1082 | 
            +
             | 
| 1083 | 
            +
                                n_lines_before_read = @lines.size
         | 
| 1070 1084 | 
             
                                @fifo.read_lines(@lines)
         | 
| 1085 | 
            +
                                group_watcher&.update_lines_read(@path, @lines.size - n_lines_before_read)
         | 
| 1086 | 
            +
             | 
| 1087 | 
            +
                                group_watcher_limit = group_watcher&.limit_lines_reached?(@path)
         | 
| 1088 | 
            +
                                @log.debug "Reading Limit exceeded #{@path} #{group_watcher.number_lines_read}" if group_watcher_limit
         | 
| 1071 1089 |  | 
| 1072 | 
            -
                                if limit_bytes_per_second_reached? || should_shutdown_now?
         | 
| 1090 | 
            +
                                if group_watcher_limit || limit_bytes_per_second_reached? || should_shutdown_now?
         | 
| 1073 1091 | 
             
                                  # Just get out from tailing loop.
         | 
| 1074 1092 | 
             
                                  read_more = false
         | 
| 1075 1093 | 
             
                                  break
         | 
| 1076 1094 | 
             
                                end
         | 
| 1095 | 
            +
             | 
| 1077 1096 | 
             
                                if @lines.size >= @read_lines_limit
         | 
| 1078 1097 | 
             
                                  # not to use too much memory in case the file is very large
         | 
| 1079 1098 | 
             
                                  read_more = true
         | 
| @@ -50,6 +50,7 @@ module Fluent::Plugin | |
| 50 50 | 
             
                  def checkin(sock)
         | 
| 51 51 | 
             
                    @mutex.synchronize do
         | 
| 52 52 | 
             
                      if (s = @inflight_sockets.delete(sock))
         | 
| 53 | 
            +
                        s.timeout = timeout
         | 
| 53 54 | 
             
                        @available_sockets[s.key] << s
         | 
| 54 55 | 
             
                      else
         | 
| 55 56 | 
             
                        @log.debug("there is no socket #{sock}")
         | 
| @@ -122,6 +123,7 @@ module Fluent::Plugin | |
| 122 123 | 
             
                    t = Time.now
         | 
| 123 124 | 
             
                    if (s = @available_sockets[key].find { |sock| !expired_socket?(sock, time: t) })
         | 
| 124 125 | 
             
                      @inflight_sockets[s.sock] = @available_sockets[key].delete(s)
         | 
| 126 | 
            +
                      s.timeout = timeout
         | 
| 125 127 | 
             
                      s
         | 
| 126 128 | 
             
                    else
         | 
| 127 129 | 
             
                      nil
         |