Skip to main content

turbomcp_telemetry/
config.rs

1//! Telemetry configuration
2//!
3//! Provides flexible configuration for telemetry collection and export.
4
5#[cfg(feature = "opentelemetry")]
6use std::time::Duration;
7
8/// Telemetry configuration
9///
10/// Use [`TelemetryConfigBuilder`] for ergonomic configuration construction.
11///
12/// # Example
13///
14/// ```rust
15/// use turbomcp_telemetry::TelemetryConfig;
16///
17/// let config = TelemetryConfig::builder()
18///     .service_name("my-mcp-server")
19///     .service_version("1.0.0")
20///     .log_level("info,turbomcp=debug")
21///     .build();
22/// ```
23#[derive(Debug, Clone)]
24pub struct TelemetryConfig {
25    /// Service name for telemetry identification
26    pub service_name: String,
27    /// Service version
28    pub service_version: String,
29    /// Log level filter (e.g., "info", "debug", "info,turbomcp=debug")
30    pub log_level: String,
31    /// Enable JSON-formatted log output
32    pub json_logs: bool,
33    /// Output logs to stderr (required for STDIO transport)
34    pub stderr_output: bool,
35
36    /// OpenTelemetry OTLP endpoint (e.g., "http://localhost:4317")
37    #[cfg(feature = "opentelemetry")]
38    pub otlp_endpoint: Option<String>,
39    /// OTLP protocol (grpc or http)
40    #[cfg(feature = "opentelemetry")]
41    pub otlp_protocol: OtlpProtocol,
42    /// Trace sampling ratio (0.0 to 1.0)
43    #[cfg(feature = "opentelemetry")]
44    pub sampling_ratio: f64,
45    /// Export timeout
46    #[cfg(feature = "opentelemetry")]
47    pub export_timeout: Duration,
48
49    /// Prometheus metrics endpoint port
50    #[cfg(feature = "prometheus")]
51    pub prometheus_port: Option<u16>,
52    /// Prometheus metrics endpoint path
53    #[cfg(feature = "prometheus")]
54    pub prometheus_path: String,
55
56    /// Additional resource attributes
57    pub resource_attributes: Vec<(String, String)>,
58}
59
60impl Default for TelemetryConfig {
61    fn default() -> Self {
62        Self {
63            service_name: "turbomcp-service".to_string(),
64            service_version: env!("CARGO_PKG_VERSION").to_string(),
65            log_level: "info,turbomcp=debug".to_string(),
66            json_logs: true,
67            stderr_output: true,
68
69            #[cfg(feature = "opentelemetry")]
70            otlp_endpoint: None,
71            #[cfg(feature = "opentelemetry")]
72            otlp_protocol: OtlpProtocol::Grpc,
73            #[cfg(feature = "opentelemetry")]
74            sampling_ratio: 1.0,
75            #[cfg(feature = "opentelemetry")]
76            export_timeout: Duration::from_secs(10),
77
78            #[cfg(feature = "prometheus")]
79            prometheus_port: None,
80            #[cfg(feature = "prometheus")]
81            prometheus_path: "/metrics".to_string(),
82
83            resource_attributes: Vec::new(),
84        }
85    }
86}
87
88impl TelemetryConfig {
89    /// Create a new configuration builder
90    #[must_use]
91    pub fn builder() -> TelemetryConfigBuilder {
92        TelemetryConfigBuilder::default()
93    }
94
95    /// Initialize telemetry with this configuration
96    ///
97    /// Returns a guard that ensures proper cleanup on drop.
98    pub fn init(self) -> Result<crate::TelemetryGuard, crate::TelemetryError> {
99        crate::TelemetryGuard::init(self)
100    }
101}
102
103/// OTLP protocol variant
104#[cfg(feature = "opentelemetry")]
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum OtlpProtocol {
107    /// gRPC protocol (port 4317)
108    #[default]
109    Grpc,
110    /// HTTP/protobuf protocol (port 4318)
111    Http,
112}
113
114/// Builder for [`TelemetryConfig`]
115#[derive(Debug, Clone, Default)]
116pub struct TelemetryConfigBuilder {
117    service_name: Option<String>,
118    service_version: Option<String>,
119    log_level: Option<String>,
120    json_logs: Option<bool>,
121    stderr_output: Option<bool>,
122
123    #[cfg(feature = "opentelemetry")]
124    otlp_endpoint: Option<String>,
125    #[cfg(feature = "opentelemetry")]
126    otlp_protocol: Option<OtlpProtocol>,
127    #[cfg(feature = "opentelemetry")]
128    sampling_ratio: Option<f64>,
129    #[cfg(feature = "opentelemetry")]
130    export_timeout: Option<Duration>,
131
132    #[cfg(feature = "prometheus")]
133    prometheus_port: Option<u16>,
134    #[cfg(feature = "prometheus")]
135    prometheus_path: Option<String>,
136
137    resource_attributes: Vec<(String, String)>,
138}
139
140impl TelemetryConfigBuilder {
141    /// Set the service name
142    #[must_use]
143    pub fn service_name(mut self, name: impl Into<String>) -> Self {
144        self.service_name = Some(name.into());
145        self
146    }
147
148    /// Set the service version
149    #[must_use]
150    pub fn service_version(mut self, version: impl Into<String>) -> Self {
151        self.service_version = Some(version.into());
152        self
153    }
154
155    /// Set the log level filter
156    ///
157    /// Examples: "info", "debug", "warn,turbomcp=debug,tower=info"
158    #[must_use]
159    pub fn log_level(mut self, level: impl Into<String>) -> Self {
160        self.log_level = Some(level.into());
161        self
162    }
163
164    /// Enable or disable JSON log output
165    #[must_use]
166    pub fn json_logs(mut self, enabled: bool) -> Self {
167        self.json_logs = Some(enabled);
168        self
169    }
170
171    /// Enable or disable stderr output (required for STDIO transport)
172    #[must_use]
173    pub fn stderr_output(mut self, enabled: bool) -> Self {
174        self.stderr_output = Some(enabled);
175        self
176    }
177
178    /// Set the OTLP endpoint for trace/metrics export
179    #[cfg(feature = "opentelemetry")]
180    #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))]
181    #[must_use]
182    pub fn otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
183        self.otlp_endpoint = Some(endpoint.into());
184        self
185    }
186
187    /// Set the OTLP protocol
188    #[cfg(feature = "opentelemetry")]
189    #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))]
190    #[must_use]
191    pub fn otlp_protocol(mut self, protocol: OtlpProtocol) -> Self {
192        self.otlp_protocol = Some(protocol);
193        self
194    }
195
196    /// Set the trace sampling ratio (0.0 to 1.0)
197    #[cfg(feature = "opentelemetry")]
198    #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))]
199    #[must_use]
200    pub fn sampling_ratio(mut self, ratio: f64) -> Self {
201        self.sampling_ratio = Some(ratio.clamp(0.0, 1.0));
202        self
203    }
204
205    /// Set the export timeout
206    #[cfg(feature = "opentelemetry")]
207    #[cfg_attr(docsrs, doc(cfg(feature = "opentelemetry")))]
208    #[must_use]
209    pub fn export_timeout(mut self, timeout: Duration) -> Self {
210        self.export_timeout = Some(timeout);
211        self
212    }
213
214    /// Set the Prometheus metrics endpoint port
215    #[cfg(feature = "prometheus")]
216    #[cfg_attr(docsrs, doc(cfg(feature = "prometheus")))]
217    #[must_use]
218    pub fn prometheus_port(mut self, port: u16) -> Self {
219        self.prometheus_port = Some(port);
220        self
221    }
222
223    /// Set the Prometheus metrics endpoint path
224    #[cfg(feature = "prometheus")]
225    #[cfg_attr(docsrs, doc(cfg(feature = "prometheus")))]
226    #[must_use]
227    pub fn prometheus_path(mut self, path: impl Into<String>) -> Self {
228        self.prometheus_path = Some(path.into());
229        self
230    }
231
232    /// Add a resource attribute
233    #[must_use]
234    pub fn resource_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
235        self.resource_attributes.push((key.into(), value.into()));
236        self
237    }
238
239    /// Add the deployment environment as a resource attribute
240    #[must_use]
241    pub fn environment(self, env: impl Into<String>) -> Self {
242        self.resource_attribute("deployment.environment", env)
243    }
244
245    /// Build the configuration
246    #[must_use]
247    pub fn build(self) -> TelemetryConfig {
248        let defaults = TelemetryConfig::default();
249
250        TelemetryConfig {
251            service_name: self.service_name.unwrap_or(defaults.service_name),
252            service_version: self.service_version.unwrap_or(defaults.service_version),
253            log_level: self.log_level.unwrap_or(defaults.log_level),
254            json_logs: self.json_logs.unwrap_or(defaults.json_logs),
255            stderr_output: self.stderr_output.unwrap_or(defaults.stderr_output),
256
257            #[cfg(feature = "opentelemetry")]
258            otlp_endpoint: self.otlp_endpoint.or(defaults.otlp_endpoint),
259            #[cfg(feature = "opentelemetry")]
260            otlp_protocol: self.otlp_protocol.unwrap_or(defaults.otlp_protocol),
261            #[cfg(feature = "opentelemetry")]
262            sampling_ratio: self.sampling_ratio.unwrap_or(defaults.sampling_ratio),
263            #[cfg(feature = "opentelemetry")]
264            export_timeout: self.export_timeout.unwrap_or(defaults.export_timeout),
265
266            #[cfg(feature = "prometheus")]
267            prometheus_port: self.prometheus_port.or(defaults.prometheus_port),
268            #[cfg(feature = "prometheus")]
269            prometheus_path: self.prometheus_path.unwrap_or(defaults.prometheus_path),
270
271            resource_attributes: if self.resource_attributes.is_empty() {
272                defaults.resource_attributes
273            } else {
274                self.resource_attributes
275            },
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_default_config() {
286        let config = TelemetryConfig::default();
287        assert_eq!(config.service_name, "turbomcp-service");
288        assert!(config.json_logs);
289        assert!(config.stderr_output);
290    }
291
292    #[test]
293    fn test_builder() {
294        let config = TelemetryConfig::builder()
295            .service_name("test-service")
296            .service_version("2.0.0")
297            .log_level("debug")
298            .json_logs(false)
299            .environment("production")
300            .build();
301
302        assert_eq!(config.service_name, "test-service");
303        assert_eq!(config.service_version, "2.0.0");
304        assert_eq!(config.log_level, "debug");
305        assert!(!config.json_logs);
306        assert_eq!(config.resource_attributes.len(), 1);
307        assert_eq!(
308            config.resource_attributes[0],
309            (
310                "deployment.environment".to_string(),
311                "production".to_string()
312            )
313        );
314    }
315
316    #[cfg(feature = "opentelemetry")]
317    #[test]
318    fn test_otlp_config() {
319        let config = TelemetryConfig::builder()
320            .otlp_endpoint("http://localhost:4317")
321            .otlp_protocol(OtlpProtocol::Grpc)
322            .sampling_ratio(0.5)
323            .build();
324
325        assert_eq!(
326            config.otlp_endpoint,
327            Some("http://localhost:4317".to_string())
328        );
329        assert_eq!(config.otlp_protocol, OtlpProtocol::Grpc);
330        assert!((config.sampling_ratio - 0.5).abs() < f64::EPSILON);
331    }
332
333    #[cfg(feature = "prometheus")]
334    #[test]
335    fn test_prometheus_config() {
336        let config = TelemetryConfig::builder()
337            .prometheus_port(9090)
338            .prometheus_path("/custom-metrics")
339            .build();
340
341        assert_eq!(config.prometheus_port, Some(9090));
342        assert_eq!(config.prometheus_path, "/custom-metrics");
343    }
344}