1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use tracing_error::ErrorLayer;
4use std::collections::HashMap;
5use std::fmt;
6use std::fs::OpenOptions;
7use std::time::Duration;
8use tracing_subscriber::{EnvFilter, Layer as _};
9use tracing_subscriber::layer::SubscriberExt;
10use tracing_subscriber::util::SubscriberInitExt;
11
12use opentelemetry::{Key, KeyValue, Value};
14use opentelemetry_sdk::resource::Resource;
15#[cfg(feature = "detector_telemetry")]
16use opentelemetry_sdk::resource::TelemetryResourceDetector;
17use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
18#[cfg(feature = "detector_hostresource")]
19use opentelemetry_resource_detectors::HostResourceDetector;
20#[cfg(feature = "detector_os")]
21use opentelemetry_resource_detectors::OsResourceDetector;
22#[cfg(feature = "detector_process")]
23use opentelemetry_resource_detectors::ProcessResourceDetector;
24
25use opentelemetry_semantic_conventions::attribute;
26
27use opentelemetry_otlp::SpanExporter;
29use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
30#[cfg(not(feature = "tokio_console"))]
31use opentelemetry::trace::TracerProvider as _; #[cfg(not(feature = "tokio_console"))]
33use tracing_opentelemetry::OpenTelemetryLayer;
34
35use opentelemetry_otlp::MetricExporter;
37use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
38use tracing_opentelemetry::MetricsLayer;
39#[cfg(feature = "exponential_histograms")]
40use opentelemetry_sdk::metrics::{Aggregation, InstrumentKind, Stream};
41
42use opentelemetry_otlp::LogExporter;
44use opentelemetry_sdk::logs::SdkLoggerProvider;
45#[cfg(not(feature = "tokio_console"))]
46use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
47
48pub use opentelemetry_otlp::ExporterBuildError;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51pub struct TracingOtelConfig {
52 collector_url: String,
53
54 #[serde(default, skip_serializing)]
55 collector_auth_header: Option<SecretString>,
56}
57impl TracingOtelConfig {
58 pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
59 Self {
60 collector_url,
61 collector_auth_header,
62 }
63 }
64 pub fn collector_url(&self) -> &str {
65 &self.collector_url
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct TracingConfig {
71 #[serde(default)]
76 filter: Option<String>,
77
78 #[serde(default)]
79 log_file_path: Option<String>,
80
81 #[serde(default = "TracingConfig::ansi_output_default")]
82 ansi_output: bool,
83
84 #[serde(default)]
86 deployment_env: Option<String>,
87
88 metrics_interval: Option<u64>,
90
91 #[serde(default, flatten)]
92 otel_config: Option<TracingOtelConfig>,
93}
94impl TracingConfig {
95 pub fn new(
96 collector_url: Option<String>,
97 collector_auth_header: Option<SecretString>,
98 log_file_path: Option<String>,
99 ansi_output: Option<bool>,
100 filter: Option<String>,
101 deployment_env: Option<String>,
102 metrics_interval: Option<u64>,
103 ) -> Self {
104 Self {
105 filter,
106 log_file_path,
107 deployment_env,
108 metrics_interval,
109 ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
110 otel_config: collector_url.map(|url| TracingOtelConfig {
111 collector_url: url,
112 collector_auth_header,
113 }),
114 }
115 }
116 pub fn metrics_interval_duration(&self) -> Option<Duration> {
117 self.metrics_interval.map(Duration::from_secs)
118 }
119 pub fn log_file_path(&self) -> Option<&str> {
120 self.log_file_path.as_deref()
121 }
122 pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
123 &self.otel_config
124 }
125 pub fn ansi_output_default() -> bool {
126 true
127 }
128}
129impl Default for TracingConfig {
130 fn default() -> Self {
131 Self {
132 ansi_output: Self::ansi_output_default(),
133 filter: None,
134 deployment_env: None,
135 log_file_path: None,
136 otel_config: None,
137 metrics_interval: None,
138 }
139 }
140}
141
142pub mod custom_attribute {
145 pub const DEPLOYMENT_BUILD_TYPE: &str = "deployment.build_type";
146 #[cfg(feature = "attrs_crate_name")]
147 pub const SERVICE_CRATE_NAME: &str = "service.crate_name";
148 #[cfg(feature = "attrs_version_expanded")]
149 pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
150 #[cfg(feature = "attrs_version_expanded")]
151 pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
152 #[cfg(feature = "attrs_version_expanded")]
153 pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
154
155 #[cfg(feature = "attrs_origin")]
156 pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
157 #[cfg(feature = "attrs_origin")]
158 pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
159}
160#[rustfmt::skip]
161pub fn get_build_env() -> &'static str {
162 #[cfg(debug_assertions)]
163 { "debug" }
164 #[cfg(not(debug_assertions))]
165 { "release" }
166}
167pub fn get_origin_package_name() -> Option<&'static str> {
168 let package_name = env!("CARGO_PKG_NAME");
169 if package_name.is_empty() {
170 None
171 } else {
172 Some(package_name)
173 }
174}
175pub fn get_origin_crate_name() -> Option<&'static str> {
176 let package_name = env!("CARGO_CRATE_NAME");
177 if package_name.is_empty() {
178 None
179 } else {
180 Some(package_name)
181 }
182}
183
184fn build_otel_resource(service_attrs: &ServiceAttributeStore, deployment_env: Option<String>, custom_attrs: Vec<KeyValue>) -> Resource {
185 let mut builder = Resource::builder_empty()
187 .with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name));
188
189 #[cfg(feature = "attrs_crate_name")]
190 {
191 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
192 }
193
194 builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_attrs.version));
196
197 #[cfg(feature = "attrs_version_expanded")] {
198 builder = builder
199 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, service_attrs.version_major))
200 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, service_attrs.version_minor))
201 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, service_attrs.version_patch));
202 }
203
204 #[cfg(feature = "attrs_origin")]
205 {
206 if let Some(origin_package_name) = get_origin_package_name() {
208 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
209 }
210 if let Some(origin_crate_name) = get_origin_crate_name() {
211 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
212 }
213 }
214
215 builder = builder.with_attribute(KeyValue::new(custom_attribute::DEPLOYMENT_BUILD_TYPE, get_build_env()));
217
218 if let Some(env) = deployment_env {
220 builder = builder.with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, env));
221 }
222
223 for attr in custom_attrs {
225 builder = builder.with_attribute(attr);
226 }
227
228 #[cfg(feature = "detector_telemetry")]
229 {
230 builder = builder.with_detector(Box::new(TelemetryResourceDetector));
232 }
233 #[cfg(feature = "detector_hostresource")]
234 {
235 builder = builder.with_detector(Box::new(HostResourceDetector::default()));
237 }
238 #[cfg(feature = "detector_process")]
239 {
240 builder = builder.with_detector(Box::new(ProcessResourceDetector));
242 }
243 #[cfg(feature = "detector_os")]
244 {
245 builder = builder.with_detector(Box::new(OsResourceDetector));
247 }
248
249 builder.build()
250}
251
252fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
253 let mut headers: HashMap<String, String> = HashMap::new();
254
255 if let Some(auth_header) = auth_header_val.as_ref() {
257 headers.insert("Authorization".into(), auth_header.expose_secret().into());
258 }
259
260 headers
261}
262
263fn init_otel_traces_provider(
265 collector_endpoint: &str,
266 headers: HashMap<String, String>,
267 resource: Resource,
268) -> Result<SdkTracerProvider, ExporterBuildError> {
269 let exporter = SpanExporter::builder()
270 .with_http()
271 .with_headers(headers)
272 .with_endpoint(format!("{collector_endpoint}/v1/traces"))
273 .with_protocol(Protocol::HttpBinary)
274 .build()?;
276
277 let provider = SdkTracerProvider::builder()
278 .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
280 .with_resource(resource)
281 .with_batch_exporter(exporter)
282 .build();
283
284 Ok(provider)
285}
286fn init_otel_logs_provider(
287 collector_endpoint: &str,
288 headers: HashMap<String, String>,
289 resource: Resource,
290) -> Result<SdkLoggerProvider, ExporterBuildError> {
291 let exporter = LogExporter::builder()
292 .with_http()
293 .with_headers(headers)
294 .with_endpoint(format!("{collector_endpoint}/v1/logs"))
295 .with_protocol(Protocol::HttpBinary)
296 .build()?;
298
299 let provider = SdkLoggerProvider::builder()
300 .with_resource(resource)
301 .with_batch_exporter(exporter)
302 .build();
303
304 Ok(provider)
305}
306fn init_otel_metrics_provider(
307 collector_endpoint: &str,
308 headers: HashMap<String, String>,
309 resource: Resource,
310 interval: Option<Duration>,
311) -> Result<SdkMeterProvider, ExporterBuildError> {
312 let exporter = MetricExporter::builder()
313 .with_http()
314 .with_headers(headers)
315 .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
316 .with_protocol(Protocol::HttpBinary)
317 .build()?;
319
320 let mut periodic = PeriodicReader::builder(exporter);
321 if let Some(duration) = interval {
322 periodic = periodic.with_interval(duration);
323 }
324 let mut builder = SdkMeterProvider::builder();
325 builder = builder
326 .with_resource(resource)
327 .with_reader(periodic.build());
328 #[cfg(feature = "exponential_histograms")]
329 {
330 builder = builder.with_view(|inst| {
331 if let InstrumentKind::Histogram = inst.kind() {
332 let s = Stream::builder()
333 .with_aggregation(Aggregation::Base2ExponentialHistogram {
334 max_size: 160,
335 max_scale: 20,
336 record_min_max: true,
337 })
338 .build()
339 .unwrap();
340 Some(s)
341 } else {
342 None
343 }
344 });
345 }
346 let provider = builder.build();
347
348 Ok(provider)
349}
350
351#[derive(Debug, Clone)]
368pub struct ServiceAttributeStore {
369 pub crate_name: &'static str,
370 pub pkg_name: &'static str,
371 pub version: &'static str,
372 pub version_major: &'static str,
373 pub version_minor: &'static str,
374 pub version_patch: &'static str,
375}
376impl ServiceAttributeStore {
377 pub fn dump(&self) {
378 let service_name = self.pkg_name;
379 let crate_name = self.crate_name;
380 let service_version = self.version;
381 let service_version_major = self.version_major;
382 let service_version_minor = self.version_minor;
383 let service_version_patch = self.version_patch;
384 let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -");
385 let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -");
386 let build_env = get_build_env();
387
388 println!();
389 println!("Resolved tracing attributes");
390 println!("--------------------");
391 println!("service_name (pkg_name): {service_name}");
392 println!("service_crate_name: {crate_name}");
393 println!("service_version: {service_version}");
394 println!("service_version_major: {service_version_major}");
395 println!("service_version_minor: {service_version_minor}");
396 println!("service_version_patch: {service_version_patch}");
397 println!("origin_pkg_name: {origin_pkg_name}");
398 println!("origin_crate_name: {origin_crate_name}");
399 println!("build_env: {build_env}");
400 println!();
401 }
402}
403
404#[macro_export]
409macro_rules! build_attrs {
410 () => (
414 tracing_kickstart::ServiceAttributeStore {
415 crate_name: env!("CARGO_CRATE_NAME"),
416 pkg_name: env!("CARGO_PKG_NAME"),
417 version: env!("CARGO_PKG_VERSION"),
418 version_major: env!("CARGO_PKG_VERSION_MAJOR"),
419 version_minor: env!("CARGO_PKG_VERSION_MINOR"),
420 version_patch: env!("CARGO_PKG_VERSION_PATCH"),
421 }
422 )
423}
424fn validate_non_empty_filter_str(filter: &str, source_name: &'static str) -> bool {
425 if filter.is_empty() {
426 println!("Ignoring empty filter string sourced from {source_name}");
427 false
428 } else {
429 true
430 }
431}
432fn validate_non_empty_filter(filter: &EnvFilter, source_name: &'static str) -> bool {
433 let filter_str = filter.to_string();
434 validate_non_empty_filter_str(&filter_str, source_name)
435}
436
437
438pub fn init(
499 service_attrs: ServiceAttributeStore,
500 config: &TracingConfig,
501 default_env_filter: Option<&str>,
502 custom_resource_attrs: Option<Vec<(Key, Value)>>,
503) -> Result<TraceProviders, ExporterBuildError> {
504 let base_filter: EnvFilter = {
506 if let Some(filter_str) = &config.filter && validate_non_empty_filter_str(filter_str, "provided config") {
508 println!("Resolved tracing EnvFilter from provided config: {filter_str:?}");
509 filter_str.into()
510 }
511 else if let Ok(filter) = EnvFilter::try_from_default_env() && validate_non_empty_filter(&filter, "RUST_LOG") {
513 println!("Resolved tracing EnvFilter from `RUST_LOG`: {:?}", filter.to_string());
514 filter
515 }
516 else if let Some(filter_str) = default_env_filter && validate_non_empty_filter_str(filter_str, "`tracing_kickstart::init()`") {
518 println!("Resolving tracing EnvFilter from `tracing_kickstart::init(.., default_env_filter)`: {filter_str:?}");
519 filter_str.into()
520 }
521 else {
523 let filter_str = format!(
524 "info,{}=debug",
525 service_attrs.crate_name
526 );
527 println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
528 filter_str.into()
529 }
530 };
531
532 #[cfg(feature = "tokio_console")]
534 let registry_filter = EnvFilter::new(format!("{base_filter},tokio=trace,runtime=trace"));
535 #[cfg(not(feature = "tokio_console"))]
536 let registry_filter = base_filter.clone();
537
538 println!("Using base tracing filters: {base_filter}");
540 if registry_filter.to_string() != base_filter.to_string() {
541 println!("Registry tracing filter: {registry_filter}");
542 }
543
544 let layer = tracing_subscriber::registry()
546 .with(registry_filter)
547 .with(ErrorLayer::default());
548
549 let layer = layer.with(
551 tracing_subscriber::fmt::layer()
552 .with_ansi(config.ansi_output)
553 .with_filter(base_filter.clone()) );
555
556 let file_logging_layer = config.log_file_path.as_ref().map(|path| {
558 let file = OpenOptions::new()
559 .write(true)
560 .create(true)
561 .truncate(true)
562 .open(path)
563 .expect("Log file should be writable");
564
565 tracing_subscriber::fmt::layer()
566 .with_ansi(false)
567 .with_writer(file)
568 .with_filter(base_filter)
569 });
570 let layer = layer.with(file_logging_layer);
571
572 #[cfg(feature = "tokio_console")]
574 let layer = layer.with(console_subscriber::spawn());
575
576 let mut providers_handle = TraceProviders::default();
578
579 if let Some(otel_config) = &config.otel_config {
581 println!("Initializing OTEL config");
582 let endpoint = &otel_config.collector_url;
583 let headers = build_otel_headers(&otel_config.collector_auth_header);
584 let custom_attrs = custom_resource_attrs
585 .unwrap_or_default()
586 .into_iter()
587 .map(|(k,v)| KeyValue::new(k, v))
588 .collect();
589 let resource = build_otel_resource(&service_attrs, config.deployment_env.clone(), custom_attrs);
590
591 let traces_provider = init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
593 #[cfg(not(feature = "tokio_console"))]
596 let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)).with_level(true));
597 providers_handle.traces = Some(traces_provider);
598
599 let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
601 #[cfg(not(feature = "tokio_console"))]
604 let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
605 providers_handle.logs = Some(logs_provider);
606
607 let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource, config.metrics_interval_duration())?;
609 let layer = layer.with(MetricsLayer::new(metrics_provider.clone()));
611 providers_handle.metrics = Some(metrics_provider);
612
613 layer.init();
614 println!("{:-<1$}", "-", 30);
615 tracing::info!("OTEL tracing configured");
616 } else {
617 layer.init();
618 println!("{:-<1$}", "-", 30);
619 tracing::warn!("OTEL tracing disabled");
620 }
621
622 Ok(providers_handle)
623}
624
625#[derive(Default, Clone)]
629pub struct TraceProviders {
630 pub traces: Option<SdkTracerProvider>,
631 pub logs: Option<SdkLoggerProvider>,
632 pub metrics: Option<SdkMeterProvider>,
633}
634impl TraceProviders {
635 pub fn register_globally(&self) {
637 if let Some(provider) = &self.traces {
639 tracing::info!("Traces provider registered globally");
640 opentelemetry::global::set_tracer_provider(provider.clone());
641 }
642 if let Some(provider) = &self.metrics {
644 tracing::info!("Metrics provider registered globally");
645 opentelemetry::global::set_meter_provider(provider.clone());
646 }
647 }
648
649 pub fn shutdown(self) {
651 if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
653 println!("error shutting down traces provider: {error}");
654 }
655 if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
657 println!("error shutting down logs provider: {error}");
658 }
659 if let Some(provider) = self.metrics && let Err(error) = provider.shutdown() {
661 println!("error shutting down metrics provider: {error}");
662 }
663 }
664}
665impl fmt::Debug for TraceProviders {
666 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
667 write!(f, "TraceProviders (")?;
668 let mut i = 0;
669 if self.traces.is_some() {
670 write!(f, "SdkTracerProvider")?;
671 i += 1;
672 }
673 if self.logs.is_some() {
674 if i > 0 {
675 write!(f, ", ")?;
676 }
677 write!(f, "SdkLoggerProvider")?;
678 i += 1;
679 }
680 if self.metrics.is_some() {
681 if i > 0 {
682 write!(f, ", ")?;
683 }
684 write!(f, "SdkMeterProvider")?;
685 }
686 write!(f, ")")
687 }
688}