feat: harden community production foundation through story 1.5
This commit is contained in:
@@ -0,0 +1,275 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize, de};
|
||||
use uuid::Uuid;
|
||||
|
||||
const TRACEPARENT_VERSION: &str = "00";
|
||||
const ZERO_TRACE_ID: &str = "00000000000000000000000000000000";
|
||||
const ZERO_PARENT_ID: &str = "0000000000000000";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct RequestId(String);
|
||||
|
||||
impl RequestId {
|
||||
pub const MAX_LEN: usize = 128;
|
||||
|
||||
pub fn generate() -> Self {
|
||||
Self(Uuid::now_v7().to_string())
|
||||
}
|
||||
|
||||
pub fn resolve(candidate: Option<&str>) -> Self {
|
||||
candidate
|
||||
.filter(|value| Self::is_valid(value))
|
||||
.map(|value| Self(value.to_owned()))
|
||||
.unwrap_or_else(Self::generate)
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, CorrelationError> {
|
||||
Self::is_valid(value)
|
||||
.then(|| Self(value.to_owned()))
|
||||
.ok_or(CorrelationError::InvalidRequestId)
|
||||
}
|
||||
|
||||
pub fn is_valid(value: &str) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.len() <= Self::MAX_LEN
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_graphic() && byte != b',' && byte != b';')
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequestId {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RequestId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Self::parse(&value).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct TraceId(String);
|
||||
|
||||
impl TraceId {
|
||||
pub const LEN: usize = 32;
|
||||
|
||||
pub fn generate() -> Self {
|
||||
let value = Uuid::now_v7().simple().to_string();
|
||||
debug_assert_ne!(value, ZERO_TRACE_ID);
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, CorrelationError> {
|
||||
if is_lower_hex(value, Self::LEN) && value != ZERO_TRACE_ID {
|
||||
Ok(Self(value.to_owned()))
|
||||
} else {
|
||||
Err(CorrelationError::InvalidTraceId)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TraceId {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TraceId {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = String::deserialize(deserializer)?;
|
||||
Self::parse(&value).map_err(de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct TraceContext {
|
||||
trace_id: TraceId,
|
||||
traceparent: String,
|
||||
}
|
||||
|
||||
impl TraceContext {
|
||||
pub const TRACEPARENT_LEN: usize = 55;
|
||||
pub const TRACESTATE_MAX_BYTES: usize = 512;
|
||||
pub const TRACESTATE_MAX_MEMBERS: usize = 32;
|
||||
pub const BAGGAGE_MAX_BYTES: usize = 8_192;
|
||||
pub const BAGGAGE_MAX_MEMBERS: usize = 64;
|
||||
|
||||
pub fn generate() -> Self {
|
||||
let trace_id = TraceId::generate();
|
||||
let mut parent_id = Uuid::now_v7().simple().to_string()[..16].to_owned();
|
||||
if parent_id == ZERO_PARENT_ID {
|
||||
parent_id.replace_range(15..16, "1");
|
||||
}
|
||||
// A context generated outside an SDK span must not claim that a sampler
|
||||
// selected it. Ingress replaces this seed with the actual local span
|
||||
// context before application code runs.
|
||||
let traceparent = format!("{TRACEPARENT_VERSION}-{trace_id}-{parent_id}-00");
|
||||
Self {
|
||||
trace_id,
|
||||
traceparent,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(value: &str) -> Result<Self, CorrelationError> {
|
||||
if value.len() != Self::TRACEPARENT_LEN {
|
||||
return Err(CorrelationError::InvalidTraceparent);
|
||||
}
|
||||
let bytes = value.as_bytes();
|
||||
if bytes[2] != b'-' || bytes[35] != b'-' || bytes[52] != b'-' {
|
||||
return Err(CorrelationError::InvalidTraceparent);
|
||||
}
|
||||
let version = &value[0..2];
|
||||
let trace_id = &value[3..35];
|
||||
let parent_id = &value[36..52];
|
||||
let flags = &value[53..55];
|
||||
if version != TRACEPARENT_VERSION
|
||||
|| !is_lower_hex(parent_id, 16)
|
||||
|| parent_id == ZERO_PARENT_ID
|
||||
|| !matches!(flags, "00" | "01")
|
||||
{
|
||||
return Err(CorrelationError::InvalidTraceparent);
|
||||
}
|
||||
Ok(Self {
|
||||
trace_id: TraceId::parse(trace_id).map_err(|_| CorrelationError::InvalidTraceparent)?,
|
||||
traceparent: value.to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_span_parts(
|
||||
trace_id: &str,
|
||||
span_id: &str,
|
||||
sampled: bool,
|
||||
) -> Result<Self, CorrelationError> {
|
||||
let flags = if sampled { "01" } else { "00" };
|
||||
Self::parse(&format!(
|
||||
"{TRACEPARENT_VERSION}-{trace_id}-{span_id}-{flags}"
|
||||
))
|
||||
}
|
||||
|
||||
pub fn continue_local(&self) -> Self {
|
||||
let mut span_id = Uuid::now_v7().simple().to_string()[..16].to_owned();
|
||||
if span_id == ZERO_PARENT_ID {
|
||||
span_id.replace_range(15..16, "1");
|
||||
}
|
||||
let sampled = self.traceparent.ends_with("-01");
|
||||
Self::from_span_parts(self.trace_id.as_str(), &span_id, sampled)
|
||||
.expect("generated span identity is canonical")
|
||||
}
|
||||
|
||||
pub fn trace_id(&self) -> &TraceId {
|
||||
&self.trace_id
|
||||
}
|
||||
|
||||
pub fn traceparent(&self) -> &str {
|
||||
&self.traceparent
|
||||
}
|
||||
|
||||
pub fn tracestate_within_budget(value: &str) -> bool {
|
||||
header_list_within_budget(
|
||||
value,
|
||||
Self::TRACESTATE_MAX_BYTES,
|
||||
Self::TRACESTATE_MAX_MEMBERS,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn baggage_within_budget(value: &str) -> bool {
|
||||
header_list_within_budget(value, Self::BAGGAGE_MAX_BYTES, Self::BAGGAGE_MAX_MEMBERS)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TraceContext {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct WireTraceContext {
|
||||
trace_id: TraceId,
|
||||
traceparent: String,
|
||||
}
|
||||
|
||||
let wire = WireTraceContext::deserialize(deserializer)?;
|
||||
let context = Self::parse(&wire.traceparent).map_err(de::Error::custom)?;
|
||||
if context.trace_id != wire.trace_id {
|
||||
return Err(de::Error::custom(CorrelationError::InvalidTraceparent));
|
||||
}
|
||||
Ok(context)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CorrelationContext {
|
||||
request_id: RequestId,
|
||||
trace_context: TraceContext,
|
||||
}
|
||||
|
||||
impl CorrelationContext {
|
||||
pub fn new(request_id: RequestId, trace_context: TraceContext) -> Self {
|
||||
Self {
|
||||
request_id,
|
||||
trace_context,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate() -> Self {
|
||||
Self::new(RequestId::generate(), TraceContext::generate())
|
||||
}
|
||||
|
||||
pub fn request_id(&self) -> &RequestId {
|
||||
&self.request_id
|
||||
}
|
||||
|
||||
pub fn trace_context(&self) -> &TraceContext {
|
||||
&self.trace_context
|
||||
}
|
||||
|
||||
pub fn trace_id(&self) -> &TraceId {
|
||||
self.trace_context.trace_id()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum CorrelationError {
|
||||
#[error("invalid request identity")]
|
||||
InvalidRequestId,
|
||||
#[error("invalid trace identity")]
|
||||
InvalidTraceId,
|
||||
#[error("invalid trace parent")]
|
||||
InvalidTraceparent,
|
||||
}
|
||||
|
||||
fn is_lower_hex(value: &str, expected_len: usize) -> bool {
|
||||
value.len() == expected_len
|
||||
&& value
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
|
||||
}
|
||||
|
||||
fn header_list_within_budget(value: &str, max_bytes: usize, max_members: usize) -> bool {
|
||||
!value.is_empty()
|
||||
&& value.len() <= max_bytes
|
||||
&& value.is_ascii()
|
||||
&& !value.bytes().any(|byte| byte.is_ascii_control())
|
||||
&& value.split(',').count() <= max_members
|
||||
&& value.split(',').all(|member| !member.trim().is_empty())
|
||||
}
|
||||
@@ -4,7 +4,10 @@ use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{AgentId, InvocationSource, Protocol, Target, WorkspaceId};
|
||||
use crate::{
|
||||
AgentId, CorrelationContext, InvocationSource, Protocol, RequestId, Target, TraceContext,
|
||||
WorkspaceId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -20,8 +23,8 @@ pub struct ResponseCacheScope {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeRequestContext {
|
||||
pub request_id: String,
|
||||
pub correlation_id: String,
|
||||
pub request_id: RequestId,
|
||||
pub trace_context: TraceContext,
|
||||
pub response_cache_scope: Option<ResponseCacheScope>,
|
||||
pub metering_context: Option<MeteringContext>,
|
||||
}
|
||||
@@ -34,10 +37,10 @@ pub struct MeteringContext {
|
||||
}
|
||||
|
||||
impl RuntimeRequestContext {
|
||||
pub fn new(request_id: impl Into<String>, correlation_id: impl Into<String>) -> Self {
|
||||
pub fn new(request_id: RequestId, trace_context: TraceContext) -> Self {
|
||||
Self {
|
||||
request_id: request_id.into(),
|
||||
correlation_id: correlation_id.into(),
|
||||
request_id,
|
||||
trace_context,
|
||||
response_cache_scope: None,
|
||||
metering_context: None,
|
||||
}
|
||||
@@ -45,13 +48,33 @@ impl RuntimeRequestContext {
|
||||
|
||||
pub fn from_request_id(request_id: impl Into<String>) -> Self {
|
||||
let request_id = request_id.into();
|
||||
Self::new(request_id.clone(), request_id)
|
||||
Self::new(
|
||||
RequestId::resolve(Some(&request_id)),
|
||||
TraceContext::generate(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_correlation(context: &CorrelationContext) -> Self {
|
||||
Self::new(
|
||||
context.request_id().clone(),
|
||||
context.trace_context().clone(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn outbound_headers(&self) -> BTreeMap<String, String> {
|
||||
BTreeMap::from([
|
||||
("x-request-id".to_owned(), self.request_id.clone()),
|
||||
("x-correlation-id".to_owned(), self.correlation_id.clone()),
|
||||
("x-request-id".to_owned(), self.request_id.to_string()),
|
||||
(
|
||||
"x-trace-id".to_owned(),
|
||||
self.trace_context.trace_id().to_string(),
|
||||
),
|
||||
(
|
||||
"traceparent".to_owned(),
|
||||
self.trace_context.traceparent().to_owned(),
|
||||
),
|
||||
// Compatibility alias only. It is intentionally the product Request ID,
|
||||
// never the W3C Trace ID.
|
||||
("x-correlation-id".to_owned(), self.request_id.to_string()),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod agent;
|
||||
pub mod approval;
|
||||
pub mod auth;
|
||||
pub mod cache;
|
||||
pub mod correlation;
|
||||
pub mod edition;
|
||||
pub mod ext;
|
||||
pub mod ids;
|
||||
@@ -113,6 +114,7 @@ pub use cache::{
|
||||
ParseCacheBackendError, RateLimitBucketState, RateLimitDecision, RateLimitStateStore,
|
||||
ReplayGuardStatus, ReplayGuardStore, ResponseCacheStore,
|
||||
};
|
||||
pub use correlation::{CorrelationContext, CorrelationError, RequestId, TraceContext, TraceId};
|
||||
pub use edition::{
|
||||
EditionCapabilities, EditionLimits, MachineAccessMode, OperationSecurityLevel, ProductEdition,
|
||||
};
|
||||
|
||||
@@ -118,6 +118,7 @@ pub struct InvocationLog {
|
||||
pub tool_name: String,
|
||||
pub message: String,
|
||||
pub request_id: Option<String>,
|
||||
pub trace_id: Option<String>,
|
||||
pub status_code: Option<u16>,
|
||||
pub duration_ms: u64,
|
||||
pub error_kind: Option<String>,
|
||||
@@ -169,6 +170,7 @@ mod tests {
|
||||
tool_name: "create_lead".to_owned(),
|
||||
message: "ok".to_owned(),
|
||||
request_id: Some("req_01".to_owned()),
|
||||
trace_id: Some("0af7651916cd43dd8448eb211c80319c".to_owned()),
|
||||
status_code: Some(200),
|
||||
duration_ms: 123,
|
||||
error_kind: None,
|
||||
|
||||
Reference in New Issue
Block a user