use std::{convert::Infallible, net::Ipv4Addr};
use kynos::{
error::rejection::HeaderRejection,
extract::params::header::{DecodeHeaders, EncodeHeaders, HeaderParams},
http::{self, HeaderMap, HeaderName, HeaderValue},
middleware::{Continued, Interceptor, Next},
prelude::*,
response::status::NoContent,
server::Server,
};
use opentelemetry::{propagation::Extractor, trace::TracerProvider as _};
use opentelemetry_semantic_conventions::trace as semconv;
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
#[derive(Clone, Debug, Default)]
struct TraceContext {
traceparent: Option<String>,
tracestate: Option<String>,
}
impl HeaderParams for TraceContext {
const NAMES: &'static [&'static str] = &["traceparent", "tracestate"];
}
impl DecodeHeaders for TraceContext {
fn decode(headers: &HeaderMap) -> Result<Self, HeaderRejection> {
let read = |name: &str| {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(ToOwned::to_owned)
};
Ok(Self {
traceparent: read("traceparent"),
tracestate: read("tracestate"),
})
}
}
impl EncodeHeaders for TraceContext {
fn encode(&self) -> Vec<(HeaderName, HeaderValue)> {
Vec::new()
}
}
impl Extractor for TraceContext {
fn get(&self, key: &str) -> Option<&str> {
match key {
"traceparent" => self.traceparent.as_deref(),
"tracestate" => self.tracestate.as_deref(),
_ => None,
}
}
fn keys(&self) -> Vec<&str> {
Self::NAMES.to_vec()
}
}
#[derive(Clone, Copy, Debug)]
struct Telemetry;
impl<C: Sync + 'static> Interceptor<C> for Telemetry {
type Reads = TraceContext;
type Adds = ();
type Short = Infallible;
async fn intercept(
&self,
request: http::Request,
reads: TraceContext,
context: &C,
next: Next<'_, C>,
) -> Result<Continued<()>, Infallible> {
let _ = context;
let route = next.route();
let span = tracing::info_span!(
"http.server.request",
otel.name = %format!("{} {}", request.method(), route.path()),
otel.kind = "server",
{ semconv::HTTP_REQUEST_METHOD } = %request.method(),
{ semconv::HTTP_ROUTE } = route.path(),
{ semconv::URL_PATH } = request.uri().path(),
{ semconv::HTTP_RESPONSE_STATUS_CODE } = tracing::field::Empty,
);
if let Err(error) = span.set_parent(opentelemetry::global::get_text_map_propagator(
|propagator| propagator.extract(&reads),
)) {
tracing::warn!(%error, "could not join the upstream trace");
}
let continued = {
let _entered = span.enter();
next.run(request).await
};
span.record(
semconv::HTTP_RESPONSE_STATUS_CODE,
continued.status().as_u16(),
);
Ok(continued)
}
}
#[derive(Schema, PathParams)]
struct OrderPath {
id: u64,
}
#[kynos::get("/orders/{id}")]
async fn order(Path(path): Path<OrderPath>) -> NoContent {
tracing::info!(order.id = path.id, "looked up an order");
NoContent
}
fn install_telemetry()
-> Result<opentelemetry_sdk::trace::SdkTracerProvider, Box<dyn std::error::Error>> {
use tracing_subscriber::layer::SubscriberExt as _;
opentelemetry::global::set_text_map_propagator(
opentelemetry_sdk::propagation::TraceContextPropagator::new(),
);
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.build()?;
let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(
opentelemetry_sdk::Resource::builder()
.with_service_name("kynos-orders")
.build(),
)
.build();
let subscriber = tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with(tracing_subscriber::fmt::layer())
.with(tracing_opentelemetry::layer().with_tracer(provider.tracer("kynos")));
tracing::subscriber::set_global_default(subscriber)?;
Ok(provider)
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let provider = install_telemetry()?;
let router = Router::<()>::new()
.mount(kynos::routes![order])
.intercept(Telemetry);
println!("{}", router.openapi()?.to_json()?);
Server::new(router.build(())?)
.bind((Ipv4Addr::UNSPECIFIED, 3000))
.serve()
.await?;
provider.shutdown()?;
Ok(())
}