Skip to main content

tracing_kickstart/
trace.rs

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
11// opentelemetry - base
12use 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
26// opentelemetry - traces
27use opentelemetry::trace::TracerProvider as _; // for tracer trait
28use opentelemetry_otlp::SpanExporter;
29use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
30use tracing_opentelemetry::OpenTelemetryLayer;
31
32// opentelemetry - metrics
33use opentelemetry_otlp::MetricExporter;
34use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
35use tracing_opentelemetry::MetricsLayer;
36
37// opentelemetry - logs
38use 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    /// Custom env filter which takes priority over RUST_LOG
66    ///
67    /// This is beneficial when loading app conf from env,
68    /// as it allows overriding the env filter without setting a global RUST_LOG
69    #[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    /// If set, will be user as the value for the `deployment.environment.name` attribute
79    #[serde(default)]
80    deployment_env: Option<String>,
81
82    /// If set, will configure the metrics export period (duration in seconds)
83    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
136// -- custom attributes + attribute helpers
137
138pub 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    // root/primary service name + package name
180    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    // version
189    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        // returns the name of the package that contains the associated tracing call
201        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    // build mode: release/debug
210    builder = builder.with_attribute(KeyValue::new(custom_attribute::DEPLOYMENT_BUILD_TYPE, get_build_env()));
211
212    // deployment env set from config/runtime env
213    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        // telemetry sdk stack attrs
220        builder = builder.with_detector(Box::new(TelemetryResourceDetector));
221    }
222    #[cfg(feature = "detector_hostresource")]
223    {
224        // host id, host arch
225        builder = builder.with_detector(Box::new(HostResourceDetector::default()));
226    }
227    #[cfg(feature = "detector_process")]
228    {
229        // process args, pid
230        builder = builder.with_detector(Box::new(ProcessResourceDetector));
231    }
232    #[cfg(feature = "detector_os")]
233    {
234        // os
235        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    // add auth headers if provided
245    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
252// Construct TracerProvider for OpenTelemetryLayer
253fn 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        // .with_timeout(std::time::Duration::from_secs(3))
264        .build()?;
265
266    let provider = SdkTracerProvider::builder()
267        // Customize sampling strategy
268        .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        // .with_timeout(std::time::Duration::from_secs(3))
286        .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        // .with_timeout(std::time::Duration::from_secs(3))
307        .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/// Compile-time attributes to be provided by the owning application/service.
322///
323/// Used as a set of parameters to pass to [`init()`]
324///
325/// Service attributes can be generated and passed in using the `build_attrs` macro, e.g.:
326///
327/// ```
328/// use tracing_kickstart::TracingConfig;
329///
330/// let attrs = tracing_kickstart::build_attrs!();
331/// let conf = TracingConfig::default();
332///
333/// let tracing_providers = tracing_kickstart::init(attrs, &conf).unwrap();
334/// ```
335#[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/// Generates service attributes using env! calls.
373///
374/// This is done using a macro to allow for the `env!(..)` calls to be scoped from the
375/// parent package/crate, rather than from `tracing-kickstart`.
376#[macro_export]
377macro_rules! build_attrs {
378    // This macro takes an argument of designator `ident` and
379    // creates a function named `$func_name`.
380    // The `ident` designator is used for variable/function names.
381    () => (
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
393/// Initialize tracing
394///
395/// Service attributes can be generated and passed in using the `build_attrs` macro, e.g.:
396///
397/// ```
398/// use tracing_kickstart::TracingConfig;
399///
400/// let attrs = tracing_kickstart::build_attrs!();
401/// let conf = TracingConfig::default();
402/// let custom_env_filter = None;
403/// // let custom_env_filter = "warn,example_app=debug"
404///
405/// let tracing_providers = tracing_kickstart::init(attrs, &conf, custom_env_filter).unwrap();
406///
407/// // Optionally register all configured providers globally
408/// tracing_providers.register_globally();
409///
410/// // do some work..
411///
412/// tracing_providers.shutdown();
413/// ```
414///
415/// ## `EnvFilter`
416///
417/// The EnvFilter is resolved using the first available from:
418/// - `TracingConfig::filter` (typically set from app config env var, e.g. `APP__TRACING__FILTER=app=warn`)
419/// - `RUST_LOG` env var
420/// - The `default_env_filter` parameter in this function (used to overide the default fallback)
421/// - default fallback (library defined, set to `"warn,{crate_name}=debug,tracing_kickstart=debug`)
422/// ---
423/// Regardless of how the `EnvFilter` is resolved, all required filters for `console_subscriber` will be added
424/// **if the console_subscriber** feature flag is enabled.
425// if tracing config is none, otel providers won't be handled
426pub fn init(service_attrs: ServiceAttributeStore, config: &TracingConfig, default_env_filter: Option<&str>) -> Result<TraceProviders, ExporterBuildError> {
427    // resolve the env filter in the following priority
428    let filter: EnvFilter = {
429        // config env filter
430        if let Some(filter_str) = &config.filter {
431            println!("Resolved tracing EnvFilter from provided config: {filter_str:?}");
432            filter_str.into()
433        }
434        // `RUST LOG`
435        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        // function parameter (`default_env_filter`)
440        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        // library-defined fallback env filter
445        else {
446            let filter_str = format!(
447                "warn,{}=debug,tracing_kickstart=debug", // include self in default filter
448                service_attrs.crate_name
449            );
450            println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
451            filter_str.into()
452        }
453    };
454
455    // add env filters for tokio console subscriber (controlled by feature flag)
456    #[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    // build base layers
468    let layer = tracing_subscriber::registry()
469    .with(filter);
470
471    // stdout layer
472    let layer = layer.with(
473        tracing_subscriber::fmt::layer()
474        .with_ansi(config.ansi_output)
475    );
476
477    // conditionally add log file layer if path is provided in config
478    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    // conditionally add tokio console layer
493    #[cfg(feature = "tokio_console")]
494    let layer = layer.with(console_subscriber::spawn());
495
496    // default has all 3 provider field options set to None
497    let mut providers_handle = TraceProviders::default();
498
499    // init open telemetry providers
500    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        // traces
507        let traces_provider =
508            init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
509        // - add tracing layer for tracing/span -> otel/trace
510        let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)));
511        providers_handle.traces = Some(traces_provider);
512
513        // logs
514        let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
515        // - add tracing layer for tracing -> otel/logs
516        let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
517        providers_handle.logs = Some(logs_provider);
518
519        // metrics
520        let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource, config.metrics_interval_duration())?;
521        // - add layer for tracing events -> otel/metrics
522        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// ---- Struct for containing otel providers
536
537// TODO: alternatively use `opentelemetry::global::set_x_provider()` fns
538#[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    /// Calls `opentelemetry::global::set_x_provider(..); for all configured providers, where applicable`
546    pub fn register_globally(&self) {
547        // register traces
548        if let Some(provider) = &self.traces {
549            tracing::info!("Traces provider registered globally");
550            opentelemetry::global::set_tracer_provider(provider.clone());
551        }
552        // register metrics
553        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    /// Triggers shutdown for each provider that has been set
560    pub fn shutdown(self) {
561        // shutdown traces
562        if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
563            println!("error shutting down traces provider: {error}");
564        }
565        // shutdown logs
566        if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
567            println!("error shutting down logs provider: {error}");
568        }
569        // shutdown metrics
570        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}