1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fmt;
5use std::fs::OpenOptions;
6use std::time::Duration;
7use tracing_subscriber::EnvFilter;
8use tracing_subscriber::layer::SubscriberExt;
9use tracing_subscriber::util::SubscriberInitExt;
10
11use opentelemetry::KeyValue;
13use opentelemetry_sdk::resource::Resource;
14#[cfg(feature = "detector_telemetry")]
15use opentelemetry_sdk::resource::TelemetryResourceDetector;
16use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
17#[cfg(feature = "detector_hostresource")]
18use opentelemetry_resource_detectors::HostResourceDetector;
19#[cfg(feature = "detector_os")]
20use opentelemetry_resource_detectors::OsResourceDetector;
21#[cfg(feature = "detector_process")]
22use opentelemetry_resource_detectors::ProcessResourceDetector;
23
24use opentelemetry_semantic_conventions::attribute;
25
26use opentelemetry::trace::TracerProvider as _; use opentelemetry_otlp::SpanExporter;
29use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
30use tracing_opentelemetry::OpenTelemetryLayer;
31
32use opentelemetry_otlp::MetricExporter;
34use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
35use tracing_opentelemetry::MetricsLayer;
36
37use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
39use opentelemetry_otlp::LogExporter;
40use opentelemetry_sdk::logs::SdkLoggerProvider;
41
42pub use opentelemetry_otlp::ExporterBuildError;
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct TracingOtelConfig {
46 collector_url: String,
47
48 #[serde(default, skip_serializing)]
49 collector_auth_header: Option<SecretString>,
50}
51impl TracingOtelConfig {
52 pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
53 Self {
54 collector_url,
55 collector_auth_header,
56 }
57 }
58 pub fn collector_url(&self) -> &str {
59 &self.collector_url
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct TracingConfig {
65 #[serde(default)]
70 filter: Option<String>,
71
72 #[serde(default)]
73 log_file_path: Option<String>,
74
75 #[serde(default = "TracingConfig::ansi_output_default")]
76 ansi_output: bool,
77
78 #[serde(default)]
80 deployment_env: Option<String>,
81
82 metrics_interval: Option<u64>,
84
85 #[serde(default, flatten)]
86 otel_config: Option<TracingOtelConfig>,
87}
88impl TracingConfig {
89 pub fn new(
90 collector_url: Option<String>,
91 collector_auth_header: Option<SecretString>,
92 log_file_path: Option<String>,
93 ansi_output: Option<bool>,
94 filter: Option<String>,
95 deployment_env: Option<String>,
96 metrics_interval: Option<u64>,
97 ) -> Self {
98 Self {
99 filter,
100 log_file_path,
101 deployment_env,
102 metrics_interval,
103 ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
104 otel_config: collector_url.map(|url| TracingOtelConfig {
105 collector_url: url,
106 collector_auth_header,
107 }),
108 }
109 }
110 pub fn metrics_interval_duration(&self) -> Option<Duration> {
111 self.metrics_interval.map(Duration::from_secs)
112 }
113 pub fn log_file_path(&self) -> Option<&str> {
114 self.log_file_path.as_deref()
115 }
116 pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
117 &self.otel_config
118 }
119 pub fn ansi_output_default() -> bool {
120 true
121 }
122}
123impl Default for TracingConfig {
124 fn default() -> Self {
125 Self {
126 ansi_output: Self::ansi_output_default(),
127 filter: None,
128 deployment_env: None,
129 log_file_path: None,
130 otel_config: None,
131 metrics_interval: None,
132 }
133 }
134}
135
136pub mod custom_attribute {
139 pub const DEPLOYMENT_BUILD_TYPE: &str = "deployment.build_type";
140 #[cfg(feature = "attrs_crate_name")]
141 pub const SERVICE_CRATE_NAME: &str = "service.crate_name";
142 #[cfg(feature = "attrs_version_expanded")]
143 pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
144 #[cfg(feature = "attrs_version_expanded")]
145 pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
146 #[cfg(feature = "attrs_version_expanded")]
147 pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
148
149 #[cfg(feature = "attrs_origin")]
150 pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
151 #[cfg(feature = "attrs_origin")]
152 pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
153}
154#[rustfmt::skip]
155pub fn get_build_env() -> &'static str {
156 #[cfg(debug_assertions)]
157 { "debug" }
158 #[cfg(not(debug_assertions))]
159 { "release" }
160}
161pub fn get_origin_package_name() -> Option<&'static str> {
162 let package_name = env!("CARGO_PKG_NAME");
163 if package_name.is_empty() {
164 None
165 } else {
166 Some(package_name)
167 }
168}
169pub fn get_origin_crate_name() -> Option<&'static str> {
170 let package_name = env!("CARGO_CRATE_NAME");
171 if package_name.is_empty() {
172 None
173 } else {
174 Some(package_name)
175 }
176}
177
178fn build_otel_resource(service_attrs: &ServiceAttributeStore, deployment_env: Option<String>) -> Resource {
179 let mut builder = Resource::builder_empty()
181 .with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name));
182
183 #[cfg(feature = "attrs_crate_name")]
184 {
185 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
186 }
187
188 builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_attrs.version));
190
191 #[cfg(feature = "attrs_version_expanded")] {
192 builder = builder
193 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, service_attrs.version_major))
194 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, service_attrs.version_minor))
195 .with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, service_attrs.version_patch));
196 }
197
198 #[cfg(feature = "attrs_origin")]
199 {
200 if let Some(origin_package_name) = get_origin_package_name() {
202 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
203 }
204 if let Some(origin_crate_name) = get_origin_crate_name() {
205 builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
206 }
207 }
208
209 builder = builder.with_attribute(KeyValue::new(custom_attribute::DEPLOYMENT_BUILD_TYPE, get_build_env()));
211
212 if let Some(env) = deployment_env {
214 builder = builder.with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, env));
215 }
216
217 #[cfg(feature = "detector_telemetry")]
218 {
219 builder = builder.with_detector(Box::new(TelemetryResourceDetector));
221 }
222 #[cfg(feature = "detector_hostresource")]
223 {
224 builder = builder.with_detector(Box::new(HostResourceDetector::default()));
226 }
227 #[cfg(feature = "detector_process")]
228 {
229 builder = builder.with_detector(Box::new(ProcessResourceDetector));
231 }
232 #[cfg(feature = "detector_os")]
233 {
234 builder = builder.with_detector(Box::new(OsResourceDetector));
236 }
237
238 builder.build()
239}
240
241fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
242 let mut headers: HashMap<String, String> = HashMap::new();
243
244 if let Some(auth_header) = auth_header_val.as_ref() {
246 headers.insert("Authorization".into(), auth_header.expose_secret().into());
247 }
248
249 headers
250}
251
252fn init_otel_traces_provider(
254 collector_endpoint: &str,
255 headers: HashMap<String, String>,
256 resource: Resource,
257) -> Result<SdkTracerProvider, ExporterBuildError> {
258 let exporter = SpanExporter::builder()
259 .with_http()
260 .with_headers(headers)
261 .with_endpoint(format!("{collector_endpoint}/v1/traces"))
262 .with_protocol(Protocol::HttpBinary)
263 .build()?;
265
266 let provider = SdkTracerProvider::builder()
267 .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
269 .with_resource(resource)
270 .with_batch_exporter(exporter)
271 .build();
272
273 Ok(provider)
274}
275fn init_otel_logs_provider(
276 collector_endpoint: &str,
277 headers: HashMap<String, String>,
278 resource: Resource,
279) -> Result<SdkLoggerProvider, ExporterBuildError> {
280 let exporter = LogExporter::builder()
281 .with_http()
282 .with_headers(headers)
283 .with_endpoint(format!("{collector_endpoint}/v1/logs"))
284 .with_protocol(Protocol::HttpBinary)
285 .build()?;
287
288 let provider = SdkLoggerProvider::builder()
289 .with_resource(resource)
290 .with_batch_exporter(exporter)
291 .build();
292
293 Ok(provider)
294}
295fn init_otel_metrics_provider(
296 collector_endpoint: &str,
297 headers: HashMap<String, String>,
298 resource: Resource,
299 interval: Option<Duration>,
300) -> Result<SdkMeterProvider, ExporterBuildError> {
301 let exporter = MetricExporter::builder()
302 .with_http()
303 .with_headers(headers)
304 .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
305 .with_protocol(Protocol::HttpBinary)
306 .build()?;
308
309 let mut periodic = PeriodicReader::builder(exporter);
310 if let Some(duration) = interval {
311 periodic = periodic.with_interval(duration);
312 }
313 let provider = SdkMeterProvider::builder()
314 .with_resource(resource)
315 .with_reader(periodic.build())
316 .build();
317
318 Ok(provider)
319}
320
321#[derive(Debug, Clone)]
336pub struct ServiceAttributeStore {
337 pub crate_name: &'static str,
338 pub pkg_name: &'static str,
339 pub version: &'static str,
340 pub version_major: &'static str,
341 pub version_minor: &'static str,
342 pub version_patch: &'static str,
343}
344impl ServiceAttributeStore {
345 pub fn dump(&self) {
346 let service_name = self.pkg_name;
347 let crate_name = self.crate_name;
348 let service_version = self.version;
349 let service_version_major = self.version_major;
350 let service_version_minor = self.version_minor;
351 let service_version_patch = self.version_patch;
352 let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -");
353 let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -");
354 let build_env = get_build_env();
355
356 println!();
357 println!("Resolved tracing attributes");
358 println!("--------------------");
359 println!("service_name (pkg_name): {service_name}");
360 println!("service_crate_name: {crate_name}");
361 println!("service_version: {service_version}");
362 println!("service_version_major: {service_version_major}");
363 println!("service_version_minor: {service_version_minor}");
364 println!("service_version_patch: {service_version_patch}");
365 println!("origin_pkg_name: {origin_pkg_name}");
366 println!("origin_crate_name: {origin_crate_name}");
367 println!("build_env: {build_env}");
368 println!();
369 }
370}
371
372#[macro_export]
377macro_rules! build_attrs {
378 () => (
382 tracing_kickstart::ServiceAttributeStore {
383 crate_name: env!("CARGO_CRATE_NAME"),
384 pkg_name: env!("CARGO_PKG_NAME"),
385 version: env!("CARGO_PKG_VERSION"),
386 version_major: env!("CARGO_PKG_VERSION_MAJOR"),
387 version_minor: env!("CARGO_PKG_VERSION_MINOR"),
388 version_patch: env!("CARGO_PKG_VERSION_PATCH"),
389 }
390 )
391}
392
393pub fn init(service_attrs: ServiceAttributeStore, config: &TracingConfig, default_env_filter: Option<&str>) -> Result<TraceProviders, ExporterBuildError> {
427 let filter: EnvFilter = {
429 if let Some(filter_str) = &config.filter {
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() {
436 println!("Resolved tracing EnvFilter from `RUST_LOG`: {:?}", filter.to_string());
437 filter
438 }
439 else if let Some(filter_str) = default_env_filter {
441 println!("Resolving tracing EnvFilter from `tracing_kickstart::init(.., default_env_filter)`: {filter_str:?}");
442 filter_str.into()
443 }
444 else {
446 let filter_str = format!(
447 "warn,{}=debug,tracing_kickstart=debug", service_attrs.crate_name
449 );
450 println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
451 filter_str.into()
452 }
453 };
454
455 #[cfg(feature = "tokio_console")]
457 let filter = {
458 let mut filter_str = filter.to_string();
459 if !filter_str.is_empty() {
460 filter_str.push(',');
461 }
462 filter_str.push_str("tokio=trace,runtime=trace");
463 EnvFilter::from(filter_str)
464 };
465 println!("Launching with tracing filter: {}", filter);
466
467 let layer = tracing_subscriber::registry()
469 .with(filter);
470
471 let layer = layer.with(
473 tracing_subscriber::fmt::layer()
474 .with_ansi(config.ansi_output)
475 );
476
477 let file_logging_layer = match &config.log_file_path {
479 Some(file_path) => {
480 let file = OpenOptions::new()
481 .write(true)
482 .create(true)
483 .truncate(true)
484 .open(file_path)
485 .expect("Log file should be writable");
486 Some(tracing_subscriber::fmt::layer().with_ansi(false).with_writer(file))
487 },
488 None => None
489 };
490 let layer = layer.with(file_logging_layer);
491
492 #[cfg(feature = "tokio_console")]
494 let layer = layer.with(console_subscriber::spawn());
495
496 let mut providers_handle = TraceProviders::default();
498
499 if let Some(otel_config) = &config.otel_config {
501 println!("Initializing OTEL config");
502 let endpoint = &otel_config.collector_url;
503 let headers = build_otel_headers(&otel_config.collector_auth_header);
504 let resource = build_otel_resource(&service_attrs, config.deployment_env.clone());
505
506 let traces_provider =
508 init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
509 let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)));
511 providers_handle.traces = Some(traces_provider);
512
513 let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
515 let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
517 providers_handle.logs = Some(logs_provider);
518
519 let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource, config.metrics_interval_duration())?;
521 let layer = layer.with(MetricsLayer::new(metrics_provider.clone()));
523 providers_handle.metrics = Some(metrics_provider);
524
525 layer.init();
526 tracing::info!("OTEL tracing configured");
527 } else {
528 layer.init();
529 tracing::warn!("OTEL tracing disabled");
530 }
531
532 Ok(providers_handle)
533}
534
535#[derive(Default, Clone)]
539pub struct TraceProviders {
540 pub traces: Option<SdkTracerProvider>,
541 pub logs: Option<SdkLoggerProvider>,
542 pub metrics: Option<SdkMeterProvider>,
543}
544impl TraceProviders {
545 pub fn register_globally(&self) {
547 if let Some(provider) = &self.traces {
549 tracing::info!("Traces provider registered globally");
550 opentelemetry::global::set_tracer_provider(provider.clone());
551 }
552 if let Some(provider) = &self.metrics {
554 tracing::info!("Metrics provider registered globally");
555 opentelemetry::global::set_meter_provider(provider.clone());
556 }
557 }
558
559 pub fn shutdown(self) {
561 if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
563 println!("error shutting down traces provider: {error}");
564 }
565 if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
567 println!("error shutting down logs provider: {error}");
568 }
569 if let Some(provider) = self.metrics && let Err(error) = provider.shutdown() {
571 println!("error shutting down metrics provider: {error}");
572 }
573 }
574}
575impl fmt::Debug for TraceProviders {
576 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577 write!(f, "TraceProviders (")?;
578 let mut i = 0;
579 if self.traces.is_some() {
580 write!(f, "SdkTracerProvider")?;
581 i += 1;
582 }
583 if self.logs.is_some() {
584 if i > 0 {
585 write!(f, ", ")?;
586 }
587 write!(f, "SdkLoggerProvider")?;
588 i += 1;
589 }
590 if self.metrics.is_some() {
591 if i > 0 {
592 write!(f, ", ")?;
593 }
594 write!(f, "SdkMeterProvider")?;
595 }
596 write!(f, ")")
597 }
598}