1use crate::conf::TracingConfig;
2
3use secrecy::{ExposeSecret, SecretString};
4use tracing_error::ErrorLayer;
5use std::collections::HashMap;
6use std::fmt;
7use std::fs::OpenOptions;
8use std::time::Duration;
9use tracing_subscriber::{EnvFilter, Layer as _};
10use tracing_subscriber::layer::SubscriberExt;
11use tracing_subscriber::util::SubscriberInitExt;
12
13use opentelemetry::KeyValue;
15use opentelemetry_sdk::resource::Resource;
16#[cfg(feature = "detector_telemetry")]
17use opentelemetry_sdk::resource::TelemetryResourceDetector;
18use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
19#[cfg(feature = "detector_hostresource")]
20use opentelemetry_resource_detectors::HostResourceDetector;
21#[cfg(feature = "detector_os")]
22use opentelemetry_resource_detectors::OsResourceDetector;
23#[cfg(feature = "detector_process")]
24use opentelemetry_resource_detectors::ProcessResourceDetector;
25
26use opentelemetry_semantic_conventions::attribute;
27
28use opentelemetry_otlp::SpanExporter;
30use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
31#[cfg(not(feature = "tokio_console"))]
32use opentelemetry::trace::TracerProvider as _; #[cfg(not(feature = "tokio_console"))]
34use tracing_opentelemetry::OpenTelemetryLayer;
35
36use opentelemetry_otlp::MetricExporter;
38use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
39use tracing_opentelemetry::MetricsLayer;
40#[cfg(feature = "exponential_histograms")]
41use opentelemetry_sdk::metrics::{Aggregation, InstrumentKind, Stream};
42
43use opentelemetry_otlp::LogExporter;
45use opentelemetry_sdk::logs::SdkLoggerProvider;
46#[cfg(not(feature = "tokio_console"))]
47use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
48
49pub use opentelemetry_otlp::ExporterBuildError;
50
51pub mod custom_attribute {
54 pub const DEPLOYMENT_BUILD_TYPE: &str = "deployment.build_type";
55 #[cfg(feature = "attrs_crate_name")]
56 pub const SERVICE_CRATE_NAME: &str = "service.crate_name";
57 #[cfg(feature = "attrs_version_expanded")]
58 pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
59 #[cfg(feature = "attrs_version_expanded")]
60 pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
61 #[cfg(feature = "attrs_version_expanded")]
62 pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
63
64 #[cfg(feature = "attrs_origin")]
65 pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
66 #[cfg(feature = "attrs_origin")]
67 pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
68}
69#[rustfmt::skip]
70pub fn get_build_env() -> &'static str {
71 #[cfg(debug_assertions)]
72 { "debug" }
73 #[cfg(not(debug_assertions))]
74 { "release" }
75}
76pub fn get_origin_package_name() -> Option<&'static str> {
77 let package_name = env!("CARGO_PKG_NAME");
78 if package_name.is_empty() {
79 None
80 } else {
81 Some(package_name)
82 }
83}
84pub fn get_origin_crate_name() -> Option<&'static str> {
85 let package_name = env!("CARGO_CRATE_NAME");
86 if package_name.is_empty() {
87 None
88 } else {
89 Some(package_name)
90 }
91}
92
93fn build_otel_resource(service_attrs: &ServiceAttributeStore, deployment_env: Option<String>, custom_attrs: Vec<KeyValue>) -> Resource {
94 let mut builder = Resource::builder_empty()
96 .with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name));
97
98 #[cfg(feature = "attrs_crate_name")]
99 {
100 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
101 }
102
103 builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_attrs.version));
105
106 #[cfg(feature = "attrs_version_expanded")] {
107 builder = builder
108 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, service_attrs.version_major))
109 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, service_attrs.version_minor))
110 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, service_attrs.version_patch));
111 }
112
113 #[cfg(feature = "attrs_origin")]
114 {
115 if let Some(origin_package_name) = get_origin_package_name() {
117 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
118 }
119 if let Some(origin_crate_name) = get_origin_crate_name() {
120 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
121 }
122 }
123
124 builder = builder.with_attribute(KeyValue::new(custom_attribute::DEPLOYMENT_BUILD_TYPE, get_build_env()));
126
127 if let Some(env) = deployment_env {
129 builder = builder.with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, env));
130 }
131
132 for attr in custom_attrs {
134 builder = builder.with_attribute(attr);
135 }
136
137 #[cfg(feature = "detector_telemetry")]
138 {
139 builder = builder.with_detector(Box::new(TelemetryResourceDetector));
141 }
142 #[cfg(feature = "detector_hostresource")]
143 {
144 builder = builder.with_detector(Box::new(HostResourceDetector::default()));
146 }
147 #[cfg(feature = "detector_process")]
148 {
149 builder = builder.with_detector(Box::new(ProcessResourceDetector));
151 }
152 #[cfg(feature = "detector_os")]
153 {
154 builder = builder.with_detector(Box::new(OsResourceDetector));
156 }
157
158 builder.build()
159}
160
161fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
162 let mut headers: HashMap<String, String> = HashMap::new();
163
164 if let Some(auth_header) = auth_header_val.as_ref() {
166 headers.insert("Authorization".into(), auth_header.expose_secret().into());
167 }
168
169 headers
170}
171
172fn init_otel_traces_provider(
174 collector_endpoint: &str,
175 headers: HashMap<String, String>,
176 resource: Resource,
177) -> Result<SdkTracerProvider, ExporterBuildError> {
178 let exporter = SpanExporter::builder()
179 .with_http()
180 .with_headers(headers)
181 .with_endpoint(format!("{collector_endpoint}/v1/traces"))
182 .with_protocol(Protocol::HttpBinary)
183 .build()?;
185
186 let provider = SdkTracerProvider::builder()
187 .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
189 .with_resource(resource)
190 .with_batch_exporter(exporter)
191 .build();
192
193 Ok(provider)
194}
195fn init_otel_logs_provider(
196 collector_endpoint: &str,
197 headers: HashMap<String, String>,
198 resource: Resource,
199) -> Result<SdkLoggerProvider, ExporterBuildError> {
200 let exporter = LogExporter::builder()
201 .with_http()
202 .with_headers(headers)
203 .with_endpoint(format!("{collector_endpoint}/v1/logs"))
204 .with_protocol(Protocol::HttpBinary)
205 .build()?;
207
208 let provider = SdkLoggerProvider::builder()
209 .with_resource(resource)
210 .with_batch_exporter(exporter)
211 .build();
212
213 Ok(provider)
214}
215fn init_otel_metrics_provider(
216 collector_endpoint: &str,
217 headers: HashMap<String, String>,
218 resource: Resource,
219 interval: Option<Duration>,
220) -> Result<SdkMeterProvider, ExporterBuildError> {
221 let exporter = MetricExporter::builder()
222 .with_http()
223 .with_headers(headers)
224 .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
225 .with_protocol(Protocol::HttpBinary)
226 .build()?;
228
229 let mut periodic = PeriodicReader::builder(exporter);
230 if let Some(duration) = interval {
231 periodic = periodic.with_interval(duration);
232 }
233 let mut builder = SdkMeterProvider::builder();
234 builder = builder
235 .with_resource(resource)
236 .with_reader(periodic.build());
237 #[cfg(feature = "exponential_histograms")]
238 {
239 builder = builder.with_view(|inst| {
240 if let InstrumentKind::Histogram = inst.kind() {
241 let s = Stream::builder()
242 .with_aggregation(Aggregation::Base2ExponentialHistogram {
243 max_size: 160,
244 max_scale: 20,
245 record_min_max: true,
246 })
247 .build()
248 .unwrap();
249 Some(s)
250 } else {
251 None
252 }
253 });
254 }
255 let provider = builder.build();
256
257 Ok(provider)
258}
259
260#[derive(Debug, Clone)]
266pub struct ServiceAttributeStore {
267 pub crate_name: &'static str,
268 pub pkg_name: &'static str,
269 pub version: &'static str,
270 pub version_major: &'static str,
271 pub version_minor: &'static str,
272 pub version_patch: &'static str,
273}
274impl ServiceAttributeStore {
275 pub fn dump(&self) {
276 let service_name = self.pkg_name;
277 let crate_name = self.crate_name;
278 let service_version = self.version;
279 let service_version_major = self.version_major;
280 let service_version_minor = self.version_minor;
281 let service_version_patch = self.version_patch;
282 let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -");
283 let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -");
284 let build_env = get_build_env();
285
286 println!();
287 println!("Resolved tracing attributes");
288 println!("--------------------");
289 println!("service_name (pkg_name): {service_name}");
290 println!("service_crate_name: {crate_name}");
291 println!("service_version: {service_version}");
292 println!("service_version_major: {service_version_major}");
293 println!("service_version_minor: {service_version_minor}");
294 println!("service_version_patch: {service_version_patch}");
295 println!("origin_pkg_name: {origin_pkg_name}");
296 println!("origin_crate_name: {origin_crate_name}");
297 println!("build_env: {build_env}");
298 println!();
299 }
300}
301
302#[macro_export]
307macro_rules! build_attrs {
308 () => (
312 tracing_kickstart::ServiceAttributeStore {
313 crate_name: env!("CARGO_CRATE_NAME"),
314 pkg_name: env!("CARGO_PKG_NAME"),
315 version: env!("CARGO_PKG_VERSION"),
316 version_major: env!("CARGO_PKG_VERSION_MAJOR"),
317 version_minor: env!("CARGO_PKG_VERSION_MINOR"),
318 version_patch: env!("CARGO_PKG_VERSION_PATCH"),
319 }
320 )
321}
322fn validate_non_empty_filter_str(filter: &str, source_name: &'static str) -> bool {
323 if filter.is_empty() {
324 println!("Ignoring empty filter string sourced from {source_name}");
325 false
326 } else {
327 true
328 }
329}
330fn validate_non_empty_filter(filter: &EnvFilter, source_name: &'static str) -> bool {
331 let filter_str = filter.to_string();
332 validate_non_empty_filter_str(&filter_str, source_name)
333}
334
335
336pub fn init(
424 service_attrs: ServiceAttributeStore,
425 config: TracingConfig,
426) -> Result<TraceProviders, ExporterBuildError> {
427 let mut base_filter: EnvFilter = {
429 if let Some(filter_str) = &config.filter && validate_non_empty_filter_str(filter_str, "provided config") {
431 println!("Resolved tracing EnvFilter from provided config: {filter_str:?}");
432 filter_str.into()
433 }
434 else if let Ok(filter) = EnvFilter::try_from_default_env() && validate_non_empty_filter(&filter, "RUST_LOG") {
436 println!("Resolved tracing EnvFilter from `RUST_LOG`: {:?}", filter.to_string());
437 filter
438 }
439 else {
441 let filter_str = format!(
442 "info,{}=debug",
443 service_attrs.crate_name
444 );
445 println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
446 filter_str.into()
447 }
448 };
449
450 if let Some(append) = config.filter_append.as_ref() {
452 println!("Will merge EnvFilter with provided `filter_append`: {append:#?}");
453
454 for dir in append.split(",").filter(|s| !s.is_empty()) {
455 base_filter = base_filter.add_directive(dir.parse().expect("valid filter_append syntax"));
456 }
457 }
458 #[cfg(feature = "tokio_console")]
460 let registry_filter = EnvFilter::new(format!("{base_filter},tokio=trace,runtime=trace"));
461 #[cfg(not(feature = "tokio_console"))]
462 let registry_filter = base_filter.clone();
463
464 println!("Using base tracing filters: {base_filter}");
466 if registry_filter.to_string() != base_filter.to_string() {
467 println!("Registry tracing filter: {registry_filter}");
468 }
469
470 let layer = tracing_subscriber::registry()
472 .with(registry_filter)
473 .with(ErrorLayer::default());
474
475 let layer = layer.with(
477 tracing_subscriber::fmt::layer()
478 .with_ansi(config.ansi_output)
479 .with_ansi_sanitization(config.ansi_sanitization)
480 .with_filter(base_filter.clone()) );
482
483 let file_logging_layer = config.log_file_path.as_ref().map(|path| {
485 let file = OpenOptions::new()
486 .write(true)
487 .create(true)
488 .truncate(true)
489 .open(path)
490 .expect("Log file should be writable");
491
492 tracing_subscriber::fmt::layer()
493 .with_ansi(false)
494 .with_writer(file)
495 .with_filter(base_filter)
496 });
497 let layer = layer.with(file_logging_layer);
498
499 #[cfg(feature = "tokio_console")]
501 let layer = layer.with(console_subscriber::spawn());
502
503 let mut providers_handle = TraceProviders::default();
505
506 if let Some(otel_config) = &config.otel_config {
508 println!("Initializing OTEL config");
509 let endpoint = &otel_config.collector_url;
510 let headers = build_otel_headers(&otel_config.collector_auth_header);
511 let custom_attrs = config.custom_resource_attrs
512 .clone()
513 .unwrap_or_default()
514 .into_iter()
515 .map(|(k,v)| KeyValue::new(k, v))
516 .collect();
517 let resource = build_otel_resource(&service_attrs, config.deployment_env.clone(), custom_attrs);
518
519 let traces_provider = init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
521 #[cfg(not(feature = "tokio_console"))]
524 let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)).with_level(true));
525 providers_handle.traces = Some(traces_provider);
526
527 let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
529 #[cfg(not(feature = "tokio_console"))]
532 let layer = {
533 #[allow(unused_mut)]
534 let mut bridge_builder = OpenTelemetryTracingBridge::builder(&logs_provider);
535 #[cfg(feature = "otel_span_attributes")]
536 {
537 use opentelemetry_appender_tracing::layer::TracingSpanAttributes;
538 bridge_builder = bridge_builder.with_tracing_span_attributes(TracingSpanAttributes::all());
539 }
540 layer.with(bridge_builder.build())
541 };
542 providers_handle.logs = Some(logs_provider);
543
544 let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource, config.metrics_interval_duration())?;
546 let layer = layer.with(MetricsLayer::new(metrics_provider.clone()));
548 providers_handle.metrics = Some(metrics_provider);
549
550 layer.init();
551 println!("{:-<1$}", "-", 30);
552 tracing::info!("OTEL tracing configured");
553 } else {
554 layer.init();
555 println!("{:-<1$}", "-", 30);
556 tracing::warn!("OTEL tracing disabled");
557 }
558
559 Ok(providers_handle)
560}
561
562#[derive(Default, Clone)]
566pub struct TraceProviders {
567 pub traces: Option<SdkTracerProvider>,
568 pub logs: Option<SdkLoggerProvider>,
569 pub metrics: Option<SdkMeterProvider>,
570}
571impl TraceProviders {
572 pub fn register_globally(&self) {
574 if let Some(provider) = &self.traces {
576 tracing::info!("Traces provider registered globally");
577 opentelemetry::global::set_tracer_provider(provider.clone());
578 }
579 if let Some(provider) = &self.metrics {
581 tracing::info!("Metrics provider registered globally");
582 opentelemetry::global::set_meter_provider(provider.clone());
583 }
584 }
585
586 pub fn shutdown(self) {
588 if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
590 println!("error shutting down traces provider: {error}");
591 }
592 if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
594 println!("error shutting down logs provider: {error}");
595 }
596 if let Some(provider) = self.metrics && let Err(error) = provider.shutdown() {
598 println!("error shutting down metrics provider: {error}");
599 }
600 }
601}
602impl fmt::Debug for TraceProviders {
603 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
604 write!(f, "TraceProviders (")?;
605 let mut i = 0;
606 if self.traces.is_some() {
607 write!(f, "SdkTracerProvider")?;
608 i += 1;
609 }
610 if self.logs.is_some() {
611 if i > 0 {
612 write!(f, ", ")?;
613 }
614 write!(f, "SdkLoggerProvider")?;
615 i += 1;
616 }
617 if self.metrics.is_some() {
618 if i > 0 {
619 write!(f, ", ")?;
620 }
621 write!(f, "SdkMeterProvider")?;
622 }
623 write!(f, ")")
624 }
625}