init4_bin_base/utils/
otlp.rs1use crate::utils::from_env::{EnvItemInfo, FromEnv, FromEnvErr, FromEnvVar};
2use opentelemetry::{trace::TracerProvider, KeyValue};
3use opentelemetry_sdk::trace::SdkTracerProvider;
4use opentelemetry_sdk::Resource;
5use opentelemetry_semantic_conventions::{
6 attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION},
7 SCHEMA_URL,
8};
9use std::time::Duration;
10use tracing::level_filters::LevelFilter;
11use tracing_subscriber::Layer;
12use url::Url;
13
14const OTEL_ENDPOINT: &str = "OTEL_EXPORTER_OTLP_ENDPOINT";
15const OTEL_LEVEL: &str = "OTEL_LEVEL";
16const OTEL_TIMEOUT: &str = "OTEL_TIMEOUT";
17const OTEL_ENVIRONMENT: &str = "OTEL_ENVIRONMENT_NAME";
18
19#[derive(Debug)]
36pub struct OtelGuard(SdkTracerProvider, tracing::Level);
37
38impl OtelGuard {
39 fn tracer(&self, s: &'static str) -> opentelemetry_sdk::trace::Tracer {
41 self.0.tracer(s)
42 }
43
44 pub fn layer<S>(&self) -> impl Layer<S>
46 where
47 S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
48 {
49 let tracer = self.tracer("tracing-otel-subscriber");
50 tracing_opentelemetry::layer()
51 .with_tracer(tracer)
52 .with_filter(LevelFilter::from_level(self.1))
53 }
54}
55
56impl Drop for OtelGuard {
57 fn drop(&mut self) {
58 if let Err(err) = self.0.shutdown() {
59 eprintln!("{err:?}");
60 }
61 }
62}
63
64#[derive(Debug, Clone)]
77#[non_exhaustive]
78pub struct OtelConfig {
79 pub endpoint: Url,
82
83 pub level: tracing::Level,
85
86 pub timeout: Duration,
88
89 pub environment: String,
91}
92
93impl FromEnv for OtelConfig {
94 type Error = url::ParseError;
95
96 fn inventory() -> Vec<&'static EnvItemInfo> {
97 vec![
98 &EnvItemInfo {
99 var: OTEL_ENDPOINT,
100 description:
101 "OTLP endpoint to send traces to, a url. If missing, disables OTLP exporting.",
102 optional: true,
103 },
104 &EnvItemInfo {
105 var: OTEL_LEVEL,
106 description: "OTLP level to export, defaults to DEBUG. Permissible values are: TRACE, DEBUG, INFO, WARN, ERROR, OFF",
107 optional: true,
108 },
109 &EnvItemInfo {
110 var: OTEL_TIMEOUT,
111 description: "OTLP timeout in milliseconds",
112 optional: true,
113 },
114 &EnvItemInfo {
115 var: OTEL_ENVIRONMENT,
116 description: "OTLP environment name, a string",
117 optional: true,
118 },
119 ]
120 }
121
122 fn from_env() -> Result<Self, FromEnvErr<Self::Error>> {
123 let endpoint = Url::from_env_var(OTEL_ENDPOINT)?;
125
126 let level = tracing::Level::from_env_var(OTEL_LEVEL).unwrap_or(tracing::Level::DEBUG);
127
128 let timeout = Duration::from_env_var(OTEL_TIMEOUT).unwrap_or(Duration::from_millis(1000));
129
130 let environment = String::from_env_var(OTEL_ENVIRONMENT).unwrap_or("unknown".into());
131
132 Ok(Self {
133 endpoint,
134 level,
135 timeout,
136 environment,
137 })
138 }
139}
140
141impl OtelConfig {
142 pub fn load() -> Option<Self> {
156 Self::from_env().ok()
157 }
158
159 fn resource(&self) -> Resource {
160 Resource::builder()
161 .with_schema_url(
162 [
163 KeyValue::new(SERVICE_NAME, env!("CARGO_PKG_NAME")),
164 KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")),
165 KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, self.environment.clone()),
166 ],
167 SCHEMA_URL,
168 )
169 .build()
170 }
171
172 pub fn provider(&self) -> OtelGuard {
175 let exporter = opentelemetry_otlp::SpanExporter::builder()
176 .with_http()
177 .build()
178 .unwrap();
179
180 let provider = SdkTracerProvider::builder()
181 .with_resource(self.resource())
184 .with_batch_exporter(exporter)
185 .build();
186
187 OtelGuard(provider, self.level)
188 }
189}
190
191#[cfg(test)]
192mod test {
193 use super::*;
194
195 const URL: &str = "http://localhost:4317";
196
197 fn clear_env() {
198 std::env::remove_var(OTEL_ENDPOINT);
199 std::env::remove_var(OTEL_LEVEL);
200 std::env::remove_var(OTEL_TIMEOUT);
201 std::env::remove_var(OTEL_ENVIRONMENT);
202 }
203
204 fn run_clear_env<F>(f: F)
205 where
206 F: FnOnce(),
207 {
208 f();
209 clear_env();
210 }
211
212 #[test]
213 #[serial_test::serial]
214
215 fn test_env_read() {
216 run_clear_env(|| {
217 std::env::set_var(OTEL_ENDPOINT, URL);
218
219 let cfg = OtelConfig::load().unwrap();
220 assert_eq!(cfg.endpoint, URL.parse().unwrap());
221 assert_eq!(cfg.level, tracing::Level::DEBUG);
222 assert_eq!(cfg.timeout, std::time::Duration::from_millis(1000));
223 assert_eq!(cfg.environment, "unknown");
224 })
225 }
226
227 #[test]
228 #[serial_test::serial]
229 fn test_env_read_level() {
230 run_clear_env(|| {
231 std::env::set_var(OTEL_ENDPOINT, URL);
232 std::env::set_var(OTEL_LEVEL, "WARN");
233
234 let cfg = OtelConfig::load().unwrap();
235 assert_eq!(cfg.level, tracing::Level::WARN);
236 })
237 }
238
239 #[test]
240 #[serial_test::serial]
241 fn test_env_read_timeout() {
242 run_clear_env(|| {
243 std::env::set_var(OTEL_ENDPOINT, URL);
244 std::env::set_var(OTEL_TIMEOUT, "500");
245
246 let cfg = OtelConfig::load().unwrap();
247 assert_eq!(cfg.timeout, std::time::Duration::from_millis(500));
248 })
249 }
250
251 #[test]
252 #[serial_test::serial]
253 fn invalid_url() {
254 run_clear_env(|| {
255 std::env::set_var(OTEL_ENDPOINT, "not a url");
256
257 let cfg = OtelConfig::load();
258 assert!(cfg.is_none());
259 })
260 }
261}