mas_config/sections/
telemetry.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize, de::Error as _};
9use serde_with::skip_serializing_none;
10use url::Url;
11
12use super::ConfigurationSection;
13
14fn sample_rate_example() -> f64 {
15    0.5
16}
17
18/// Propagation format for incoming and outgoing requests
19#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
20#[serde(rename_all = "lowercase")]
21pub enum Propagator {
22    /// Propagate according to the W3C Trace Context specification
23    TraceContext,
24
25    /// Propagate according to the W3C Baggage specification
26    Baggage,
27
28    /// Propagate trace context with Jaeger compatible headers
29    Jaeger,
30}
31
32#[allow(clippy::unnecessary_wraps)]
33fn otlp_endpoint_default() -> Option<String> {
34    Some("https://localhost:4318".to_owned())
35}
36
37/// Exporter to use when exporting traces
38#[skip_serializing_none]
39#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)]
40#[serde(rename_all = "lowercase")]
41pub enum TracingExporterKind {
42    /// Don't export traces
43    #[default]
44    None,
45
46    /// Export traces to the standard output. Only useful for debugging
47    Stdout,
48
49    /// Export traces to an OpenTelemetry protocol compatible endpoint
50    Otlp,
51}
52
53/// Configuration related to exporting traces
54#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
55pub struct TracingConfig {
56    /// Exporter to use when exporting traces
57    #[serde(default)]
58    pub exporter: TracingExporterKind,
59
60    /// OTLP exporter: OTLP over HTTP compatible endpoint
61    #[serde(skip_serializing_if = "Option::is_none")]
62    #[schemars(url, default = "otlp_endpoint_default")]
63    pub endpoint: Option<Url>,
64
65    /// List of propagation formats to use for incoming and outgoing requests
66    #[serde(default)]
67    pub propagators: Vec<Propagator>,
68
69    /// Sample rate for traces
70    ///
71    /// Defaults to `1.0` if not set.
72    #[serde(skip_serializing_if = "Option::is_none")]
73    #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))]
74    pub sample_rate: Option<f64>,
75}
76
77impl TracingConfig {
78    /// Returns true if all fields are at their default values
79    fn is_default(&self) -> bool {
80        matches!(self.exporter, TracingExporterKind::None)
81            && self.endpoint.is_none()
82            && self.propagators.is_empty()
83    }
84}
85
86/// Exporter to use when exporting metrics
87#[skip_serializing_none]
88#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)]
89#[serde(rename_all = "lowercase")]
90pub enum MetricsExporterKind {
91    /// Don't export metrics
92    #[default]
93    None,
94
95    /// Export metrics to stdout. Only useful for debugging
96    Stdout,
97
98    /// Export metrics to an OpenTelemetry protocol compatible endpoint
99    Otlp,
100
101    /// Export metrics via Prometheus. An HTTP listener with the `prometheus`
102    /// resource must be setup to expose the Promethes metrics.
103    Prometheus,
104}
105
106/// Configuration related to exporting metrics
107#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
108pub struct MetricsConfig {
109    /// Exporter to use when exporting metrics
110    #[serde(default)]
111    pub exporter: MetricsExporterKind,
112
113    /// OTLP exporter: OTLP over HTTP compatible endpoint
114    #[serde(skip_serializing_if = "Option::is_none")]
115    #[schemars(url, default = "otlp_endpoint_default")]
116    pub endpoint: Option<Url>,
117}
118
119impl MetricsConfig {
120    /// Returns true if all fields are at their default values
121    fn is_default(&self) -> bool {
122        matches!(self.exporter, MetricsExporterKind::None) && self.endpoint.is_none()
123    }
124}
125
126fn sentry_dsn_example() -> &'static str {
127    "https://public@host:port/1"
128}
129
130fn sentry_environment_example() -> &'static str {
131    "production"
132}
133
134/// Configuration related to the Sentry integration
135#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
136pub struct SentryConfig {
137    /// Sentry DSN
138    #[schemars(url, example = "sentry_dsn_example")]
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub dsn: Option<String>,
141
142    /// Environment to use when sending events to Sentry
143    ///
144    /// Defaults to `production` if not set.
145    #[schemars(example = "sentry_environment_example")]
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub environment: Option<String>,
148
149    /// Sample rate for event submissions
150    ///
151    /// Defaults to `1.0` if not set.
152    #[serde(skip_serializing_if = "Option::is_none")]
153    #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))]
154    pub sample_rate: Option<f32>,
155
156    /// Sample rate for tracing transactions
157    ///
158    /// Defaults to `0.0` if not set.
159    #[serde(skip_serializing_if = "Option::is_none")]
160    #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))]
161    pub traces_sample_rate: Option<f32>,
162}
163
164impl SentryConfig {
165    /// Returns true if all fields are at their default values
166    fn is_default(&self) -> bool {
167        self.dsn.is_none()
168    }
169}
170
171/// Configuration related to sending monitoring data
172#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
173pub struct TelemetryConfig {
174    /// Configuration related to exporting traces
175    #[serde(default, skip_serializing_if = "TracingConfig::is_default")]
176    pub tracing: TracingConfig,
177
178    /// Configuration related to exporting metrics
179    #[serde(default, skip_serializing_if = "MetricsConfig::is_default")]
180    pub metrics: MetricsConfig,
181
182    /// Configuration related to the Sentry integration
183    #[serde(default, skip_serializing_if = "SentryConfig::is_default")]
184    pub sentry: SentryConfig,
185}
186
187impl TelemetryConfig {
188    /// Returns true if all fields are at their default values
189    pub(crate) fn is_default(&self) -> bool {
190        self.tracing.is_default() && self.metrics.is_default() && self.sentry.is_default()
191    }
192}
193
194impl ConfigurationSection for TelemetryConfig {
195    const PATH: Option<&'static str> = Some("telemetry");
196
197    fn validate(&self, _figment: &figment::Figment) -> Result<(), figment::Error> {
198        if let Some(sample_rate) = self.sentry.sample_rate {
199            if !(0.0..=1.0).contains(&sample_rate) {
200                return Err(figment::error::Error::custom(
201                    "Sentry sample rate must be between 0.0 and 1.0",
202                )
203                .with_path("sentry.sample_rate"));
204            }
205        }
206
207        if let Some(sample_rate) = self.sentry.traces_sample_rate {
208            if !(0.0..=1.0).contains(&sample_rate) {
209                return Err(figment::error::Error::custom(
210                    "Sentry sample rate must be between 0.0 and 1.0",
211                )
212                .with_path("sentry.traces_sample_rate"));
213            }
214        }
215
216        if let Some(sample_rate) = self.tracing.sample_rate {
217            if !(0.0..=1.0).contains(&sample_rate) {
218                return Err(figment::error::Error::custom(
219                    "Tracing sample rate must be between 0.0 and 1.0",
220                )
221                .with_path("tracing.sample_rate"));
222            }
223        }
224
225        Ok(())
226    }
227}