fluentd 1.19.2 → 1.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +81 -0
- data/Rakefile +7 -0
- data/lib/fluent/command/cat.rb +1 -1
- data/lib/fluent/config/basic_parser.rb +0 -1
- data/lib/fluent/config/literal_parser.rb +18 -7
- data/lib/fluent/config/types.rb +10 -2
- data/lib/fluent/configurable.rb +1 -0
- data/lib/fluent/daemon.rb +2 -1
- data/lib/fluent/engine.rb +1 -1
- data/lib/fluent/env.rb +1 -0
- data/lib/fluent/event.rb +2 -1
- data/lib/fluent/plugin/base.rb +7 -2
- data/lib/fluent/plugin/buf_file.rb +4 -4
- data/lib/fluent/plugin/buf_file_single.rb +2 -2
- data/lib/fluent/plugin/buf_memory.rb +1 -1
- data/lib/fluent/plugin/buffer/chunk.rb +16 -7
- data/lib/fluent/plugin/buffer/file_chunk.rb +7 -5
- data/lib/fluent/plugin/buffer/file_single_chunk.rb +7 -5
- data/lib/fluent/plugin/buffer/memory_chunk.rb +1 -1
- data/lib/fluent/plugin/buffer.rb +26 -5
- data/lib/fluent/plugin/compressable.rb +7 -62
- data/lib/fluent/plugin/extractor.rb +132 -0
- data/lib/fluent/plugin/filter_record_transformer.rb +2 -1
- data/lib/fluent/plugin/in_debug_agent.rb +1 -1
- data/lib/fluent/plugin/in_forward.rb +3 -1
- data/lib/fluent/plugin/in_http.rb +91 -6
- data/lib/fluent/plugin/in_monitor_agent.rb +21 -23
- data/lib/fluent/plugin/in_sample.rb +2 -1
- data/lib/fluent/plugin/in_syslog.rb +70 -4
- data/lib/fluent/plugin/out_file.rb +10 -0
- data/lib/fluent/plugin/out_forward/connection_manager.rb +18 -0
- data/lib/fluent/plugin/out_forward/socket_cache.rb +45 -10
- data/lib/fluent/plugin/out_forward.rb +13 -2
- data/lib/fluent/plugin/out_http.rb +31 -1
- data/lib/fluent/plugin/output.rb +69 -5
- data/lib/fluent/plugin/parser_csv.rb +5 -0
- data/lib/fluent/plugin/parser_json.rb +6 -1
- data/lib/fluent/plugin/parser_syslog.rb +3 -3
- data/lib/fluent/plugin/sd_file.rb +2 -1
- data/lib/fluent/plugin/storage_local.rb +4 -4
- data/lib/fluent/supervisor.rb +22 -1
- data/lib/fluent/test/base.rb +5 -0
- data/lib/fluent/version.rb +1 -1
- metadata +3 -3
- data/.deepsource.toml +0 -13
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Fluentd
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
|
|
17
|
+
require 'stringio'
|
|
18
|
+
require 'zlib'
|
|
19
|
+
require 'zstd-ruby'
|
|
20
|
+
require 'fluent/error'
|
|
21
|
+
|
|
22
|
+
module Fluent
|
|
23
|
+
module Plugin
|
|
24
|
+
module Extractor
|
|
25
|
+
class SizeLimitError < UnrecoverableError; end
|
|
26
|
+
|
|
27
|
+
BYTES_TO_READ = 64 * 1024
|
|
28
|
+
INFLATE_BYTES_TO_READ = 1024
|
|
29
|
+
# Zstd::StreamReader#read takes a number of compressed bytes, unlike
|
|
30
|
+
# Zlib::GzipReader#read which takes decompressed bytes
|
|
31
|
+
ZSTD_BYTES_TO_READ = 1024
|
|
32
|
+
|
|
33
|
+
def self.decompress_gzip(compressed_data, limit:)
|
|
34
|
+
io = StringIO.new(compressed_data)
|
|
35
|
+
out = ''
|
|
36
|
+
loop do
|
|
37
|
+
reader = Zlib::GzipReader.new(io)
|
|
38
|
+
while (chunk = reader.read(BYTES_TO_READ))
|
|
39
|
+
out << chunk
|
|
40
|
+
if out.bytesize > limit
|
|
41
|
+
raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
unused = reader.unused
|
|
46
|
+
reader.finish
|
|
47
|
+
unless unused.nil?
|
|
48
|
+
adjust = unused.length
|
|
49
|
+
io.pos -= adjust
|
|
50
|
+
end
|
|
51
|
+
break if io.eof?
|
|
52
|
+
end
|
|
53
|
+
out
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.decompress_zstd(compressed_data, limit:)
|
|
57
|
+
io = StringIO.new(compressed_data)
|
|
58
|
+
reader = Zstd::StreamReader.new(io)
|
|
59
|
+
out = ''
|
|
60
|
+
loop do
|
|
61
|
+
out << reader.read(ZSTD_BYTES_TO_READ)
|
|
62
|
+
if out.bytesize > limit
|
|
63
|
+
raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
|
|
67
|
+
break if io.eof?
|
|
68
|
+
end
|
|
69
|
+
out
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.decompress_deflate(compressed_data, limit:)
|
|
73
|
+
io = StringIO.new(compressed_data)
|
|
74
|
+
out = ''
|
|
75
|
+
begin
|
|
76
|
+
zstream = Zlib::Inflate.new
|
|
77
|
+
while (chunk = io.read(INFLATE_BYTES_TO_READ))
|
|
78
|
+
out << zstream.inflate(chunk)
|
|
79
|
+
if out.bytesize > limit
|
|
80
|
+
raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
out << zstream.finish
|
|
84
|
+
if out.bytesize > limit
|
|
85
|
+
raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
|
|
86
|
+
end
|
|
87
|
+
ensure
|
|
88
|
+
zstream&.close
|
|
89
|
+
end
|
|
90
|
+
out
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.io_decompress_gzip(input, output, limit:)
|
|
94
|
+
size = 0
|
|
95
|
+
loop do
|
|
96
|
+
reader = Zlib::GzipReader.new(input)
|
|
97
|
+
while (chunk = reader.read(BYTES_TO_READ))
|
|
98
|
+
size += chunk.bytesize
|
|
99
|
+
if size > limit
|
|
100
|
+
raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
|
|
101
|
+
end
|
|
102
|
+
output.write(chunk)
|
|
103
|
+
end
|
|
104
|
+
unused = reader.unused
|
|
105
|
+
reader.finish
|
|
106
|
+
unless unused.nil?
|
|
107
|
+
adjust = unused.length
|
|
108
|
+
input.pos -= adjust
|
|
109
|
+
end
|
|
110
|
+
break if input.eof?
|
|
111
|
+
end
|
|
112
|
+
output
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.io_decompress_zstd(input, output, limit:)
|
|
116
|
+
reader = Zstd::StreamReader.new(input)
|
|
117
|
+
size = 0
|
|
118
|
+
loop do
|
|
119
|
+
chunk = reader.read(ZSTD_BYTES_TO_READ)
|
|
120
|
+
size += chunk.bytesize
|
|
121
|
+
if size > limit
|
|
122
|
+
raise SizeLimitError, "Decompressed data exceeds limit of #{limit} bytes"
|
|
123
|
+
end
|
|
124
|
+
output.write(chunk)
|
|
125
|
+
# Zstd::StreamReader doesn't provide unused data, so we have to manually adjust the position
|
|
126
|
+
break if input.eof?
|
|
127
|
+
end
|
|
128
|
+
output
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -18,6 +18,7 @@ require 'socket'
|
|
|
18
18
|
require 'json'
|
|
19
19
|
require 'ostruct'
|
|
20
20
|
|
|
21
|
+
require 'fluent/env'
|
|
21
22
|
require 'fluent/plugin/filter'
|
|
22
23
|
require 'fluent/config/error'
|
|
23
24
|
require 'fluent/event'
|
|
@@ -116,7 +117,7 @@ module Fluent::Plugin
|
|
|
116
117
|
|
|
117
118
|
def parse_value(value_str)
|
|
118
119
|
if value_str.start_with?('{', '[')
|
|
119
|
-
JSON.parse(value_str)
|
|
120
|
+
JSON.parse(value_str, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
|
|
120
121
|
else
|
|
121
122
|
value_str
|
|
122
123
|
end
|
|
@@ -26,7 +26,7 @@ module Fluent::Plugin
|
|
|
26
26
|
super
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
config_param :bind, :string, default: '
|
|
29
|
+
config_param :bind, :string, default: '127.0.0.1'
|
|
30
30
|
config_param :port, :integer, default: 24230
|
|
31
31
|
config_param :unix_path, :string, default: nil
|
|
32
32
|
#config_param :unix_mode # TODO
|
|
@@ -54,6 +54,8 @@ module Fluent::Plugin
|
|
|
54
54
|
config_param :chunk_size_warn_limit, :size, default: nil
|
|
55
55
|
desc 'Received chunk is dropped if it is larger than this value.'
|
|
56
56
|
config_param :chunk_size_limit, :size, default: nil
|
|
57
|
+
desc 'The size limit of the decompressed element.'
|
|
58
|
+
config_param :decompression_size_limit, :size, default: 256*1024*1024
|
|
57
59
|
desc 'Skip an event if incoming event is invalid.'
|
|
58
60
|
config_param :skip_invalid_event, :bool, default: true
|
|
59
61
|
|
|
@@ -311,7 +313,7 @@ module Fluent::Plugin
|
|
|
311
313
|
size = option['size'] || 0
|
|
312
314
|
|
|
313
315
|
if option['compressed'] && option['compressed'] != 'text'
|
|
314
|
-
es = Fluent::CompressedMessagePackEventStream.new(entries, nil, size.to_i, compress: option['compressed'].to_sym)
|
|
316
|
+
es = Fluent::CompressedMessagePackEventStream.new(entries, nil, size.to_i, compress: option['compressed'].to_sym, decompression_size_limit: @decompression_size_limit)
|
|
315
317
|
else
|
|
316
318
|
es = Fluent::MessagePackEventStream.new(entries, nil, size.to_i)
|
|
317
319
|
end
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
# limitations under the License.
|
|
15
15
|
#
|
|
16
16
|
|
|
17
|
+
require 'fluent/plugin/extractor'
|
|
17
18
|
require 'fluent/plugin/input'
|
|
18
19
|
require 'fluent/plugin/parser'
|
|
19
20
|
require 'fluent/event'
|
|
@@ -23,6 +24,8 @@ require 'webrick/httputils'
|
|
|
23
24
|
require 'uri'
|
|
24
25
|
require 'socket'
|
|
25
26
|
require 'json'
|
|
27
|
+
require 'ipaddr'
|
|
28
|
+
require 'openssl'
|
|
26
29
|
|
|
27
30
|
module Fluent::Plugin
|
|
28
31
|
class InHttpParser < Parser
|
|
@@ -64,6 +67,8 @@ module Fluent::Plugin
|
|
|
64
67
|
config_param :bind, :string, default: '0.0.0.0'
|
|
65
68
|
desc 'The size limit of the POSTed element. Default is 32MB.'
|
|
66
69
|
config_param :body_size_limit, :size, default: 32*1024*1024 # TODO default
|
|
70
|
+
desc 'The size limit of the decompressed element.'
|
|
71
|
+
config_param :decompression_size_limit, :size, default: 256*1024*1024 # TODO default
|
|
67
72
|
desc 'The timeout limit for keeping the connection alive.'
|
|
68
73
|
config_param :keepalive_timeout, :time, default: 10 # TODO default
|
|
69
74
|
config_param :backlog, :integer, default: nil
|
|
@@ -87,6 +92,22 @@ module Fluent::Plugin
|
|
|
87
92
|
desc "Add prefix to incoming tag"
|
|
88
93
|
config_param :add_tag_prefix, :string, default: nil
|
|
89
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
|
+
|
|
90
111
|
config_section :parse do
|
|
91
112
|
config_set_default :@type, 'in_http'
|
|
92
113
|
end
|
|
@@ -97,6 +118,7 @@ module Fluent::Plugin
|
|
|
97
118
|
super
|
|
98
119
|
|
|
99
120
|
@km = nil
|
|
121
|
+
@nodes = []
|
|
100
122
|
@format_name = nil
|
|
101
123
|
@parser_time_key = nil
|
|
102
124
|
|
|
@@ -124,6 +146,8 @@ module Fluent::Plugin
|
|
|
124
146
|
|
|
125
147
|
raise Fluent::ConfigError, "'add_tag_prefix' parameter must not be empty" if @add_tag_prefix && @add_tag_prefix.empty?
|
|
126
148
|
|
|
149
|
+
configure_security if @security
|
|
150
|
+
|
|
127
151
|
m = if @parser_configs.first['@type'] == 'in_http'
|
|
128
152
|
@parser_msgpack = parser_create(usage: 'parser_in_http_msgpack', type: 'msgpack')
|
|
129
153
|
@parser_msgpack.time_key = nil
|
|
@@ -257,11 +281,33 @@ module Fluent::Plugin
|
|
|
257
281
|
|
|
258
282
|
private
|
|
259
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
|
+
|
|
260
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
|
+
|
|
261
307
|
handler = Handler.new(conn, @km, method(:on_request),
|
|
262
|
-
@body_size_limit, @format_name, log,
|
|
308
|
+
@body_size_limit, @decompression_size_limit, @format_name, log,
|
|
263
309
|
@cors_allow_origins, @cors_allow_credentials,
|
|
264
|
-
@add_query_params)
|
|
310
|
+
@add_query_params, @auth)
|
|
265
311
|
|
|
266
312
|
conn.on(:data) do |data|
|
|
267
313
|
handler.on_read(data)
|
|
@@ -343,12 +389,13 @@ module Fluent::Plugin
|
|
|
343
389
|
class Handler
|
|
344
390
|
attr_reader :content_type
|
|
345
391
|
|
|
346
|
-
def initialize(io, km, callback, body_size_limit, format_name, log,
|
|
347
|
-
cors_allow_origins, cors_allow_credentials, add_query_params)
|
|
392
|
+
def initialize(io, km, callback, body_size_limit, decompression_size_limit, format_name, log,
|
|
393
|
+
cors_allow_origins, cors_allow_credentials, add_query_params, auth = nil)
|
|
348
394
|
@io = io
|
|
349
395
|
@km = km
|
|
350
396
|
@callback = callback
|
|
351
397
|
@body_size_limit = body_size_limit
|
|
398
|
+
@decompression_size_limit = decompression_size_limit
|
|
352
399
|
@next_close = false
|
|
353
400
|
@format_name = format_name
|
|
354
401
|
@log = log
|
|
@@ -356,6 +403,8 @@ module Fluent::Plugin
|
|
|
356
403
|
@cors_allow_credentials = cors_allow_credentials
|
|
357
404
|
@idle = 0
|
|
358
405
|
@add_query_params = add_query_params
|
|
406
|
+
@auth = auth
|
|
407
|
+
@authorization = nil
|
|
359
408
|
@km.add(self)
|
|
360
409
|
|
|
361
410
|
@remote_port, @remote_addr = io.remote_port, io.remote_addr
|
|
@@ -395,6 +444,7 @@ module Fluent::Plugin
|
|
|
395
444
|
@env = {}
|
|
396
445
|
@content_type = ""
|
|
397
446
|
@content_encoding = ""
|
|
447
|
+
@authorization = nil
|
|
398
448
|
headers.each_pair {|k,v|
|
|
399
449
|
@env["HTTP_#{k.tr('-','_').upcase}"] = v
|
|
400
450
|
case k
|
|
@@ -422,8 +472,22 @@ module Fluent::Plugin
|
|
|
422
472
|
@access_control_request_method = v
|
|
423
473
|
when /\AAccess-Control-Request-Headers\Z/i
|
|
424
474
|
@access_control_request_headers = v
|
|
475
|
+
when /\AAuthorization\Z/i
|
|
476
|
+
@authorization = v.is_a?(Array) ? v.first : v
|
|
425
477
|
end
|
|
426
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
|
+
|
|
427
491
|
if expect
|
|
428
492
|
if expect == '100-continue'.freeze
|
|
429
493
|
if !size || size < @body_size_limit
|
|
@@ -438,6 +502,8 @@ module Fluent::Plugin
|
|
|
438
502
|
end
|
|
439
503
|
|
|
440
504
|
def on_body(chunk)
|
|
505
|
+
return if closing?
|
|
506
|
+
|
|
441
507
|
if @body.bytesize + chunk.bytesize > @body_size_limit
|
|
442
508
|
unless closing?
|
|
443
509
|
send_response_and_close("413 Request Entity Too Large", {}, "Too large")
|
|
@@ -448,6 +514,8 @@ module Fluent::Plugin
|
|
|
448
514
|
end
|
|
449
515
|
|
|
450
516
|
RES_200_STATUS = "200 OK".freeze
|
|
517
|
+
RES_401_STATUS = "401 Unauthorized".freeze
|
|
518
|
+
AUTH_CHALLENGE = 'Basic realm="fluentd"'.freeze
|
|
451
519
|
RES_403_STATUS = "403 Forbidden".freeze
|
|
452
520
|
|
|
453
521
|
# Azure App Service sends GET requests for health checking purpose.
|
|
@@ -518,9 +586,9 @@ module Fluent::Plugin
|
|
|
518
586
|
# For now, we only support 'gzip' and 'deflate'.
|
|
519
587
|
begin
|
|
520
588
|
if @content_encoding == 'gzip'.freeze
|
|
521
|
-
@body =
|
|
589
|
+
@body = Extractor.decompress_gzip(@body, limit: @decompression_size_limit)
|
|
522
590
|
elsif @content_encoding == 'deflate'.freeze
|
|
523
|
-
@body =
|
|
591
|
+
@body = Extractor.decompress_deflate(@body, limit: @decompression_size_limit)
|
|
524
592
|
end
|
|
525
593
|
rescue
|
|
526
594
|
@log.warn 'fails to decode payload', error: $!.to_s
|
|
@@ -647,6 +715,23 @@ module Fluent::Plugin
|
|
|
647
715
|
def parse_query(query)
|
|
648
716
|
query.nil? ? {} : Hash[URI.decode_www_form(query, Encoding::ASCII_8BIT)]
|
|
649
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
|
|
650
735
|
end
|
|
651
736
|
end
|
|
652
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'
|
|
@@ -37,9 +38,11 @@ module Fluent::Plugin
|
|
|
37
38
|
desc 'Determine the rate to emit internal metrics as events.'
|
|
38
39
|
config_param :emit_interval, :time, default: 60
|
|
39
40
|
desc 'Determine whether to include the config information.'
|
|
40
|
-
config_param :include_config, :bool, default:
|
|
41
|
+
config_param :include_config, :bool, default: false
|
|
41
42
|
desc 'Determine whether to include the retry information.'
|
|
42
|
-
config_param :include_retry, :bool, default:
|
|
43
|
+
config_param :include_retry, :bool, default: false
|
|
44
|
+
desc 'Determine whether to include the debug information.'
|
|
45
|
+
config_param :include_debug_info, :bool, default: false
|
|
43
46
|
|
|
44
47
|
class APIHandler
|
|
45
48
|
def initialize(agent)
|
|
@@ -99,7 +102,7 @@ module Fluent::Plugin
|
|
|
99
102
|
end
|
|
100
103
|
|
|
101
104
|
def render_ltsv(obj, code: 200)
|
|
102
|
-
normalized = JSON.parse(obj.to_json)
|
|
105
|
+
normalized = JSON.parse(obj.to_json, **Fluent::DEFAULT_JSON_PARSE_OPTIONS)
|
|
103
106
|
text = ''
|
|
104
107
|
normalized.each do |hash|
|
|
105
108
|
row = []
|
|
@@ -151,28 +154,23 @@ module Fluent::Plugin
|
|
|
151
154
|
# parse ?=query string
|
|
152
155
|
qs.merge!(req.query || {})
|
|
153
156
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
opts[:pretty_json] = true
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
if ivars = qs['with_ivars'.freeze].first
|
|
163
|
-
opts[:ivars] = ivars.split(',')
|
|
164
|
-
end
|
|
157
|
+
opts = {
|
|
158
|
+
query: qs,
|
|
159
|
+
with_config: @agent.include_config,
|
|
160
|
+
with_retry: @agent.include_retry
|
|
161
|
+
}
|
|
165
162
|
|
|
166
|
-
if
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
if @agent.include_debug_info
|
|
164
|
+
# if ?debug=1 is set, set :with_debug_info for get_monitor_info
|
|
165
|
+
# and :pretty_json for render_json_error
|
|
166
|
+
if qs['debug'.freeze].first
|
|
167
|
+
opts[:with_debug_info] = true
|
|
168
|
+
opts[:pretty_json] = true
|
|
169
|
+
end
|
|
171
170
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
opts[:with_retry] = @agent.include_retry
|
|
171
|
+
if ivars = qs['with_ivars'.freeze].first
|
|
172
|
+
opts[:ivars] = ivars.split(',')
|
|
173
|
+
end
|
|
176
174
|
end
|
|
177
175
|
|
|
178
176
|
opts
|
|
@@ -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
|
|
@@ -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:
|
|
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
|
-
|
|
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
|
|
@@ -117,7 +117,17 @@ module Fluent::Plugin
|
|
|
117
117
|
configured_time_slice_format = conf['time_slice_format']
|
|
118
118
|
|
|
119
119
|
if conf.elements(name: 'buffer').empty?
|
|
120
|
+
# no <buffer> section, default time chunk key and timekey (1d) will be used.
|
|
121
|
+
log.warn "default timekey interval (1d) will be used because of missing <buffer> section. To change the output frequency, please modify the timekey value"
|
|
122
|
+
|
|
120
123
|
conf.add_element('buffer', 'time')
|
|
124
|
+
else
|
|
125
|
+
unless conf.elements(name: 'buffer').first.has_key?('timekey')
|
|
126
|
+
if conf.elements(name: 'buffer').first.arg != "[]"
|
|
127
|
+
# with <buffer> section (except <buffer []>), and no timekey
|
|
128
|
+
log.warn "default timekey interval (1d) will be used. To change the output frequency, please modify the timekey value"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
121
131
|
end
|
|
122
132
|
buffer_conf = conf.elements(name: 'buffer').first
|
|
123
133
|
# Fluent::PluginId#configure is not called yet, so we can't use #plugin_root_dir here.
|
|
@@ -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)
|