fluent-plugin-s3 1.8.5 → 1.8.6
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/.github/workflows/linux.yml +2 -2
- data/.github/workflows/stale-actions.yml +1 -1
- data/ChangeLog +8 -0
- data/Gemfile +1 -1
- data/Gemfile.v0.12 +1 -1
- data/VERSION +1 -1
- data/docs/input.md +11 -2
- data/docs/output.md +1 -1
- data/lib/fluent/plugin/in_s3.rb +67 -20
- data/lib/fluent/plugin/out_s3.rb +5 -5
- data/lib/fluent/plugin/s3_extractor_gzip_command.rb +1 -1
- data/lib/fluent/plugin/s3_extractor_lzma2.rb +3 -3
- data/lib/fluent/plugin/s3_extractor_lzo.rb +3 -3
- data/test/test_in_s3.rb +277 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d21a6392c01d12f1eb7d42afcb9a068157020c0627772895dedad47f1807c788
|
|
4
|
+
data.tar.gz: 22cef56213063ea0493c2b1599d2ff8019ab1f0da44e1c6598497100b695859f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e516fbe5805ae366cbd0c588a64b45d8b4b80d0b4afa65e0ebc8eea2e59c1ea6f50d119493b2516f5db94db0b9d53b83a72022badf3179c624269a1493dafa9
|
|
7
|
+
data.tar.gz: 7b3f6921d9a6f88eb802b93d8613c0f706abb1162be56e833fe4e65b3084bc7a2c7c10296a05467127e6451fdb10ea66457bc5f4eb2bfe62fe965935d2822649
|
data/.github/workflows/linux.yml
CHANGED
|
@@ -25,8 +25,8 @@ jobs:
|
|
|
25
25
|
- ubuntu-latest
|
|
26
26
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
|
|
27
27
|
steps:
|
|
28
|
-
- uses: actions/checkout@
|
|
29
|
-
- uses: ruby/setup-ruby@
|
|
28
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
+
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
30
30
|
with:
|
|
31
31
|
ruby-version: ${{ matrix.ruby }}
|
|
32
32
|
- name: unit testing
|
|
@@ -10,7 +10,7 @@ jobs:
|
|
|
10
10
|
issues: write
|
|
11
11
|
pull-requests: write
|
|
12
12
|
steps:
|
|
13
|
-
- uses: actions/stale@
|
|
13
|
+
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
|
14
14
|
with:
|
|
15
15
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
16
16
|
days-before-stale: 30
|
data/ChangeLog
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
Release 1.8.6 - 2026/09/04
|
|
2
|
+
|
|
3
|
+
* in_s3: stop retrying SQS messages that can never succeed (GitHub: #484)
|
|
4
|
+
* in_s3: allow duplicate keys in JSON parser (GitHub: #481)
|
|
5
|
+
* in_s3/out_s3: mask `proxy_uri` in the configuration dump (GitHub: #486)
|
|
6
|
+
* out_s3: build the chunk debug message only when debug is on (GitHub: #485)
|
|
7
|
+
* out_s3: fix parameter descriptions of `checksum_algorithm` and `s3_metadata` (GitHub: #487)
|
|
8
|
+
|
|
1
9
|
Release 1.8.5 - 2026/06/25
|
|
2
10
|
|
|
3
11
|
* in_s3: enforce size limits on decompressed payloads
|
data/Gemfile
CHANGED
data/Gemfile.v0.12
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.8.
|
|
1
|
+
1.8.6
|
data/docs/input.md
CHANGED
|
@@ -33,6 +33,7 @@ Whether or not object metadata should be added to the record. Defaults to `false
|
|
|
33
33
|
|
|
34
34
|
The size limit of the decompressed data. The default is `256m` (256 MiB).
|
|
35
35
|
This parameter is designed to prevent memory exhaustion when extracting highly compressed objects from S3.
|
|
36
|
+
An object over the limit is never ingested and its SQS message is deleted, so raising the limit afterwards will not bring the notification back. See [Handling of failed messages](#handling-of-failed-messages).
|
|
36
37
|
|
|
37
38
|
## match_regexp
|
|
38
39
|
|
|
@@ -75,7 +76,7 @@ Check AWS key on start. Default is true.
|
|
|
75
76
|
|
|
76
77
|
## proxy_uri
|
|
77
78
|
|
|
78
|
-
URI of proxy environment.
|
|
79
|
+
URI of proxy environment. The value is masked in the configuration dump, so credentials embedded in the URI are not written to the log.
|
|
79
80
|
|
|
80
81
|
## \<sqs\> section
|
|
81
82
|
|
|
@@ -109,4 +110,12 @@ Interval to retry polling SQS if polling unsuccessful, in seconds. Default is 30
|
|
|
109
110
|
|
|
110
111
|
### event_bridge_mode
|
|
111
112
|
When true, Amazon S3 Event Notification should be configured using the EventBridge integration. Default is false.
|
|
112
|
-
See [Configure S3 event notification using EventBridge](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) for additional information.
|
|
113
|
+
See [Configure S3 event notification using EventBridge](https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventBridge.html) for additional information.
|
|
114
|
+
|
|
115
|
+
# Handling of failed messages
|
|
116
|
+
|
|
117
|
+
When handling an SQS message fails, the plugin either deletes the message or leaves it in the queue for redelivery.
|
|
118
|
+
|
|
119
|
+
A failure that cannot change on redelivery is reported with a warning and the message is deleted. This covers a body that is not an S3 notification, an object key that S3 would reject, an object that the configured `store_as` cannot extract, an object over `decompression_size_limit`, and a line that the configured parser refuses. The object in S3 is not touched, but it is never ingested and the notification is gone, so a mistake in `store_as`, `<parse>` or `decompression_size_limit` loses the notifications that arrive while it is in effect. The warning names the object key, truncated to 256 bytes, so that such an object can be ingested by hand once the configuration is corrected.
|
|
120
|
+
|
|
121
|
+
Every other failure, such as a networking error, a throttle, a missing object or a bug in the plugin, leaves the message in the queue and SQS redelivers it after the visibility timeout. Configure a [redrive policy with a dead letter queue](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-dead-letter-queues.html) so that such a message is moved aside after a bounded number of attempts instead of being retried until the retention period expires.
|
data/docs/output.md
CHANGED
|
@@ -312,7 +312,7 @@ Check AWS key on start. Default is true.
|
|
|
312
312
|
|
|
313
313
|
## proxy_uri
|
|
314
314
|
|
|
315
|
-
uri of proxy environment.
|
|
315
|
+
uri of proxy environment. The value is masked in the configuration dump, so credentials embedded in the URI are not written to the log.
|
|
316
316
|
|
|
317
317
|
## path
|
|
318
318
|
|
data/lib/fluent/plugin/in_s3.rb
CHANGED
|
@@ -5,6 +5,8 @@ require 'aws-sdk-s3'
|
|
|
5
5
|
require 'aws-sdk-sqs'
|
|
6
6
|
require 'aws-sdk-sqs/queue_poller'
|
|
7
7
|
require 'cgi/util'
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'msgpack'
|
|
8
10
|
require 'zlib'
|
|
9
11
|
require 'time'
|
|
10
12
|
require 'tempfile'
|
|
@@ -22,7 +24,17 @@ module Fluent::Plugin
|
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
DEFAULT_PARSE_TYPE = "none"
|
|
27
|
+
UNRECOVERABLE_ERRORS = [
|
|
28
|
+
Fluent::UnrecoverableError,
|
|
29
|
+
TypeError,
|
|
30
|
+
ArgumentError,
|
|
31
|
+
EncodingError,
|
|
32
|
+
JSON::ParserError,
|
|
33
|
+
MessagePack::UnpackError,
|
|
34
|
+
Zlib::Error,
|
|
35
|
+
].freeze
|
|
25
36
|
DECOMPRESSION_SIZE_LIMIT = 256 * 1024 * 1024
|
|
37
|
+
LOG_MAX_BYTES = 256
|
|
26
38
|
|
|
27
39
|
desc "Use aws-sdk-ruby bundled cert"
|
|
28
40
|
config_param :use_bundled_cert, :bool, default: false
|
|
@@ -97,7 +109,7 @@ module Fluent::Plugin
|
|
|
97
109
|
desc "Check AWS key on start"
|
|
98
110
|
config_param :check_apikey_on_start, :bool, default: true
|
|
99
111
|
desc "URI of proxy environment"
|
|
100
|
-
config_param :proxy_uri, :string, default: nil
|
|
112
|
+
config_param :proxy_uri, :string, default: nil, secret: true
|
|
101
113
|
desc "Optional RegEx to match incoming messages"
|
|
102
114
|
config_param :match_regexp, :regexp, default: nil
|
|
103
115
|
|
|
@@ -213,17 +225,18 @@ module Fluent::Plugin
|
|
|
213
225
|
begin
|
|
214
226
|
@poller.poll(options) do |message|
|
|
215
227
|
begin
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
228
|
+
raw_key, key = object_key_from(message)
|
|
229
|
+
next if raw_key.nil?
|
|
230
|
+
|
|
231
|
+
process(raw_key, key)
|
|
232
|
+
rescue *UNRECOVERABLE_ERRORS => e
|
|
233
|
+
attributes = { message_id: message.message_id, error_class: e.class.to_s, error: e.message }
|
|
234
|
+
attributes[:key] = loggable(raw_key) if raw_key
|
|
235
|
+
log.warn("Skipping SQS message that cannot be handled", attributes)
|
|
236
|
+
log.warn_backtrace(e.backtrace)
|
|
237
|
+
next
|
|
225
238
|
rescue => e
|
|
226
|
-
log.warn(error: e)
|
|
239
|
+
log.warn("Failed to handle SQS message", message_id: message.message_id, error: e)
|
|
227
240
|
log.warn_backtrace(e.backtrace)
|
|
228
241
|
throw :skip_delete
|
|
229
242
|
end
|
|
@@ -235,6 +248,36 @@ module Fluent::Plugin
|
|
|
235
248
|
end
|
|
236
249
|
end
|
|
237
250
|
|
|
251
|
+
def loggable(value)
|
|
252
|
+
value.byteslice(0, LOG_MAX_BYTES).scrub("?")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def object_key_from(message)
|
|
256
|
+
log.debug { "SQS message #{message.message_id}: #{message.body.dump}" }
|
|
257
|
+
|
|
258
|
+
body = JSON.parse(message.body, allow_duplicate_key: true)
|
|
259
|
+
unless body.is_a?(Hash)
|
|
260
|
+
log.warn("Skipping SQS message whose body is not a JSON object", message_id: message.message_id)
|
|
261
|
+
return nil
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
unless is_valid_queue(body)
|
|
265
|
+
log.warn("Skipping SQS message that is not an S3 notification", message_id: message.message_id)
|
|
266
|
+
return nil
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
raw_key = get_raw_key(body)
|
|
270
|
+
if raw_key.nil?
|
|
271
|
+
log.warn("Skipping SQS message with a malformed notification body", message_id: message.message_id)
|
|
272
|
+
return nil
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
key = CGI.unescape(raw_key)
|
|
276
|
+
return nil if @match_regexp && !@match_regexp.match?(key)
|
|
277
|
+
|
|
278
|
+
[raw_key, key]
|
|
279
|
+
end
|
|
280
|
+
|
|
238
281
|
def is_valid_queue(body)
|
|
239
282
|
if @sqs.event_bridge_mode
|
|
240
283
|
log.debug("checking for eventbridge property")
|
|
@@ -247,9 +290,9 @@ module Fluent::Plugin
|
|
|
247
290
|
|
|
248
291
|
def get_raw_key(body)
|
|
249
292
|
if @sqs.event_bridge_mode
|
|
250
|
-
body
|
|
293
|
+
body.dig("detail", "object", "key")
|
|
251
294
|
else
|
|
252
|
-
body
|
|
295
|
+
body.dig("Records", 0, "s3", "object", "key")
|
|
253
296
|
end
|
|
254
297
|
end
|
|
255
298
|
|
|
@@ -348,10 +391,7 @@ module Fluent::Plugin
|
|
|
348
391
|
raise "can't call S3 API. Please check your credentials or s3_region configuration. error = #{e.inspect}"
|
|
349
392
|
end
|
|
350
393
|
|
|
351
|
-
def process(
|
|
352
|
-
raw_key = get_raw_key(body)
|
|
353
|
-
key = CGI.unescape(raw_key)
|
|
354
|
-
|
|
394
|
+
def process(raw_key, key)
|
|
355
395
|
io = @bucket.object(key).get.body
|
|
356
396
|
content = @extractor.extract(io)
|
|
357
397
|
es = Fluent::MultiEventStream.new
|
|
@@ -368,7 +408,8 @@ module Fluent::Plugin
|
|
|
368
408
|
end
|
|
369
409
|
|
|
370
410
|
class Extractor
|
|
371
|
-
class SizeLimitError <
|
|
411
|
+
class SizeLimitError < Fluent::UnrecoverableError; end
|
|
412
|
+
class CommandError < Fluent::UnrecoverableError; end
|
|
372
413
|
|
|
373
414
|
include Fluent::Configurable
|
|
374
415
|
|
|
@@ -431,10 +472,16 @@ module Fluent::Plugin
|
|
|
431
472
|
end
|
|
432
473
|
end
|
|
433
474
|
|
|
434
|
-
|
|
475
|
+
status = wait_thr.value
|
|
476
|
+
if status.success?
|
|
435
477
|
out
|
|
478
|
+
elsif status.signaled?
|
|
479
|
+
# not a CommandError: a signal comes from outside the data, so a retry may succeed
|
|
480
|
+
raise "Command #{command} was terminated by signal #{status.termsig}"
|
|
436
481
|
else
|
|
437
|
-
|
|
482
|
+
stderr_tail = stderr.read.strip[0, 200]
|
|
483
|
+
reason = stderr_tail.empty? ? "" : ": #{stderr_tail}"
|
|
484
|
+
raise CommandError, "Command #{command} exited with #{status.exitstatus}#{reason}"
|
|
438
485
|
end
|
|
439
486
|
end
|
|
440
487
|
end
|
data/lib/fluent/plugin/out_s3.rb
CHANGED
|
@@ -121,7 +121,7 @@ module Fluent::Plugin
|
|
|
121
121
|
desc "Check AWS key on start"
|
|
122
122
|
config_param :check_apikey_on_start, :bool, default: true
|
|
123
123
|
desc "URI of proxy environment"
|
|
124
|
-
config_param :proxy_uri, :string, default: nil
|
|
124
|
+
config_param :proxy_uri, :string, default: nil, secret: true
|
|
125
125
|
desc "Use S3 reduced redundancy storage for 33% cheaper pricing. Deprecated. Use storage_class instead"
|
|
126
126
|
config_param :reduced_redundancy, :bool, default: false, deprecated: "Use storage_class parameter instead."
|
|
127
127
|
desc "The type of storage to use for the object(STANDARD,REDUCED_REDUNDANCY,STANDARD_IA)"
|
|
@@ -162,9 +162,9 @@ module Fluent::Plugin
|
|
|
162
162
|
config_param :warn_for_delay, :time, default: nil
|
|
163
163
|
desc "Arbitrary S3 tag-set for the object"
|
|
164
164
|
config_param :tagging, :string, default: nil
|
|
165
|
-
desc "Arbitrary S3 metadata headers to set for the object"
|
|
166
|
-
config_param :checksum_algorithm, :string, default: nil
|
|
167
165
|
desc "Indicates the algorithm you want Amazon S3 to use to create the checksum for the object (CRC32,CRC32C,SHA1,SHA256)"
|
|
166
|
+
config_param :checksum_algorithm, :string, default: nil
|
|
167
|
+
desc "Arbitrary S3 metadata headers to set for the object"
|
|
168
168
|
config_param :s3_metadata, :hash, default: nil
|
|
169
169
|
config_section :bucket_lifecycle_rule, param_name: :bucket_lifecycle_rules, multi: true do
|
|
170
170
|
desc "A unique ID for this rule"
|
|
@@ -362,7 +362,7 @@ module Fluent::Plugin
|
|
|
362
362
|
begin
|
|
363
363
|
@compressor.compress(chunk, tmp)
|
|
364
364
|
tmp.rewind
|
|
365
|
-
log.debug "out_s3: write chunk #{dump_unique_id_hex(chunk.unique_id)} with metadata #{chunk.metadata} to s3://#{@s3_bucket}/#{s3path}"
|
|
365
|
+
log.debug { "out_s3: write chunk #{dump_unique_id_hex(chunk.unique_id)} with metadata #{chunk.metadata} to s3://#{@s3_bucket}/#{s3path}" }
|
|
366
366
|
|
|
367
367
|
put_options = {
|
|
368
368
|
body: tmp,
|
|
@@ -390,7 +390,7 @@ module Fluent::Plugin
|
|
|
390
390
|
end
|
|
391
391
|
@bucket.object(s3path).put(put_options)
|
|
392
392
|
|
|
393
|
-
log.debug "out_s3: completed to write chunk #{dump_unique_id_hex(chunk.unique_id)} with metadata #{chunk.metadata} to s3://#{@s3_bucket}/#{s3path}"
|
|
393
|
+
log.debug { "out_s3: completed to write chunk #{dump_unique_id_hex(chunk.unique_id)} with metadata #{chunk.metadata} to s3://#{@s3_bucket}/#{s3path}" }
|
|
394
394
|
|
|
395
395
|
@values_for_s3_object_chunk.delete(chunk.unique_id)
|
|
396
396
|
|
|
@@ -21,7 +21,7 @@ module Fluent::Plugin
|
|
|
21
21
|
def extract(io)
|
|
22
22
|
begin
|
|
23
23
|
extract_with_command("gzip #{@command_parameter}", io, "gzip-temp")
|
|
24
|
-
rescue
|
|
24
|
+
rescue Fluent::UnrecoverableError
|
|
25
25
|
raise
|
|
26
26
|
rescue => e
|
|
27
27
|
log.warn "gzip command execution failed: #{e.message}. Fallback to GzipExtractor."
|
|
@@ -21,10 +21,10 @@ module Fluent::Plugin
|
|
|
21
21
|
def extract(io)
|
|
22
22
|
begin
|
|
23
23
|
extract_with_command("xz #{@command_parameter}", io, "xz-temp")
|
|
24
|
-
rescue
|
|
24
|
+
rescue Fluent::UnrecoverableError
|
|
25
25
|
raise
|
|
26
|
-
rescue
|
|
27
|
-
raise "Failed to extract
|
|
26
|
+
rescue => e
|
|
27
|
+
raise "Failed to extract with the xz command: #{e.message}"
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -21,10 +21,10 @@ module Fluent::Plugin
|
|
|
21
21
|
def extract(io)
|
|
22
22
|
begin
|
|
23
23
|
extract_with_command("lzop #{@command_parameter}", io, "lzop-temp")
|
|
24
|
-
rescue
|
|
24
|
+
rescue Fluent::UnrecoverableError
|
|
25
25
|
raise
|
|
26
|
-
rescue
|
|
27
|
-
raise "Failed to extract
|
|
26
|
+
rescue => e
|
|
27
|
+
raise "Failed to extract with the lzop command: #{e.message}"
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
data/test/test_in_s3.rb
CHANGED
|
@@ -834,4 +834,281 @@ EOS
|
|
|
834
834
|
assert_equal({ "message" => "aaa" }, events.first[2])
|
|
835
835
|
end
|
|
836
836
|
|
|
837
|
+
POLL_CONFIG = CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\n"
|
|
838
|
+
|
|
839
|
+
EVENT_BRIDGE_POLL_CONFIG =
|
|
840
|
+
CONFIG.sub("<sqs>", "<sqs>\n event_bridge_mode true") +
|
|
841
|
+
"\ncheck_apikey_on_start false\nstore_as text\nformat none\n"
|
|
842
|
+
|
|
843
|
+
def notification(key)
|
|
844
|
+
JSON.generate({ "Records" => [{ "s3" => { "object" => { "key" => key } } }] })
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def gzipped(text)
|
|
848
|
+
buffer = StringIO.new(String.new, "wb")
|
|
849
|
+
Zlib::GzipWriter.wrap(buffer) { |gz| gz.write(text) }
|
|
850
|
+
buffer.string
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def stub_s3_object(payload: "aaa", get_error: nil)
|
|
854
|
+
s3_response = stub(Object.new)
|
|
855
|
+
s3_response.body { StringIO.new(payload) }
|
|
856
|
+
s3_object = stub(Object.new)
|
|
857
|
+
s3_object.get { raise get_error if get_error; s3_response }
|
|
858
|
+
@s3_bucket.object(anything).at_least(0) do |key|
|
|
859
|
+
raise ArgumentError, ":key must not be blank" if key.to_s.empty?
|
|
860
|
+
raise ArgumentError, "invalid byte sequence in UTF-8" unless key.to_s.valid_encoding?
|
|
861
|
+
s3_object
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def poll_once(d, message)
|
|
866
|
+
deleted = []
|
|
867
|
+
@sqs_poller.delete_messages(anything) { |messages| deleted.concat(messages) }
|
|
868
|
+
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
869
|
+
config.before_request.call(stats) if config.before_request
|
|
870
|
+
stats.request_count += 1
|
|
871
|
+
if stats.request_count >= 1
|
|
872
|
+
d.instance.instance_variable_set(:@running, false)
|
|
873
|
+
end
|
|
874
|
+
[message]
|
|
875
|
+
end
|
|
876
|
+
deleted
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def warn_logs(d)
|
|
880
|
+
d.logs.select { |log| log.include?("[warn]") }
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
data(
|
|
884
|
+
"body is not JSON" => "hello",
|
|
885
|
+
"body is empty" => "",
|
|
886
|
+
"body is not a JSON object" => "[]",
|
|
887
|
+
"body is null" => "null",
|
|
888
|
+
"Records is empty" => '{"Records":[]}',
|
|
889
|
+
"record without s3" => '{"Records":[{}]}',
|
|
890
|
+
"record without object" => '{"Records":[{"s3":{}}]}',
|
|
891
|
+
"record without key" => '{"Records":[{"s3":{"object":{}}}]}',
|
|
892
|
+
"Records is a string" => '{"Records":"test_key"}',
|
|
893
|
+
"Records is an object" => '{"Records":{"s3":{}}}',
|
|
894
|
+
"key is not a string" => '{"Records":[{"s3":{"object":{"key":1}}}]}',
|
|
895
|
+
"key is empty" => '{"Records":[{"s3":{"object":{"key":""}}}]}',
|
|
896
|
+
"key holds invalid utf-8" => %Q({"Records":[{"s3":{"object":{"key":"a\xFF\xFEb"}}}]}),
|
|
897
|
+
"key decodes to invalid utf-8" => '{"Records":[{"s3":{"object":{"key":"%FF"}}}]}',
|
|
898
|
+
)
|
|
899
|
+
def test_unrecoverable_message_is_deleted(body)
|
|
900
|
+
setup_mocks
|
|
901
|
+
stub_s3_object
|
|
902
|
+
d = create_driver(POLL_CONFIG)
|
|
903
|
+
|
|
904
|
+
message = Struct::StubMessage.new(1, 1, body)
|
|
905
|
+
deleted = poll_once(d, message)
|
|
906
|
+
|
|
907
|
+
assert_nothing_raised { d.run {} }
|
|
908
|
+
assert_equal([message], deleted)
|
|
909
|
+
assert_true(warn_logs(d).any? { |log| log.include?("Skipping SQS message") })
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def test_unrecoverable_message_in_event_bridge_mode_is_deleted
|
|
913
|
+
setup_mocks
|
|
914
|
+
stub_s3_object
|
|
915
|
+
d = create_driver(EVENT_BRIDGE_POLL_CONFIG)
|
|
916
|
+
|
|
917
|
+
message = Struct::StubMessage.new(1, 1, '{"detail":{}}')
|
|
918
|
+
deleted = poll_once(d, message)
|
|
919
|
+
|
|
920
|
+
assert_nothing_raised { d.run {} }
|
|
921
|
+
assert_equal([message], deleted)
|
|
922
|
+
assert_true(warn_logs(d).any? { |log| log.include?("Skipping SQS message") })
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
data(
|
|
926
|
+
"no Records property" => "{}",
|
|
927
|
+
"sns envelope" => '{"Type":"Notification","Message":"{}"}',
|
|
928
|
+
"s3 test event" => '{"Service":"Amazon S3","Event":"s3:TestEvent"}',
|
|
929
|
+
)
|
|
930
|
+
def test_message_that_is_not_an_s3_notification_is_reported(body)
|
|
931
|
+
setup_mocks
|
|
932
|
+
stub_s3_object
|
|
933
|
+
d = create_driver(POLL_CONFIG)
|
|
934
|
+
mock(d.instance).process(anything).never
|
|
935
|
+
|
|
936
|
+
message = Struct::StubMessage.new(1, 1, body)
|
|
937
|
+
deleted = poll_once(d, message)
|
|
938
|
+
d.run {}
|
|
939
|
+
|
|
940
|
+
assert_equal([message], deleted)
|
|
941
|
+
assert_true(warn_logs(d).any? { |log| log.include?("not an S3 notification") })
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
data(
|
|
945
|
+
"missing object" => "NoSuchKey",
|
|
946
|
+
"missing bucket" => "NoSuchBucket",
|
|
947
|
+
"access denied" => "AccessDenied",
|
|
948
|
+
"throttled" => "SlowDown",
|
|
949
|
+
)
|
|
950
|
+
def test_service_error_keeps_the_message_for_redelivery(error_name)
|
|
951
|
+
setup_mocks
|
|
952
|
+
stub_s3_object(get_error: Aws::S3::Errors.const_get(error_name).new(nil, error_name))
|
|
953
|
+
d = create_driver(POLL_CONFIG)
|
|
954
|
+
|
|
955
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
956
|
+
deleted = poll_once(d, message)
|
|
957
|
+
d.run {}
|
|
958
|
+
|
|
959
|
+
assert_equal([], deleted)
|
|
960
|
+
assert_true(warn_logs(d).any? { |log| log.include?("Failed to handle SQS message") })
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def test_networking_error_keeps_the_message_for_redelivery
|
|
964
|
+
setup_mocks
|
|
965
|
+
stub_s3_object(get_error: Seahorse::Client::NetworkingError.new(Errno::ECONNRESET.new))
|
|
966
|
+
d = create_driver(POLL_CONFIG)
|
|
967
|
+
|
|
968
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
969
|
+
deleted = poll_once(d, message)
|
|
970
|
+
d.run {}
|
|
971
|
+
|
|
972
|
+
assert_equal([], deleted)
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
data(
|
|
976
|
+
"gzip" => "gzip",
|
|
977
|
+
"gzip_command" => "gzip_command",
|
|
978
|
+
"lzo" => "lzo",
|
|
979
|
+
"lzma2" => "lzma2",
|
|
980
|
+
)
|
|
981
|
+
def test_corrupt_archive_is_deleted(store_type)
|
|
982
|
+
setup_mocks
|
|
983
|
+
stub_s3_object(payload: "this is not an archive")
|
|
984
|
+
begin
|
|
985
|
+
d = create_driver(POLL_CONFIG.sub("store_as text", "store_as #{store_type}"))
|
|
986
|
+
rescue Fluent::ConfigError => e
|
|
987
|
+
pend(e.message)
|
|
988
|
+
end
|
|
989
|
+
|
|
990
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
991
|
+
deleted = poll_once(d, message)
|
|
992
|
+
d.run {}
|
|
993
|
+
|
|
994
|
+
assert_equal([message], deleted)
|
|
995
|
+
assert_true(warn_logs(d).any? { |log| log.include?("Skipping SQS message") })
|
|
996
|
+
end
|
|
997
|
+
|
|
998
|
+
def test_object_over_the_decompression_limit_is_deleted
|
|
999
|
+
setup_mocks
|
|
1000
|
+
stub_s3_object(payload: gzipped("a" * 100))
|
|
1001
|
+
d = create_driver(POLL_CONFIG.sub("store_as text", "store_as gzip") + "\ndecompression_size_limit 10\n")
|
|
1002
|
+
|
|
1003
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
1004
|
+
deleted = poll_once(d, message)
|
|
1005
|
+
d.run {}
|
|
1006
|
+
|
|
1007
|
+
assert_equal([message], deleted)
|
|
1008
|
+
assert_true(d.logs.any? { |log| log.include?("Extracted data exceeds limit") })
|
|
1009
|
+
end
|
|
1010
|
+
|
|
1011
|
+
def test_unrecoverable_message_is_not_redelivered
|
|
1012
|
+
setup_mocks
|
|
1013
|
+
stub_s3_object
|
|
1014
|
+
d = create_driver(POLL_CONFIG)
|
|
1015
|
+
|
|
1016
|
+
deleted = []
|
|
1017
|
+
attempts = 0
|
|
1018
|
+
@sqs_poller.delete_messages(anything) { |messages| deleted.concat(messages) }
|
|
1019
|
+
message = Struct::StubMessage.new("mid-1", "rh-1", '{"Records":[]}')
|
|
1020
|
+
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
1021
|
+
config.before_request.call(stats) if config.before_request
|
|
1022
|
+
stats.request_count += 1
|
|
1023
|
+
if stats.request_count >= 5
|
|
1024
|
+
d.instance.instance_variable_set(:@running, false)
|
|
1025
|
+
end
|
|
1026
|
+
next [] unless deleted.empty?
|
|
1027
|
+
attempts += 1
|
|
1028
|
+
[message]
|
|
1029
|
+
end
|
|
1030
|
+
d.run {}
|
|
1031
|
+
|
|
1032
|
+
assert_equal([message], deleted)
|
|
1033
|
+
assert_equal(1, attempts)
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
def test_a_bug_in_the_plugin_keeps_the_message_for_redelivery
|
|
1037
|
+
setup_mocks
|
|
1038
|
+
stub_s3_object
|
|
1039
|
+
d = create_driver(POLL_CONFIG)
|
|
1040
|
+
stub(d.instance).is_valid_queue(anything) { raise NoMethodError, "undefined method 'foo' for nil" }
|
|
1041
|
+
|
|
1042
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
1043
|
+
deleted = poll_once(d, message)
|
|
1044
|
+
d.run {}
|
|
1045
|
+
|
|
1046
|
+
assert_equal([], deleted)
|
|
1047
|
+
assert_true(warn_logs(d).any? { |log| log.include?("Failed to handle SQS message") })
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
def test_unparsable_object_is_deleted
|
|
1051
|
+
setup_mocks
|
|
1052
|
+
stub_s3_object(payload: "\xC1")
|
|
1053
|
+
d = create_driver(POLL_CONFIG.sub("format none", "<parse>\n @type msgpack\n</parse>"))
|
|
1054
|
+
|
|
1055
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
1056
|
+
deleted = poll_once(d, message)
|
|
1057
|
+
d.run {}
|
|
1058
|
+
|
|
1059
|
+
assert_equal([message], deleted)
|
|
1060
|
+
assert_true(warn_logs(d).any? { |log| log.include?("Skipping SQS message") })
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
def test_deleting_warn_names_the_object_key
|
|
1064
|
+
setup_mocks
|
|
1065
|
+
stub_s3_object(payload: "this is not an archive")
|
|
1066
|
+
d = create_driver(POLL_CONFIG.sub("store_as text", "store_as gzip"))
|
|
1067
|
+
|
|
1068
|
+
message = Struct::StubMessage.new(1, 1, notification("path/to/broken.gz"))
|
|
1069
|
+
deleted = poll_once(d, message)
|
|
1070
|
+
d.run {}
|
|
1071
|
+
|
|
1072
|
+
assert_equal([message], deleted)
|
|
1073
|
+
assert_true(warn_logs(d).any? { |log| log.include?(%Q(key="path/to/broken.gz")) })
|
|
1074
|
+
assert_true(warn_logs(d).any? { |log| log.include?("in_s3.rb:") })
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
def test_deleting_warn_truncates_a_long_object_key
|
|
1078
|
+
setup_mocks
|
|
1079
|
+
stub_s3_object(payload: "this is not an archive")
|
|
1080
|
+
d = create_driver(POLL_CONFIG.sub("store_as text", "store_as gzip"))
|
|
1081
|
+
|
|
1082
|
+
message = Struct::StubMessage.new(1, 1, notification("\u3042" * 400))
|
|
1083
|
+
deleted = poll_once(d, message)
|
|
1084
|
+
d.run {}
|
|
1085
|
+
|
|
1086
|
+
assert_equal([message], deleted)
|
|
1087
|
+
line = warn_logs(d).find { |log| log.include?("Skipping SQS message") }
|
|
1088
|
+
assert_operator(line[/key="(.*)"/, 1].length, :<=, Fluent::Plugin::S3Input::LOG_MAX_BYTES)
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
def test_a_truncated_object_key_is_still_valid_utf8
|
|
1092
|
+
d = create_driver(POLL_CONFIG)
|
|
1093
|
+
logged = d.instance.__send__(:loggable, "\u3042" * 400)
|
|
1094
|
+
|
|
1095
|
+
assert_true(logged.valid_encoding?)
|
|
1096
|
+
assert_operator(logged.bytesize, :<=, Fluent::Plugin::S3Input::LOG_MAX_BYTES)
|
|
1097
|
+
assert_nothing_raised { JSON.generate([logged]) }
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
def test_valid_message_is_processed_and_deleted
|
|
1101
|
+
setup_mocks
|
|
1102
|
+
stub_s3_object
|
|
1103
|
+
d = create_driver(POLL_CONFIG)
|
|
1104
|
+
|
|
1105
|
+
message = Struct::StubMessage.new(1, 1, notification("test_key"))
|
|
1106
|
+
deleted = poll_once(d, message)
|
|
1107
|
+
d.run(expect_emits: 1)
|
|
1108
|
+
|
|
1109
|
+
assert_equal([message], deleted)
|
|
1110
|
+
assert_equal({ "message" => "aaa" }, d.events.first[2])
|
|
1111
|
+
assert_equal([], warn_logs(d))
|
|
1112
|
+
end
|
|
1113
|
+
|
|
837
1114
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluent-plugin-s3
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.8.
|
|
4
|
+
version: 1.8.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sadayuki Furuhashi
|
|
@@ -214,7 +214,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
214
214
|
- !ruby/object:Gem::Version
|
|
215
215
|
version: '0'
|
|
216
216
|
requirements: []
|
|
217
|
-
rubygems_version: 4.0.
|
|
217
|
+
rubygems_version: 4.0.18
|
|
218
218
|
specification_version: 4
|
|
219
219
|
summary: Amazon S3 output plugin for Fluentd event collector
|
|
220
220
|
test_files:
|