stoffel/observability/
tracing.rs1use opentelemetry::trace::TracerProvider as _;
7use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
8use serde::{Deserialize, Serialize};
9use tracing::Level;
10use tracing_subscriber::filter::LevelFilter;
11use tracing_subscriber::fmt;
12use tracing_subscriber::prelude::*;
13
14use crate::error::{Error, Result};
15
16#[derive(Debug, Clone)]
17pub struct TracingConfig {
18 max_level: Level,
19 ansi: bool,
20 compact: bool,
21 service_name: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct TracingConfigSummary {
26 pub max_level: String,
27 pub ansi: bool,
28 pub compact: bool,
29 pub service_name: String,
30}
31
32impl Default for TracingConfig {
33 fn default() -> Self {
34 Self {
35 max_level: Level::INFO,
36 ansi: true,
37 compact: true,
38 service_name: "stoffel-rust-sdk".to_owned(),
39 }
40 }
41}
42
43impl TracingConfig {
44 pub fn builder() -> TracingConfigBuilder {
45 TracingConfigBuilder::default()
46 }
47
48 pub fn max_level(&self) -> Level {
49 self.max_level
50 }
51
52 pub fn ansi(&self) -> bool {
53 self.ansi
54 }
55
56 pub fn compact(&self) -> bool {
57 self.compact
58 }
59
60 pub fn service_name(&self) -> &str {
61 &self.service_name
62 }
63
64 pub fn summary(&self) -> TracingConfigSummary {
65 TracingConfigSummary {
66 max_level: self.max_level.to_string(),
67 ansi: self.ansi,
68 compact: self.compact,
69 service_name: self.service_name.clone(),
70 }
71 }
72
73 pub fn validate(&self) -> Result<()> {
74 if self.service_name.trim().is_empty() {
75 return Err(Error::Configuration(
76 "tracing service_name must not be empty".to_owned(),
77 ));
78 }
79 Ok(())
80 }
81
82 pub fn install(self) -> Result<()> {
83 self.validate()?;
84 if self.compact {
85 fmt()
86 .with_max_level(self.max_level)
87 .with_ansi(self.ansi)
88 .compact()
89 .try_init()
90 } else {
91 fmt()
92 .with_max_level(self.max_level)
93 .with_ansi(self.ansi)
94 .try_init()
95 }
96 .map_err(|error| Error::Configuration(format!("failed to initialize tracing: {error}")))
97 }
98
99 pub fn install_opentelemetry_stdout(self) -> Result<OpenTelemetryGuard> {
100 self.validate()?;
101 let provider = SdkTracerProvider::builder()
102 .with_simple_exporter(opentelemetry_stdout::SpanExporter::default())
103 .with_resource(
104 Resource::builder()
105 .with_service_name(self.service_name.clone())
106 .build(),
107 )
108 .build();
109 let level_filter = LevelFilter::from_level(self.max_level);
110 let init_result = if self.compact {
111 let tracer = provider.tracer("stoffel-rust-sdk");
112 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
113 tracing_subscriber::registry()
114 .with(
115 fmt::layer()
116 .compact()
117 .with_ansi(self.ansi)
118 .with_filter(level_filter),
119 )
120 .with(telemetry)
121 .try_init()
122 } else {
123 let tracer = provider.tracer("stoffel-rust-sdk");
124 let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
125 tracing_subscriber::registry()
126 .with(fmt::layer().with_ansi(self.ansi).with_filter(level_filter))
127 .with(telemetry)
128 .try_init()
129 };
130 init_result.map_err(|error| {
131 Error::Configuration(format!(
132 "failed to initialize OpenTelemetry tracing: {error}"
133 ))
134 })?;
135
136 Ok(OpenTelemetryGuard {
137 provider: Some(provider),
138 })
139 }
140}
141
142#[derive(Debug)]
143pub struct OpenTelemetryGuard {
144 provider: Option<SdkTracerProvider>,
145}
146
147impl OpenTelemetryGuard {
148 pub fn shutdown(mut self) -> Result<()> {
149 shutdown_provider(self.provider.take())
150 }
151}
152
153impl Drop for OpenTelemetryGuard {
154 fn drop(&mut self) {
155 let _ = shutdown_provider(self.provider.take());
156 }
157}
158
159fn shutdown_provider(provider: Option<SdkTracerProvider>) -> Result<()> {
160 if let Some(provider) = provider {
161 provider.shutdown().map_err(|error| {
162 Error::Configuration(format!(
163 "failed to shut down OpenTelemetry tracer provider: {error}"
164 ))
165 })?;
166 }
167 Ok(())
168}
169
170#[derive(Debug, Clone, Default)]
171pub struct TracingConfigBuilder {
172 config: TracingConfig,
173}
174
175impl TracingConfigBuilder {
176 pub fn max_level(mut self, level: Level) -> Self {
177 self.config.max_level = level;
178 self
179 }
180
181 pub fn ansi(mut self, ansi: bool) -> Self {
182 self.config.ansi = ansi;
183 self
184 }
185
186 pub fn compact(mut self, compact: bool) -> Self {
187 self.config.compact = compact;
188 self
189 }
190
191 pub fn service_name(mut self, service_name: impl Into<String>) -> Self {
192 self.config.service_name = service_name.into();
193 self
194 }
195
196 pub fn build(self) -> TracingConfig {
197 self.config
198 }
199
200 pub fn install(self) -> Result<()> {
201 self.build().install()
202 }
203
204 pub fn install_opentelemetry_stdout(self) -> Result<OpenTelemetryGuard> {
205 self.build().install_opentelemetry_stdout()
206 }
207}
208
209pub fn init_tracing() -> Result<()> {
210 TracingConfig::default().install()
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn shutdown_provider_allows_absent_provider() {
219 shutdown_provider(None).expect("empty OpenTelemetry guard shutdown is a no-op");
220 }
221
222 #[test]
223 fn dropping_empty_guard_is_a_noop() {
224 drop(OpenTelemetryGuard { provider: None });
225 }
226}