use std::{fmt, sync::Arc};
pub const MAX_TRACE_CORRELATION_ID_BYTES: usize = 256;
macro_rules! string_id {
($name:ident) => {
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(Arc<str>);
impl $name {
pub fn new(value: impl Into<Arc<str>>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self::new(value)
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
};
}
string_id!(ApplicationId);
string_id!(ModuleId);
string_id!(ServiceId);
string_id!(OperationId);
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct InlineCorrelation {
bytes: [u8; MAX_TRACE_CORRELATION_ID_BYTES],
len: u16,
}
impl InlineCorrelation {
fn copy(value: &str) -> Self {
let mut text = Self { bytes: [0; MAX_TRACE_CORRELATION_ID_BYTES], len: value.len() as u16 };
text.bytes[..value.len()].copy_from_slice(value.as_bytes());
text
}
fn as_str(&self) -> &str {
std::str::from_utf8(&self.bytes[..usize::from(self.len)]).expect("validated correlation")
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RpcCorrelationId(InlineCorrelation);
impl RpcCorrelationId {
pub fn new(value: &str) -> Option<Self> {
if value.trim().is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
return None;
}
Some(Self(InlineCorrelation::copy(value)))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct TraceId(u128);
impl TraceId {
pub const fn from_u128(value: u128) -> Self {
Self(value)
}
pub const fn as_u128(self) -> u128 {
self.0
}
}
impl fmt::Display for TraceId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{:032x}", self.0)
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TraceCorrelationId(InlineCorrelation);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TraceCorrelationIdError {
Empty,
TooLong,
ControlCharacter,
}
impl TraceCorrelationId {
#[doc(hidden)]
pub fn from_generated(trace_id: TraceId) -> Self {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut text = InlineCorrelation { bytes: [0; MAX_TRACE_CORRELATION_ID_BYTES], len: 32 };
let mut value = trace_id.as_u128();
for index in (0..32).rev() {
text.bytes[index] = HEX[(value & 0x0f) as usize];
value >>= 4;
}
Self(text)
}
pub fn new(value: impl Into<Arc<str>>) -> Result<Self, TraceCorrelationIdError> {
let value = value.into();
Self::from_borrowed(&value)
}
#[doc(hidden)]
pub fn from_borrowed(value: &str) -> Result<Self, TraceCorrelationIdError> {
if value.is_empty() {
return Err(TraceCorrelationIdError::Empty);
}
if value.len() > MAX_TRACE_CORRELATION_ID_BYTES {
return Err(TraceCorrelationIdError::TooLong);
}
if value.chars().any(char::is_control) {
return Err(TraceCorrelationIdError::ControlCharacter);
}
Ok(Self(InlineCorrelation::copy(value)))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl fmt::Display for TraceCorrelationId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct SpanId(u64);
impl SpanId {
pub const fn from_u64(value: u64) -> Self {
Self(value)
}
pub const fn as_u64(self) -> u64 {
self.0
}
}
impl fmt::Display for SpanId {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{:016x}", self.0)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CallContext {
application: ApplicationId,
module: ModuleId,
service: ServiceId,
operation: OperationId,
trace_id: TraceId,
trace_correlation_id: TraceCorrelationId,
span_id: SpanId,
rpc_correlation_id: Option<RpcCorrelationId>,
}
impl CallContext {
pub fn new(
application: ApplicationId,
module: ModuleId,
service: ServiceId,
operation: OperationId,
trace_id: TraceId,
span_id: SpanId,
) -> Self {
let trace_correlation_id = TraceCorrelationId::from_generated(trace_id);
Self {
application,
module,
service,
operation,
trace_id,
trace_correlation_id,
span_id,
rpc_correlation_id: None,
}
}
pub fn with_trace_correlation_id(mut self, trace_correlation_id: TraceCorrelationId) -> Self {
self.trace_correlation_id = trace_correlation_id;
self
}
pub fn application(&self) -> &ApplicationId {
&self.application
}
pub fn with_rpc_correlation_id(mut self, rpc: Option<RpcCorrelationId>) -> Self {
self.rpc_correlation_id = rpc;
self
}
pub fn rpc_correlation_id(&self) -> Option<&RpcCorrelationId> {
self.rpc_correlation_id.as_ref()
}
pub fn module(&self) -> &ModuleId {
&self.module
}
pub fn service(&self) -> &ServiceId {
&self.service
}
pub fn operation(&self) -> &OperationId {
&self.operation
}
pub const fn trace_id(&self) -> TraceId {
self.trace_id
}
pub fn trace_correlation_id(&self) -> &TraceCorrelationId {
&self.trace_correlation_id
}
pub const fn span_id(&self) -> SpanId {
self.span_id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protocol_rpc_validation_does_not_mint_or_replace_identity() {
for value in ["0", "0.1", "0.12.3"] {
assert_eq!(RpcCorrelationId::new(value).unwrap().as_str(), value);
}
for value in ["", " ", "0\n1", "0\u{7f}"] {
assert!(RpcCorrelationId::new(value).is_none());
}
assert!(RpcCorrelationId::new(&"1".repeat(256)).is_some());
assert!(RpcCorrelationId::new(&"1".repeat(257)).is_none());
}
#[test]
fn inline_correlations_preserve_generated_hex_and_bounded_utf8() {
let trace = TraceId::from_u128(0x1234_5678_90ab_cdef_0123_4567_89ab_cdef);
assert_eq!(TraceCorrelationId::from_generated(trace).as_str(), trace.to_string());
let inbound = format!("{}a", "界".repeat(85));
assert_eq!(TraceCorrelationId::from_borrowed(&inbound).unwrap().as_str(), inbound);
assert_eq!(RpcCorrelationId::new(&inbound).unwrap().as_str(), inbound);
assert!(!std::mem::needs_drop::<TraceCorrelationId>());
assert!(!std::mem::needs_drop::<RpcCorrelationId>());
}
#[test]
fn identifiers_have_stable_display_forms() {
assert_eq!(TraceId::from_u128(42).to_string().len(), 32);
assert_eq!(SpanId::from_u64(42).to_string().len(), 16);
assert_eq!(ServiceId::from("orders").to_string(), "orders");
}
#[test]
fn opaque_trace_correlation_id_is_bounded_and_rejects_controls() {
assert_eq!(
TraceCorrelationId::new("trace-1").unwrap().as_str(),
"trace-1"
);
assert_eq!(
TraceCorrelationId::new(""),
Err(TraceCorrelationIdError::Empty)
);
assert_eq!(
TraceCorrelationId::new("x".repeat(MAX_TRACE_CORRELATION_ID_BYTES + 1)),
Err(TraceCorrelationIdError::TooLong)
);
assert_eq!(
TraceCorrelationId::new("trace\n1"),
Err(TraceCorrelationIdError::ControlCharacter)
);
}
}