From 1001c4f06519627f3a138227158a8a41861bab62 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Thu, 1 Oct 2026 18:41:21 +0200 Subject: [PATCH 1/7] Add failing tests for retrying and reporting rejected batches The HTTP outlet treats every response as delivered: 429 and 5xx are never retried, Retry-After is ignored, and 401 or 413 drop the batch without a word. Co-Authored-By: Claude Opus 5.5 --- spec/logtail/log_devices/http_spec.rb | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/spec/logtail/log_devices/http_spec.rb b/spec/logtail/log_devices/http_spec.rb index 2f6ac4e..79b4d6c 100755 --- a/spec/logtail/log_devices/http_spec.rb +++ b/spec/logtail/log_devices/http_spec.rb @@ -208,6 +208,96 @@ end end + context "when Better Stack answers with an error status" do + # Raised to leave the outlet's endless loop, see above. + let(:stop_outlet) { Class.new(Exception) } + let(:http_device) { described_class.new("MYKEY", flush_continuously: false, requests_per_conn: 1) } + let(:request_queue) { http_device.instance_variable_get(:@request_queue) } + let(:waits) { [] } + let(:not_reported_again) { "Further rejections with this status won't be reported.\n" } + + before do + allow(http_device).to receive(:sleep) { |seconds| waits << seconds } + # Leave the loop once there is nothing left to deliver. + allow(request_queue).to receive(:deq).and_wrap_original { |deq| deq.call || raise(stop_outlet) } + # A status is reported once per process, so none has been reported yet in each example. + stub_const("Logtail::LogDevices::HTTP::REPORTED_REJECTIONS", []) + end + + def queue_batch(size) + size.times { |i| http_device.write(Logtail::LogEntry.new("INFO", Time.now, nil, "line #{i}", nil, nil)) } + http_device.send(:flush_async) + end + + it "retries a batch answered with 500 and delivers it once" do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return({status: 500}, {status: 202}) + queue_batch(1) + + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + expect(stub).to have_been_requested.twice + expect(waits).to eq([1]) + end + + it "waits before retrying a batch answered with 429 at least as long as Retry-After says" do + stub = stub_request(:post, "https://in.logs.betterstack.com/"). + to_return({status: 429, headers: {"Retry-After" => "3"}}, {status: 202}) + queue_batch(1) + + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + expect(stub).to have_been_requested.twice + expect(waits).to eq([3]) + end + + it "reads Retry-After as a date too, and waits 60 seconds at most" do + Timecop.freeze(Time.utc(2026, 10, 1, 12, 0, 0)) do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return( + {status: 503, headers: {"Retry-After" => (Time.now + 5).httpdate}}, + {status: 503, headers: {"Retry-After" => "3600"}}, + {status: 202} + ) + queue_batch(1) + + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + expect(stub).to have_been_requested.times(3) + expect(waits).to eq([5, 60]) + end + end + + it "drops a batch answered with 503 three times, backing off in between" do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: 503) + queue_batch(1) + + expect do + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + end.not_to output.to_stderr + expect(stub).to have_been_requested.times(3) + expect(waits).to eq([1, 2, 4]) + end + + it "drops batches answered with 401 and warns only once" do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: [401, "Unauthorized"]) + queue_batch(2) + queue_batch(2) + + expect do + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + end.to output("Logtail: Better Stack rejected 2 log lines with HTTP 401 Unauthorized - check your source token. #{not_reported_again}").to_stderr + expect(stub).to have_been_requested.twice + expect(waits).to eq([]) + end + + it "drops a batch answered with 413 and warns" do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: [413, "Payload Too Large"]) + queue_batch(1) + + expect do + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + end.to output("Logtail: Better Stack rejected 1 log line with HTTP 413 Payload Too Large. #{not_reported_again}").to_stderr + expect(stub).to have_been_requested.once + expect(waits).to eq([]) + end + end + it "lets close stop the outlet while it waits to reconnect" do connection_attempts = 0 allow_any_instance_of(Net::HTTP).to receive(:start) do From f7dfdfd5a1cbae5cafffb31a661f5c382668caff Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Thu, 1 Oct 2026 18:49:15 +0200 Subject: [PATCH 2/7] Check the rejection warnings without matching all of stderr Under `ruby -w`, warnings from other code reach stderr while the outlet runs and broke the exact stderr matches. Expect the warning through `warn` instead, and match the 413 one as a line of stderr. Co-Authored-By: Claude Opus 5.5 --- spec/logtail/log_devices/http_spec.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/spec/logtail/log_devices/http_spec.rb b/spec/logtail/log_devices/http_spec.rb index 79b4d6c..c67bd74 100755 --- a/spec/logtail/log_devices/http_spec.rb +++ b/spec/logtail/log_devices/http_spec.rb @@ -214,7 +214,6 @@ let(:http_device) { described_class.new("MYKEY", flush_continuously: false, requests_per_conn: 1) } let(:request_queue) { http_device.instance_variable_get(:@request_queue) } let(:waits) { [] } - let(:not_reported_again) { "Further rejections with this status won't be reported.\n" } before do allow(http_device).to receive(:sleep) { |seconds| waits << seconds } @@ -266,10 +265,9 @@ def queue_batch(size) it "drops a batch answered with 503 three times, backing off in between" do stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: 503) queue_batch(1) + expect(http_device).not_to receive(:warn) - expect do - expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) - end.not_to output.to_stderr + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) expect(stub).to have_been_requested.times(3) expect(waits).to eq([1, 2, 4]) end @@ -278,10 +276,10 @@ def queue_batch(size) stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: [401, "Unauthorized"]) queue_batch(2) queue_batch(2) + expect(http_device).to receive(:warn).once.with("Logtail: Better Stack rejected 2 log lines with HTTP 401 " \ + "Unauthorized - check your source token. Further rejections with this status won't be reported.") - expect do - expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) - end.to output("Logtail: Better Stack rejected 2 log lines with HTTP 401 Unauthorized - check your source token. #{not_reported_again}").to_stderr + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) expect(stub).to have_been_requested.twice expect(waits).to eq([]) end @@ -292,7 +290,7 @@ def queue_batch(size) expect do expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) - end.to output("Logtail: Better Stack rejected 1 log line with HTTP 413 Payload Too Large. #{not_reported_again}").to_stderr + end.to output(/^Logtail: Better Stack rejected 1 log line with HTTP 413 Payload Too Large\. Further rejections with this status won't be reported\.$/).to_stderr expect(stub).to have_been_requested.once expect(waits).to eq([]) end From 96bcffb5c41c0b0536c0726e2f44b7a30150b4c6 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Thu, 1 Oct 2026 18:49:15 +0200 Subject: [PATCH 3/7] Retry batches answered with 429 or 5xx, and report rejected batches once deliver_requests treated every response as a delivery. Now: - 2xx: delivered, the reconnect wait starts over. - 429 and 5xx: the request goes back on the queue like after a failed connection, with the same backoff and limit of 3 attempts, without starting the wait over. Retry-After (seconds or an HTTP date) sets the minimum wait, capped at 60 s. - Any other status: the batch is dropped, and the first rejection with each status in the process prints a warning through Kernel#warn, e.g. "Logtail: Better Stack rejected 37 log lines with HTTP 401 Unauthorized - check your source token. Further rejections with this status won't be reported." RequestAttempt takes the batch's line count for the warning, and the retry-or-drop logic moved into a helper that the exception path uses too. Co-Authored-By: Claude Opus 5.5 --- lib/logtail/log_devices/http.rb | 79 +++++++++++++++---- .../log_devices/http/request_attempt.rb | 5 +- 2 files changed, 65 insertions(+), 19 deletions(-) diff --git a/lib/logtail/log_devices/http.rb b/lib/logtail/log_devices/http.rb index eeb6c7a..0c716ec 100755 --- a/lib/logtail/log_devices/http.rb +++ b/lib/logtail/log_devices/http.rb @@ -1,5 +1,6 @@ require "msgpack" require "net/https" +require "time" require "zlib" require "logtail/config" @@ -24,6 +25,10 @@ class HTTP USER_AGENT = "Logtail Ruby/#{Logtail::VERSION} (HTTP)".freeze INITIAL_RECONNECT_WAIT = 1 # second MAX_RECONNECT_WAIT = 30 # seconds + MAX_RETRY_AFTER = 60 # seconds + # The HTTP statuses of rejected batches this process has warned about, see {#report_rejected_batch}. + REPORTED_REJECTIONS = [] + REPORTED_REJECTIONS_LOCK = Mutex.new # Instantiates a new HTTP log device that can be passed to {Logtail::Logger#initialize}. # @@ -236,7 +241,7 @@ def flush_async req = build_request(msgs) if !req.nil? Logtail::Config.instance.debug { "New request placed on queue" } - request_attempt = RequestAttempt.new(req) + request_attempt = RequestAttempt.new(req, msgs.size) @request_queue.enq(request_attempt) end end @@ -302,9 +307,10 @@ def build_http end # Creates a loop that processes the `@request_queue` on an interval. After a failed - # connection it waits before reconnecting, twice as long after every consecutive - # failure up to {MAX_RECONNECT_WAIT}, so an unreachable host is not retried in a busy - # loop. A delivered request starts the wait over (see {#deliver_requests}). + # connection, or a 429 or 5xx response, it waits before reconnecting, twice as long after + # every consecutive failure up to {MAX_RECONNECT_WAIT}, so an unreachable host is not + # retried in a busy loop. A Retry-After header can make the wait longer. A delivered + # request starts the wait over (see {#deliver_requests}). def request_outlet loop do http = build_http @@ -334,7 +340,8 @@ def request_outlet # Creates a loop that delivers requests over an open (kept alive) HTTP connection. # If the connection dies, the request is thrown back onto the queue and # the method returns. It is the responsibility of the caller to implement retries - # and establish a new connection. + # and establish a new connection. A 429 or 5xx response is handled the same way, and + # a request rejected with any other status is dropped (see {#report_rejected_batch}). def deliver_requests(conn) num_reqs = 0 @@ -355,16 +362,7 @@ def deliver_requests(conn) resp = conn.request(request_attempt.request) rescue => e Logtail::Config.instance.debug { "#deliver_requests error: #{e.message}" } - - # Throw the request back on the queue for a retry if it has been attempted less - # than 3 times - if request_attempt.attempts < 3 - Logtail::Config.instance.debug { "Request is being retried, #{request_attempt.attempts} previous attempts" } - @request_queue.enq(request_attempt) - else - Logtail::Config.instance.debug { "Request is being dropped, #{request_attempt.attempts} previous attempts" } - end - + retry_or_drop(request_attempt) return false ensure @requests_in_flight -= 1 @@ -373,21 +371,68 @@ def deliver_requests(conn) num_reqs += 1 @last_resp = resp - @reconnect_wait = INITIAL_RECONNECT_WAIT + delivered = resp.code.start_with?("2") Logtail::Config.instance.debug do - if resp.code == "202" + if delivered "Logs successfully sent! View your logs at https://telemetry.betterstack.com" else "Log delivery failed! status: #{resp.code}, body: #{resp.body}" end end + + # Too many requests or a server error: retry the request like after a failed + # connection, without starting the wait over, and wait as long as Retry-After asks. + if resp.code == "429" || resp.code.start_with?("5") + retry_or_drop(request_attempt) + @reconnect_wait = [@reconnect_wait, retry_after(resp)].max + return false + end + + report_rejected_batch(request_attempt, resp) unless delivered + @reconnect_wait = INITIAL_RECONNECT_WAIT end end true end + # Throws the request back on the queue for a retry if it has been attempted less + # than 3 times + def retry_or_drop(request_attempt) + if request_attempt.attempts < 3 + Logtail::Config.instance.debug { "Request is being retried, #{request_attempt.attempts} previous attempts" } + @request_queue.enq(request_attempt) + else + Logtail::Config.instance.debug { "Request is being dropped, #{request_attempt.attempts} previous attempts" } + end + end + + # The seconds to wait before a retry that the Retry-After header asks for, given in + # seconds or as an HTTP date, at most {MAX_RETRY_AFTER}. 0 without a valid header. + def retry_after(resp) + value = resp["Retry-After"].to_s.strip + seconds = value.match?(/\A\d+\z/) ? value.to_i : (Time.httpdate(value) - Time.now).ceil + seconds.clamp(0, MAX_RETRY_AFTER) + rescue ArgumentError + 0 + end + + # Warns about a batch Better Stack rejected, once per HTTP status in this process. It + # goes to stderr, never to a Logtail logger, whose lines would be rejected the same way. + def report_rejected_batch(request_attempt, resp) + first_rejection = REPORTED_REJECTIONS_LOCK.synchronize do + !REPORTED_REJECTIONS.include?(resp.code) && REPORTED_REJECTIONS.push(resp.code) + end + return unless first_rejection + + lines = request_attempt.line_count + status = "HTTP #{resp.code} #{resp.message}".strip + hint = " - check your source token" if resp.code == "401" || resp.code == "403" + warn("Logtail: Better Stack rejected #{lines || "some"} log #{lines == 1 ? "line" : "lines"} " \ + "with #{status}#{hint}. Further rejections with this status won't be reported.") + end + # Builds the `Authorization` header value for HTTP delivery to the Logtail API. def authorization_payload "Bearer #{@source_token}" diff --git a/lib/logtail/log_devices/http/request_attempt.rb b/lib/logtail/log_devices/http/request_attempt.rb index 803e434..7bea258 100755 --- a/lib/logtail/log_devices/http/request_attempt.rb +++ b/lib/logtail/log_devices/http/request_attempt.rb @@ -4,11 +4,12 @@ class HTTP # Represents an attempt to deliver a request. Requests can be retried, hence # why we keep track of the number of attempts. class RequestAttempt - attr_reader :attempts, :request + attr_reader :attempts, :request, :line_count - def initialize(req) + def initialize(req, line_count = nil) @attempts = 0 @request = req + @line_count = line_count end def attempted! From d7e3b3d45f7358b43ac34db59ea838563e5b5ee5 Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Fri, 2 Oct 2026 08:29:26 +0200 Subject: [PATCH 4/7] Add a failing test for retrying a batch answered with 408 Better Stack's ingesting proxy answers 408 Request Time-out, and closes the connection, when a new connection stays unused for more than about 10 to 15 seconds. The outlet opens a new connection after every requests_per_conn requests and then waits for lines, so after a quiet spell the next batch gets that 408. It is dropped with a warning that Better Stack rejected it, although the server never read the request. Co-Authored-By: Claude Opus 5.5 --- spec/logtail/log_devices/http_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/logtail/log_devices/http_spec.rb b/spec/logtail/log_devices/http_spec.rb index c67bd74..a80c2e6 100755 --- a/spec/logtail/log_devices/http_spec.rb +++ b/spec/logtail/log_devices/http_spec.rb @@ -237,6 +237,18 @@ def queue_batch(size) expect(waits).to eq([1]) end + it "retries a batch answered with 408 without a warning, and delivers it once" do + # Better Stack answers 408 when a new connection stays unused for more than about 10 seconds + stub = stub_request(:post, "https://in.logs.betterstack.com/"). + to_return({status: [408, "Request Time-out"], headers: {"Connection" => "close"}}, {status: 202}) + queue_batch(1) + expect(http_device).not_to receive(:warn) + + expect { http_device.send(:request_outlet) }.to raise_error(stop_outlet) + expect(stub).to have_been_requested.twice + expect(waits).to eq([1]) + end + it "waits before retrying a batch answered with 429 at least as long as Retry-After says" do stub = stub_request(:post, "https://in.logs.betterstack.com/"). to_return({status: 429, headers: {"Retry-After" => "3"}}, {status: 202}) From 5c499ac6352efe5f182105dd62e1bdfd958a503d Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Fri, 2 Oct 2026 08:29:42 +0200 Subject: [PATCH 5/7] Retry a batch answered with 408 like one answered with 429 or 5xx A 408 means the server didn't read the request, so the batch is kept and retried with the same backoff and attempt limit, waiting as long as a Retry-After header asks, and no rejection is reported. Co-Authored-By: Claude Opus 5.5 --- lib/logtail/log_devices/http.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/logtail/log_devices/http.rb b/lib/logtail/log_devices/http.rb index 0c716ec..222fcba 100755 --- a/lib/logtail/log_devices/http.rb +++ b/lib/logtail/log_devices/http.rb @@ -381,9 +381,11 @@ def deliver_requests(conn) end end - # Too many requests or a server error: retry the request like after a failed - # connection, without starting the wait over, and wait as long as Retry-After asks. - if resp.code == "429" || resp.code.start_with?("5") + # A request the server didn't read (408, which Better Stack also sends for a new + # connection that stayed unused too long), too many requests or a server error: retry + # the request like after a failed connection, without starting the wait over, and + # wait as long as Retry-After asks. + if resp.code == "408" || resp.code == "429" || resp.code.start_with?("5") retry_or_drop(request_attempt) @reconnect_wait = [@reconnect_wait, retry_after(resp)].max return false From 351ce5aa8f5f6b226bb3a92663be254ecffd8aae Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Fri, 2 Oct 2026 13:44:01 +0200 Subject: [PATCH 6/7] Add failing tests for rejected and 408 responses in a synchronous delivery With #57 and #58 merged, a delivery in the calling thread (flush without an outlet thread, lines written after close or during shutdown) counts any HTTP response as delivered. So a batch rejected with 401 or 403 isn't reported, and lines written after close keep going out after a 429 or 5xx. Three of the four new examples fail for that. The fourth, a batch answered with 408, 429 or 5xx is neither retried nor reported, passes already and keeps the fix from reporting those statuses as rejected. Co-Authored-By: Claude Opus 5.5 --- spec/logtail/log_devices/http_spec.rb | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/spec/logtail/log_devices/http_spec.rb b/spec/logtail/log_devices/http_spec.rb index 7c399f3..b41f27a 100755 --- a/spec/logtail/log_devices/http_spec.rb +++ b/spec/logtail/log_devices/http_spec.rb @@ -94,6 +94,26 @@ http.close expect(Process.clock_gettime(Process::CLOCK_MONOTONIC) - closing).to be < 1 end + + it "drops the lines written after it once Better Stack answered one of them with 429 or 5xx" do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return({status: 503}, {status: 202}) + http.close + + 2.times { http.write(Logtail::LogEntry.new("INFO", Time.now, nil, "test log message", nil, nil)) } + + expect(stub).to have_been_requested.once + end + + it "reports a line written after it that Better Stack rejects" do + # A status is reported once per process, so none has been reported yet. + stub_const("Logtail::LogDevices::HTTP::REPORTED_REJECTIONS", []) + stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: [403, "Forbidden"]) + http.close + expect(http).to receive(:warn).once.with("Logtail: Better Stack rejected 1 log line with HTTP 403 " \ + "Forbidden - check your source token. Further rejections with this status won't be reported.") + + http.write(Logtail::LogEntry.new("INFO", Time.now, nil, "test log message", nil, nil)) + end end # Testing a private method because it helps break down our tests @@ -168,6 +188,41 @@ http.instance_variable_get(:@request_outlet_thread).kill.join end + context "when Better Stack answers the delivery in the calling thread with an error status" do + let(:http) { described_class.new("MYKEY", flush_continuously: false) } + + # A status is reported once per process, so none has been reported yet in each example. + before { stub_const("Logtail::LogDevices::HTTP::REPORTED_REJECTIONS", []) } + + it "drops batches rejected with 401 and warns only once" do + stub = stub_request(:post, "https://in.logs.betterstack.com/").to_return(status: [401, "Unauthorized"]) + expect(http).to receive(:warn).once.with("Logtail: Better Stack rejected 2 log lines with HTTP 401 " \ + "Unauthorized - check your source token. Further rejections with this status won't be reported.") + + 2.times do + 2.times { |i| http.write(Logtail::LogEntry.new("INFO", time, nil, "line #{i}", nil, nil)) } + http.flush + end + + expect(stub).to have_been_requested.twice + http.close + end + + it "neither retries nor reports a batch answered with 408, 429 or 5xx" do + stub = stub_request(:post, "https://in.logs.betterstack.com/"). + to_return({status: 503}, {status: 429}, {status: [408, "Request Time-out"]}) + expect(http).not_to receive(:warn) + + 3.times do + http.write(Logtail::LogEntry.new("INFO", time, nil, "test log message", nil, nil)) + http.flush + end + + expect(stub).to have_been_requested.times(3) + http.close + end + end + it "waits up to 5 seconds for a slow host when it delivers in the calling thread" do # Takes 3 seconds to answer each request slow_ingest = Class.new(LocalIngestServer) do From 49fa9faca9b6af7c6e7d125910362189458c01ec Mon Sep 17 00:00:00 2001 From: Petr Heinz Date: Fri, 2 Oct 2026 13:45:36 +0200 Subject: [PATCH 7/7] Apply the rejected-batch handling, including 408, to synchronous delivery deliver_synchronously now counts only 2xx as delivered. 408, 429 and 5xx are logged at debug level and not retried, since nothing would deliver the retry; any other status is dropped and reported once per status through report_rejected_batch. It takes the RequestAttempts, so the warning can count the lines. Co-Authored-By: Claude Opus 5.5 --- lib/logtail/log_devices/http.rb | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/logtail/log_devices/http.rb b/lib/logtail/log_devices/http.rb index 159ac3b..7449a61 100755 --- a/lib/logtail/log_devices/http.rb +++ b/lib/logtail/log_devices/http.rb @@ -270,7 +270,7 @@ def reset_if_forked def dequeue_requests requests = [] while @request_queue.size > 0 && (request_attempt = @request_queue.deq) - requests << request_attempt.request + requests << request_attempt end requests end @@ -465,10 +465,12 @@ def flush_async end # Sends the requests in the calling thread, for when no outlet thread delivers them. - # Returns whether all of them were sent. Errors only go to the debug log, also those that - # aren't StandardErrors (WebMock refuses to connect with one); signals are raised as usual. - def deliver_synchronously(requests) - return true if requests.empty? + # Returns whether all of them were delivered. A request answered with 408, 429 or 5xx isn't + # retried, nothing would deliver the retry; one rejected with any other status that isn't + # 2xx is reported like in {#deliver_requests}. Errors only go to the debug log, also those + # that aren't StandardErrors (WebMock refuses to connect with one); signals are raised as usual. + def deliver_synchronously(request_attempts) + return true if request_attempts.empty? http = build_http http.open_timeout = http.read_timeout = SYNCHRONOUS_DELIVERY_TIMEOUT @@ -482,8 +484,16 @@ def deliver_synchronously(requests) http.open_timeout = nil http.start end - requests.each { |request| @last_resp = http.request(request) } - true + delivered = true + request_attempts.each do |request_attempt| + resp = @last_resp = http.request(request_attempt.request) + next if resp.code.start_with?("2") + + delivered = false + Logtail::Config.instance.debug { "Log delivery failed! status: #{resp.code}, body: #{resp.body}" } + report_rejected_batch(request_attempt, resp) unless resp.code == "408" || resp.code == "429" || resp.code.start_with?("5") + end + delivered rescue SignalException raise rescue Exception => e @@ -533,7 +543,7 @@ def deliver_late_lines if @late_delivery_failed Logtail::Config.instance.debug { "Dropping #{msgs.size} log lines, an earlier synchronous delivery failed" } else - @late_delivery_failed = !deliver_synchronously([build_request(msgs)]) + @late_delivery_failed = !deliver_synchronously([RequestAttempt.new(build_request(msgs), msgs.size)]) end true end