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
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(&self, _figment: &figment::Figment) -> Result<(), figment::Error> {
186        if let Some(sample_rate) = self.sentry.sample_rate {
187            if !(0.0..=1.0).contains(&sample_rate) {
188                return Err(figment::error::Error::custom(
189                    "Sentry sample rate must be between 0.0 and 1.0",
190                )
191                .with_path("sentry.sample_rate"));
192            }
193        }
194
195        if let Some(sample_rate) = self.sentry.traces_sample_rate {
196            if !(0.0..=1.0).contains(&sample_rate) {
197                return Err(figment::error::Error::custom(
198                    "Sentry sample rate must be between 0.0 and 1.0",
199                )
200                .with_path("sentry.traces_sample_rate"));
201            }
202        }
203
204        if let Some(sample_rate) = self.tracing.sample_rate {
205            if !(0.0..=1.0).contains(&sample_rate) {
206                return Err(figment::error::Error::custom(
207                    "Tracing sample rate must be between 0.0 and 1.0",
208                )
209                .with_path("tracing.sample_rate"));
210            }
211        }
212
213        Ok(())
214    }
215}