use aws_smithy_observability::instruments::Histogram;
use aws_smithy_observability::{AttributeValue, Attributes};
use aws_smithy_runtime_api::box_error::BoxError;
use aws_smithy_runtime_api::client::interceptors::context::{
BeforeDeserializationInterceptorContextMut, BeforeTransmitInterceptorContextMut,
};
use aws_smithy_runtime_api::client::interceptors::Intercept;
use aws_smithy_runtime_api::client::orchestrator::Metadata;
use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
use aws_smithy_types::body::SdkBody;
use aws_smithy_types::config_bag::ConfigBag;
use http_body_1x::{Body, Frame};
use std::mem;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use crate::client::metrics::OperationTelemetry;
struct CountingBody<B> {
inner: B,
transferred: u64,
instrument: Arc<dyn Histogram>,
attributes: Attributes,
}
impl<B> CountingBody<B> {
fn new(inner: B, instrument: Arc<dyn Histogram>, attributes: Attributes) -> Self {
Self {
inner,
transferred: 0,
instrument,
attributes,
}
}
}
impl<B> Drop for CountingBody<B> {
fn drop(&mut self) {
self.instrument
.record(self.transferred as f64, Some(&self.attributes), None);
}
}
impl<B> Body for CountingBody<B>
where
B: Body<Data = bytes::Bytes, Error = BoxError> + Unpin,
{
type Data = bytes::Bytes;
type Error = BoxError;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let this = &mut *self;
match Pin::new(&mut this.inner).poll_frame(cx) {
Poll::Ready(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
this.transferred += data.len() as u64;
}
Poll::Ready(Some(Ok(frame)))
}
other => other,
}
}
fn is_end_stream(&self) -> bool {
self.inner.is_end_stream()
}
fn size_hint(&self) -> http_body_1x::SizeHint {
self.inner.size_hint()
}
}
fn wrap(body: SdkBody, instrument: Arc<dyn Histogram>, attributes: Attributes) -> SdkBody {
body.map_preserve_contents(move |b| {
SdkBody::from_body_1_x(CountingBody::new(b, instrument.clone(), attributes.clone()))
})
}
fn rpc_attributes(cfg: &ConfigBag) -> Attributes {
let mut attrs = Attributes::new();
if let Some(md) = cfg.load::<Metadata>() {
attrs.set("rpc.service", AttributeValue::String(md.service().into()));
attrs.set("rpc.method", AttributeValue::String(md.name().into()));
}
attrs
}
#[derive(Debug, Default)]
pub(crate) struct TelemetryBytesInterceptor;
impl Intercept for TelemetryBytesInterceptor {
fn name(&self) -> &'static str {
"TelemetryBytesInterceptor"
}
fn modify_before_transmit(
&self,
context: &mut BeforeTransmitInterceptorContextMut<'_>,
_runtime_components: &RuntimeComponents,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let Some(instruments) = cfg.load::<OperationTelemetry>() else {
return Ok(());
};
let instrument = instruments.request_body_size.clone();
let attributes = rpc_attributes(cfg);
let body = mem::replace(context.request_mut().body_mut(), SdkBody::taken());
*context.request_mut().body_mut() = wrap(body, instrument, attributes);
Ok(())
}
fn modify_before_deserialization(
&self,
context: &mut BeforeDeserializationInterceptorContextMut<'_>,
_runtime_components: &RuntimeComponents,
cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let Some(instruments) = cfg.load::<OperationTelemetry>() else {
return Ok(());
};
let instrument = instruments.response_body_size.clone();
let attributes = rpc_attributes(cfg);
let body = mem::replace(context.response_mut().body_mut(), SdkBody::taken());
*context.response_mut().body_mut() = wrap(body, instrument, attributes);
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use aws_smithy_observability::instruments::Histogram;
use futures_util::StreamExt;
use http_body_util::BodyExt;
use std::sync::Mutex;
#[derive(Debug, Default)]
struct RecordingHistogram {
values: Mutex<Vec<f64>>,
}
impl Histogram for RecordingHistogram {
fn record(
&self,
value: f64,
_attributes: Option<&Attributes>,
_context: Option<&dyn aws_smithy_observability::Context>,
) {
self.values.lock().unwrap().push(value);
}
}
async fn drain(body: SdkBody) {
let _ = body.collect().await.expect("body drains");
}
#[tokio::test]
async fn records_all_bytes_of_a_streaming_body_on_completion() {
let hist = Arc::new(RecordingHistogram::default());
let stream = futures_util::stream::iter(vec![
Ok::<_, BoxError>(bytes::Bytes::from_static(b"hello ")),
Ok(bytes::Bytes::from_static(b"world")),
]);
let streaming = SdkBody::from_body_1_x(http_body_util::StreamBody::new(
stream.map(|r| r.map(Frame::data)),
));
assert_eq!(None, streaming.content_length(), "precondition: streaming");
drain(wrap(streaming, hist.clone(), Attributes::new())).await;
assert_eq!(vec![11.0], *hist.values.lock().unwrap());
}
#[tokio::test]
async fn empty_body_records_zero() {
let hist = Arc::new(RecordingHistogram::default());
drain(wrap(SdkBody::empty(), hist.clone(), Attributes::new())).await;
assert_eq!(vec![0.0], *hist.values.lock().unwrap());
}
#[tokio::test]
async fn each_body_records_independently() {
let hist = Arc::new(RecordingHistogram::default());
drain(wrap(SdkBody::from("abc"), hist.clone(), Attributes::new())).await;
drain(wrap(SdkBody::from("de"), hist.clone(), Attributes::new())).await;
let mut recorded = hist.values.lock().unwrap().clone();
recorded.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(vec![2.0, 3.0], recorded);
}
}