#[cfg(feature = "tracing")]
use std::fmt;
use std::time::Instant;
#[cfg(feature = "tracing")]
use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode, Uri};
#[cfg(feature = "tracing")]
use http::{HeaderName, HeaderValue};
#[cfg(feature = "tracing")]
use tracing::Level;
use crate::error::Error;
#[cfg(feature = "tracing")]
use crate::{
constants::request_id,
error::{ErrorKind, format_endpoint},
redact::is_secret,
text::{Backslash, MAX_NAME_CHARS, SafeText},
};
#[cfg(feature = "tracing")]
pub(crate) const TARGET: &str = "typesafe_sdk";
#[derive(Clone, Copy)]
pub(crate) struct Exchange<'a> {
#[cfg(feature = "tracing")]
method: &'a Method,
#[cfg(feature = "tracing")]
uri: &'a Uri,
#[cfg(feature = "tracing")]
retry: u32,
#[cfg(not(feature = "tracing"))]
request: std::marker::PhantomData<&'a ()>,
}
impl<'a> Exchange<'a> {
#[cfg(feature = "tracing")]
pub(crate) fn new(method: &'a Method, uri: &'a Uri, retry: u32) -> Self {
Self { method, uri, retry }
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn new(_: &'a Method, _: &'a Uri, _: u32) -> Self {
Self { request: std::marker::PhantomData }
}
}
#[cfg(feature = "tracing")]
pub(crate) fn sending(exchange: Exchange<'_>, headers: &HeaderMap, body: Option<&Bytes>) {
tracing::debug!(
target: TARGET,
method = %exchange.method,
endpoint = %exchange.uri,
retry = exchange.retry,
headers = ?redact(headers),
body_len = body.map_or(0, Bytes::len),
"sending request"
);
if let Some(body) = body {
tracing::trace!(
target: TARGET,
method = %exchange.method,
endpoint = %exchange.uri,
body = %Lossy(body),
"request body"
);
}
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn sending(_: Exchange<'_>, _: &HeaderMap, _: Option<&bytes::Bytes>) {}
type Started = Option<Instant>;
#[cfg(feature = "tracing")]
pub(crate) fn clock() -> Started {
tracing::enabled!(target: TARGET, Level::INFO).then(Instant::now)
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn clock() -> Started {
None
}
#[cfg(feature = "tracing")]
pub(crate) fn responded(
exchange: Exchange<'_>,
status: StatusCode,
headers: &HeaderMap,
started: Started,
) {
tracing::info!(
target: TARGET,
"{} <- {} in {} (request {})",
Endpoint(exchange),
status.as_u16(),
Elapsed(started),
RequestId(headers),
);
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn responded(_: Exchange<'_>, _: StatusCode, _: &HeaderMap, _: Started) {}
#[cfg(feature = "tracing")]
pub(crate) fn received(
exchange: Exchange<'_>,
status: StatusCode,
headers: &HeaderMap,
body: &Bytes,
started: Started,
) {
tracing::debug!(
target: TARGET,
method = %exchange.method,
endpoint = %exchange.uri,
status = status.as_u16(),
request_id = %RequestId(headers),
elapsed = %Elapsed(started),
headers = ?redact(headers),
body_len = body.len(),
"received response"
);
tracing::trace!(
target: TARGET,
method = %exchange.method,
endpoint = %exchange.uri,
body = %Lossy(body),
"response body"
);
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn received(
_: Exchange<'_>,
_: StatusCode,
_: &HeaderMap,
_: &bytes::Bytes,
_: Started,
) {
}
#[cfg(feature = "tracing")]
pub(crate) fn failed(exchange: Exchange<'_>, error: &Error, started: Started) {
tracing::info!(target: TARGET, "{} <- {}", Endpoint(exchange), failure_word(error));
tracing::debug!(
target: TARGET,
method = %exchange.method,
endpoint = %exchange.uri,
elapsed = %Elapsed(started),
failure = failure_word(error),
"request failed"
);
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn failed(_: Exchange<'_>, _: &Error, _: Started) {}
#[cfg(feature = "tracing")]
pub(crate) fn retrying(exchange: Exchange<'_>) {
tracing::info!(target: TARGET, "{} retry {}", Endpoint(exchange), exchange.retry);
}
#[cfg(not(feature = "tracing"))]
pub(crate) fn retrying(_: Exchange<'_>) {}
#[cfg(feature = "tracing")]
fn failure_word(error: &Error) -> &'static str {
match error.kind() {
ErrorKind::Timeout { .. } => "timeout",
ErrorKind::Connection => "connection error",
ErrorKind::ResponseTooLarge { .. } => "response too large",
ErrorKind::Api(_) => "api error",
ErrorKind::ResponseValidation(_) => "invalid response",
ErrorKind::InvalidRequest => "invalid request",
ErrorKind::Config => "config error",
}
}
#[cfg(feature = "tracing")]
struct Endpoint<'a>(Exchange<'a>);
#[cfg(feature = "tracing")]
impl fmt::Display for Endpoint<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&format_endpoint(self.0.method, self.0.uri))
}
}
#[cfg(feature = "tracing")]
struct Elapsed(Started);
#[cfg(feature = "tracing")]
impl fmt::Display for Elapsed {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(started) => write!(formatter, "{}ms", started.elapsed().as_millis()),
None => formatter.write_str("-"),
}
}
}
#[cfg(feature = "tracing")]
struct RequestId<'a>(&'a HeaderMap);
#[cfg(feature = "tracing")]
impl fmt::Display for RequestId<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match request_id(self.0) {
Some(id) => {
let mut shown = SafeText::new(MAX_NAME_CHARS, Backslash::Keep);
shown.untrusted(id, MAX_NAME_CHARS);
formatter.write_str(&shown.into_string())
}
None => formatter.write_str("-"),
}
}
}
#[cfg(feature = "tracing")]
pub(crate) struct ServerName<'a>(pub(crate) &'a str);
#[cfg(feature = "tracing")]
impl fmt::Display for ServerName<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut shown = SafeText::new(MAX_NAME_CHARS, Backslash::Double);
shown.untrusted(self.0, MAX_NAME_CHARS);
formatter.write_str(&shown.into_string())
}
}
#[cfg(feature = "tracing")]
struct Lossy<'a>(&'a [u8]);
#[cfg(feature = "tracing")]
impl fmt::Display for Lossy<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write as _;
let mut shown = SafeText::new(usize::MAX, Backslash::Keep);
let mut writer = shown.untrusted_writer();
for chunk in self.0.utf8_chunks() {
writer.write_str(chunk.valid())?;
if !chunk.invalid().is_empty() {
writer.write_str("\u{fffd}")?;
}
}
drop(writer);
formatter.write_str(&shown.into_string())
}
}
#[cfg(feature = "tracing")]
const REDACTED: &str = "***";
#[cfg(feature = "tracing")]
fn redact(headers: &HeaderMap) -> RedactedHeaders<'_> {
RedactedHeaders(headers)
}
#[cfg(feature = "tracing")]
#[derive(Clone, Copy)]
struct RedactedHeaders<'a>(&'a HeaderMap);
#[cfg(feature = "tracing")]
impl RedactedHeaders<'_> {
fn entries(&self) -> impl Iterator<Item = (&HeaderName, Option<&HeaderValue>)> {
self.0.iter().map(|(name, value)| (name, (!is_secret(name, value)).then_some(value)))
}
}
#[cfg(feature = "tracing")]
impl fmt::Debug for RedactedHeaders<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_map()
.entries(self.entries().map(|(name, value)| (name, Shown(value))))
.finish()
}
}
#[cfg(feature = "tracing")]
struct Shown<'a>(Option<&'a HeaderValue>);
#[cfg(feature = "tracing")]
impl fmt::Debug for Shown<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
Some(value) => fmt::Debug::fmt(value, formatter),
None => fmt::Debug::fmt(REDACTED, formatter),
}
}
}
#[cfg(all(test, feature = "tracing"))]
#[path = "telemetry_tests.rs"]
mod tests;