Skip to main content

camel_core/shared/observability/domain/
config.rs

1use serde::{Deserialize, Deserializer};
2
3/// Configuration for the Tracer EIP (Enterprise Integration Pattern).
4///
5/// This struct defines how message tracing should be performed throughout
6/// Camel routes. Use `CamelContext::set_tracer_config` to apply configuration
7/// programmatically, or configure via `Camel.toml` as shown in the module documentation.
8#[derive(Clone, Debug, Default)]
9pub struct TracerConfig {
10    /// SPAN enablement (metrics-configuration Req 1: `tracer.enabled` gates
11    /// spans ONLY). Explicit values win over the "otel/prometheus imply
12    /// tracing" rule in `effective_tracer_config` (camel-config), in both
13    /// directions.
14    pub enabled: bool,
15
16    /// Whether `enabled` was explicitly present at the serde boundary.
17    ///
18    /// Not read from input keys: the custom `Deserialize` impl below sets it
19    /// from the presence/absence of the `enabled` key, so it is skipped by
20    /// every serde path. Explicit values win over the "otel/prometheus imply
21    /// tracing" rule in `effective_tracer_config` (camel-config), in both
22    /// directions.
23    pub tracing_enabled_explicit: bool,
24
25    /// PIPELINE enablement: whether routes are wrapped with the observability
26    /// adapters at all. Unlike `enabled`, this is not a TOML key — the
27    /// effective-config assembly (camel-config) raises it whenever an
28    /// exporter (otel/prometheus) or tracing itself is active, because the
29    /// pipeline carries the metric families incl. the non-disableable error
30    /// family (metrics-collection-wiring MODIFIED requirement). Programmatic
31    /// users leave it `false` and pipeline wrapping follows `enabled`.
32    pub pipeline_enabled: bool,
33
34    pub detail_level: DetailLevel,
35
36    pub outputs: TracerOutputs,
37
38    /// Metric-family levers (`[observability.metrics]`). Not a
39    /// `[observability.tracer]` key: the levers deserialize at their own
40    /// table and camel-config attaches them here during effective-config
41    /// assembly, so the tracer serde boundary below never reads them.
42    pub metrics_levers: MetricsLeversConfig,
43}
44
45/// Deserializes `TracerConfig` with serde-boundary detection for `enabled`:
46/// an absent key means "not explicitly set" (`enabled = false`,
47/// `tracing_enabled_explicit = false`), while any explicit `enabled` value
48/// keeps the flag set so callers can honor it over implied enabling.
49///
50/// The intermediate `Raw` struct mirrors the public field set and its serde
51/// attributes (`#[serde(default)]` behavior on other fields is unchanged).
52impl<'de> Deserialize<'de> for TracerConfig {
53    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
54    where
55        D: Deserializer<'de>,
56    {
57        #[derive(Deserialize)]
58        struct Raw {
59            #[serde(default)]
60            enabled: Option<bool>,
61            #[serde(default = "default_detail_level")]
62            detail_level: DetailLevel,
63            #[serde(default)]
64            outputs: TracerOutputs,
65        }
66
67        let raw = Raw::deserialize(deserializer)?;
68        Ok(Self {
69            enabled: raw.enabled.unwrap_or(false),
70            tracing_enabled_explicit: raw.enabled.is_some(),
71            pipeline_enabled: false,
72            detail_level: raw.detail_level,
73            outputs: raw.outputs,
74            metrics_levers: MetricsLeversConfig::default(),
75        })
76    }
77}
78
79#[derive(Debug, Clone, Deserialize, Default)]
80pub struct TracerOutputs {
81    #[serde(default)]
82    pub stdout: StdoutOutput,
83
84    #[serde(default)]
85    pub file: Option<FileOutput>,
86}
87
88#[derive(Debug, Clone, Deserialize)]
89pub struct StdoutOutput {
90    #[serde(default = "default_true")]
91    pub enabled: bool,
92
93    #[serde(default = "default_format")]
94    pub format: OutputFormat,
95}
96
97impl Default for StdoutOutput {
98    fn default() -> Self {
99        Self {
100            enabled: true,
101            format: OutputFormat::Json,
102        }
103    }
104}
105
106#[derive(Debug, Clone, Deserialize)]
107pub struct FileOutput {
108    pub enabled: bool,
109    pub path: String,
110    #[serde(default = "default_format")]
111    pub format: OutputFormat,
112}
113
114/// Controls the level of detail captured in trace spans.
115///
116/// Each variant progressively adds more fields to the trace output:
117///
118/// - `Minimal`: Includes the core span attributes (correlation_id, route_id,
119///   and step_index). `duration_ms` is a `camel_tracer` log field at
120///   every detail level, not a span attribute.
121/// - `Medium`: Includes Minimal fields plus headers_count, body_type, has_error,
122///   and output_body_type
123/// - `Full`: Includes all fields from Minimal and Medium plus up to 3 message headers
124#[derive(Debug, Clone, Deserialize, Default, PartialEq, Eq, PartialOrd, Ord)]
125#[serde(rename_all = "lowercase")]
126pub enum DetailLevel {
127    #[default]
128    Minimal,
129    Medium,
130    Full,
131}
132
133#[derive(Debug, Clone, Deserialize, Default)]
134#[serde(rename_all = "lowercase")]
135pub enum OutputFormat {
136    #[default]
137    Json,
138    Plain,
139}
140
141/// Metric-family levers for `[observability.metrics]` in Camel.toml
142/// (dashboard-observability D3).
143///
144/// `enabled` is the master switch for the non-error families; `exchange`,
145/// `duration`, and `components` are per-family opt-outs (a family flows
146/// only when `enabled && <family>`). No lever exists for the error family —
147/// `camel_errors_total` is structurally non-disableable
148/// (metrics-configuration Req 2).
149#[derive(Clone, Debug, PartialEq)]
150pub struct MetricsLeversConfig {
151    pub enabled: bool,
152    pub exchange: bool,
153    pub duration: bool,
154    pub components: bool,
155}
156
157impl MetricsLeversConfig {
158    /// Whether the exchanges counter family may flow.
159    pub fn exchanges_enabled(&self) -> bool {
160        self.enabled && self.exchange
161    }
162
163    /// Whether the duration histogram family may flow.
164    pub fn durations_enabled(&self) -> bool {
165        self.enabled && self.duration
166    }
167
168    /// Whether the uniform component-operations counter family may flow
169    /// (`camel_component_operations_total`; the error family is never
170    /// gated by any lever).
171    pub fn components_enabled(&self) -> bool {
172        self.enabled && self.components
173    }
174}
175
176impl Default for MetricsLeversConfig {
177    fn default() -> Self {
178        Self {
179            enabled: true,
180            exchange: true,
181            duration: true,
182            components: false,
183        }
184    }
185}
186
187/// Deserializes `MetricsLeversConfig` via a `Raw` intermediate with
188/// `Option<bool>` fields so an absent table and an absent key both mean
189/// "default" (same serde-boundary technique as `TracerConfig`). Unknown
190/// keys are denied, consistent with the sibling observability tables.
191impl<'de> Deserialize<'de> for MetricsLeversConfig {
192    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
193    where
194        D: Deserializer<'de>,
195    {
196        #[derive(Deserialize)]
197        #[serde(deny_unknown_fields)]
198        struct Raw {
199            #[serde(default)]
200            enabled: Option<bool>,
201            #[serde(default)]
202            exchange: Option<bool>,
203            #[serde(default)]
204            duration: Option<bool>,
205            #[serde(default)]
206            components: Option<bool>,
207        }
208
209        let raw = Raw::deserialize(deserializer)?;
210        Ok(Self {
211            enabled: raw.enabled.unwrap_or(true),
212            exchange: raw.exchange.unwrap_or(true),
213            duration: raw.duration.unwrap_or(true),
214            components: raw.components.unwrap_or(false),
215        })
216    }
217}
218
219fn default_detail_level() -> DetailLevel {
220    DetailLevel::Minimal
221}
222fn default_format() -> OutputFormat {
223    OutputFormat::Json
224}
225fn default_true() -> bool {
226    true
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    #[test]
234    fn tracer_config_defaults_are_stable() {
235        let cfg = TracerConfig::default();
236        assert!(!cfg.enabled);
237        assert_eq!(cfg.detail_level, DetailLevel::Minimal);
238        assert!(cfg.outputs.stdout.enabled);
239        assert!(matches!(cfg.outputs.stdout.format, OutputFormat::Json));
240        assert!(cfg.outputs.file.is_none());
241    }
242
243    #[test]
244    fn tracer_config_deserializes_lowercase_enums() {
245        let cfg: TracerConfig = serde_json::from_str(
246            r#"{
247  "enabled": true,
248  "detail_level": "full",
249  "outputs": {
250    "stdout": { "enabled": false, "format": "plain" },
251    "file": { "enabled": true, "path": "/tmp/trace.log", "format": "json" }
252  }
253}"#,
254        )
255        .unwrap();
256
257        assert!(cfg.enabled);
258        assert_eq!(cfg.detail_level, DetailLevel::Full);
259        assert!(!cfg.outputs.stdout.enabled);
260        assert!(matches!(cfg.outputs.stdout.format, OutputFormat::Plain));
261        assert_eq!(cfg.outputs.file.as_ref().unwrap().path, "/tmp/trace.log");
262    }
263}