Version 0.4.1

This commit is contained in:
photino 2023-01-19 03:20:18 +08:00
parent 920fb46a65
commit 01e04ba23c
18 changed files with 318 additions and 110 deletions

View File

@ -14,8 +14,8 @@ productivity and performance.
- 🚀 Out-of-the-box features for rapid application development.
- ✨ Minimal design, modular architecture and high-level abstractions.
- ⚡ Embrace practical conventions to get the best performance.
- 🐘 Highly optimized ORM for PostgreSQL built with [`sqlx`].
- Lightweight scheduler for sync and async cron jobs.
- 🐘 Highly optimized ORM for PostgreSQL built on top of [`sqlx`].
- 🕗 Lightweight scheduler for sync and async cron jobs.
- 📊 Support for `logging`, [`tracing`] and [`metrics`].
## Getting started

View File

@ -26,11 +26,12 @@ username = "postgres"
password = "QAx01wnh1i5ER713zfHmZi6dIUYn/Iq9ag+iUGtvKzEFJFYW"
[tracing]
filter = "debug,sqlx=trace,tower_http=trace,zino=trace,zino_core=trace"
display-filename = true
display-line-number = true
filter = "info,sqlx=trace,tower_http=trace,zino=trace,zino_core=trace"
display-filename = false
display-line-number = false
display-span-list = false
[metrics]
exporter = "prometheus"
host = "127.0.0.1"
port = 9000
port = 9000

View File

@ -2,8 +2,7 @@ use serde_json::json;
use zino::{Request, RequestContext, Response};
pub(crate) async fn index(req: Request) -> zino::Result {
let mut res = Response::default();
res.set_context(&req);
let mut res = Response::default().provide_context(&req);
res.set_data(json!({
"method": "GET",
"path": "/stats",

View File

@ -22,8 +22,7 @@ pub(crate) async fn new(mut req: Request) -> zino::Result {
pub(crate) async fn update(mut req: Request) -> zino::Result {
let mut user = User::new();
let validation = req.parse_body().await.map(|body| user.read_map(body))?;
let mut res = Response::from(validation);
res.set_context(&req);
let res = Response::from(validation).provide_context(&req);
Ok(res.into())
}
@ -52,6 +51,7 @@ pub(crate) async fn view(mut req: Request) -> zino::Result {
req.try_send(event)?;
let user = User::find_one(query).await.map_err(Rejection::from)?;
req.fetch("http://localhost:6081/stats", None).await.map_err(Rejection::from)?;
let state_data = req.state_data_mut();
let counter = state_data

View File

@ -34,6 +34,7 @@ metrics-exporter-tcp = "0.7.0"
parking_lot = "0.12.1"
rand = "0.8.5"
reqwest-middleware = "0.2.0"
reqwest-retry = "0.2.1"
reqwest-tracing = "0.4.0"
serde_json = "1.0.91"
serde_qs = "0.11.0"
@ -54,7 +55,7 @@ features = ["derive"]
[dependencies.reqwest]
version = "0.11.13"
features = ["cookies", "gzip", "brotli", "deflate", "json", "multipart"]
features = ["cookies", "gzip", "brotli", "deflate", "json", "multipart", "stream"]
[dependencies.sqlx]
version = "0.6.2"

View File

@ -1,12 +1,19 @@
use crate::application::Application;
use reqwest::{Client, Request, Response};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, Extension, Result};
use reqwest_tracing::{
default_on_request_end, reqwest_otel_span, OtelName, ReqwestOtelSpanBackend, TracingMiddleware,
use crate::{application::Application, trace::TraceContext, BoxError, Map, Uuid};
use reqwest::{
header::{self, HeaderMap, HeaderName},
Client, IntoUrl, Method, Request, Response, Url,
};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, Error, RequestBuilder};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use reqwest_tracing::{ReqwestOtelSpanBackend, TracingMiddleware};
use serde_json::Value;
use std::{
borrow::Cow,
sync::OnceLock,
time::{Duration, Instant},
};
use std::{sync::OnceLock, time::Instant};
use task_local_extensions::Extensions;
use tracing::Span;
use tracing::{field::Empty, Span};
pub(super) fn init<APP: Application + ?Sized>() {
let name = APP::name();
@ -19,37 +26,167 @@ pub(super) fn init<APP: Application + ?Sized>() {
.deflate(true)
.build()
.unwrap_or_else(|err| panic!("failed to create an HTTP client: {err}"));
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
let client = ClientBuilder::new(reqwest_client)
.with_init(Extension(OtelName("zino-bot".into())))
.with(TracingMiddleware::default())
.with(TracingMiddleware::<RequestTiming>::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build();
SHARED_HTTP_CLIENT
.set(client)
.expect("failed to set an HTTP client for the application");
}
/// Constructs a request builder.
pub(crate) fn request_builder(
resource: impl IntoUrl,
options: impl Into<Option<Map>>,
) -> Result<RequestBuilder, BoxError> {
let options = options.into().unwrap_or_default();
if options.is_empty() {
let request_builder = SHARED_HTTP_CLIENT
.get()
.ok_or("failed to get the global http client")?
.request(Method::GET, resource);
return Ok(request_builder);
}
let method = options
.get("method")
.and_then(|s| s.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(Method::GET);
let mut request_builder = SHARED_HTTP_CLIENT
.get()
.ok_or("failed to get the global http client")?
.request(method, resource);
let mut headers = HeaderMap::new();
if let Some(query) = options.get("query") {
request_builder = request_builder.query(query);
}
if let Some(body) = options.get("body") {
let content_type = options
.get("content_type")
.and_then(|t| t.as_str())
.unwrap_or_default();
match body {
Value::String(value) => {
if content_type == "json" {
request_builder = request_builder
.json(value)
.header(header::CONTENT_TYPE, "application/json");
} else {
request_builder = request_builder
.form(value)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded");
}
}
Value::Object(value) => {
if content_type == "form" {
request_builder = request_builder
.form(value)
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded");
} else {
request_builder = request_builder
.json(value)
.header(header::CONTENT_TYPE, "application/json");
}
}
_ => tracing::warn!("unsupported body format"),
}
}
if let Some(map) = options.get("headers").and_then(|t| t.as_object()) {
for (key, value) in map {
if let Ok(header_name) = HeaderName::try_from(key) {
if let Some(header_value) = value.as_str().and_then(|s| s.parse().ok()) {
headers.insert(header_name, header_value);
}
}
}
}
if !headers.is_empty() {
request_builder = request_builder.headers(headers);
}
if let Some(timeout) = options.get("timeout").and_then(|t| t.as_u64()) {
request_builder = request_builder.timeout(Duration::from_millis(timeout));
}
Ok(request_builder)
}
/// Request timing.
struct RequestTiming;
impl ReqwestOtelSpanBackend for RequestTiming {
fn on_request_start(req: &Request, extension: &mut Extensions) -> Span {
extension.insert(Instant::now());
reqwest_otel_span!(
name = "example-request",
req,
time_elapsed = tracing::field::Empty
fn on_request_start(request: &Request, extensions: &mut Extensions) -> Span {
let url = request.url();
let headers = request.headers();
let traceparent = headers.get("traceparent").and_then(|v| v.to_str().ok());
extensions.insert(Instant::now());
tracing::info_span!(
"HTTP request",
"otel.kind" = "client",
"otel.name" = "zino-bot",
"http.method" = request.method().as_str(),
"http.scheme" = url.scheme(),
"http.url" = remove_credentials(url).as_ref(),
"http.request.header.traceparent" = traceparent,
"http.response.header.traceparent" = Empty,
"http.status_code" = Empty,
"http.client.duration" = Empty,
"net.peer.name" = url.domain(),
"net.peer.port" = url.port(),
"zino.trace_id" = traceparent
.and_then(TraceContext::from_traceparent)
.map(|ctx| Uuid::from_u128(ctx.trace_id()).to_string()),
"zino.request_id" = Empty,
"zino.session_id" = headers.get("session-id").and_then(|v| v.to_str().ok()),
id = Empty,
)
}
fn on_request_end(span: &Span, outcome: &Result<Response>, extensions: &mut Extensions) {
let latency_micros = extensions
fn on_request_end(span: &Span, outcome: &Result<Response, Error>, extensions: &mut Extensions) {
let latency_millis = extensions
.get::<Instant>()
.and_then(|t| u64::try_from(t.elapsed().as_micros()).ok());
default_on_request_end(span, outcome);
span.record("latency_micros", latency_micros);
.and_then(|t| u64::try_from(t.elapsed().as_millis()).ok());
span.record("http.client.duration", latency_millis);
span.record("id", span.id().map(|t| t.into_u64()));
match outcome {
Ok(response) => {
let headers = response.headers();
span.record(
"http.response.header.traceparent",
headers.get("traceparent").and_then(|v| v.to_str().ok()),
);
span.record(
"zino.request_id",
headers.get("x-request-id").and_then(|v| v.to_str().ok()),
);
span.record("http.status_code", response.status().as_u16());
tracing::info!("finished HTTP request");
}
Err(err) => {
if let Error::Reqwest(err) = err {
span.record(
"http.status_code",
err.status().map(|status_code| status_code.as_u16()),
);
}
tracing::error!("{err}");
}
}
}
}
fn remove_credentials(url: &Url) -> Cow<'_, str> {
if !url.username().is_empty() || url.password().is_some() {
let mut url = url.clone();
url.set_username("")
.and_then(|_| url.set_password(None))
.ok();
url.to_string().into()
} else {
url.as_ref().into()
}
}
/// Shared HTTP client.
pub(crate) static SHARED_HTTP_CLIENT: OnceLock<ClientWithMiddleware> = OnceLock::new();
static SHARED_HTTP_CLIENT: OnceLock<ClientWithMiddleware> = OnceLock::new();

View File

@ -3,16 +3,18 @@
use crate::{
schedule::{AsyncCronJob, CronJob, Job, JobScheduler},
state::State,
trace::TraceContext,
BoxError, Map,
};
use reqwest::{Method, Response, Url};
use reqwest::{IntoUrl, Response};
use std::{collections::HashMap, env, path::PathBuf, sync::LazyLock, thread};
use toml::value::Table;
mod http_client;
mod metrics_exporter;
mod tracing_subscriber;
pub(crate) mod http_client;
/// Application.
pub trait Application {
/// Router.
@ -48,16 +50,12 @@ pub trait Application {
/// Makes an HTTP request to the provided resource
/// using [`reqwest`](https://crates.io/crates/reqwest).
async fn fetch(resource: Url, options: Map) -> Result<Response, BoxError> {
let method = options
.get("method")
.and_then(|s| s.as_str())
.and_then(|s| s.parse().ok())
.unwrap_or(Method::GET);
http_client::SHARED_HTTP_CLIENT
.get()
.ok_or("failed to get the global http client")?
.request(method, resource)
async fn fetch(
resource: impl IntoUrl,
options: impl Into<Option<Map>>,
) -> Result<Response, BoxError> {
http_client::request_builder(resource, options)?
.header("traceparent", TraceContext::new().to_string())
.send()
.await
.map_err(BoxError::from)

View File

@ -39,4 +39,5 @@ pub type SharedString = std::borrow::Cow<'static, str>;
pub type BoxError = Box<dyn std::error::Error + Sync + Send + 'static>;
/// An owned dynamically typed future.
pub type BoxFuture<'a, T = ()> = futures::future::BoxFuture<'a, T>;
pub type BoxFuture<'a, T = ()> =
std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;

View File

@ -1,16 +1,18 @@
//! Request context and validation.
use crate::{
application::http_client,
authentication::{Authentication, ParseSecurityTokenError, SecurityToken, SessionId},
channel::{CloudEvent, Subscription},
database::{Model, Query},
datetime::DateTime,
response::{Rejection, Response, ResponseCode},
trace::TraceContext,
Map, Uuid,
BoxError, Map, Uuid,
};
use http::uri::Uri;
use hyper::body::Bytes;
use reqwest::IntoUrl;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::time::{Duration, Instant};
@ -66,9 +68,10 @@ pub trait RequestContext {
let request_id = self
.get_header("x-request-id")
.and_then(|s| s.parse().ok())
.unwrap_or(Uuid::new_v4());
let trace_context = self.trace_context();
let trace_id = trace_context.map_or(Uuid::nil(), |t| Uuid::from_u128(t.trace_id()));
.unwrap_or_else(Uuid::new_v4);
let trace_id = self
.get_trace_context()
.map_or_else(Uuid::new_v4, |t| Uuid::from_u128(t.trace_id()));
let session_id = self.get_header("session-id").and_then(|s| s.parse().ok());
let mut ctx = Context::new(request_id);
@ -77,13 +80,23 @@ pub trait RequestContext {
ctx
}
/// Returns the trace context.
/// Returns the trace context by parsing the `traceparent` header.
#[inline]
fn trace_context(&self) -> Option<TraceContext> {
fn get_trace_context(&self) -> Option<TraceContext> {
let traceparent = self.get_header("traceparent")?;
TraceContext::from_traceparent(traceparent)
}
/// Creates a new `TraceContext`.
fn new_trace_context(&self) -> TraceContext {
self.get_trace_context()
.or_else(|| {
self.get_context()
.map(|ctx| TraceContext::with_trace_id(ctx.trace_id()))
})
.unwrap_or_default()
}
/// Returns the start time.
#[inline]
fn start_time(&self) -> Instant {
@ -335,8 +348,7 @@ pub trait RequestContext {
Ok(data) => {
let validation = query.read_map(data);
if validation.is_success() {
let mut res = Response::new(S::OK);
res.set_context(self);
let res = Response::with_context(S::OK, self);
Ok(res)
} else {
Err(validation.into())
@ -359,8 +371,7 @@ pub trait RequestContext {
Ok(data) => {
let validation = model.read_map(data);
if validation.is_success() {
let mut res = Response::new(S::OK);
res.set_context(self);
let res = Response::with_context(S::OK, self);
Ok(res)
} else {
Err(validation.into())
@ -370,6 +381,20 @@ pub trait RequestContext {
}
}
/// Makes an HTTP request to the provided resource
/// using [`reqwest`](https://crates.io/crates/reqwest).
async fn fetch(
&self,
resource: impl IntoUrl,
options: impl Into<Option<Map>>,
) -> Result<reqwest::Response, BoxError> {
http_client::request_builder(resource, options)?
.header("traceparent", self.new_trace_context().to_string())
.send()
.await
.map_err(BoxError::from)
}
/// Creates a new subscription instance.
fn subscription(&self) -> Subscription {
let mut subscription = self.parse_query::<Subscription>().unwrap_or_default();

View File

@ -157,10 +157,19 @@ impl<S: ResponseCode> Response<S> {
} else {
res.detail = message;
}
res.trace_context = ctx.trace_context().map(|t| t.child());
res.trace_context = Some(ctx.new_trace_context().child());
res
}
/// Provides the request context for the response.
pub fn provide_context<T: RequestContext>(mut self, ctx: &T) -> Self {
self.instance = (!self.is_success()).then(|| ctx.request_path().to_string().into());
self.start_time = ctx.start_time();
self.request_id = ctx.request_id();
self.trace_context = Some(ctx.new_trace_context().child());
self
}
/// Sets the code.
pub fn set_code(&mut self, code: S) {
let success = code.is_success();
@ -179,14 +188,6 @@ impl<S: ResponseCode> Response<S> {
}
}
/// Sets the request context.
pub fn set_context<T: RequestContext>(&mut self, ctx: &T) {
self.instance = (!self.is_success()).then(|| ctx.request_path().to_string().into());
self.start_time = ctx.start_time();
self.request_id = ctx.request_id();
self.trace_context = ctx.trace_context().map(|t| t.child());
}
/// Sets a URI reference that identifies the specific occurrence of the problem.
pub fn set_instance(&mut self, instance: impl Into<Option<SharedString>>) {
self.instance = instance.into();

View File

@ -75,6 +75,13 @@ impl From<Validation> for Rejection {
}
}
impl From<BoxError> for Rejection {
#[inline]
fn from(err: BoxError) -> Self {
InternalServerError(err)
}
}
impl From<sqlx::Error> for Rejection {
/// Converts to this type from the input type `sqlx::Error`.
#[inline]

View File

@ -183,7 +183,9 @@ impl JobScheduler {
}
}
}
duration.to_std().unwrap_or(Duration::from_millis(500))
duration
.to_std()
.unwrap_or_else(|_| Duration::from_millis(500))
}
}
}

View File

@ -1,3 +1,4 @@
use crate::Uuid;
use std::fmt;
use tracing::Span;
@ -16,7 +17,6 @@ pub struct TraceContext {
/// Version of the traceparent header.
version: u8,
/// Globally unique identifier.
/// All bytes as zero is considered an invalid value.
trace_id: u128,
/// Identifier of the request known by the caller.
parent_id: Option<u64>,
@ -34,7 +34,22 @@ impl TraceContext {
Self {
span_id,
version: 0,
trace_id: rand::random(),
trace_id: Uuid::new_v4().as_u128(),
parent_id: None,
trace_flags: FLAG_SAMPLED | FLAG_RANDOM_TRACE_ID,
}
}
/// Creates a new instance with the specific `trace-id`.
pub fn with_trace_id(trace_id: Uuid) -> Self {
let span_id = Span::current()
.id()
.map(|t| t.into_u64())
.unwrap_or_else(rand::random);
Self {
span_id,
version: 0,
trace_id: trace_id.as_u128(),
parent_id: None,
trace_flags: FLAG_SAMPLED | FLAG_RANDOM_TRACE_ID,
}

View File

@ -8,8 +8,8 @@ productivity and performance.
- 🚀 Out-of-the-box features for rapid application development.
- ✨ Minimal design, modular architecture and high-level abstractions.
- ⚡ Embrace practical conventions to get the best performance.
- 🐘 Highly optimized ORM for PostgreSQL built with [`sqlx`].
- Lightweight scheduler for sync and async cron jobs.
- 🐘 Highly optimized ORM for PostgreSQL built on top of [`sqlx`].
- 🕗 Lightweight scheduler for sync and async cron jobs.
- 📊 Support for `logging`, [`tracing`] and [`metrics`].
## Getting started

View File

@ -6,8 +6,8 @@
//! - 🚀 Out-of-the-box features for rapid application development.
//! - ✨ Minimal design, modular architecture and high-level abstractions.
//! - ⚡ Embrace practical conventions to get the best performance.
//! - 🐘 Highly optimized ORM for PostgreSQL built with [`sqlx`].
//! - Lightweight scheduler for sync and async cron jobs.
//! - 🐘 Highly optimized ORM for PostgreSQL built on top of [`sqlx`].
//! - 🕗 Lightweight scheduler for sync and async cron jobs.
//! - 📊 Support for `logging`, [`tracing`] and [`metrics`].
//!
//! ## Getting started

View File

@ -21,7 +21,7 @@ pub(crate) static CORS_MIDDLEWARE: LazyLock<CorsLayer> = LazyLock::new(|| {
.collect::<Vec<_>>();
AllowOrigin::list(origins)
})
.unwrap_or(AllowOrigin::mirror_request());
.unwrap_or_else(AllowOrigin::mirror_request);
let allow_methods = cors
.get("allow-methods")
.and_then(|t| t.as_array())
@ -32,7 +32,7 @@ pub(crate) static CORS_MIDDLEWARE: LazyLock<CorsLayer> = LazyLock::new(|| {
.collect::<Vec<_>>();
AllowMethods::list(methods)
})
.unwrap_or(AllowMethods::mirror_request());
.unwrap_or_else(AllowMethods::mirror_request);
let allow_headers = cors
.get("allow-headers")
.and_then(|t| t.as_array())
@ -43,7 +43,7 @@ pub(crate) static CORS_MIDDLEWARE: LazyLock<CorsLayer> = LazyLock::new(|| {
.collect::<Vec<_>>();
AllowHeaders::list(header_names)
})
.unwrap_or(AllowHeaders::mirror_request());
.unwrap_or_else(AllowHeaders::mirror_request);
let expose_headers = cors
.get("expose-headers")
.and_then(|t| t.as_array())
@ -54,7 +54,7 @@ pub(crate) static CORS_MIDDLEWARE: LazyLock<CorsLayer> = LazyLock::new(|| {
.collect::<Vec<_>>();
ExposeHeaders::list(header_names)
})
.unwrap_or(ExposeHeaders::any());
.unwrap_or_else(ExposeHeaders::any);
let max_age = cors
.get("max-age")
.and_then(|t| t.as_integer().and_then(|i| i.try_into().ok()))

View File

@ -1,3 +1,4 @@
use crate::AxumCluster;
use axum::{
body::{Body, BoxBody, Bytes},
http::{HeaderMap, Request, Response},
@ -8,7 +9,7 @@ use tower_http::{
trace::TraceLayer,
};
use tracing::{field::Empty, Span};
use zino_core::{trace::TraceContext, Uuid};
use zino_core::{application::Application, trace::TraceContext, Uuid};
// Type aliases.
type NewMakeSpan = fn(&Request<Body>) -> Span;
@ -41,67 +42,87 @@ pub(crate) static TRACING_MIDDLEWARE: LazyLock<NewTraceLayer> = LazyLock::new(||
fn new_make_span(request: &Request<Body>) -> Span {
let uri = request.uri();
let headers = request.headers();
tracing::info_span!(
"http-request",
method = request.method().as_str(),
path = uri.path(),
query = uri.query(),
span_id = Empty,
request_id = Empty,
trace_id = Empty,
session_id = Empty,
"HTTP request",
"otel.kind" = "server",
"otel.name" = AxumCluster::name(),
"http.method" = request.method().as_str(),
"http.scheme" = uri.scheme_str(),
"http.target" = uri.path_and_query().map(|t| t.as_str()),
"http.user_agent" = headers.get("user-agent").and_then(|v| v.to_str().ok()),
"http.request.header.traceparent" = Empty,
"http.response.header.traceparent" = Empty,
"http.status_code" = Empty,
"http.server.duration" = Empty,
"net.host.name" = uri.host(),
"net.host.port" = uri.port_u16(),
"zino.request_id" = Empty,
"zino.trace_id" = Empty,
"zino.session_id" = Empty,
id = Empty,
)
}
fn new_on_request(request: &Request<Body>, span: &Span) {
let headers = request.headers();
let session_id = headers.get("session-id").and_then(|v| v.to_str().ok());
span.record("session_id", session_id);
span.record("span_id", span.id().map(|t| t.into_u64()));
span.record(
"http.request.header.traceparent",
headers.get("traceparent").and_then(|v| v.to_str().ok()),
);
span.record(
"zino.session_id",
headers.get("session-id").and_then(|v| v.to_str().ok()),
);
span.record("id", span.id().map(|t| t.into_u64()));
tracing::debug!("started processing request");
}
fn new_on_response(response: &Response<BoxBody>, latency: Duration, span: &Span) {
let headers = response.headers();
let request_id = headers.get("x-request-id").and_then(|v| v.to_str().ok());
span.record("request_id", request_id);
let trace_id = headers
.get("traceparent")
.and_then(|v| v.to_str().ok().and_then(TraceContext::from_traceparent))
.map(|trace_context| Uuid::from_u128(trace_context.trace_id()).to_string());
span.record("trace_id", trace_id);
tracing::info!(
status_code = response.status().as_u16(),
latency_micros = u64::try_from(latency.as_micros()).ok(),
"finished processing request",
let traceparent = headers.get("traceparent").and_then(|v| v.to_str().ok());
span.record("http.response.header.traceparent", traceparent);
span.record(
"zino.trace_id",
traceparent
.and_then(TraceContext::from_traceparent)
.map(|ctx| Uuid::from_u128(ctx.trace_id()).to_string()),
);
span.record(
"zino.request_id",
headers.get("x-request-id").and_then(|v| v.to_str().ok()),
);
span.record("http.status_code", response.status().as_u16());
span.record(
"http.server.duration",
u64::try_from(latency.as_millis()).ok(),
);
tracing::info!("finished processing request");
}
fn new_on_body_chunk(chunk: &Bytes, _latency: Duration, _span: &Span) {
tracing::debug!(chunk_size = chunk.len(), "sending body chunk");
tracing::debug!("flushed {} bytes", chunk.len());
}
fn new_on_eos(_trailers: Option<&HeaderMap>, stream_duration: Duration, _span: &Span) {
tracing::debug!(
stream_duration_micros = u64::try_from(stream_duration.as_micros()).ok(),
stream_duration = u64::try_from(stream_duration.as_millis()).ok(),
"end of stream",
);
}
fn new_on_failure(error: StatusInRangeFailureClass, latency: Duration, _span: &Span) {
let latency = u64::try_from(latency.as_micros()).ok();
fn new_on_failure(error: StatusInRangeFailureClass, latency: Duration, span: &Span) {
span.record(
"http.server.duration",
u64::try_from(latency.as_millis()).ok(),
);
match error {
StatusInRangeFailureClass::StatusCode(status_code) => {
tracing::error!(
status_code = status_code.as_u16(),
latency_micros = latency,
"response failed",
);
span.record("http.status_code", status_code.as_u16());
tracing::error!("response failed");
}
StatusInRangeFailureClass::Error(err) => {
tracing::error!(latency_micros = latency, err);
tracing::error!(err);
}
}
}