Skip to main content

synwire_core/observability/
tracing_config.rs

1//! Tracing and batch configuration types.
2//!
3//! These are only available when the `tracing` feature is enabled.
4
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8/// Configuration for batch processing of observability events.
9///
10/// Controls how events are batched before export.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct BatchConfig {
13    /// Maximum number of events in a batch before flushing.
14    pub max_batch_size: usize,
15    /// Maximum time to wait before flushing an incomplete batch.
16    #[serde(with = "duration_secs")]
17    pub max_wait: Duration,
18    /// Maximum number of concurrent export operations.
19    pub max_concurrent_exports: usize,
20}
21
22impl Default for BatchConfig {
23    fn default() -> Self {
24        Self {
25            max_batch_size: 512,
26            max_wait: Duration::from_secs(5),
27            max_concurrent_exports: 4,
28        }
29    }
30}
31
32/// Top-level tracing configuration.
33///
34/// Controls whether tracing is enabled, what content is captured, and batch
35/// export behaviour.
36///
37/// # Feature gate
38///
39/// This type is only available when the `tracing` feature is enabled. Tracing
40/// is opt-in, not a default feature, to avoid pulling in OpenTelemetry
41/// dependencies unless explicitly needed.
42///
43/// # Example
44///
45/// ```
46/// # #[cfg(feature = "tracing")]
47/// # {
48/// use synwire_core::observability::TracingConfig;
49///
50/// let config = TracingConfig::builder()
51///     .enabled(true)
52///     .service_name("my-agent".to_owned())
53///     .build();
54/// assert!(config.enabled);
55/// # }
56/// ```
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct TracingConfig {
59    /// Whether tracing is enabled.
60    pub enabled: bool,
61    /// The service name reported to the tracing backend.
62    pub service_name: String,
63    /// Content filter for traces.
64    pub content_filter: super::TraceContentFilter,
65    /// Batch export configuration.
66    pub batch: BatchConfig,
67}
68
69impl Default for TracingConfig {
70    fn default() -> Self {
71        Self {
72            enabled: false,
73            service_name: "synwire".to_owned(),
74            content_filter: super::TraceContentFilter::default(),
75            batch: BatchConfig::default(),
76        }
77    }
78}
79
80impl TracingConfig {
81    /// Creates a builder for `TracingConfig`.
82    pub fn builder() -> TracingConfigBuilder {
83        TracingConfigBuilder::default()
84    }
85}
86
87/// Builder for [`TracingConfig`].
88#[derive(Debug, Default)]
89pub struct TracingConfigBuilder {
90    config: TracingConfig,
91}
92
93impl TracingConfigBuilder {
94    /// Sets whether tracing is enabled.
95    pub const fn enabled(mut self, value: bool) -> Self {
96        self.config.enabled = value;
97        self
98    }
99
100    /// Sets the service name.
101    pub fn service_name(mut self, name: String) -> Self {
102        self.config.service_name = name;
103        self
104    }
105
106    /// Sets the content filter.
107    pub const fn content_filter(mut self, filter: super::TraceContentFilter) -> Self {
108        self.config.content_filter = filter;
109        self
110    }
111
112    /// Sets the batch configuration.
113    pub const fn batch(mut self, batch: BatchConfig) -> Self {
114        self.config.batch = batch;
115        self
116    }
117
118    /// Builds the [`TracingConfig`].
119    pub fn build(self) -> TracingConfig {
120        self.config
121    }
122}
123
124/// Serde helper for `Duration` as seconds (f64).
125mod duration_secs {
126    use serde::{Deserialize, Deserializer, Serialize, Serializer};
127    use std::time::Duration;
128
129    /// Serialises a `Duration` as seconds (f64).
130    pub fn serialize<S: Serializer>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
131        duration.as_secs_f64().serialize(serializer)
132    }
133
134    /// Deserialises a `Duration` from seconds (f64).
135    pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Duration, D::Error> {
136        let secs = f64::deserialize(deserializer)?;
137        Ok(Duration::from_secs_f64(secs))
138    }
139}
140
141#[cfg(test)]
142#[allow(clippy::unwrap_used)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn default_config_is_disabled() {
148        let config = TracingConfig::default();
149        assert!(!config.enabled);
150        assert_eq!(config.service_name, "synwire");
151    }
152
153    #[test]
154    fn builder_overrides() {
155        let config = TracingConfig::builder()
156            .enabled(true)
157            .service_name("test-agent".to_owned())
158            .build();
159        assert!(config.enabled);
160        assert_eq!(config.service_name, "test-agent");
161    }
162
163    #[test]
164    fn batch_config_defaults() {
165        let batch = BatchConfig::default();
166        assert_eq!(batch.max_batch_size, 512);
167        assert_eq!(batch.max_wait, Duration::from_secs(5));
168        assert_eq!(batch.max_concurrent_exports, 4);
169    }
170
171    #[test]
172    fn tracing_config_serialization_roundtrip() {
173        let config = TracingConfig::builder()
174            .enabled(true)
175            .service_name("roundtrip".to_owned())
176            .build();
177        let json = serde_json::to_string(&config).unwrap();
178        let deserialized: TracingConfig = serde_json::from_str(&json).unwrap();
179        assert_eq!(deserialized.enabled, config.enabled);
180        assert_eq!(deserialized.service_name, config.service_name);
181    }
182}