mas_config/sections/
telemetry.rs

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