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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ad2143d493bcb5b5805df10225cdb068bffc8ae24a1a6838bde343b18a845ed
4
- data.tar.gz: faf67f2a0e65e73b385944113a485f509365b3b9f1e8059ef1b06df8c6719607
3
+ metadata.gz: d21a6392c01d12f1eb7d42afcb9a068157020c0627772895dedad47f1807c788
4
+ data.tar.gz: 22cef56213063ea0493c2b1599d2ff8019ab1f0da44e1c6598497100b695859f
5
5
  SHA512:
6
- metadata.gz: 51c6cd59240d0d2055260b779e9887ab5a8fc0839246cf9154566271de72e30a73ba0587ae2945dd275c1b16b71c43e165a77e43419e73279fbb938904790a48
7
- data.tar.gz: ff8aa7a1c32cf273527165eee2dec5df4df217d20e5047c53908469bf13fd8baf6b803781e30d09126d83e0ba4658bf8b28ae2b034e403f627ae9a9e98b312a3
6
+ metadata.gz: 9e516fbe5805ae366cbd0c588a64b45d8b4b80d0b4afa65e0ebc8eea2e59c1ea6f50d119493b2516f5db94db0b9d53b83a72022badf3179c624269a1493dafa9
7
+ data.tar.gz: 7b3f6921d9a6f88eb802b93d8613c0f706abb1162be56e833fe4e65b3084bc7a2c7c10296a05467127e6451fdb10ea66457bc5f4eb2bfe62fe965935d2822649
@@ -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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
29
- - uses: ruby/setup-ruby@afeafc3d1ab54a631816aba4c914a0081c12ff2f # v1.310.0
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@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
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
@@ -1,3 +1,3 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  gemspec
data/Gemfile.v0.12 CHANGED
@@ -1,4 +1,4 @@
1
- source "http://rubygems.org"
1
+ source "https://rubygems.org"
2
2
 
3
3
  gem 'json', '= 1.8.3'
4
4
  gem 'fluentd', '~> 0.12.0'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.5
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
 
@@ -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
- body = JSON.parse(message.body)
217
- log.debug(body)
218
- next unless is_valid_queue(body) # skip test queue
219
- if @match_regexp
220
- raw_key = get_raw_key(body)
221
- key = CGI.unescape(raw_key)
222
- next unless @match_regexp.match?(key)
223
- end
224
- process(body)
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["detail"]["object"]["key"]
293
+ body.dig("detail", "object", "key")
251
294
  else
252
- body["Records"].first["s3"]["object"]["key"]
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(body)
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 < StandardError; end
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
- if wait_thr.value.success?
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
- raise "Command execution failed: #{command} (status: #{wait_thr.value})"
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
@@ -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 SizeLimitError
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 SizeLimitError
24
+ rescue Fluent::UnrecoverableError
25
25
  raise
26
- rescue
27
- raise "Failed to extract #{path} with xz command."
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 SizeLimitError
24
+ rescue Fluent::UnrecoverableError
25
25
  raise
26
- rescue
27
- raise "Failed to extract #{path} with lzop command."
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.5
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.10
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: