Skip to content

Commit d27d01f

Browse files
committed
feat(sinks): log response body when HTTP response failed
1 parent 11aa135 commit d27d01f

File tree

4 files changed

+95
-10
lines changed

4 files changed

+95
-10
lines changed

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ heim = { git = "https://github.com/vectordotdev/heim.git", branch = "update-nix"
438438
mlua = { version = "0.10.5", default-features = false, features = ["lua54", "send", "vendored", "macros"], optional = true }
439439
sysinfo = "0.37.2"
440440
byteorder = "1.5.0"
441+
brotli = "8.0.2"
441442

442443
[target.'cfg(windows)'.dependencies]
443444
windows-service = "0.8.0"
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added HTTP response body previews to error logs. When an HTTP sink request fails, Vector will now attempt to decompress (gzip, zstd, br, deflate) and log the first 1024 characters of the response body to help troubleshooting.
2+
3+
authors: Keuin

src/sinks/util/http.rs

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
use aws_credential_types::provider::SharedCredentialsProvider;
33
#[cfg(feature = "aws-core")]
44
use aws_types::region::Region;
5+
use brotli::Decompressor as BrotliDecoder;
56
use bytes::{Buf, Bytes};
7+
use flate2::read::GzDecoder;
68
use futures::{Sink, future::BoxFuture};
79
use headers::HeaderName;
810
use http::{HeaderValue, Request, Response, StatusCode, header};
911
use http_body::Body as _;
12+
use zstd::stream::read::Decoder as ZstdDecoder;
1013

1114
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1215
pub struct OrderedHeaderName(HeaderName);
@@ -38,21 +41,24 @@ impl PartialOrd for OrderedHeaderName {
3841
Some(self.cmp(other))
3942
}
4043
}
44+
use flate2::bufread::ZlibDecoder;
45+
use http::header::{CONTENT_ENCODING, CONTENT_TYPE};
46+
use hyper::Body;
47+
use pin_project::pin_project;
48+
use snafu::{ResultExt, Snafu};
49+
use std::io::Read;
4150
use std::{
4251
collections::BTreeMap,
4352
fmt,
4453
future::Future,
4554
hash::Hash,
55+
io,
4656
marker::PhantomData,
4757
pin::Pin,
4858
sync::Arc,
4959
task::{Context, Poll, ready},
5060
time::Duration,
5161
};
52-
53-
use hyper::Body;
54-
use pin_project::pin_project;
55-
use snafu::{ResultExt, Snafu};
5662
use tower::{Service, ServiceBuilder};
5763
use tower_http::decompression::DecompressionLayer;
5864
use vector_lib::{
@@ -580,15 +586,89 @@ impl<Req: Clone + Send + Sync + 'static> RetryLogic for HttpRetryLogic<Req> {
580586
StatusCode::NOT_IMPLEMENTED => {
581587
RetryAction::DontRetry("endpoint not implemented".into())
582588
}
583-
_ if status.is_server_error() => RetryAction::Retry(
584-
format!("{}: {}", status, String::from_utf8_lossy(response.body())).into(),
585-
),
586589
_ if status.is_success() => RetryAction::Successful,
587-
_ => RetryAction::DontRetry(format!("response status: {status}").into()),
590+
_ => {
591+
let body_preview = get_response_preview(response)
592+
.unwrap_or_else(|err| format!("cannot peek: {err:?}"));
593+
if status.is_server_error() {
594+
RetryAction::Retry(format!("{status}: {body_preview}").into())
595+
} else {
596+
RetryAction::DontRetry(
597+
format!("response status: {status}, body: {body_preview}").into(),
598+
)
599+
}
600+
}
588601
}
589602
}
590603
}
591604

605+
#[derive(Debug, Snafu)]
606+
pub enum ResponsePreviewError {
607+
#[snafu(display("Cannot preview a binary response content: {}", content_type))]
608+
BinaryContent { content_type: String },
609+
#[snafu(display("Unknown encoding of content in HTTP response: {}", content_encoding))]
610+
UnknownEncoding { content_encoding: String },
611+
#[snafu(display("Error reading data: {:?}", err))]
612+
IOError { err: io::Error },
613+
}
614+
615+
/// Try to decompress and read the first 1024 bytes from the HTTP response body.
616+
fn get_response_preview(response: &Response<Bytes>) -> crate::Result<String> {
617+
const BROTLI_INTERNAL_BUFFER_SIZE: usize = 4096;
618+
const PEEK_UTF8_CHARACTERS: usize = 1024;
619+
const UNKNOWN_CONTENT_TYPE: &str = "unspecified";
620+
621+
// skip binary data
622+
let content_type = response
623+
.headers()
624+
.get(CONTENT_TYPE)
625+
.and_then(|v| v.to_str().ok())
626+
.unwrap_or(UNKNOWN_CONTENT_TYPE);
627+
if content_type.starts_with("image/")
628+
|| content_type.starts_with("video/")
629+
|| content_type.starts_with("audio/")
630+
|| content_type == "application/octet-stream"
631+
{
632+
return Err(Box::new(ResponsePreviewError::BinaryContent {
633+
content_type: content_type.to_string(),
634+
}));
635+
}
636+
637+
let body_bytes = response.body().as_ref();
638+
let content_encoding = response
639+
.headers()
640+
.get(CONTENT_ENCODING)
641+
.and_then(|v| v.to_str().ok())
642+
.unwrap_or_else(|| "unknown");
643+
644+
// handle different compression methods in HTTP response
645+
let mut reader: Box<dyn Read> = match content_encoding {
646+
"gzip" => Box::new(GzDecoder::new(body_bytes)),
647+
"deflate" => Box::new(ZlibDecoder::new(body_bytes)),
648+
"br" => Box::new(BrotliDecoder::new(body_bytes, BROTLI_INTERNAL_BUFFER_SIZE)),
649+
"zstd" => Box::new(
650+
ZstdDecoder::new(body_bytes).unwrap_or_else(|_| ZstdDecoder::new(body_bytes).unwrap()),
651+
),
652+
// unspecified or identity encoding, treat as utf-8 directly
653+
UNKNOWN_CONTENT_TYPE | "identity" => Box::new(body_bytes),
654+
encoding => {
655+
return Err(Box::new(ResponsePreviewError::UnknownEncoding {
656+
content_encoding: encoding.to_string(),
657+
}));
658+
}
659+
};
660+
661+
// one utf-8 char takes up to 4 bytes, read at most that number of bytes for utf-8 decoding
662+
let mut buf = [0u8; PEEK_UTF8_CHARACTERS * 4];
663+
match reader.read(&mut buf) {
664+
Ok(cnt) => Ok(String::from_utf8_lossy(&buf[..cnt])
665+
.chars()
666+
.take(PEEK_UTF8_CHARACTERS)
667+
.collect()),
668+
Err(why) => Err(Box::new(ResponsePreviewError::IOError { err: why })),
669+
}
670+
}
671+
592672
/// A more generic version of `HttpRetryLogic` that accepts anything that can be converted
593673
/// to a status code
594674
#[derive(Debug)]

0 commit comments

Comments
 (0)