Files
crank/crates/crank-trace/src/lib.rs
T

237 lines
6.9 KiB
Rust

//! Закрытый семантический контракт spans Crank.
//!
//! Этот crate не настраивает subscriber и не знает об OTLP. Он ограничивает
//! имена и атрибуты стадий статическим словарём, чтобы продуктовые crate не
//! могли случайно экспортировать пользовательские данные.
use std::future::Future;
use opentelemetry::{
Context,
trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState},
};
use tracing::{Instrument, Span, field::Empty, info_span};
use tracing_opentelemetry::OpenTelemetrySpanExt;
pub fn set_parent_from_trace_context(span: &Span, context: &crank_core::TraceContext) -> bool {
let mut parts = context.traceparent().split('-');
let (Some("00"), Some(trace_id), Some(parent_id), Some(flags), None) = (
parts.next(),
parts.next(),
parts.next(),
parts.next(),
parts.next(),
) else {
return false;
};
let (Ok(trace_id), Ok(parent_id)) = (TraceId::from_hex(trace_id), SpanId::from_hex(parent_id))
else {
return false;
};
let flags = match flags {
"00" => TraceFlags::default(),
"01" => TraceFlags::SAMPLED,
_ => return false,
};
let parent = SpanContext::new(trace_id, parent_id, flags, true, TraceState::default());
span.set_parent(Context::new().with_remote_span_context(parent))
.is_ok()
}
pub fn trace_context_for_span(span: &Span) -> Option<crank_core::TraceContext> {
let context = span.context();
let span_context = context.span().span_context().clone();
span_context.is_valid().then(|| {
crank_core::TraceContext::from_span_parts(
&span_context.trace_id().to_string(),
&span_context.span_id().to_string(),
span_context.is_sampled(),
)
.ok()
})?
}
macro_rules! stage_span {
($name:literal) => {
info_span!(
target: "crank::trace",
$name,
outcome = Empty,
error.category = Empty,
)
};
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Stage {
McpRateLimit,
McpAccessCheck,
McpCatalogLoad,
McpToolsResolve,
ApprovalCheck,
RuntimeExecute,
RuntimeArgumentsMap,
RuntimeIdempotency,
UpstreamHttp,
RuntimeResponseTransform,
AuthResolve,
ApprovalRecovery,
HistoryWrite,
DbQuery,
}
impl Stage {
pub fn span(self) -> Span {
match self {
Self::McpRateLimit => stage_span!("mcp.rate_limit"),
Self::McpAccessCheck => stage_span!("mcp.access.check"),
Self::McpCatalogLoad => stage_span!("mcp.catalog.load"),
Self::McpToolsResolve => stage_span!("mcp.tools.resolve"),
Self::ApprovalCheck => stage_span!("approval.check"),
Self::RuntimeExecute => stage_span!("runtime.execute"),
Self::RuntimeArgumentsMap => stage_span!("runtime.arguments.map"),
Self::RuntimeIdempotency => stage_span!("runtime.idempotency"),
Self::UpstreamHttp => stage_span!("upstream.http"),
Self::RuntimeResponseTransform => stage_span!("runtime.response.transform"),
Self::AuthResolve => stage_span!("auth.resolve"),
Self::ApprovalRecovery => stage_span!("approval.recovery"),
Self::HistoryWrite => stage_span!("history.write"),
Self::DbQuery => info_span!(
target: "crank::trace",
"db.query",
outcome = Empty,
error.category = Empty,
db.system = "postgresql",
db.operation = Empty,
),
}
}
}
pub async fn observe_db_query<T, E>(
operation: DbOperation,
future: impl Future<Output = Result<T, E>>,
) -> Result<T, E> {
let span = operation.span();
let result = future.instrument(span.clone()).await;
match &result {
Ok(_) => StageOutcome::Success.record(&span),
Err(_) => {
StageOutcome::Error.record(&span);
ErrorCategory::Database.record(&span);
}
}
result
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StageOutcome {
Success,
Error,
Allowed,
Denied,
Required,
Replay,
Execute,
Skipped,
CacheHit,
}
impl StageOutcome {
pub fn record(self, span: &Span) {
span.record("outcome", self.as_str());
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Error => "error",
Self::Allowed => "allowed",
Self::Denied => "denied",
Self::Required => "required",
Self::Replay => "replay",
Self::Execute => "execute",
Self::Skipped => "skipped",
Self::CacheHit => "cache_hit",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorCategory {
Access,
RateLimit,
Catalog,
Approval,
Idempotency,
Schema,
Mapping,
Upstream,
Transformation,
History,
Database,
Concurrency,
Configuration,
Internal,
}
impl ErrorCategory {
pub fn record(self, span: &Span) {
span.record("error.category", self.as_str());
}
pub const fn as_str(self) -> &'static str {
match self {
Self::Access => "access",
Self::RateLimit => "rate_limit",
Self::Catalog => "catalog",
Self::Approval => "approval",
Self::Idempotency => "idempotency",
Self::Schema => "schema",
Self::Mapping => "mapping",
Self::Upstream => "upstream",
Self::Transformation => "transformation",
Self::History => "history",
Self::Database => "database",
Self::Concurrency => "concurrency",
Self::Configuration => "configuration",
Self::Internal => "internal",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DbOperation {
MachineAccessRead,
MachineAccessTouch,
CatalogLoad,
ApprovalRead,
ApprovalWrite,
AuthProfileRead,
SecretRead,
SecretTouch,
InvocationHistoryWrite,
}
impl DbOperation {
pub fn span(self) -> Span {
let span = Stage::DbQuery.span();
span.record("db.operation", self.as_str());
span
}
pub const fn as_str(self) -> &'static str {
match self {
Self::MachineAccessRead => "machine_access.read",
Self::MachineAccessTouch => "machine_access.touch",
Self::CatalogLoad => "catalog.load",
Self::ApprovalRead => "approval.read",
Self::ApprovalWrite => "approval.write",
Self::AuthProfileRead => "auth_profile.read",
Self::SecretRead => "secret.read",
Self::SecretTouch => "secret.touch",
Self::InvocationHistoryWrite => "invocation_history.write",
}
}
}