1#[cfg(feature = "opentelemetry")]
6use std::time::Duration;
7
8#[derive(Debug, Clone)]
24pub struct TelemetryConfig {
25 pub service_name: String,
27 pub service_version: String,
29 pub log_level: String,
31 pub json_logs: bool,
33 pub stderr_output: bool,
35
36 #[cfg(feature = "opentelemetry")]
38 pub otlp_endpoint: Option<String>,
39 #[cfg(feature = "opentelemetry")]
41 pub otlp_protocol: OtlpProtocol,
42 #[cfg(feature = "opentelemetry")]
44 pub sampling_ratio: f64,
45 #[cfg(feature = "opentelemetry")]
47 pub export_timeout: Duration,
48
49 #[cfg(feature = "prometheus")]
51 pub prometheus_port: Option<u16>,
52 #[cfg(feature = "prometheus")]
54 pub prometheus_path: String,
55
56 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 #[must_use]
91 pub fn builder() -> TelemetryConfigBuilder {
92 TelemetryConfigBuilder::default()
93 }
94
95 pub fn init(self) -> Result<crate::TelemetryGuard, crate::TelemetryError> {
99 crate::TelemetryGuard::init(self)
100 }
101}
102
103#[cfg(feature = "opentelemetry")]
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
106pub enum OtlpProtocol {
107 #[default]
109 Grpc,
110 Http,
112}
113
114#[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 #[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 #[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 #[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 #[must_use]
166 pub fn json_logs(mut self, enabled: bool) -> Self {
167 self.json_logs = Some(enabled);
168 self
169 }
170
171 #[must_use]
173 pub fn stderr_output(mut self, enabled: bool) -> Self {
174 self.stderr_output = Some(enabled);
175 self
176 }
177
178 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
241 pub fn environment(self, env: impl Into<String>) -> Self {
242 self.resource_attribute("deployment.environment", env)
243 }
244
245 #[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}