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
@@ -15,7 +15,7 @@
15
15
  #
16
16
 
17
17
  require 'fluent/plugin/input'
18
- require 'yajl'
18
+ require 'json'
19
19
 
20
20
  module Fluent::Plugin
21
21
  class ExecInput < Fluent::Plugin::Input
@@ -43,6 +43,8 @@ module Fluent::Plugin
43
43
  config_param :tag, :string, default: nil
44
44
  desc 'The interval time between periodic program runs.'
45
45
  config_param :run_interval, :time, default: nil
46
+ desc 'Command (program) execution timeout.'
47
+ config_param :command_timeout, :time, default: nil
46
48
  desc 'The default block size to read if parser requires partial read.'
47
49
  config_param :read_block_size, :size, default: 10240 # 10k
48
50
  desc 'The encoding to receive the result of the command, especially for non-ascii characters.'
@@ -86,9 +88,9 @@ module Fluent::Plugin
86
88
  options[:external_encoding] = @encoding if @encoding
87
89
 
88
90
  if @run_interval
89
- child_process_execute(:exec_input, @command, interval: @run_interval, **options, &method(:run))
91
+ child_process_execute(:exec_input, @command, interval: @run_interval, wait_timeout: @command_timeout, **options, &method(:run))
90
92
  else
91
- child_process_execute(:exec_input, @command, immediate: true, **options, &method(:run))
93
+ child_process_execute(:exec_input, @command, immediate: true, wait_timeout: @command_timeout, **options, &method(:run))
92
94
  end
93
95
  end
94
96
 
@@ -115,7 +117,7 @@ module Fluent::Plugin
115
117
  time ||= extract_time_from_record(record) || Fluent::EventTime.now
116
118
  router.emit(tag, time, record)
117
119
  rescue => e
118
- log.error "exec failed to emit", tag: tag, record: Yajl.dump(record), error: e
120
+ log.error "exec failed to emit", tag: tag, record: JSON.generate(record), error: e
119
121
  router.emit_error_event(tag, time, record, e) if tag && time && record
120
122
  end
121
123
  end
@@ -15,9 +15,10 @@
15
15
  #
16
16
 
17
17
 
18
+ require 'fluent/env'
18
19
  require 'fluent/plugin/input'
19
20
  require 'fluent/msgpack_factory'
20
- require 'yajl'
21
+ require 'json'
21
22
  require 'digest'
22
23
  require 'securerandom'
23
24
 
@@ -31,6 +32,12 @@ module Fluent::Plugin
31
32
  helpers :server
32
33
 
33
34
  LISTEN_PORT = 24224
35
+ OPTION_ACK = 'ack'.freeze
36
+ OPTION_CHUNK = 'chunk'.freeze
37
+ OPTION_COMPRESSED = 'compressed'.freeze
38
+ OPTION_FLUENT_SIGNAL = 'fluent_signal'.freeze
39
+ OPTION_SIZE = 'size'.freeze
40
+ OPTION_TEXT = 'text'.freeze
34
41
 
35
42
  desc 'The port to listen to.'
36
43
  config_param :port, :integer, default: LISTEN_PORT
@@ -250,13 +257,15 @@ module Fluent::Plugin
250
257
  unless feeder
251
258
  first = data[0]
252
259
  if first == '{' || first == '[' # json
253
- parser = Yajl::Parser.new
254
- parser.on_parse_complete = ->(obj){
255
- block.call(obj, bytes, serializer)
256
- bytes = 0
257
- }
260
+ parser = JSON::ResumableParser.new(**Fluent::DEFAULT_JSON_PARSE_OPTIONS)
258
261
  serializer = :to_json.to_proc
259
- feeder = ->(d){ parser << d }
262
+ feeder = ->(d){
263
+ parser << d
264
+ while parser.parse
265
+ block.call(parser.value, bytes, serializer)
266
+ bytes = 0
267
+ end
268
+ }
260
269
  else # msgpack
261
270
  parser = Fluent::MessagePackFactory.msgpack_unpacker
262
271
  serializer = :to_msgpack.to_proc
@@ -275,8 +284,8 @@ module Fluent::Plugin
275
284
  end
276
285
 
277
286
  def response(option)
278
- if option && option['chunk']
279
- return { 'ack' => option['chunk'] }
287
+ if option && option[OPTION_CHUNK]
288
+ return { OPTION_ACK => option[OPTION_CHUNK] }
280
289
  end
281
290
  nil
282
291
  end
@@ -310,14 +319,19 @@ module Fluent::Plugin
310
319
  when String
311
320
  # PackedForward
312
321
  option = msg[2] || {}
313
- size = option['size'] || 0
322
+ size = option[OPTION_SIZE] || 0
323
+ compressed = option[OPTION_COMPRESSED]
314
324
 
315
- if option['compressed'] && option['compressed'] != 'text'
316
- es = Fluent::CompressedMessagePackEventStream.new(entries, nil, size.to_i, compress: option['compressed'].to_sym, decompression_size_limit: @decompression_size_limit)
325
+ if compressed && compressed != OPTION_TEXT
326
+ es = Fluent::CompressedMessagePackEventStream.new(entries, nil, size.to_i, compress: compressed.to_sym, decompression_size_limit: @decompression_size_limit)
317
327
  else
318
328
  es = Fluent::MessagePackEventStream.new(entries, nil, size.to_i)
319
329
  end
320
- es = check_and_skip_invalid_event(tag, es, conn.remote_host) if @skip_invalid_event
330
+ if @skip_invalid_event
331
+ es = normalize_event_stream(tag, es, conn.remote_host)
332
+ elsif option[OPTION_FLUENT_SIGNAL] == 0
333
+ es = Fluent::MetadataTimeEventStream.new(es)
334
+ end
321
335
  if @enable_field_injection
322
336
  es = add_source_info(es, conn)
323
337
  end
@@ -325,19 +339,7 @@ module Fluent::Plugin
325
339
 
326
340
  when Array
327
341
  # Forward
328
- es = if @skip_invalid_event
329
- check_and_skip_invalid_event(tag, entries, conn.remote_host)
330
- else
331
- es = Fluent::MultiEventStream.new
332
- entries.each { |e|
333
- record = e[1]
334
- next if record.nil?
335
- time = e[0]
336
- time = Fluent::EventTime.now if time.nil? || time.to_i == 0 # `to_i == 0` for empty EventTime
337
- es.add(time, record)
338
- }
339
- es
340
- end
342
+ es = normalize_event_stream(tag, entries, conn.remote_host)
341
343
  if @enable_field_injection
342
344
  es = add_source_info(es, conn)
343
345
  end
@@ -346,7 +348,7 @@ module Fluent::Plugin
346
348
 
347
349
  else
348
350
  # Message
349
- time = msg[1]
351
+ time = extract_event_time(msg[1])
350
352
  record = msg[2]
351
353
  if @skip_invalid_event && invalid_event?(tag, time, record)
352
354
  log.warn "got invalid event and drop it:", host: conn.remote_host, tag: tag, time: time, record: record
@@ -367,21 +369,29 @@ module Fluent::Plugin
367
369
  end
368
370
 
369
371
  def invalid_event?(tag, time, record)
372
+ time = extract_event_time(time)
370
373
  !((time.is_a?(Integer) || time.is_a?(::Fluent::EventTime)) && record.is_a?(Hash) && tag.is_a?(String))
371
374
  end
372
375
 
373
- def check_and_skip_invalid_event(tag, es, remote_host)
376
+ def normalize_event_stream(tag, es, remote_host)
374
377
  new_es = Fluent::MultiEventStream.new
375
378
  es.each { |time, record|
376
- if invalid_event?(tag, time, record)
379
+ time = extract_event_time(time)
380
+ if @skip_invalid_event && invalid_event?(tag, time, record)
377
381
  log.warn "skip invalid event:", host: remote_host, tag: tag, time: time, record: record
378
382
  next
379
383
  end
384
+ next if record.nil?
385
+ time = Fluent::EventTime.now if time.nil? || time.to_i == 0 # `to_i == 0` for empty EventTime
380
386
  new_es.add(time, record)
381
387
  }
382
388
  new_es
383
389
  end
384
390
 
391
+ def extract_event_time(time)
392
+ time.is_a?(Array) ? time[0] : time
393
+ end
394
+
385
395
  def add_source_info(es, conn)
386
396
  new_es = Fluent::MultiEventStream.new
387
397
  if @source_address_key && @source_hostname_key
@@ -24,6 +24,8 @@ require 'webrick/httputils'
24
24
  require 'uri'
25
25
  require 'socket'
26
26
  require 'json'
27
+ require 'ipaddr'
28
+ require 'openssl'
27
29
 
28
30
  module Fluent::Plugin
29
31
  class InHttpParser < Parser
@@ -90,6 +92,22 @@ module Fluent::Plugin
90
92
  desc "Add prefix to incoming tag"
91
93
  config_param :add_tag_prefix, :string, default: nil
92
94
 
95
+ config_section :auth, required: false, multi: false do
96
+ desc 'The method for HTTP authentication'
97
+ config_param :method, :enum, list: [:basic], default: :basic
98
+ desc 'The username for basic authentication'
99
+ config_param :username, :string
100
+ desc 'The password for basic authentication'
101
+ config_param :password, :string, secret: true
102
+ end
103
+
104
+ config_section :security, required: false, multi: false do
105
+ config_section :client, param_name: :clients, required: false, multi: true do
106
+ desc 'Address or network of the client, IPv4 or IPv6. e.g. 192.168.1.10, 192.168.0.0/16, fd00::/8'
107
+ config_param :network, :string
108
+ end
109
+ end
110
+
93
111
  config_section :parse do
94
112
  config_set_default :@type, 'in_http'
95
113
  end
@@ -100,6 +118,7 @@ module Fluent::Plugin
100
118
  super
101
119
 
102
120
  @km = nil
121
+ @nodes = []
103
122
  @format_name = nil
104
123
  @parser_time_key = nil
105
124
 
@@ -127,6 +146,8 @@ module Fluent::Plugin
127
146
 
128
147
  raise Fluent::ConfigError, "'add_tag_prefix' parameter must not be empty" if @add_tag_prefix && @add_tag_prefix.empty?
129
148
 
149
+ configure_security if @security
150
+
130
151
  m = if @parser_configs.first['@type'] == 'in_http'
131
152
  @parser_msgpack = parser_create(usage: 'parser_in_http_msgpack', type: 'msgpack')
132
153
  @parser_msgpack.time_key = nil
@@ -260,11 +281,33 @@ module Fluent::Plugin
260
281
 
261
282
  private
262
283
 
284
+ def configure_security
285
+ @nodes = @security.clients.map do |client|
286
+ begin
287
+ IPAddr.new(client.network)
288
+ rescue ArgumentError
289
+ raise Fluent::ConfigError, "network '#{client.network}' address format is invalid"
290
+ end
291
+ end
292
+ end
293
+
294
+ def allowed_client?(address)
295
+ return true if @nodes.empty?
296
+ @nodes.any? { |node| node.include?(address) rescue false }
297
+ end
298
+
263
299
  def on_server_connect(conn)
300
+ unless allowed_client?(conn.remote_addr)
301
+ log.warn "client address does not match any allowed network", address: conn.remote_addr
302
+ conn.data {|_data| }
303
+ conn.close
304
+ return
305
+ end
306
+
264
307
  handler = Handler.new(conn, @km, method(:on_request),
265
308
  @body_size_limit, @decompression_size_limit, @format_name, log,
266
309
  @cors_allow_origins, @cors_allow_credentials,
267
- @add_query_params)
310
+ @add_query_params, @auth)
268
311
 
269
312
  conn.on(:data) do |data|
270
313
  handler.on_read(data)
@@ -347,7 +390,7 @@ module Fluent::Plugin
347
390
  attr_reader :content_type
348
391
 
349
392
  def initialize(io, km, callback, body_size_limit, decompression_size_limit, format_name, log,
350
- cors_allow_origins, cors_allow_credentials, add_query_params)
393
+ cors_allow_origins, cors_allow_credentials, add_query_params, auth = nil)
351
394
  @io = io
352
395
  @km = km
353
396
  @callback = callback
@@ -360,6 +403,8 @@ module Fluent::Plugin
360
403
  @cors_allow_credentials = cors_allow_credentials
361
404
  @idle = 0
362
405
  @add_query_params = add_query_params
406
+ @auth = auth
407
+ @authorization = nil
363
408
  @km.add(self)
364
409
 
365
410
  @remote_port, @remote_addr = io.remote_port, io.remote_addr
@@ -399,6 +444,7 @@ module Fluent::Plugin
399
444
  @env = {}
400
445
  @content_type = ""
401
446
  @content_encoding = ""
447
+ @authorization = nil
402
448
  headers.each_pair {|k,v|
403
449
  @env["HTTP_#{k.tr('-','_').upcase}"] = v
404
450
  case k
@@ -426,8 +472,22 @@ module Fluent::Plugin
426
472
  @access_control_request_method = v
427
473
  when /\AAccess-Control-Request-Headers\Z/i
428
474
  @access_control_request_headers = v
475
+ when /\AAuthorization\Z/i
476
+ @authorization = v.is_a?(Array) ? v.first : v
429
477
  end
430
478
  }
479
+
480
+ if @auth
481
+ # Never let the credential reach a record built by add_http_headers.
482
+ @env.delete("HTTP_AUTHORIZATION")
483
+
484
+ unless preflight_request? || authenticate
485
+ @log.warn "authentication failed", address: @remote_addr
486
+ send_response_and_close(RES_401_STATUS, {'WWW-Authenticate' => AUTH_CHALLENGE}, "")
487
+ return
488
+ end
489
+ end
490
+
431
491
  if expect
432
492
  if expect == '100-continue'.freeze
433
493
  if !size || size < @body_size_limit
@@ -442,6 +502,8 @@ module Fluent::Plugin
442
502
  end
443
503
 
444
504
  def on_body(chunk)
505
+ return if closing?
506
+
445
507
  if @body.bytesize + chunk.bytesize > @body_size_limit
446
508
  unless closing?
447
509
  send_response_and_close("413 Request Entity Too Large", {}, "Too large")
@@ -452,6 +514,8 @@ module Fluent::Plugin
452
514
  end
453
515
 
454
516
  RES_200_STATUS = "200 OK".freeze
517
+ RES_401_STATUS = "401 Unauthorized".freeze
518
+ AUTH_CHALLENGE = 'Basic realm="fluentd"'.freeze
455
519
  RES_403_STATUS = "403 Forbidden".freeze
456
520
 
457
521
  # Azure App Service sends GET requests for health checking purpose.
@@ -651,6 +715,23 @@ module Fluent::Plugin
651
715
  def parse_query(query)
652
716
  query.nil? ? {} : Hash[URI.decode_www_form(query, Encoding::ASCII_8BIT)]
653
717
  end
718
+
719
+ # Browsers never attach Authorization to a CORS preflight.
720
+ def preflight_request?
721
+ @parser.http_method == 'OPTIONS'.freeze
722
+ end
723
+
724
+ def authenticate
725
+ scheme, credentials = @authorization.to_s.split(' ', 2)
726
+ return false unless scheme == 'Basic'.freeze
727
+
728
+ username, password = credentials.to_s.unpack1('m').to_s.split(':', 2)
729
+ username_matched = OpenSSL.secure_compare(username.to_s, @auth.username)
730
+ password_matched = OpenSSL.secure_compare(password.to_s, @auth.password)
731
+ username_matched && password_matched
732
+ rescue ArgumentError
733
+ false
734
+ end
654
735
  end
655
736
  end
656
737
  end
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'json'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/config/types'
20
21
  require 'fluent/plugin/input'
21
22
  require 'fluent/plugin/output'
@@ -101,7 +102,7 @@ module Fluent::Plugin
101
102
  end
102
103
 
103
104
  def render_ltsv(obj, code: 200)
104
- normalized = JSON.parse(obj.to_json)
105
+ normalized = JSON.parse(obj.to_json, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
105
106
  text = ''
106
107
  normalized.each do |hash|
107
108
  row = []
@@ -14,7 +14,7 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
- require 'yajl'
17
+ require 'json'
18
18
 
19
19
  require 'fluent/plugin/input'
20
20
 
@@ -86,7 +86,7 @@ module Fluent::Plugin
86
86
 
87
87
  router.emit(@tag, now, record)
88
88
  rescue => e
89
- log.error "object space failed to emit", error: e, tag: @tag, record: Yajl.dump(record)
89
+ log.error "object space failed to emit", error: e, tag: @tag, record: JSON.generate(record)
90
90
  log.error_backtrace
91
91
  end
92
92
  end
@@ -16,6 +16,7 @@
16
16
 
17
17
  require 'json'
18
18
 
19
+ require 'fluent/env'
19
20
  require 'fluent/plugin/input'
20
21
  require 'fluent/config/error'
21
22
 
@@ -44,7 +45,7 @@ module Fluent::Plugin
44
45
  desc "The sample data to be generated. An array of JSON hashes or a single JSON hash."
45
46
  config_param :sample, alias: :dummy, default: [{"message" => "sample"}] do |val|
46
47
  begin
47
- parsed = JSON.parse(val)
48
+ parsed = JSON.parse(val, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
48
49
  rescue JSON::ParserError => ex
49
50
  # Fluent::ConfigParseError, "got incomplete JSON" will be raised
50
51
  # at literal_parser.rb with --use-v1-config, but I had to
@@ -18,7 +18,7 @@ require 'fluent/plugin/input'
18
18
  require 'fluent/config/error'
19
19
  require 'fluent/plugin/parser'
20
20
 
21
- require 'yajl'
21
+ require 'json'
22
22
 
23
23
  module Fluent::Plugin
24
24
  class SyslogInput < Input
@@ -98,8 +98,8 @@ module Fluent::Plugin
98
98
  desc 'The field name of the facility.'
99
99
  config_param :facility_key, :string, default: nil
100
100
 
101
- desc "The max bytes of message"
102
- config_param :message_length_limit, :size, default: 2048
101
+ desc "The max bytes of message. The messages exceeding this limit are dropped (UDP, TCP/TLS with traditional frame_type) or the connection is closed (TCP/TLS with octet_count frame_type)."
102
+ config_param :message_length_limit, :size, default: 8192
103
103
 
104
104
  config_param :blocking_timeout, :time, default: 0.5
105
105
 
@@ -190,29 +190,95 @@ module Fluent::Plugin
190
190
  resolve_name: @resolve_hostname,
191
191
  send_keepalive_packet: @send_keepalive_packet
192
192
  ) do |conn|
193
+ discard_till_next_delimiter = false
193
194
  conn.data do |data|
194
195
  buffer = conn.buffer
195
196
  buffer << data
196
197
  pos = 0
197
198
  if octet_count_frame
199
+ # Once an octet-counted stream contains an untrustworthy frame header, the stream
200
+ # can't be resynchronized. So close the connection instead of skipping bytes.
201
+ close_connection = false
198
202
  while idx = buffer.index(delimiter, pos)
199
- num = Integer(buffer[pos..idx])
203
+ begin
204
+ num = Integer(buffer[pos..idx])
205
+ rescue ArgumentError
206
+ log.info "The message length is not a number, closing connection"
207
+ log.on_debug { log.debug "The message length is not a number:", head: buffer[pos, 32].dump }
208
+ close_connection = true
209
+ break
210
+ end
211
+
212
+ if num < 0
213
+ log.info "The message length is negative, closing connection:", size: num
214
+ close_connection = true
215
+ break
216
+ end
217
+
218
+ if num > @message_length_limit
219
+ log.info "The message length exceeds 'message_length_limit', closing connection:", limit: @message_length_limit, size: num
220
+ close_connection = true
221
+ break
222
+ end
223
+
200
224
  msg = buffer[idx + delimiter_size, num]
201
225
  if msg.size != num
226
+ # The frame is not fully received yet.
202
227
  break
203
228
  end
204
229
 
205
230
  pos = idx + delimiter_size + num
206
231
  message_handler(msg, conn)
207
232
  end
233
+ buffer.slice!(0, pos) if pos > 0
234
+
235
+ # If the remaining buffer has no delimiter, no message length can be parsed from it.
236
+ # Since a valid frame header is much shorter than the limit, such a buffer never
237
+ # becomes a valid frame. (Note that a partially received valid frame always has a
238
+ # delimiter after its length, so this doesn't drop it even if it slightly exceeds
239
+ # the limit with its length header.)
240
+ if !close_connection && !buffer.index(delimiter) && buffer.bytesize > @message_length_limit
241
+ log.info "The buffer size exceeds 'message_length_limit', closing connection:", limit: @message_length_limit, size: buffer.bytesize
242
+ log.on_debug { log.debug "The buffer size exceeds 'message_length_limit':", head: buffer[...32] }
243
+ close_connection = true
244
+ end
245
+
246
+ if close_connection
247
+ buffer.clear
248
+ conn.close
249
+ next
250
+ end
208
251
  else
209
252
  while idx = buffer.index(delimiter, pos)
210
253
  msg = buffer[pos...idx]
211
254
  pos = idx + delimiter_size
255
+
256
+ if discard_till_next_delimiter
257
+ discard_till_next_delimiter = false
258
+ next
259
+ end
260
+
261
+ if msg.bytesize > @message_length_limit
262
+ log.info "The received data is larger than 'message_length_limit', dropped:", limit: @message_length_limit, size: msg.bytesize
263
+ log.on_debug { log.debug "The received data is larger than 'message_length_limit':", head: msg[...32] }
264
+ next
265
+ end
266
+
212
267
  message_handler(msg, conn)
213
268
  end
269
+ buffer.slice!(0, pos) if pos > 0
270
+
271
+ # If the buffer size exceeds the limit here, it means that the next message will
272
+ # definitely exceed the limit. So we should clear the buffer here. Otherwise, it will
273
+ # keep storing useless data until the next delimiter comes.
274
+ if buffer.bytesize > @message_length_limit
275
+ log.info "The buffer size exceeds 'message_length_limit', cleared:", limit: @message_length_limit, size: buffer.bytesize
276
+ log.on_debug { log.debug "The buffer size exceeds 'message_length_limit':", head: buffer[...32] }
277
+ buffer.clear
278
+ # We should discard the subsequent data until the next delimiter comes.
279
+ discard_till_next_delimiter = true
280
+ end
214
281
  end
215
- buffer.slice!(0, pos) if pos > 0
216
282
  end
217
283
  end
218
284
  end
@@ -274,7 +340,7 @@ module Fluent::Plugin
274
340
  def emit(tag, time, record)
275
341
  router.emit(tag, time, record)
276
342
  rescue => e
277
- log.error "syslog failed to emit", error: e, tag: tag, record: Yajl.dump(record)
343
+ log.error "syslog failed to emit", error: e, tag: tag, record: JSON.generate(record)
278
344
  end
279
345
  end
280
346
  end
@@ -0,0 +1,111 @@
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
+ require 'fluent/clock'
19
+
20
+ module Fluent::Plugin
21
+ class TailInput < Fluent::Plugin::Input
22
+ module Compatibility
23
+ # Keep these methods as compatibility entry points for plugins which
24
+ # inherit TailInput and override line processing methods. TailInput is a
25
+ # built-in plugin, not a public inheritance API, and these methods may be
26
+ # removed in a future release.
27
+ def flush_buffer(tw, buf)
28
+ @line_feeder.flush_buffer(tw, buf)
29
+ end
30
+
31
+ # Feeds lines through the per-file feed, or through LineFeeder for watchers
32
+ # created by plugins using the deprecated LineBufferTimerFlusher.
33
+ #
34
+ # @return true if no error or unrecoverable error happens in emit action. false if got BufferOverflowError
35
+ def receive_lines(lines, tail_watcher)
36
+ return async_receive_lines(lines, tail_watcher) if @worker_pool
37
+
38
+ file_feed = tail_watcher.file_feed
39
+ return file_feed.feed_lines(lines, tail_watcher) if file_feed.respond_to?(:feed_lines)
40
+
41
+ @line_feeder.feed_lines(lines, tail_watcher)
42
+ end
43
+
44
+ def convert_line_to_event(line, es, tail_watcher)
45
+ @line_feeder.convert_line_to_event(line, es, tail_watcher)
46
+ end
47
+
48
+ def parse_singleline(lines, tail_watcher)
49
+ @line_feeder.parse_singleline(lines, tail_watcher)
50
+ end
51
+
52
+ # TailInput's default #parse_multilines path starts the flush timer before
53
+ # parsing. An override that skips super must call
54
+ # tail_watcher.line_buffer_timer_flusher.reset_timer when needed.
55
+ def parse_multilines(lines, tail_watcher)
56
+ @line_feeder.parse_multilines(lines, tail_watcher)
57
+ end
58
+ end
59
+
60
+ class TailWatcher
61
+ # The object which keeps the line buffer of the multiline mode of the file:
62
+ # the FileFeed built by LineFeeder#new_file_feed, or the deprecated
63
+ # LineBufferTimerFlusher which a plugin overriding TailInput#setup_watcher
64
+ # passes to keep working without changes.
65
+ def line_buffer_timer_flusher
66
+ @file_feed
67
+ end
68
+
69
+ # Kept for compatibility with the plugins which build it by themselves and
70
+ # pass it to TailWatcher in the overridden TailInput#setup_watcher.
71
+ # LineFeeder#feed_lines feeds the lines of a watcher which keeps it, and
72
+ # LineFeeder::FileFeed built by LineFeeder#new_file_feed replaces it.
73
+ class LineBufferTimerFlusher
74
+ attr_accessor :line_buffer
75
+
76
+ def initialize(log, flush_interval, &flush_method)
77
+ @log = log
78
+ @flush_interval = flush_interval
79
+ @flush_method = flush_method
80
+ @start = nil
81
+ @line_buffer = nil
82
+ end
83
+
84
+ def on_notify(tw)
85
+ unless @start && @flush_method
86
+ return
87
+ end
88
+
89
+ if Fluent::Clock.now - @start >= @flush_interval
90
+ @flush_method.call(tw, @line_buffer) if @line_buffer
91
+ @line_buffer = nil
92
+ @start = nil
93
+ end
94
+ end
95
+
96
+ def close(tw)
97
+ return unless @line_buffer
98
+
99
+ @flush_method.call(tw, @line_buffer)
100
+ @line_buffer = nil
101
+ end
102
+
103
+ def reset_timer
104
+ return unless @flush_interval
105
+
106
+ @start = Fluent::Clock.now
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end