1use std::borrow::Cow;
2
3use serde::{Deserialize, Serialize};
4
5const fn default_betterstack_host() -> Cow<'static, str> {
6 Cow::Borrowed("in-otel.logs.betterstack.com")
7}
8
9#[derive(Eq, Clone, Debug, PartialEq, Serialize, Deserialize)]
11#[typeshare::typeshare]
12pub struct TelemetrySinkStatus {
13 enabled: bool,
15}
16
17#[derive(Eq, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
19#[typeshare::typeshare]
20pub struct TelemetryConfigResponse {
21 betterstack: Option<TelemetrySinkStatus>,
22 datadog: Option<TelemetrySinkStatus>,
23 grafana_cloud: Option<TelemetrySinkStatus>,
24}
25
26impl From<Vec<TelemetrySinkConfig>> for TelemetryConfigResponse {
27 fn from(value: Vec<TelemetrySinkConfig>) -> Self {
28 let mut instance = Self::default();
29
30 for sink in value {
31 match sink {
32 TelemetrySinkConfig::Debug(_) => {}
33 TelemetrySinkConfig::Betterstack(_) => {
34 instance.betterstack = Some(TelemetrySinkStatus { enabled: true })
35 }
36 TelemetrySinkConfig::Datadog(_) => {
37 instance.datadog = Some(TelemetrySinkStatus { enabled: true })
38 }
39 TelemetrySinkConfig::GrafanaCloud(_) => {
40 instance.grafana_cloud = Some(TelemetrySinkStatus { enabled: true })
41 }
42 }
43 }
44
45 instance
46 }
47}
48
49#[derive(
51 Eq,
53 Clone,
54 PartialEq,
55 Serialize,
57 Deserialize,
58 strum::AsRefStr,
60 strum::EnumDiscriminants,
61)]
62#[cfg_attr(feature = "integration-tests", derive(Debug))]
63#[cfg_attr(any(test, feature = "integration-tests"), derive(strum::EnumIter))]
64#[serde(tag = "type", content = "content", rename_all = "snake_case")]
65#[strum(serialize_all = "snake_case")]
66#[typeshare::typeshare]
67#[cfg_attr(
68 any(test, feature = "integration-tests"),
69 strum_discriminants(derive(strum::EnumIter))
70)]
71#[strum_discriminants(derive(Serialize, Deserialize, strum::AsRefStr))]
72#[strum_discriminants(serde(rename_all = "snake_case"))]
73#[strum_discriminants(strum(serialize_all = "snake_case"))]
74pub enum TelemetrySinkConfig {
75 Betterstack(BetterstackConfig),
77
78 Datadog(DatadogConfig),
80
81 GrafanaCloud(GrafanaCloudConfig),
83
84 #[doc(hidden)]
86 #[typeshare(skip)]
87 #[strum_discriminants(doc(hidden))]
88 Debug(serde_json::Value),
89}
90
91impl TelemetrySinkConfig {
92 pub fn as_db_type(&self) -> String {
93 format!("project::telemetry::{}::config", self.as_ref())
94 }
95}
96
97impl TelemetrySinkConfigDiscriminants {
98 pub fn as_db_type(&self) -> String {
99 format!("project::telemetry::{}::config", self.as_ref())
100 }
101}
102
103#[derive(Eq, Clone, PartialEq, Serialize, Deserialize)]
104#[cfg_attr(feature = "integration-tests", derive(Debug))]
105#[typeshare::typeshare]
106pub struct BetterstackConfig {
107 #[serde(default = "default_betterstack_host")]
108 pub ingesting_host: Cow<'static, str>,
109 pub source_token: String,
110}
111
112#[cfg(any(test, feature = "integration-tests"))]
113impl Default for BetterstackConfig {
114 fn default() -> Self {
115 Self {
116 source_token: "some-source-token".into(),
117 ingesting_host: default_betterstack_host(),
118 }
119 }
120}
121
122#[derive(Eq, Clone, PartialEq, Serialize, Deserialize)]
123#[cfg_attr(feature = "integration-tests", derive(Debug))]
124#[typeshare::typeshare]
125pub struct DatadogConfig {
126 pub api_key: String,
127}
128
129#[cfg(any(test, feature = "integration-tests"))]
130impl Default for DatadogConfig {
131 fn default() -> Self {
132 Self {
133 api_key: "some-api-key".into(),
134 }
135 }
136}
137
138#[derive(Eq, Clone, PartialEq, Serialize, Deserialize)]
139#[cfg_attr(feature = "integration-tests", derive(Debug))]
140#[typeshare::typeshare]
141pub struct GrafanaCloudConfig {
142 pub token: String,
143 pub endpoint: String,
144 pub instance_id: String,
145}
146
147#[cfg(any(test, feature = "integration-tests"))]
148impl Default for GrafanaCloudConfig {
149 fn default() -> Self {
150 Self {
151 token: "some-auth-token".into(),
152 instance_id: String::from("0000000"),
153 endpoint: "https://prometheus-env-id-env-region.grafana.net/api/prom/push".into(),
154 }
155 }
156}
157
158#[cfg(feature = "integration-tests")]
159impl From<BetterstackConfig> for TelemetrySinkConfig {
160 fn from(value: BetterstackConfig) -> Self {
161 TelemetrySinkConfig::Betterstack(value)
162 }
163}
164
165#[cfg(feature = "integration-tests")]
166impl From<DatadogConfig> for TelemetrySinkConfig {
167 fn from(value: DatadogConfig) -> Self {
168 TelemetrySinkConfig::Datadog(value)
169 }
170}
171
172#[cfg(feature = "integration-tests")]
173impl From<GrafanaCloudConfig> for TelemetrySinkConfig {
174 fn from(value: GrafanaCloudConfig) -> Self {
175 TelemetrySinkConfig::GrafanaCloud(value)
176 }
177}
178
179#[cfg(feature = "integration-tests")]
180impl std::str::FromStr for TelemetrySinkConfig {
181 type Err = serde_json::Error;
182
183 fn from_str(config: &str) -> Result<Self, Self::Err> {
184 serde_json::from_str::<BetterstackConfig>(config)
185 .map(Self::from)
186 .inspect_err(|error| {
187 tracing::debug!(
188 %config,
189 %error,
190 "cannot deserialize config as valid Betterstack configuration",
191 )
192 })
193 .or(serde_json::from_str::<DatadogConfig>(config)
194 .map(Self::from)
195 .inspect_err(|error| {
196 tracing::debug!(
197 %config,
198 %error,
199 "cannot deserialize config as valid DataDog configuration",
200 )
201 }))
202 .or(serde_json::from_str::<GrafanaCloudConfig>(config)
203 .map(Self::from)
204 .inspect_err(|error| {
205 tracing::debug!(
206 %config,
207 %error,
208 "cannot deserialize config as valid GrafanaCloud configuration",
209 )
210 }))
211 .map_err(|_| {
212 <serde_json::Error as serde::de::Error>::custom(format!(
213 "configuration does not match any known external telemetry sink: {}",
214 config
215 ))
216 })
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn sink_config_enum() {
226 for variant in <TelemetrySinkConfig as strum::IntoEnumIterator>::iter() {
227 match variant {
228 sink @ TelemetrySinkConfig::Betterstack(_) => {
229 assert_eq!("betterstack", sink.as_ref());
230 assert_eq!("project::telemetry::betterstack::config", sink.as_db_type());
231 }
232 sink @ TelemetrySinkConfig::Datadog(_) => {
233 assert_eq!("datadog", sink.as_ref());
234 assert_eq!("project::telemetry::datadog::config", sink.as_db_type());
235 }
236 sink @ TelemetrySinkConfig::GrafanaCloud(_) => {
237 assert_eq!("grafana_cloud", sink.as_ref());
238 assert_eq!(
239 "project::telemetry::grafana_cloud::config",
240 sink.as_db_type()
241 );
242 }
243 sink @ TelemetrySinkConfig::Debug(_) => {
244 assert_eq!("debug", sink.as_ref());
245 assert_eq!("project::telemetry::debug::config", sink.as_db_type());
246 }
247 }
248 }
249
250 for variant in <TelemetrySinkConfigDiscriminants as strum::IntoEnumIterator>::iter() {
251 match variant {
252 discriminant @ TelemetrySinkConfigDiscriminants::Betterstack => {
253 assert_eq!("betterstack", discriminant.as_ref());
254 assert_eq!(
255 r#""betterstack""#,
256 serde_json::to_string(&discriminant).unwrap()
257 );
258 }
259 discriminant @ TelemetrySinkConfigDiscriminants::Datadog => {
260 assert_eq!("datadog", discriminant.as_ref());
261 assert_eq!(
262 r#""datadog""#,
263 serde_json::to_string(&discriminant).unwrap()
264 );
265 }
266 discriminant @ TelemetrySinkConfigDiscriminants::GrafanaCloud => {
267 assert_eq!("grafana_cloud", discriminant.as_ref());
268 assert_eq!(
269 r#""grafana_cloud""#,
270 serde_json::to_string(&discriminant).unwrap()
271 );
272 }
273 discriminant @ TelemetrySinkConfigDiscriminants::Debug => {
274 assert_eq!("debug", discriminant.as_ref());
275 assert_eq!(r#""debug""#, serde_json::to_string(&discriminant).unwrap());
276 }
277 }
278 }
279 }
280}