1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt;
5use std::fs::OpenOptions;
6use tracing_subscriber::EnvFilter;
7use tracing_subscriber::layer::SubscriberExt;
8use tracing_subscriber::util::SubscriberInitExt;
9
10use opentelemetry::KeyValue;
12use opentelemetry_sdk::resource::{Resource, TelemetryResourceDetector};
13use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
14use opentelemetry_resource_detectors::{
15 HostResourceDetector, OsResourceDetector, ProcessResourceDetector,
16};
17use opentelemetry_semantic_conventions::attribute;
18
19use opentelemetry::trace::TracerProvider as _; use opentelemetry_otlp::SpanExporter;
22use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
23use tracing_opentelemetry::OpenTelemetryLayer;
24
25use opentelemetry_otlp::MetricExporter;
27use opentelemetry_sdk::metrics::SdkMeterProvider;
28use tracing_opentelemetry::MetricsLayer;
29
30use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
32use opentelemetry_otlp::LogExporter;
33use opentelemetry_sdk::logs::SdkLoggerProvider;
34
35pub use opentelemetry_otlp::ExporterBuildError;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct TracingOtelConfig {
39 collector_url: String,
40
41 #[serde(default, skip_serializing)]
42 collector_auth_header: Option<SecretString>,
43}
44impl TracingOtelConfig {
45 pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
46 Self {
47 collector_url,
48 collector_auth_header,
49 }
50 }
51 pub fn collector_url(&self) -> &str {
52 &self.collector_url
53 }
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct TracingConfig {
58 #[serde(default)]
63 filter: Option<String>,
64
65 #[serde(default)]
66 log_file_path: Option<String>,
67
68 #[serde(default = "TracingConfig::ansi_output_default")]
69 ansi_output: bool,
70
71 #[serde(default, flatten)]
72 otel_config: Option<TracingOtelConfig>,
73}
74impl TracingConfig {
75 pub fn new(collector_url: Option<String>, collector_auth_header: Option<SecretString>, log_file_path: Option<String>, ansi_output: Option<bool>, filter: Option<String>) -> Self {
76 Self {
77 filter,
78 log_file_path,
79 ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
80 otel_config: collector_url.map(|url| TracingOtelConfig {
81 collector_url: url,
82 collector_auth_header,
83 }),
84 }
85 }
86 pub fn log_file_path(&self) -> Option<&str> {
87 self.log_file_path.as_deref()
88 }
89 pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
90 &self.otel_config
91 }
92 pub fn ansi_output_default() -> bool {
93 true
94 }
95}
96impl Default for TracingConfig {
97 fn default() -> Self {
98 Self {
99 ansi_output: Self::ansi_output_default(),
100 filter: None,
101 log_file_path: None,
102 otel_config: None,
103 }
104 }
105}
106
107pub mod custom_attribute {
110 pub const SERVICE_CRATE_NAME: &str = "service.crate_name";
111 pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
112 pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
113 pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
114 pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
115 pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
116}
117#[rustfmt::skip]
118pub fn get_build_env() -> &'static str {
119 #[cfg(debug_assertions)]
120 { "debug" }
121 #[cfg(not(debug_assertions))]
122 { "release" }
123}
124pub fn get_origin_package_name() -> Option<&'static str> {
125 let package_name = env!("CARGO_PKG_NAME");
126 if package_name.is_empty() {
127 None
128 } else {
129 Some(package_name)
130 }
131}
132pub fn get_origin_crate_name() -> Option<&'static str> {
133 let package_name = env!("CARGO_CRATE_NAME");
134 if package_name.is_empty() {
135 None
136 } else {
137 Some(package_name)
138 }
139}
140
141fn build_otel_resource(service_attrs: &ServiceAttributeStore) -> Resource {
142 let mut builder = Resource::builder_empty()
144 .with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name))
145 .with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
146
147 builder = builder
149 .with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_attrs.version))
150 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, service_attrs.version_major))
151 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, service_attrs.version_minor))
152 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, service_attrs.version_patch));
153
154 if let Some(origin_package_name) = get_origin_package_name() {
156 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
157 }
158 if let Some(origin_crate_name) = get_origin_crate_name() {
159 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
160 }
161
162 builder
163 .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()
169}
170
171fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
172 let mut headers: HashMap<String, String> = HashMap::new();
173
174 if let Some(auth_header) = auth_header_val.as_ref() {
176 headers.insert("Authorization".into(), auth_header.expose_secret().into());
177 }
178
179 headers
180}
181
182fn init_otel_traces_provider(
184 collector_endpoint: &str,
185 headers: HashMap<String, String>,
186 resource: Resource,
187) -> Result<SdkTracerProvider, ExporterBuildError> {
188 let exporter = SpanExporter::builder()
189 .with_http()
190 .with_headers(headers)
191 .with_endpoint(format!("{collector_endpoint}/v1/traces"))
192 .with_protocol(Protocol::HttpBinary)
193 .build()?;
195
196 let provider = SdkTracerProvider::builder()
197 .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
199 .with_resource(resource)
200 .with_batch_exporter(exporter)
201 .build();
202
203 Ok(provider)
204}
205fn init_otel_logs_provider(
206 collector_endpoint: &str,
207 headers: HashMap<String, String>,
208 resource: Resource,
209) -> Result<SdkLoggerProvider, ExporterBuildError> {
210 let exporter = LogExporter::builder()
211 .with_http()
212 .with_headers(headers)
213 .with_endpoint(format!("{collector_endpoint}/v1/logs"))
214 .with_protocol(Protocol::HttpBinary)
215 .build()?;
217
218 let provider = SdkLoggerProvider::builder()
219 .with_resource(resource)
220 .with_batch_exporter(exporter)
221 .build();
222
223 Ok(provider)
224}
225fn init_otel_metrics_provider(
226 collector_endpoint: &str,
227 headers: HashMap<String, String>,
228 resource: Resource,
229) -> Result<SdkMeterProvider, ExporterBuildError> {
230 let exporter = MetricExporter::builder()
231 .with_http()
232 .with_headers(headers)
233 .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
234 .with_protocol(Protocol::HttpBinary)
235 .build()?;
237
238 let provider = SdkMeterProvider::builder()
239 .with_resource(resource)
240 .with_periodic_exporter(exporter)
241 .build();
242
243 Ok(provider)
244}
245
246#[derive(Debug, Clone)]
261pub struct ServiceAttributeStore {
262 pub crate_name: &'static str,
263 pub pkg_name: &'static str,
264 pub version: &'static str,
265 pub version_major: &'static str,
266 pub version_minor: &'static str,
267 pub version_patch: &'static str,
268}
269impl ServiceAttributeStore {
270 pub fn dump(&self) {
271
272 let service_name = self.pkg_name;
273 let crate_name = self.crate_name;
274 let service_version = self.version;
275 let service_version_major = self.version_major;
276 let service_version_minor = self.version_minor;
277 let service_version_patch = self.version_patch;
278 let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -");
279 let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -");
280 let build_env = get_build_env();
281
282 println!();
283 println!("Resolved tracing attributes");
284 println!("--------------------");
285 println!("service_name (pkg_name): {service_name}");
286 println!("service_crate_name: {crate_name}");
287 println!("service_version: {service_version}");
288 println!("service_version_major: {service_version_major}");
289 println!("service_version_minor: {service_version_minor}");
290 println!("service_version_patch: {service_version_patch}");
291 println!("origin_pkg_name: {origin_pkg_name}");
292 println!("origin_crate_name: {origin_crate_name}");
293 println!("build_env: {build_env}");
294 println!();
295 }
296}
297
298#[macro_export]
303macro_rules! build_attrs {
304 () => (
308 tracing_kickstart::ServiceAttributeStore {
309 crate_name: env!("CARGO_CRATE_NAME"),
310 pkg_name: env!("CARGO_PKG_NAME"),
311 version: env!("CARGO_PKG_VERSION"),
312 version_major: env!("CARGO_PKG_VERSION_MAJOR"),
313 version_minor: env!("CARGO_PKG_VERSION_MINOR"),
314 version_patch: env!("CARGO_PKG_VERSION_PATCH"),
315 }
316 )
317}
318
319pub fn init(service_attrs: ServiceAttributeStore, config: &TracingConfig, default_env_filter: Option<&str>) -> Result<TraceProviders, ExporterBuildError> {
353 let filter: EnvFilter = {
355 if let Some(filter_str) = &config.filter {
357 println!("Resolved tracing EnvFilter from provided config: {filter_str:?}");
358 filter_str.into()
359 }
360 else if let Ok(filter) = EnvFilter::try_from_default_env() {
362 println!("Resolved tracing EnvFilter from `RUST_LOG`: {:?}", filter.to_string());
363 filter
364 }
365 else if let Some(filter_str) = default_env_filter {
367 println!("Resolving tracing EnvFilter from `tracing_kickstart::init(.., default_env_filter)`: {filter_str:?}");
368 filter_str.into()
369 }
370 else {
372 let filter_str = format!(
373 "warn,{}=debug,tracing_kickstart=debug", service_attrs.crate_name
375 );
376 println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
377 filter_str.into()
378 }
379 };
380
381 #[cfg(feature = "tokio_console")]
383 let filter = {
384 let mut filter_str = filter.to_string();
385 if !filter_str.is_empty() {
386 filter_str.push(',');
387 }
388 filter_str.push_str("tokio=trace,runtime=trace");
389 EnvFilter::from(filter_str)
390 };
391 println!("Launching with tracing filter: {}", filter);
392
393 let layer = tracing_subscriber::registry()
395 .with(filter);
396
397 let layer = layer.with(
399 tracing_subscriber::fmt::layer()
400 .with_ansi(config.ansi_output)
401 );
402
403 let file_logging_layer = match &config.log_file_path {
405 Some(file_path) => {
406 let file = OpenOptions::new()
407 .write(true)
408 .create(true)
409 .truncate(true)
410 .open(file_path)
411 .expect("Log file should be writable");
412 Some(tracing_subscriber::fmt::layer().with_ansi(false).with_writer(file))
413 },
414 None => None
415 };
416 let layer = layer.with(file_logging_layer);
417
418 #[cfg(feature = "tokio_console")]
420 let layer = layer.with(console_subscriber::spawn());
421
422 let mut providers_handle = TraceProviders::default();
424
425 if let Some(otel_config) = &config.otel_config {
427 println!("Initializing OTEL config");
428 let endpoint = &otel_config.collector_url;
429 let headers = build_otel_headers(&otel_config.collector_auth_header);
430 let resource = build_otel_resource(&service_attrs);
431
432 let traces_provider =
434 init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
435 let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)));
437 providers_handle.traces = Some(traces_provider);
438
439 let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
441 let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
443 providers_handle.logs = Some(logs_provider);
444
445 let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource)?;
447 let layer = layer.with(MetricsLayer::new(metrics_provider.clone()));
449 providers_handle.metrics = Some(metrics_provider);
450
451 layer.init();
452 tracing::info!("OTEL tracing configured");
453 } else {
454 layer.init();
455 tracing::warn!("OTEL tracing disabled");
456 }
457
458 Ok(providers_handle)
459}
460
461#[derive(Default, Clone)]
465pub struct TraceProviders {
466 pub traces: Option<SdkTracerProvider>,
467 pub logs: Option<SdkLoggerProvider>,
468 pub metrics: Option<SdkMeterProvider>,
469}
470impl TraceProviders {
471 pub fn register_globally(&self) {
473 if let Some(provider) = &self.traces {
475 tracing::info!("Traces provider registered globally");
476 opentelemetry::global::set_tracer_provider(provider.clone());
477 }
478 if let Some(provider) = &self.metrics {
480 tracing::info!("Metrics provider registered globally");
481 opentelemetry::global::set_meter_provider(provider.clone());
482 }
483 }
484
485 pub fn shutdown(self) {
487 if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
489 println!("error shutting down traces provider: {error}");
490 }
491 if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
493 println!("error shutting down logs provider: {error}");
494 }
495 if let Some(provider) = self.metrics && let Err(error) = provider.shutdown() {
497 println!("error shutting down metrics provider: {error}");
498 }
499 }
500}
501impl fmt::Debug for TraceProviders {
502 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
503 write!(f, "TraceProviders (")?;
504 let mut i = 0;
505 if self.traces.is_some() {
506 write!(f, "SdkTracerProvider")?;
507 i += 1;
508 }
509 if self.logs.is_some() {
510 if i > 0 {
511 write!(f, ", ")?;
512 }
513 write!(f, "SdkLoggerProvider")?;
514 i += 1;
515 }
516 if self.metrics.is_some() {
517 if i > 0 {
518 write!(f, ", ")?;
519 }
520 write!(f, "SdkMeterProvider")?;
521 }
522 write!(f, ")")
523 }
524}