1use std::collections::BTreeSet;
6use std::fmt;
7use std::ops::RangeInclusive;
8
9use rustev_contract::redaction::{DigestKey, RedactionPolicy, RedactorError};
10
11use crate::project::Bindings;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum AckMode {
16 #[default]
19 Accepted,
20 Exported,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum ParentSampling {
27 #[default]
28 Honor,
29 Ignore,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
34pub enum RootSampling {
35 #[default]
36 Always,
37 Never,
38 Ratio {
41 per_million: u32,
42 },
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
47pub enum AlwaysSampleOn {
48 Unresolved,
50 Refusal,
52 Fallback,
54 Cancelled,
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct Sampling {
61 pub parent: ParentSampling,
62 pub root: RootSampling,
63 pub always_sample_on: BTreeSet<AlwaysSampleOn>,
64}
65
66#[derive(Debug, Clone)]
69pub struct SinkConfig {
70 pub schema: u32,
72 pub ack: AckMode,
73 pub policy: RedactionPolicy,
76 pub key: Option<DigestKey>,
78 pub bindings: Bindings,
79 pub sampling: Sampling,
80 pub logs: bool,
83 pub max_queued_spans: usize,
84 pub batch_size: usize,
86 pub export_timeout_ms: u64,
87 pub value_bound: usize,
89 pub max_pending_contexts: usize,
90 pub context_ttl_ms: u64,
91 pub join_buffer: usize,
93}
94
95impl Default for SinkConfig {
96 fn default() -> Self {
97 SinkConfig {
98 schema: 1,
99 ack: AckMode::Accepted,
100 policy: RedactionPolicy::default(),
101 key: None,
102 bindings: Bindings::default(),
103 sampling: Sampling::default(),
104 logs: false,
105 max_queued_spans: 65_536,
106 batch_size: 512,
107 export_timeout_ms: 10_000,
108 value_bound: 4096,
109 max_pending_contexts: 4096,
110 context_ttl_ms: 600_000,
111 join_buffer: 4096,
112 }
113 }
114}
115
116pub mod ranges {
118 use std::ops::RangeInclusive;
119 pub const MAX_QUEUED_SPANS: RangeInclusive<usize> = 1..=1_048_576;
120 pub const BATCH_SIZE: RangeInclusive<usize> = 1..=8192;
121 pub const EXPORT_TIMEOUT_MS: RangeInclusive<u64> = 1..=600_000;
122 pub const VALUE_BOUND: RangeInclusive<usize> = 16..=4096;
123 pub const MAX_PENDING_CONTEXTS: RangeInclusive<usize> = 1..=65_536;
124 pub const CONTEXT_TTL_MS: RangeInclusive<u64> = 1..=86_400_000;
125 pub const JOIN_BUFFER: RangeInclusive<usize> = 1..=65_536;
126 pub const RATIO_PER_MILLION: RangeInclusive<u32> = 0..=1_000_000;
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub enum ConfigError {
132 OutOfRange { setting: &'static str, value: u64 },
134 UnknownSchema { schema: u32 },
136 Redaction(RedactorError),
138 LogsUnsupported,
140 NoRuntime,
142}
143
144impl fmt::Display for ConfigError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 ConfigError::OutOfRange { setting, value } => {
148 write!(f, "{setting} = {value} is outside its stated range")
149 }
150 ConfigError::UnknownSchema { schema } => {
151 write!(
152 f,
153 "span schema rustev.otel/{schema} is not emitted by this build"
154 )
155 }
156 ConfigError::Redaction(e) => write!(f, "{e}"),
157 ConfigError::LogsUnsupported => f.write_str("logs are on and the exporter takes none"),
158 ConfigError::NoRuntime => f.write_str("the sink needs a Tokio runtime"),
159 }
160 }
161}
162
163impl std::error::Error for ConfigError {}
164
165fn check<T: PartialOrd + Copy + Into<u64>>(
166 setting: &'static str,
167 v: T,
168 r: RangeInclusive<T>,
169) -> Result<(), ConfigError> {
170 if r.contains(&v) {
171 Ok(())
172 } else {
173 Err(ConfigError::OutOfRange {
174 setting,
175 value: v.into(),
176 })
177 }
178}
179
180fn check_usize(
181 setting: &'static str,
182 v: usize,
183 r: RangeInclusive<usize>,
184) -> Result<(), ConfigError> {
185 if r.contains(&v) {
186 Ok(())
187 } else {
188 Err(ConfigError::OutOfRange {
189 setting,
190 value: v as u64,
191 })
192 }
193}
194
195impl SinkConfig {
196 pub fn validate(&self) -> Result<(), ConfigError> {
198 if self.schema != 1 {
199 return Err(ConfigError::UnknownSchema {
200 schema: self.schema,
201 });
202 }
203 check_usize(
204 "max_queued_spans",
205 self.max_queued_spans,
206 ranges::MAX_QUEUED_SPANS,
207 )?;
208 check_usize("batch_size", self.batch_size, ranges::BATCH_SIZE)?;
209 check(
210 "export_timeout_ms",
211 self.export_timeout_ms,
212 ranges::EXPORT_TIMEOUT_MS,
213 )?;
214 check_usize("value_bound", self.value_bound, ranges::VALUE_BOUND)?;
215 check_usize(
216 "max_pending_contexts",
217 self.max_pending_contexts,
218 ranges::MAX_PENDING_CONTEXTS,
219 )?;
220 check(
221 "context_ttl_ms",
222 self.context_ttl_ms,
223 ranges::CONTEXT_TTL_MS,
224 )?;
225 check_usize("join_buffer", self.join_buffer, ranges::JOIN_BUFFER)?;
226 if let RootSampling::Ratio { per_million } = self.sampling.root {
227 check("ratio.per_million", per_million, ranges::RATIO_PER_MILLION)?;
228 }
229 Ok(())
230 }
231}