use std::borrow::Cow;
use std::collections::HashMap;
use std::future::Future;
use std::sync::Arc;
use std::time::{Duration, Instant};
mod names;
#[cfg(feature = "otel")]
mod otel;
#[cfg(feature = "tracing")]
use crate::handler::InboundTracePolicy;
use crate::handler::RequestHandler;
use crate::metrics::{Metrics, RpcCall};
#[cfg(feature = "tracing")]
use names::OTHER;
pub use names::{RpcSystem, WireStatus, a2a_method};
#[cfg(feature = "otel")]
pub use otel::current_recorded_span;
#[cfg(feature = "otel")]
use otel::remote_parent;
#[cfg(feature = "tracing")]
const METHOD_ORIGINAL_MAX: usize = 128;
const CANCELLED: &str = "cancelled";
pub struct ServerSpan {
system: RpcSystem,
method: &'static str,
metrics: Arc<dyn Metrics>,
#[cfg(feature = "tracing")]
span: tracing::Span,
#[cfg(feature = "tracing")]
untraced: bool,
}
#[cfg(feature = "tracing")]
tokio::task_local! {
static UNTRACED: bool;
}
#[cfg(feature = "tracing")]
fn untraced() -> bool {
UNTRACED.try_with(|u| *u).unwrap_or(false)
}
impl ServerSpan {
#[cfg_attr(not(feature = "otel"), allow(unused_variables))]
pub fn open(
handler: &RequestHandler,
system: RpcSystem,
method: &str,
headers: Option<&HashMap<String, String>>,
) -> Self {
let qualified = a2a_method(method);
#[cfg(feature = "tracing")]
let span = {
let policy = handler.inbound_trace_policy;
if policy == InboundTracePolicy::Drop {
tracing::Span::none()
} else {
let name = if qualified == OTHER {
system.name()
} else {
qualified
};
let span = tracing::info_span!(
target: "a2a_protocol_server::rpc",
"a2a.rpc",
otel.name = name,
otel.kind = "server",
otel.status_code = tracing::field::Empty,
rpc.system.name = system.name(),
rpc.method = qualified,
rpc.method_original = tracing::field::Empty,
rpc.status_code = tracing::field::Empty,
error.type = tracing::field::Empty,
http.request.method = tracing::field::Empty,
http.route = tracing::field::Empty,
);
if qualified == OTHER {
span.record(
"rpc.method_original",
truncated(method, METHOD_ORIGINAL_MAX),
);
}
#[cfg(feature = "otel")]
if policy == InboundTracePolicy::Continue
&& let Some(parent) = headers.and_then(remote_parent)
{
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
let _ = span.set_parent(parent);
}
span
}
};
Self {
system,
method: qualified,
metrics: Arc::clone(&handler.metrics),
#[cfg(feature = "tracing")]
untraced: handler.inbound_trace_policy == InboundTracePolicy::Drop,
#[cfg(feature = "tracing")]
span,
}
}
#[cfg_attr(
not(feature = "tracing"),
allow(unused_variables, clippy::missing_const_for_fn)
)]
pub fn with_http(self, method: &str, route: &str) -> Self {
#[cfg(feature = "tracing")]
{
self.span.record("http.request.method", method);
self.span.record("http.route", route);
}
self
}
pub fn run<T, E, F>(self, fut: F) -> impl Future<Output = Result<T, E>>
where
E: WireStatus,
F: Future<Output = Result<T, E>>,
{
self.run_with(fut, |out| out)
}
pub fn run_with<T, E, F, R>(
self,
fut: F,
then: impl FnOnce(Result<T, E>) -> R,
) -> impl Future<Output = R>
where
E: WireStatus,
F: Future<Output = Result<T, E>>,
{
self.call(
fut,
|out, system| out.as_ref().err().map(|e| e.wire_status(system)),
then,
)
}
pub fn run_response<B, F>(self, fut: F) -> impl Future<Output = hyper::Response<B>>
where
F: Future<Output = hyper::Response<B>>,
{
self.call(
fut,
|resp, _| {
(resp.status().as_u16() >= 400)
.then(|| Cow::Owned(resp.status().as_u16().to_string()))
},
|resp| resp,
)
}
fn call<F, C, T, R>(self, fut: F, classify: C, then: T) -> Call<F, C, T>
where
F: Future,
C: FnOnce(&F::Output, RpcSystem) -> Option<Cow<'static, str>>,
T: FnOnce(F::Output) -> R,
{
#[cfg(feature = "tracing")]
let inner =
tracing::Instrument::instrument(UNTRACED.scope(self.untraced, fut), self.span.clone());
#[cfg(not(feature = "tracing"))]
let inner = fut;
Call {
inner,
pending: Pending {
call: Some(self),
started: Instant::now(),
},
finish: Some((classify, then)),
}
}
#[cfg_attr(not(feature = "tracing"), allow(clippy::unused_self))]
fn enter<R>(&self, then: impl FnOnce() -> R) -> R {
#[cfg(feature = "tracing")]
{
self.span
.in_scope(|| UNTRACED.sync_scope(self.untraced, then))
}
#[cfg(not(feature = "tracing"))]
{
then()
}
}
fn finish(&self, duration: Duration, failed_with: Option<&str>) {
let status_code = match (failed_with, self.system) {
(Some(code), _) => Some(code),
(None, RpcSystem::Grpc) => Some("OK"),
(None, _) => None,
};
#[cfg(feature = "tracing")]
{
if let Some(code) = status_code {
self.span.record("rpc.status_code", code);
}
if let Some(code) = failed_with {
self.span.record("error.type", code);
self.span.record("otel.status_code", "ERROR");
}
}
self.metrics.on_rpc_call(&RpcCall {
system: self.system.name(),
method: Some(self.method),
duration,
status_code,
error_type: failed_with,
});
}
}
impl std::fmt::Debug for ServerSpan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerSpan")
.field("system", &self.system)
.field("method", &self.method)
.finish_non_exhaustive()
}
}
#[cfg(feature = "tracing")]
type Inner<F> = tracing::instrument::Instrumented<tokio::task::futures::TaskLocalFuture<bool, F>>;
#[cfg(not(feature = "tracing"))]
type Inner<F> = F;
pin_project_lite::pin_project! {
pub struct Call<F, C, T>
where
F: Future,
{
#[pin]
inner: Inner<F>,
pending: Pending,
finish: Option<(C, T)>,
}
}
impl<F, C, T, R> Future for Call<F, C, T>
where
F: Future,
C: FnOnce(&F::Output, RpcSystem) -> Option<Cow<'static, str>>,
T: FnOnce(F::Output) -> R,
{
type Output = R;
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<R> {
let this = self.project();
let out = std::task::ready!(this.inner.poll(cx));
let (classify, then) = this.finish.take().expect("a call polled after it finished");
let call = this
.pending
.call
.take()
.expect("a call polled after it finished");
let status = classify(&out, call.system);
call.finish(this.pending.started.elapsed(), status.as_deref());
call.enter(|| then(out)).into()
}
}
struct Pending {
call: Option<ServerSpan>,
started: Instant,
}
impl Drop for Pending {
fn drop(&mut self) {
if let Some(call) = self.call.take() {
call.finish(self.started.elapsed(), Some(CANCELLED));
}
}
}
pub fn record_unrouted(
handler: &RequestHandler,
system: RpcSystem,
started: Instant,
status: &str,
) {
handler.metrics.on_rpc_call(&RpcCall {
system: system.name(),
method: None,
duration: started.elapsed(),
status_code: Some(status),
error_type: Some(status),
});
}
#[cfg(feature = "tracing")]
fn truncated(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}
pub fn in_child_span<F: Future>(name: &'static str, fut: F) -> impl Future<Output = F::Output> {
#[cfg(feature = "tracing")]
{
let untraced = untraced();
let span = if untraced {
tracing::Span::none()
} else {
tracing::info_span!(target: "a2a_protocol_server::rpc", "a2a.task", otel.name = name)
};
tracing::Instrument::instrument(UNTRACED.scope(untraced, fut), span)
}
#[cfg(not(feature = "tracing"))]
{
let _ = name;
fut
}
}
pub fn in_current_span<F: Future>(fut: F) -> impl Future<Output = F::Output> {
#[cfg(feature = "tracing")]
{
let untraced = untraced();
tracing::Instrument::instrument(UNTRACED.scope(untraced, fut), tracing::Span::current())
}
#[cfg(not(feature = "tracing"))]
{
fut
}
}
pub fn in_executor_span<F: Future>(
task_id: &str,
context_id: &str,
fut: F,
) -> impl Future<Output = F::Output> + use<F> {
#[cfg(feature = "tracing")]
{
let untraced = untraced();
let span = if untraced {
tracing::Span::none()
} else {
tracing::info_span!(
target: "a2a_protocol_server::rpc",
"a2a.task",
otel.name = "a2a.execute",
a2a.task.id = task_id,
a2a.context.id = context_id,
)
};
tracing::Instrument::instrument(UNTRACED.scope(untraced, fut), span)
}
#[cfg(not(feature = "tracing"))]
{
let _ = (task_id, context_id);
fut
}
}
#[cfg(test)]
mod tests;