1use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, HashMap};
8
9#[derive(Debug, Clone, Deserialize, Serialize, Default)]
12#[serde(deny_unknown_fields)]
13pub struct OpenTelemetryConfig {
14 #[serde(default)]
15 pub resource: OpenTelemetryResource,
16 pub exporter: Option<Exporter>,
19 #[serde(default)]
20 pub tracing: TracingConfig,
21 #[serde(default)]
22 pub metrics: MetricsConfig,
23}
24
25impl OpenTelemetryConfig {
26 #[must_use]
28 pub fn tracing_exporter(&self) -> Option<&Exporter> {
29 self.tracing.exporter.as_ref().or(self.exporter.as_ref())
30 }
31 #[must_use]
33 pub fn metrics_exporter(&self) -> Option<&Exporter> {
34 self.metrics.exporter.as_ref().or(self.exporter.as_ref())
35 }
36 #[must_use]
43 pub fn inject_trace_ids_into_logs(&self) -> bool {
44 self.tracing
45 .logs_correlation
46 .as_ref()
47 .and_then(|c| c.inject_trace_ids_into_logs)
48 .unwrap_or(true)
49 }
50}
51
52#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct OpenTelemetryResource {
56 #[serde(default = "default_service_name")]
58 pub service_name: String,
59 #[serde(default)]
61 pub attributes: BTreeMap<String, String>,
62}
63
64fn default_service_name() -> String {
66 "cf-gears".to_owned()
67}
68
69impl Default for OpenTelemetryResource {
70 fn default() -> Self {
71 Self {
72 service_name: default_service_name(),
73 attributes: BTreeMap::default(),
74 }
75 }
76}
77
78#[derive(Debug, Clone, Deserialize, Serialize, Default)]
80#[serde(deny_unknown_fields)]
81pub struct TracingConfig {
82 pub enabled: bool,
83 pub exporter: Option<Exporter>,
86 pub sampler: Option<Sampler>,
87 pub propagation: Option<Propagation>,
88 pub http: Option<HttpOpts>,
89 pub logs_correlation: Option<LogsCorrelation>,
90}
91
92#[derive(Debug, Clone, Deserialize, Serialize, Default)]
94#[serde(deny_unknown_fields)]
95pub struct MetricsConfig {
96 pub enabled: bool,
97 pub exporter: Option<Exporter>,
100 #[serde(skip_serializing_if = "Option::is_none")]
104 pub cardinality_limit: Option<usize>,
105}
106
107#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Copy)]
108#[serde(rename_all = "snake_case")]
109pub enum ExporterKind {
110 #[default]
111 OtlpGrpc,
112 OtlpHttp,
113}
114
115#[derive(Debug, Clone, Deserialize, Serialize)]
116pub struct Exporter {
117 #[serde(default)]
122 pub kind: ExporterKind,
123 pub endpoint: Option<String>,
124 pub headers: Option<HashMap<String, String>>,
125 pub timeout_ms: Option<u64>,
126}
127
128#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
129#[serde(rename_all = "snake_case")]
130pub enum Sampler {
131 ParentBasedAlwaysOn {},
132 ParentBasedRatio {
133 #[serde(skip_serializing_if = "Option::is_none")]
134 ratio: Option<f64>,
135 },
136 AlwaysOn {},
137 AlwaysOff {},
138}
139
140#[derive(Debug, Clone, Deserialize, Serialize)]
141pub struct Propagation {
142 pub w3c_trace_context: Option<bool>,
143}
144
145#[derive(Debug, Clone, Deserialize, Serialize)]
146pub struct HttpOpts {
147 pub inject_request_id_header: Option<String>,
148 pub record_headers: Option<Vec<String>>,
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize)]
152pub struct LogsCorrelation {
153 pub inject_trace_ids_into_logs: Option<bool>,
154}
155
156#[cfg(test)]
157#[cfg_attr(coverage_nightly, coverage(off))]
158mod tests {
159 use super::{LogsCorrelation, OpenTelemetryConfig, TracingConfig};
160
161 fn with_logs_correlation(lc: Option<LogsCorrelation>) -> OpenTelemetryConfig {
163 OpenTelemetryConfig {
164 tracing: TracingConfig {
165 logs_correlation: lc,
166 ..TracingConfig::default()
167 },
168 ..OpenTelemetryConfig::default()
169 }
170 }
171
172 fn flag(inject: Option<bool>) -> LogsCorrelation {
173 LogsCorrelation {
174 inject_trace_ids_into_logs: inject,
175 }
176 }
177
178 #[test]
182 fn inject_trace_ids_default_is_on_unless_explicitly_disabled() {
183 assert!(
184 OpenTelemetryConfig::default().inject_trace_ids_into_logs(),
185 "the default config must splice ids into logs"
186 );
187 assert!(
188 with_logs_correlation(None).inject_trace_ids_into_logs(),
189 "an absent logs_correlation section must default ON"
190 );
191 assert!(
192 with_logs_correlation(Some(flag(None))).inject_trace_ids_into_logs(),
193 "a logs_correlation section without the flag must default ON"
194 );
195 assert!(
196 with_logs_correlation(Some(flag(Some(true)))).inject_trace_ids_into_logs(),
197 "an explicit true stays ON"
198 );
199 assert!(
200 !with_logs_correlation(Some(flag(Some(false)))).inject_trace_ids_into_logs(),
201 "an explicit false is the only way to opt out"
202 );
203 }
204
205 #[test]
209 fn absent_opentelemetry_section_resolves_the_splice_on() {
210 let absent: Option<&OpenTelemetryConfig> = None;
211 assert!(
212 absent.is_none_or(OpenTelemetryConfig::inject_trace_ids_into_logs),
213 "no [opentelemetry] section must resolve the splice ON"
214 );
215
216 let disabled = with_logs_correlation(Some(flag(Some(false))));
217 assert!(
218 !Some(&disabled).is_none_or(OpenTelemetryConfig::inject_trace_ids_into_logs),
219 "an explicit false must still opt out when a section is present"
220 );
221 }
222}