Skip to main content

mas_config/sections/
telemetry.rs

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