fluent-plugin-s3 1.8.4 → 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/dependabot.yml +17 -0
- data/.github/workflows/linux.yml +13 -4
- data/.github/workflows/stale-actions.yml +4 -1
- data/ChangeLog +12 -0
- data/Gemfile +1 -1
- data/Gemfile.v0.12 +1 -1
- data/README.md +1 -2
- data/VERSION +1 -1
- data/docs/input.md +16 -2
- data/docs/output.md +2 -2
- data/lib/fluent/plugin/in_s3.rb +124 -24
- data/lib/fluent/plugin/out_s3.rb +5 -5
- data/lib/fluent/plugin/s3_extractor_gzip_command.rb +9 -20
- data/lib/fluent/plugin/s3_extractor_lzma2.rb +6 -14
- data/lib/fluent/plugin/s3_extractor_lzo.rb +6 -14
- data/test/test_in_s3.rb +373 -11
- 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/dependabot.yml
CHANGED
|
@@ -4,3 +4,20 @@ updates:
|
|
|
4
4
|
directory: '/'
|
|
5
5
|
schedule:
|
|
6
6
|
interval: 'monthly'
|
|
7
|
+
groups:
|
|
8
|
+
# PR: "Security update [package] from [old] to [new]"
|
|
9
|
+
# This PR should be merged in hurry
|
|
10
|
+
security-updates:
|
|
11
|
+
applies-to: security-updates
|
|
12
|
+
patterns:
|
|
13
|
+
- '*'
|
|
14
|
+
|
|
15
|
+
# PR: "Bump [package] from [old] to [new]"
|
|
16
|
+
# No need to be merged this PR in hurry. It is enough to merge
|
|
17
|
+
# once in a month.
|
|
18
|
+
monthly-updates:
|
|
19
|
+
applies-to: version-updates
|
|
20
|
+
patterns:
|
|
21
|
+
- '*'
|
|
22
|
+
# Allow to create PR both of security and normal updates.
|
|
23
|
+
open-pull-requests-limit: 1
|
data/.github/workflows/linux.yml
CHANGED
|
@@ -4,25 +4,34 @@ on:
|
|
|
4
4
|
branches: [master]
|
|
5
5
|
pull_request:
|
|
6
6
|
branches: [master]
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: '0 0 1 * *'
|
|
7
9
|
jobs:
|
|
10
|
+
ruby-versions:
|
|
11
|
+
uses: ruby/actions/.github/workflows/ruby_versions.yml@master
|
|
12
|
+
with:
|
|
13
|
+
engine: cruby
|
|
14
|
+
min_version: 2.7
|
|
8
15
|
build:
|
|
16
|
+
needs: ruby-versions
|
|
9
17
|
runs-on: ${{ matrix.os }}
|
|
10
18
|
strategy:
|
|
11
19
|
fail-fast: false
|
|
12
20
|
matrix:
|
|
13
|
-
ruby:
|
|
21
|
+
ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
|
|
22
|
+
exclude:
|
|
23
|
+
- ruby: head
|
|
14
24
|
os:
|
|
15
25
|
- ubuntu-latest
|
|
16
26
|
name: Ruby ${{ matrix.ruby }} unit testing on ${{ matrix.os }}
|
|
17
27
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
19
|
-
- uses: ruby/setup-ruby@v1
|
|
28
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
29
|
+
- uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b # v1.321.0
|
|
20
30
|
with:
|
|
21
31
|
ruby-version: ${{ matrix.ruby }}
|
|
22
32
|
- name: unit testing
|
|
23
33
|
env:
|
|
24
34
|
CI: true
|
|
25
35
|
run: |
|
|
26
|
-
gem install rake
|
|
27
36
|
bundle install --jobs 4 --retry 3
|
|
28
37
|
bundle exec rake test
|
|
@@ -6,8 +6,11 @@ on:
|
|
|
6
6
|
jobs:
|
|
7
7
|
stale:
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
|
+
permissions:
|
|
10
|
+
issues: write
|
|
11
|
+
pull-requests: write
|
|
9
12
|
steps:
|
|
10
|
-
- uses: actions/stale@
|
|
13
|
+
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
|
|
11
14
|
with:
|
|
12
15
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
13
16
|
days-before-stale: 30
|
data/ChangeLog
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
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
|
+
|
|
9
|
+
Release 1.8.5 - 2026/06/25
|
|
10
|
+
|
|
11
|
+
* in_s3: enforce size limits on decompressed payloads
|
|
12
|
+
|
|
1
13
|
Release 1.8.4 - 2026/03/04
|
|
2
14
|
|
|
3
15
|
* in_s3: add aws_profile / aws_credential_process parameters for credencials (GitHub: #464)
|
data/Gemfile
CHANGED
data/Gemfile.v0.12
CHANGED
data/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Amazon S3 plugin for [Fluentd](http://github.com/fluent/fluentd)
|
|
2
2
|
|
|
3
|
-
[
|
|
4
|
-
alt="Build Status" />](https://travis-ci.org/fluent/fluent-plugin-s3) [<img
|
|
3
|
+
[](https://github.com/fluent/fluent-plugin-s3/actions/workflows/linux.yml) [<img
|
|
5
4
|
src="https://codeclimate.com/github/fluent/fluent-plugin-s3/badges/gpa.svg"
|
|
6
5
|
/>](https://codeclimate.com/github/fluent/fluent-plugin-s3)
|
|
7
6
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.8.
|
|
1
|
+
1.8.6
|
data/docs/input.md
CHANGED
|
@@ -29,6 +29,12 @@ See also [Configuration: credentials](credentials.md) for common comprehensive p
|
|
|
29
29
|
|
|
30
30
|
Whether or not object metadata should be added to the record. Defaults to `false`. See below for details.
|
|
31
31
|
|
|
32
|
+
## decompression_size_limit
|
|
33
|
+
|
|
34
|
+
The size limit of the decompressed data. The default is `256m` (256 MiB).
|
|
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).
|
|
37
|
+
|
|
32
38
|
## match_regexp
|
|
33
39
|
|
|
34
40
|
If provided, process the S3 object only if its keys matches the regular expression
|
|
@@ -70,7 +76,7 @@ Check AWS key on start. Default is true.
|
|
|
70
76
|
|
|
71
77
|
## proxy_uri
|
|
72
78
|
|
|
73
|
-
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.
|
|
74
80
|
|
|
75
81
|
## \<sqs\> section
|
|
76
82
|
|
|
@@ -104,4 +110,12 @@ Interval to retry polling SQS if polling unsuccessful, in seconds. Default is 30
|
|
|
104
110
|
|
|
105
111
|
### event_bridge_mode
|
|
106
112
|
When true, Amazon S3 Event Notification should be configured using the EventBridge integration. Default is false.
|
|
107
|
-
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
|
@@ -209,7 +209,7 @@ parquet file page size. default: 8192 bytes
|
|
|
209
209
|
|
|
210
210
|
### parquet_row_group_size
|
|
211
211
|
|
|
212
|
-
parquet file row group size. default: 128
|
|
212
|
+
parquet file row group size. default: 128 MiB
|
|
213
213
|
|
|
214
214
|
### record_type
|
|
215
215
|
|
|
@@ -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,11 +24,24 @@ 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
|
|
36
|
+
DECOMPRESSION_SIZE_LIMIT = 256 * 1024 * 1024
|
|
37
|
+
LOG_MAX_BYTES = 256
|
|
25
38
|
|
|
26
39
|
desc "Use aws-sdk-ruby bundled cert"
|
|
27
40
|
config_param :use_bundled_cert, :bool, default: false
|
|
28
41
|
desc "Add object metadata to the records parsed out of a given object"
|
|
29
42
|
config_param :add_object_metadata, :bool, default: false
|
|
43
|
+
desc 'The size limit of the extracted element.'
|
|
44
|
+
config_param :decompression_size_limit, :size, default: DECOMPRESSION_SIZE_LIMIT
|
|
30
45
|
desc "AWS access key id"
|
|
31
46
|
config_param :aws_key_id, :string, default: nil, secret: true
|
|
32
47
|
desc "AWS secret key."
|
|
@@ -94,7 +109,7 @@ module Fluent::Plugin
|
|
|
94
109
|
desc "Check AWS key on start"
|
|
95
110
|
config_param :check_apikey_on_start, :bool, default: true
|
|
96
111
|
desc "URI of proxy environment"
|
|
97
|
-
config_param :proxy_uri, :string, default: nil
|
|
112
|
+
config_param :proxy_uri, :string, default: nil, secret: true
|
|
98
113
|
desc "Optional RegEx to match incoming messages"
|
|
99
114
|
config_param :match_regexp, :regexp, default: nil
|
|
100
115
|
|
|
@@ -159,7 +174,7 @@ module Fluent::Plugin
|
|
|
159
174
|
|
|
160
175
|
Aws.use_bundled_cert! if @use_bundled_cert
|
|
161
176
|
|
|
162
|
-
@extractor = EXTRACTOR_REGISTRY.lookup(@store_as).new(log: log)
|
|
177
|
+
@extractor = EXTRACTOR_REGISTRY.lookup(@store_as).new(log: log, decompression_size_limit: @decompression_size_limit)
|
|
163
178
|
@extractor.configure(conf)
|
|
164
179
|
|
|
165
180
|
@parser = parser_create(conf: parser_config, default_type: DEFAULT_PARSE_TYPE)
|
|
@@ -210,17 +225,18 @@ module Fluent::Plugin
|
|
|
210
225
|
begin
|
|
211
226
|
@poller.poll(options) do |message|
|
|
212
227
|
begin
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
222
238
|
rescue => e
|
|
223
|
-
log.warn(error: e)
|
|
239
|
+
log.warn("Failed to handle SQS message", message_id: message.message_id, error: e)
|
|
224
240
|
log.warn_backtrace(e.backtrace)
|
|
225
241
|
throw :skip_delete
|
|
226
242
|
end
|
|
@@ -232,6 +248,36 @@ module Fluent::Plugin
|
|
|
232
248
|
end
|
|
233
249
|
end
|
|
234
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
|
+
|
|
235
281
|
def is_valid_queue(body)
|
|
236
282
|
if @sqs.event_bridge_mode
|
|
237
283
|
log.debug("checking for eventbridge property")
|
|
@@ -244,9 +290,9 @@ module Fluent::Plugin
|
|
|
244
290
|
|
|
245
291
|
def get_raw_key(body)
|
|
246
292
|
if @sqs.event_bridge_mode
|
|
247
|
-
body
|
|
293
|
+
body.dig("detail", "object", "key")
|
|
248
294
|
else
|
|
249
|
-
body
|
|
295
|
+
body.dig("Records", 0, "s3", "object", "key")
|
|
250
296
|
end
|
|
251
297
|
end
|
|
252
298
|
|
|
@@ -345,10 +391,7 @@ module Fluent::Plugin
|
|
|
345
391
|
raise "can't call S3 API. Please check your credentials or s3_region configuration. error = #{e.inspect}"
|
|
346
392
|
end
|
|
347
393
|
|
|
348
|
-
def process(
|
|
349
|
-
raw_key = get_raw_key(body)
|
|
350
|
-
key = CGI.unescape(raw_key)
|
|
351
|
-
|
|
394
|
+
def process(raw_key, key)
|
|
352
395
|
io = @bucket.object(key).get.body
|
|
353
396
|
content = @extractor.extract(io)
|
|
354
397
|
es = Fluent::MultiEventStream.new
|
|
@@ -365,13 +408,19 @@ module Fluent::Plugin
|
|
|
365
408
|
end
|
|
366
409
|
|
|
367
410
|
class Extractor
|
|
411
|
+
class SizeLimitError < Fluent::UnrecoverableError; end
|
|
412
|
+
class CommandError < Fluent::UnrecoverableError; end
|
|
413
|
+
|
|
368
414
|
include Fluent::Configurable
|
|
369
415
|
|
|
370
416
|
attr_reader :log
|
|
371
417
|
|
|
372
|
-
|
|
418
|
+
BYTES_TO_READ = 64 * 1024
|
|
419
|
+
|
|
420
|
+
def initialize(log: $log, decompression_size_limit: DECOMPRESSION_SIZE_LIMIT, **options)
|
|
373
421
|
super()
|
|
374
422
|
@log = log
|
|
423
|
+
@decompression_size_limit = decompression_size_limit
|
|
375
424
|
end
|
|
376
425
|
|
|
377
426
|
def configure(conf)
|
|
@@ -399,6 +448,44 @@ module Fluent::Plugin
|
|
|
399
448
|
raise Fluent::ConfigError, "'#{command}' utility must be in PATH for #{algo} compression"
|
|
400
449
|
end
|
|
401
450
|
end
|
|
451
|
+
|
|
452
|
+
def extract_with_command(command, io, tempfile_basename = "s3-extractor-tmp")
|
|
453
|
+
path = if io.respond_to?(:path)
|
|
454
|
+
io.path
|
|
455
|
+
else
|
|
456
|
+
extractor = TextExtractor.new(log: log, decompression_size_limit: @decompression_size_limit)
|
|
457
|
+
temp = Tempfile.new(tempfile_basename)
|
|
458
|
+
temp.write(extractor.extract(io))
|
|
459
|
+
temp.close
|
|
460
|
+
temp.path
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
out = ''
|
|
464
|
+
begin
|
|
465
|
+
Open3.popen3("#{command} #{path}") do |stdin, stdout, stderr, wait_thr|
|
|
466
|
+
stdin.close
|
|
467
|
+
while (chunk = stdout.read(BYTES_TO_READ))
|
|
468
|
+
out << chunk
|
|
469
|
+
if out.bytesize > @decompression_size_limit
|
|
470
|
+
Process.kill("TERM", wait_thr.pid) rescue nil
|
|
471
|
+
raise SizeLimitError, "Extracted data exceeds limit of #{@decompression_size_limit} bytes"
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
status = wait_thr.value
|
|
476
|
+
if status.success?
|
|
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}"
|
|
481
|
+
else
|
|
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}"
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
402
489
|
end
|
|
403
490
|
|
|
404
491
|
class GzipExtractor < Extractor
|
|
@@ -414,19 +501,25 @@ module Fluent::Plugin
|
|
|
414
501
|
# https://bugs.ruby-lang.org/issues/11180
|
|
415
502
|
# https://github.com/exAspArk/multiple_files_gzip_reader
|
|
416
503
|
def extract(io)
|
|
417
|
-
|
|
504
|
+
out = ''
|
|
418
505
|
loop do
|
|
419
506
|
unused = nil
|
|
420
507
|
Zlib::GzipReader.wrap(io) do |gz|
|
|
421
|
-
|
|
508
|
+
while (chunk = gz.read(BYTES_TO_READ))
|
|
509
|
+
out << chunk
|
|
510
|
+
if out.bytesize > @decompression_size_limit
|
|
511
|
+
raise SizeLimitError, "Extracted data exceeds limit of #{@decompression_size_limit} bytes"
|
|
512
|
+
end
|
|
513
|
+
end
|
|
422
514
|
unused = gz.unused
|
|
423
515
|
gz.finish
|
|
424
516
|
end
|
|
425
517
|
io.pos -= unused ? unused.length : 0
|
|
426
518
|
break if io.eof?
|
|
427
519
|
end
|
|
428
|
-
|
|
429
|
-
|
|
520
|
+
out
|
|
521
|
+
ensure
|
|
522
|
+
io.close unless io.closed?
|
|
430
523
|
end
|
|
431
524
|
end
|
|
432
525
|
|
|
@@ -440,7 +533,14 @@ module Fluent::Plugin
|
|
|
440
533
|
end
|
|
441
534
|
|
|
442
535
|
def extract(io)
|
|
443
|
-
|
|
536
|
+
out = ''
|
|
537
|
+
while (chunk = io.read(BYTES_TO_READ))
|
|
538
|
+
out << chunk
|
|
539
|
+
if out.bytesize > @decompression_size_limit
|
|
540
|
+
raise SizeLimitError, "Extracted data exceeds limit of #{@decompression_size_limit} bytes"
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
out
|
|
444
544
|
end
|
|
445
545
|
end
|
|
446
546
|
|
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
|
|
|
@@ -19,26 +19,15 @@ module Fluent::Plugin
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def extract(io)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
stdout, succeeded = Open3.capture2("gzip #{@command_parameter} #{path}")
|
|
32
|
-
if succeeded
|
|
33
|
-
stdout
|
|
34
|
-
else
|
|
35
|
-
log.warn "failed to execute gzip command. Fallback to GzipReader. status = #{succeeded}"
|
|
36
|
-
begin
|
|
37
|
-
io.rewind
|
|
38
|
-
Zlib::GzipReader.wrap(io) do |gz|
|
|
39
|
-
gz.read
|
|
40
|
-
end
|
|
41
|
-
end
|
|
22
|
+
begin
|
|
23
|
+
extract_with_command("gzip #{@command_parameter}", io, "gzip-temp")
|
|
24
|
+
rescue Fluent::UnrecoverableError
|
|
25
|
+
raise
|
|
26
|
+
rescue => e
|
|
27
|
+
log.warn "gzip command execution failed: #{e.message}. Fallback to GzipExtractor."
|
|
28
|
+
io.rewind
|
|
29
|
+
extractor = GzipExtractor.new(log: log, decompression_size_limit: @decompression_size_limit)
|
|
30
|
+
extractor.extract(io)
|
|
42
31
|
end
|
|
43
32
|
end
|
|
44
33
|
end
|
|
@@ -19,20 +19,12 @@ module Fluent::Plugin
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def extract(io)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
temp.path
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
stdout, succeeded = Open3.capture2("xz #{@command_parameter} #{path}")
|
|
32
|
-
if succeeded
|
|
33
|
-
stdout
|
|
34
|
-
else
|
|
35
|
-
raise "Failed to extract #{path} with xz command."
|
|
22
|
+
begin
|
|
23
|
+
extract_with_command("xz #{@command_parameter}", io, "xz-temp")
|
|
24
|
+
rescue Fluent::UnrecoverableError
|
|
25
|
+
raise
|
|
26
|
+
rescue => e
|
|
27
|
+
raise "Failed to extract with the xz command: #{e.message}"
|
|
36
28
|
end
|
|
37
29
|
end
|
|
38
30
|
end
|
|
@@ -19,20 +19,12 @@ module Fluent::Plugin
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def extract(io)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
temp.path
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
stdout, succeeded = Open3.capture2("lzop #{@command_parameter} #{path}")
|
|
32
|
-
if succeeded
|
|
33
|
-
stdout
|
|
34
|
-
else
|
|
35
|
-
raise "Failed to extract #{path} with lzop command."
|
|
22
|
+
begin
|
|
23
|
+
extract_with_command("lzop #{@command_parameter}", io, "lzop-temp")
|
|
24
|
+
rescue Fluent::UnrecoverableError
|
|
25
|
+
raise
|
|
26
|
+
rescue => e
|
|
27
|
+
raise "Failed to extract with the lzop command: #{e.message}"
|
|
36
28
|
end
|
|
37
29
|
end
|
|
38
30
|
end
|
data/test/test_in_s3.rb
CHANGED
|
@@ -341,7 +341,7 @@ EOS
|
|
|
341
341
|
}
|
|
342
342
|
]
|
|
343
343
|
}
|
|
344
|
-
message = Struct::StubMessage.new(1, 1,
|
|
344
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
345
345
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
346
346
|
config.before_request.call(stats) if config.before_request
|
|
347
347
|
stats.request_count += 1
|
|
@@ -376,7 +376,7 @@ EOS
|
|
|
376
376
|
}
|
|
377
377
|
]
|
|
378
378
|
}
|
|
379
|
-
message = Struct::StubMessage.new(1, 1,
|
|
379
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
380
380
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
381
381
|
config.before_request.call(stats) if config.before_request
|
|
382
382
|
stats.request_count += 1
|
|
@@ -411,7 +411,7 @@ EOS
|
|
|
411
411
|
}
|
|
412
412
|
]
|
|
413
413
|
}
|
|
414
|
-
message = Struct::StubMessage.new(1, 1,
|
|
414
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
415
415
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
416
416
|
config.before_request.call(stats) if config.before_request
|
|
417
417
|
stats.request_count += 1
|
|
@@ -446,7 +446,7 @@ EOS
|
|
|
446
446
|
}
|
|
447
447
|
]
|
|
448
448
|
}
|
|
449
|
-
message = Struct::StubMessage.new(1, 1,
|
|
449
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
450
450
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
451
451
|
config.before_request.call(stats) if config.before_request
|
|
452
452
|
stats.request_count += 1
|
|
@@ -481,7 +481,7 @@ EOS
|
|
|
481
481
|
}
|
|
482
482
|
]
|
|
483
483
|
}
|
|
484
|
-
message = Struct::StubMessage.new(1, 1,
|
|
484
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
485
485
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
486
486
|
config.before_request.call(stats) if config.before_request
|
|
487
487
|
stats.request_count += 1
|
|
@@ -521,7 +521,7 @@ EOS
|
|
|
521
521
|
}
|
|
522
522
|
]
|
|
523
523
|
}
|
|
524
|
-
message = Struct::StubMessage.new(1, 1,
|
|
524
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
525
525
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
526
526
|
config.before_request.call(stats) if config.before_request
|
|
527
527
|
stats.request_count += 1
|
|
@@ -569,7 +569,7 @@ EOS
|
|
|
569
569
|
}
|
|
570
570
|
]
|
|
571
571
|
}
|
|
572
|
-
message = Struct::StubMessage.new(1, 1,
|
|
572
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
573
573
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
574
574
|
config.before_request.call(stats) if config.before_request
|
|
575
575
|
stats.request_count += 1
|
|
@@ -620,7 +620,7 @@ EOS
|
|
|
620
620
|
}
|
|
621
621
|
]
|
|
622
622
|
}
|
|
623
|
-
message = Struct::StubMessage.new(1, 1,
|
|
623
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
624
624
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
625
625
|
config.before_request.call(stats) if config.before_request
|
|
626
626
|
stats.request_count += 1
|
|
@@ -640,6 +640,91 @@ EOS
|
|
|
640
640
|
assert_equal(expected_records, events.map {|_tag, _time, record| record })
|
|
641
641
|
end
|
|
642
642
|
|
|
643
|
+
data(
|
|
644
|
+
"limit_gzip" => { type: "gzip", input: "StringIO", limit: 10, expected_error: true },
|
|
645
|
+
"limit_text" => { type: "text", input: "StringIO", limit: 10, expected_error: true },
|
|
646
|
+
"limit_gzip_command1" => { type: "gzip_command", input: "StringIO", limit: 10, expected_error: true },
|
|
647
|
+
"limit_gzip_command2" => { type: "gzip_command", input: "Tempfile", limit: 10, expected_error: true },
|
|
648
|
+
"normal_gzip_command" => { type: "gzip_command", input: "Tempfile", limit: 100, expected_error: false },
|
|
649
|
+
)
|
|
650
|
+
def test_decompression_size_limit(data)
|
|
651
|
+
store_type = data[:type]
|
|
652
|
+
input_type = data[:input]
|
|
653
|
+
limit = data[:limit]
|
|
654
|
+
setup_mocks
|
|
655
|
+
|
|
656
|
+
config = <<~CONF
|
|
657
|
+
#{CONFIG}
|
|
658
|
+
check_apikey_on_start false
|
|
659
|
+
store_as #{store_type}
|
|
660
|
+
format none
|
|
661
|
+
decompression_size_limit #{limit}
|
|
662
|
+
CONF
|
|
663
|
+
|
|
664
|
+
d = create_driver(config)
|
|
665
|
+
|
|
666
|
+
s3_object = stub(Object.new)
|
|
667
|
+
s3_response = stub(Object.new)
|
|
668
|
+
s3_response.body {
|
|
669
|
+
content = "#{'a'*10}\n#{'b'*10}\n"
|
|
670
|
+
|
|
671
|
+
# Switching between Tempfile and StringIO to cover both branches of the
|
|
672
|
+
# `io.respond_to?(:path)` condition in `extract_with_command`.
|
|
673
|
+
# This ensures that:
|
|
674
|
+
# 1. The StringIO route correctly uses TextExtractor to create a protected temporary file.
|
|
675
|
+
# 2. The Tempfile route correctly limits the output size during Open3.popen3 execution.
|
|
676
|
+
io = (input_type == "Tempfile") ? Tempfile.new : StringIO.new
|
|
677
|
+
|
|
678
|
+
case store_type
|
|
679
|
+
when "gzip", "gzip_command"
|
|
680
|
+
io.binmode
|
|
681
|
+
Zlib::GzipWriter.wrap(io) { |gz|
|
|
682
|
+
gz.write content
|
|
683
|
+
gz.finish
|
|
684
|
+
}
|
|
685
|
+
when "text"
|
|
686
|
+
io.write content
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
io.rewind
|
|
690
|
+
io
|
|
691
|
+
}
|
|
692
|
+
s3_object.get { s3_response }
|
|
693
|
+
@s3_bucket.object(anything).at_least(1) { s3_object }
|
|
694
|
+
|
|
695
|
+
body = {
|
|
696
|
+
"Records" => [
|
|
697
|
+
{
|
|
698
|
+
"s3" => {
|
|
699
|
+
"object" => {
|
|
700
|
+
"key" => "test_key"
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
]
|
|
705
|
+
}
|
|
706
|
+
message = Struct::StubMessage.new(1, 1, Yajl.dump(body))
|
|
707
|
+
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
708
|
+
config.before_request.call(stats) if config.before_request
|
|
709
|
+
stats.request_count += 1
|
|
710
|
+
if stats.request_count >= 1
|
|
711
|
+
d.instance.instance_variable_set(:@running, false)
|
|
712
|
+
end
|
|
713
|
+
[message]
|
|
714
|
+
end
|
|
715
|
+
d.run
|
|
716
|
+
|
|
717
|
+
if data[:expected_error]
|
|
718
|
+
# Verify the protection mechanism: ensure SizeLimitError is logged.
|
|
719
|
+
assert_true d.logs.any? { |l| l.include?("Extracted data exceeds limit of #{limit} bytes") }
|
|
720
|
+
else
|
|
721
|
+
# Verify the normal execution path: ensure data is correctly extracted via Open3.popen3.
|
|
722
|
+
expected_records = [{ "message" => "#{'a'*10}\n" }, { "message" => "#{'b'*10}\n" }]
|
|
723
|
+
assert_equal(expected_records, d.events.map {|_tag, _time, record| record })
|
|
724
|
+
assert_false d.logs.any? { |l| l.include?("error_class") }
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
643
728
|
def test_regexp_matching
|
|
644
729
|
setup_mocks
|
|
645
730
|
d = create_driver(CONFIG + "\ncheck_apikey_on_start false\nstore_as text\nformat none\nmatch_regexp .*_key?")
|
|
@@ -661,7 +746,7 @@ EOS
|
|
|
661
746
|
}
|
|
662
747
|
]
|
|
663
748
|
}
|
|
664
|
-
message = Struct::StubMessage.new(1, 1,
|
|
749
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
665
750
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
666
751
|
config.before_request.call(stats) if config.before_request
|
|
667
752
|
stats.request_count += 1
|
|
@@ -690,7 +775,7 @@ EOS
|
|
|
690
775
|
}
|
|
691
776
|
]
|
|
692
777
|
}
|
|
693
|
-
message = Struct::StubMessage.new(1, 1,
|
|
778
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
694
779
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
695
780
|
config.before_request.call(stats) if config.before_request
|
|
696
781
|
stats.request_count += 1
|
|
@@ -735,7 +820,7 @@ EOS
|
|
|
735
820
|
}
|
|
736
821
|
}
|
|
737
822
|
|
|
738
|
-
message = Struct::StubMessage.new(1, 1,
|
|
823
|
+
message = Struct::StubMessage.new(1, 1, JSON.generate(body))
|
|
739
824
|
@sqs_poller.get_messages(anything, anything) do |config, stats|
|
|
740
825
|
config.before_request.call(stats) if config.before_request
|
|
741
826
|
stats.request_count += 1
|
|
@@ -749,4 +834,281 @@ EOS
|
|
|
749
834
|
assert_equal({ "message" => "aaa" }, events.first[2])
|
|
750
835
|
end
|
|
751
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
|
+
|
|
752
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:
|