use std::collections::BTreeSet;
use std::fmt;
use std::ops::RangeInclusive;
use rustev_contract::redaction::{DigestKey, RedactionPolicy, RedactorError};
use crate::project::Bindings;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AckMode {
#[default]
Accepted,
Exported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ParentSampling {
#[default]
Honor,
Ignore,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RootSampling {
#[default]
Always,
Never,
Ratio {
per_million: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AlwaysSampleOn {
Unresolved,
Refusal,
Fallback,
Cancelled,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Sampling {
pub parent: ParentSampling,
pub root: RootSampling,
pub always_sample_on: BTreeSet<AlwaysSampleOn>,
}
#[derive(Debug, Clone)]
pub struct SinkConfig {
pub schema: u32,
pub ack: AckMode,
pub policy: RedactionPolicy,
pub key: Option<DigestKey>,
pub bindings: Bindings,
pub sampling: Sampling,
pub logs: bool,
pub max_queued_spans: usize,
pub batch_size: usize,
pub export_timeout_ms: u64,
pub value_bound: usize,
pub max_pending_contexts: usize,
pub context_ttl_ms: u64,
pub join_buffer: usize,
}
impl Default for SinkConfig {
fn default() -> Self {
SinkConfig {
schema: 1,
ack: AckMode::Accepted,
policy: RedactionPolicy::default(),
key: None,
bindings: Bindings::default(),
sampling: Sampling::default(),
logs: false,
max_queued_spans: 65_536,
batch_size: 512,
export_timeout_ms: 10_000,
value_bound: 4096,
max_pending_contexts: 4096,
context_ttl_ms: 600_000,
join_buffer: 4096,
}
}
}
pub mod ranges {
use std::ops::RangeInclusive;
pub const MAX_QUEUED_SPANS: RangeInclusive<usize> = 1..=1_048_576;
pub const BATCH_SIZE: RangeInclusive<usize> = 1..=8192;
pub const EXPORT_TIMEOUT_MS: RangeInclusive<u64> = 1..=600_000;
pub const VALUE_BOUND: RangeInclusive<usize> = 16..=4096;
pub const MAX_PENDING_CONTEXTS: RangeInclusive<usize> = 1..=65_536;
pub const CONTEXT_TTL_MS: RangeInclusive<u64> = 1..=86_400_000;
pub const JOIN_BUFFER: RangeInclusive<usize> = 1..=65_536;
pub const RATIO_PER_MILLION: RangeInclusive<u32> = 0..=1_000_000;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConfigError {
OutOfRange { setting: &'static str, value: u64 },
UnknownSchema { schema: u32 },
Redaction(RedactorError),
LogsUnsupported,
NoRuntime,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::OutOfRange { setting, value } => {
write!(f, "{setting} = {value} is outside its stated range")
}
ConfigError::UnknownSchema { schema } => {
write!(
f,
"span schema rustev.otel/{schema} is not emitted by this build"
)
}
ConfigError::Redaction(e) => write!(f, "{e}"),
ConfigError::LogsUnsupported => f.write_str("logs are on and the exporter takes none"),
ConfigError::NoRuntime => f.write_str("the sink needs a Tokio runtime"),
}
}
}
impl std::error::Error for ConfigError {}
fn check<T: PartialOrd + Copy + Into<u64>>(
setting: &'static str,
v: T,
r: RangeInclusive<T>,
) -> Result<(), ConfigError> {
if r.contains(&v) {
Ok(())
} else {
Err(ConfigError::OutOfRange {
setting,
value: v.into(),
})
}
}
fn check_usize(
setting: &'static str,
v: usize,
r: RangeInclusive<usize>,
) -> Result<(), ConfigError> {
if r.contains(&v) {
Ok(())
} else {
Err(ConfigError::OutOfRange {
setting,
value: v as u64,
})
}
}
impl SinkConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if self.schema != 1 {
return Err(ConfigError::UnknownSchema {
schema: self.schema,
});
}
check_usize(
"max_queued_spans",
self.max_queued_spans,
ranges::MAX_QUEUED_SPANS,
)?;
check_usize("batch_size", self.batch_size, ranges::BATCH_SIZE)?;
check(
"export_timeout_ms",
self.export_timeout_ms,
ranges::EXPORT_TIMEOUT_MS,
)?;
check_usize("value_bound", self.value_bound, ranges::VALUE_BOUND)?;
check_usize(
"max_pending_contexts",
self.max_pending_contexts,
ranges::MAX_PENDING_CONTEXTS,
)?;
check(
"context_ttl_ms",
self.context_ttl_ms,
ranges::CONTEXT_TTL_MS,
)?;
check_usize("join_buffer", self.join_buffer, ranges::JOIN_BUFFER)?;
if let RootSampling::Ratio { per_million } = self.sampling.root {
check("ratio.per_million", per_million, ranges::RATIO_PER_MILLION)?;
}
Ok(())
}
}