tracing_kickstart/
trace.rs1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs::OpenOptions;
5use tracing_subscriber::EnvFilter;
6#[cfg(feature = "tokio_console")]
7use tracing_subscriber::layer::Layer;
8use tracing_subscriber::layer::SubscriberExt;
9use tracing_subscriber::util::SubscriberInitExt;
10
11use opentelemetry::KeyValue;
13use opentelemetry_sdk::resource::{Resource, TelemetryResourceDetector};
14use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
16use opentelemetry_resource_detectors::{
17 HostResourceDetector, OsResourceDetector, ProcessResourceDetector,
18};
19use opentelemetry_semantic_conventions::attribute;
20
21use opentelemetry::trace::TracerProvider as _; use opentelemetry_otlp::SpanExporter;
24use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
25use tracing_opentelemetry::OpenTelemetryLayer;
26
27use opentelemetry_otlp::MetricExporter;
29use opentelemetry_sdk::metrics::SdkMeterProvider;
30
31use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
33use opentelemetry_otlp::LogExporter;
34use opentelemetry_sdk::logs::SdkLoggerProvider;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TracingOtelConfig {
38 collector_url: String,
39
40 #[serde(default, skip_serializing)]
41 collector_auth_header: Option<SecretString>,
42}
43impl TracingOtelConfig {
44 pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
45 Self {
46 collector_url,
47 collector_auth_header,
48 }
49 }
50 pub fn collector_url(&self) -> &str {
51 &self.collector_url
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TracingConfig {
57 #[serde(default)]
58 log_file_path: Option<String>,
59
60 #[serde(default = "TracingConfig::ansi_output_default")]
61 ansi_output: bool,
62
63 #[serde(default, flatten)]
64 otel_config: Option<TracingOtelConfig>,
65}
66impl TracingConfig {
67 pub fn new(collector_url: Option<String>, collector_auth_header: Option<SecretString>, log_file_path: Option<String>, ansi_output: Option<bool>) -> Self {
68 Self {
69 log_file_path,
70 ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
71 otel_config: collector_url.map(|url| TracingOtelConfig {
72 collector_url: url,
73 collector_auth_header,
74 }),
75 }
76 }
77 pub fn log_file_path(&self) -> Option<&str> {
78 self.log_file_path.as_ref().map(|s| s.as_str())
79 }
80 pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
81 &self.otel_config
82 }
83 pub fn ansi_output_default() -> bool {
84 true
85 }
86}
87impl Default for TracingConfig {
88 fn default() -> Self {
89 Self {
90 ansi_output: Self::ansi_output_default(),
91 log_file_path: None,
92 otel_config: None,
93 }
94 }
95}
96
97pub mod custom_attribute {
100 pub const SERVICE_CRATE_NAME: &str = "service.crate.name";
101 pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
102 pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
103 pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
104 pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
105 pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
106}
107#[rustfmt::skip]
108pub fn get_build_env() -> &'static str {
109 #[cfg(debug_assertions)]
110 { "debug" }
111 #[cfg(not(debug_assertions))]
112 { "release" }
113}
114pub fn get_service_version() -> Option<String> {
115 std::env::var("CARGO_PKG_VERSION").ok()
116}
117pub fn get_service_version_major() -> Option<String> {
118 std::env::var("CARGO_PKG_VERSION_MAJOR").ok()
119}
120pub fn get_service_version_minor() -> Option<String> {
121 std::env::var("CARGO_PKG_VERSION_MINOR").ok()
122}
123pub fn get_service_version_patch() -> Option<String> {
124 std::env::var("CARGO_PKG_VERSION_PATCH").ok()
125}
126pub fn get_origin_package_name() -> Option<&'static str> {
127 let package_name = env!("CARGO_PKG_NAME");
128 if package_name.is_empty() {
129 None
130 } else {
131 Some(package_name)
132 }
133}
134pub fn get_origin_crate_name() -> Option<&'static str> {
135 let package_name = env!("CARGO_CRATE_NAME");
136 if package_name.is_empty() {
137 None
138 } else {
139 Some(package_name)
140 }
141}
142
143fn build_otel_resource(service_attrs: &ServiceAttributeStore) -> Resource {
144 let mut builder = Resource::builder_empty();
145 builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name));
147 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
148
149 if let Some(service_version) = get_service_version() {
151 builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_version));
152 if let Some(version_part) = get_service_version_major() {
153 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, version_part));
154 if let Some(version_part) = get_service_version_minor() {
155 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, version_part));
156 if let Some(version_part) = get_service_version_patch() {
157 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, version_part));
158 }
159 }
160 }
161 }
162 if let Some(origin_package_name) = get_origin_package_name() {
164 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
165 }
166 if let Some(origin_crate_name) = get_origin_crate_name() {
167 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
168 }
169
170 builder
171 .with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, get_build_env())) .with_detector(Box::new(TelemetryResourceDetector)) .with_detector(Box::new(HostResourceDetector::default())) .with_detector(Box::new(ProcessResourceDetector)) .with_detector(Box::new(OsResourceDetector)) .build()
177}
178
179fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
180 let mut headers: HashMap<String, String> = HashMap::new();
181
182 if let Some(auth_header) = auth_header_val.as_ref() {
184 headers.insert("Authorization".into(), auth_header.expose_secret().into());
185 }
186
187 headers
188}
189
190fn init_otel_traces_provider(
192 collector_endpoint: &str,
193 headers: HashMap<String, String>,
194 resource: Resource,
195) -> color_eyre::Result<SdkTracerProvider> {
196 let exporter = SpanExporter::builder()
197 .with_http()
198 .with_headers(headers)
199 .with_endpoint(format!("{collector_endpoint}/v1/traces"))
200 .with_protocol(Protocol::HttpBinary)
201 .build()?;
203
204 let provider = SdkTracerProvider::builder()
205 .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
207 .with_resource(resource)
208 .with_batch_exporter(exporter)
209 .build();
210
211 Ok(provider)
212}
213fn init_otel_logs_provider(
214 collector_endpoint: &str,
215 headers: HashMap<String, String>,
216 resource: Resource,
217) -> color_eyre::Result<SdkLoggerProvider> {
218 let exporter = LogExporter::builder()
219 .with_http()
220 .with_headers(headers)
221 .with_endpoint(format!("{collector_endpoint}/v1/logs"))
222 .with_protocol(Protocol::HttpBinary)
223 .build()?;
225
226 let provider = SdkLoggerProvider::builder()
227 .with_resource(resource)
228 .with_batch_exporter(exporter)
229 .build();
230
231 Ok(provider)
232}
233fn init_otel_metrics_provider(
234 collector_endpoint: &str,
235 headers: HashMap<String, String>,
236 resource: Resource,
237) -> color_eyre::Result<SdkMeterProvider> {
238 let exporter = MetricExporter::builder()
239 .with_http()
240 .with_headers(headers)
241 .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
242 .with_protocol(Protocol::HttpBinary)
243 .build()?;
245
246 let provider = SdkMeterProvider::builder()
247 .with_resource(resource)
248 .with_periodic_exporter(exporter)
249 .build();
250
251 Ok(provider)
252}
253
254#[derive(Debug, Clone)]
258pub struct ServiceAttributeStore {
259 pub crate_name: &'static str,
260 pub pkg_name: &'static str,
261}
262impl ServiceAttributeStore {
263 pub fn new(crate_name: &'static str, pkg_name: &'static str) -> Self {
274 Self {
275 crate_name,
276 pkg_name,
277 }
278 }
279}
280
281#[allow(unused_mut)]
283pub fn init(service_attrs: ServiceAttributeStore, config: &TracingConfig) -> color_eyre::Result<TraceProviders> {
284 let mut prepared_env_filter = format!(
285 "warn,{}=debug,tracing_kickstart=debug", service_attrs.crate_name
287 );
288
289 #[cfg(feature = "tokio_console")]
291 { prepared_env_filter.push_str(",tokio=trace,runtime=trace"); }
292
293 let env_filter =
294 EnvFilter::try_from_default_env().unwrap_or_else(|_| prepared_env_filter.clone().into());
295
296 let layer = tracing_subscriber::registry()
298 .with(env_filter);
299 let layer = layer.with(tracing_subscriber::fmt::layer()
301 .with_ansi(config.ansi_output)
302 );
303 let file_logging_layer = match &config.log_file_path {
305 Some(file_path) => {
306 let file = OpenOptions::new()
307 .write(true)
308 .create(true)
309 .truncate(true)
310 .open(file_path)
311 .expect("Log file should be writable");
312 Some(tracing_subscriber::fmt::layer().with_ansi(false).with_writer(file))
313 },
314 None => None
315 };
316 let layer = layer.with(file_logging_layer);
317
318 #[cfg(feature = "tokio_console")]
320 { let layer = layer.with(console_subscriber::spawn()); }
321
322 let mut providers_handle = TraceProviders::default();
324
325 if let Some(otel_config) = &config.otel_config {
327 println!("Initializing OTEL config");
328 let endpoint = &otel_config.collector_url;
329 let headers = build_otel_headers(&otel_config.collector_auth_header);
330 let resource = build_otel_resource(&service_attrs);
331
332 let traces_provider =
334 init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
335 let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)));
337 providers_handle.traces = Some(traces_provider);
338
339 let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
341 let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
343 providers_handle.logs = Some(logs_provider);
344
345 let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource)?;
347 providers_handle.metrics = Some(metrics_provider);
348
349 layer.init();
350 tracing::info!("OTEL tracing configured");
351 } else {
352 layer.init();
353 tracing::warn!("OTEL tracing disabled");
354 }
355
356 tracing::debug!("Env filter: {prepared_env_filter:?}");
357
358 Ok(providers_handle)
359}
360
361pub fn dump_crate_vars(attrs: &ServiceAttributeStore) {
362 let service_name = attrs.pkg_name;
363 let crate_name = attrs.crate_name;
364 let service_version = get_service_version().unwrap_or("- unset -".into());
365 let service_version_major = get_service_version_major().unwrap_or("- unset -".into());
366 let service_version_minor = get_service_version_minor().unwrap_or("- unset -".into());
367 let service_version_patch = get_service_version_patch().unwrap_or("- unset -".into());
368 let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -".into());
369 let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -".into());
370 let build_env = get_build_env();
371
372 println!("");
373 println!("Resolved tracing attributes");
374 println!("--------------------");
375 println!("service_name (pkg_name): {service_name}");
376 println!("service_crate_name: {crate_name}");
377 println!("service_version: {service_version}");
378 println!("service_version_major: {service_version_major}");
379 println!("service_version_minor: {service_version_minor}");
380 println!("service_version_patch: {service_version_patch}");
381 println!("origin_pkg_name: {origin_pkg_name}");
382 println!("origin_crate_name: {origin_crate_name}");
383 println!("build_env: {build_env}");
384 println!("");
385}
386
387#[derive(Debug, Default, Clone)]
391pub struct TraceProviders {
392 pub traces: Option<SdkTracerProvider>,
393 pub logs: Option<SdkLoggerProvider>,
394 pub metrics: Option<SdkMeterProvider>,
395}
396impl TraceProviders {
397 pub fn shutdown(self) {
398 if let Some(provider) = self.traces {
400 if let Err(error) = provider.shutdown() {
401 println!("error shutting down traces provider: {error}");
402 }
403 }
404 if let Some(provider) = self.logs {
406 if let Err(error) = provider.shutdown() {
407 println!("error shutting down logs provider: {error}");
408 }
409 }
410 if let Some(provider) = self.metrics {
412 if let Err(error) = provider.shutdown() {
413 println!("error shutting down metrics provider: {error}");
414 }
415 }
416 }
417}