rust_telemetry/
config.rs

1// SPDX-FileCopyrightText: 2025 Famedly GmbH (info@famedly.com)
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! OpenTelemetry configuration
6//!
7//! Module containing the configuration struct for the OpenTelemetry
8
9use std::collections::{BTreeMap as Map, HashMap};
10
11use famedly_rust_utils::LevelFilter;
12use serde::Deserialize;
13use url::Url;
14
15/// Default gRPC Otel endpoint
16const DEFAULT_ENDPOINT: &str = "http://localhost:4317";
17
18/// Wrapper over [`Url`] with [`Default`] implementation `http://localhost:4317`
19#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
20#[derive(Debug, Clone, Deserialize)]
21#[repr(transparent)]
22#[serde(transparent)]
23#[allow(missing_docs)]
24pub struct OtelUrl {
25	pub url: Url,
26}
27
28impl From<Url> for OtelUrl {
29	fn from(url: Url) -> Self {
30		Self { url }
31	}
32}
33
34#[allow(clippy::expect_used)]
35impl Default for OtelUrl {
36	fn default() -> Self {
37		Self { url: Url::parse(DEFAULT_ENDPOINT).expect("Error parsing default endpoint") }
38	}
39}
40
41/// OpenTelemetry configuration
42#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43#[derive(Debug, Clone, Default, Deserialize)]
44pub struct OtelConfig {
45	/// Enables logs on stdout
46	pub stdout: Option<StdoutLogsConfig>,
47	/// Configurations for exporting traces, metrics and logs
48	pub exporter: Option<ExporterConfig>,
49}
50
51impl OtelConfig {
52	/// Helper constructor to get stdout-only config for use in tests.
53	#[must_use]
54	pub fn for_tests() -> Self {
55		OtelConfig {
56			stdout: Some(StdoutLogsConfig {
57				enabled: true,
58				level: tracing_subscriber::filter::LevelFilter::TRACE.into(),
59				general_level: tracing_subscriber::filter::LevelFilter::INFO.into(),
60				dependencies_levels: HashMap::new(),
61				json_output: false,
62			}),
63			exporter: None,
64		}
65	}
66}
67
68/// Configuration for exporting OpenTelemetry data
69#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
70#[derive(Debug, Clone, Default, Deserialize)]
71pub struct ExporterConfig {
72	/// gRPC endpoint for exporting using OTELP
73	#[serde(default)]
74	pub endpoint: OtelUrl,
75	/// Key value mapping of the OTEL resource. See [Resource semantic conventions](https://opentelemetry.io/docs/specs/semconv/resource/) for what can be set here.
76	/// Only string values are supported now.
77	/// This crate sets `service.name` and `service.version` by default.
78	#[serde(default)]
79	pub resource_metadata: Map<String, String>,
80	/// Logs exporting config
81	pub logs: Option<ProviderConfig>,
82	/// Traces exporting config
83	pub traces: Option<ProviderConfig>,
84	/// Metrics exporting config
85	pub metrics: Option<ProviderConfig>,
86}
87
88/// Stdout logs configuration
89#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
90#[derive(Debug, Clone, Deserialize)]
91pub struct StdoutLogsConfig {
92	/// Enables the stdout logs
93	#[serde(default = "true_")]
94	pub enabled: bool,
95	/// Level for the crate
96	#[serde(default = "default_level_filter")]
97	pub level: LevelFilter,
98	/// General level
99	#[serde(default = "default_level_filter")]
100	pub general_level: LevelFilter,
101	/// Level for the dependencies
102	#[serde(default)]
103	pub dependencies_levels: HashMap<String, LevelFilter>,
104	/// Output structured JSON logs
105	#[serde(default)]
106	pub json_output: bool,
107}
108
109/// Provider configuration for OpenTelemetry export
110#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
111#[derive(Debug, Clone, Deserialize)]
112pub struct ProviderConfig {
113	/// Enables provider
114	#[serde(default)]
115	pub enabled: bool,
116	/// Level for the crate
117	#[serde(default = "default_level_filter")]
118	pub level: LevelFilter,
119	/// General level
120	#[serde(default = "default_level_filter")]
121	pub general_level: LevelFilter,
122	/// Levels for the dependencies
123	#[serde(default)]
124	pub dependencies_levels: HashMap<String, LevelFilter>,
125}
126
127impl ProviderConfig {
128	/// Builds a trace filter
129	pub(crate) fn get_filter(&self, crate_name: &'static str) -> String {
130		format!(
131			"{},{}{}={}",
132			self.general_level,
133			build_dependencies_level_string(&self.dependencies_levels),
134			crate_name,
135			self.level
136		)
137	}
138}
139
140impl StdoutLogsConfig {
141	/// Builds a trace filter
142	pub(crate) fn get_filter(&self, crate_name: &'static str) -> String {
143		format!(
144			"{},{}{}={}",
145			self.general_level,
146			build_dependencies_level_string(&self.dependencies_levels),
147			crate_name,
148			self.level
149		)
150	}
151}
152
153impl Default for StdoutLogsConfig {
154	fn default() -> Self {
155		Self {
156			enabled: true,
157			level: default_level_filter(),
158			general_level: default_level_filter(),
159			dependencies_levels: HashMap::new(),
160			json_output: false,
161		}
162	}
163}
164
165impl Default for ProviderConfig {
166	fn default() -> Self {
167		Self {
168			enabled: false,
169			level: default_level_filter(),
170			general_level: default_level_filter(),
171			dependencies_levels: HashMap::new(),
172		}
173	}
174}
175
176/// Sets the default LevelFilter
177const fn default_level_filter() -> LevelFilter {
178	LevelFilter(tracing::level_filters::LevelFilter::INFO)
179}
180
181/// Workaround for [serde-rs/serde#368](https://github.com/serde-rs/serde/issues/368)
182const fn true_() -> bool {
183	true
184}
185
186/// Builds a string that configures the filter level of each dependency on the
187/// map
188fn build_dependencies_level_string(dependencies_levels: &HashMap<String, LevelFilter>) -> String {
189	let mut dependencies_levels =
190		dependencies_levels.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join(",");
191	if !dependencies_levels.is_empty() {
192		dependencies_levels.push(',');
193	}
194	dependencies_levels
195}