mirror of https://github.com/zino-rs/zino
Version 0.4.1
This commit is contained in:
parent
920fb46a65
commit
01e04ba23c
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue