code_moniker_workspace/environment/
project_config.rs1use std::path::Path;
2
3use anyhow::{Context as _, ensure};
4use serde::Deserialize;
5
6pub const PROJECT_CONFIG_FILE: &str = ".code-moniker.toml";
7
8#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
9#[serde(deny_unknown_fields)]
10pub struct TelemetryConfig {
11 #[serde(default)]
12 pub enabled: bool,
13 #[serde(default)]
14 pub service_name: Option<String>,
15 #[serde(default)]
16 pub endpoint: Option<String>,
17 #[serde(default)]
18 pub metric_export_interval_ms: Option<u64>,
19}
20
21impl TelemetryConfig {
22 fn validate(self) -> anyhow::Result<Self> {
23 if let Some(service_name) = self.service_name.as_deref() {
24 ensure!(
25 !service_name.trim().is_empty(),
26 "telemetry.service_name cannot be empty"
27 );
28 }
29 if let Some(endpoint) = self.endpoint.as_deref() {
30 ensure!(
31 !endpoint.trim().is_empty(),
32 "telemetry.endpoint cannot be empty"
33 );
34 }
35 if let Some(interval) = self.metric_export_interval_ms {
36 ensure!(
37 interval > 0,
38 "telemetry.metric_export_interval_ms must be greater than zero"
39 );
40 }
41 Ok(self)
42 }
43}
44
45#[derive(Default, Deserialize)]
46struct ProjectConfig {
47 #[serde(default)]
48 telemetry: TelemetryConfig,
49}
50
51pub fn load_telemetry_config(path: &Path) -> anyhow::Result<TelemetryConfig> {
52 if !path.exists() {
53 return Ok(TelemetryConfig::default());
54 }
55 let text = std::fs::read_to_string(path)
56 .with_context(|| format!("read project config {}", path.display()))?;
57 let config: ProjectConfig = toml::from_str(&text)
58 .with_context(|| format!("parse project config {}", path.display()))?;
59 config.telemetry.validate()
60}
61
62#[cfg(test)]
63mod tests {
64 use super::*;
65
66 #[test]
67 fn telemetry_config_is_optional_and_project_scoped() {
68 let directory = tempfile::tempdir().unwrap();
69 let path = directory.path().join(PROJECT_CONFIG_FILE);
70 assert_eq!(
71 load_telemetry_config(&path).unwrap(),
72 TelemetryConfig::default()
73 );
74 std::fs::write(
75 &path,
76 r#"
77[aliases]
78root = "workspace"
79
80[telemetry]
81enabled = true
82service_name = "code-moniker-daemon:example"
83endpoint = "http://127.0.0.1:4318"
84metric_export_interval_ms = 5000
85"#,
86 )
87 .unwrap();
88 assert_eq!(
89 load_telemetry_config(&path).unwrap(),
90 TelemetryConfig {
91 enabled: true,
92 service_name: Some("code-moniker-daemon:example".to_string()),
93 endpoint: Some("http://127.0.0.1:4318".to_string()),
94 metric_export_interval_ms: Some(5000),
95 }
96 );
97 }
98
99 #[test]
100 fn telemetry_config_rejects_invalid_owned_fields() {
101 let directory = tempfile::tempdir().unwrap();
102 let path = directory.path().join(PROJECT_CONFIG_FILE);
103 std::fs::write(&path, "[telemetry]\nmetric_export_interval_ms = 0\n").unwrap();
104 assert!(load_telemetry_config(&path).is_err());
105 std::fs::write(&path, "[telemetry]\nservice_name = \" \"\n").unwrap();
106 assert!(load_telemetry_config(&path).is_err());
107 std::fs::write(&path, "[telemetry]\nunknown = true\n").unwrap();
108 assert!(load_telemetry_config(&path).is_err());
109 }
110}