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 tracing_subscriber::EnvFilter;
7use tracing_subscriber::layer::SubscriberExt;
8use tracing_subscriber::util::SubscriberInitExt;
9
10// opentelemetry - base
11use 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
19// opentelemetry - traces
20use opentelemetry::trace::TracerProvider as _; // for tracer trait
21use opentelemetry_otlp::SpanExporter;
22use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
23use tracing_opentelemetry::OpenTelemetryLayer;
24
25// opentelemetry - metrics
26use opentelemetry_otlp::MetricExporter;
27use opentelemetry_sdk::metrics::SdkMeterProvider;
28use tracing_opentelemetry::MetricsLayer;
29
30// opentelemetry - logs
31use 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    /// Custom env filter which takes priority over RUST_LOG
59    ///
60    /// This is beneficial when loading app conf from env,
61    /// as it allows overriding the env filter without setting a global RUST_LOG
62    #[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
107// -- custom attributes + attribute helpers
108
109pub 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    // root/primary service name + package name
143    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    // version
148    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    // returns the name of the package that contains the associated tracing call
155    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())) // build mode: release/debug
164        .with_detector(Box::new(TelemetryResourceDetector)) // telemetry sdk stack attrs
165        .with_detector(Box::new(HostResourceDetector::default())) // host id, host arch
166        .with_detector(Box::new(ProcessResourceDetector)) // process args, pid
167        .with_detector(Box::new(OsResourceDetector)) // os
168        .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    // add auth headers if provided
175    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
182// Construct TracerProvider for OpenTelemetryLayer
183fn 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        // .with_timeout(std::time::Duration::from_secs(3))
194        .build()?;
195
196    let provider = SdkTracerProvider::builder()
197        // Customize sampling strategy
198        .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        // .with_timeout(std::time::Duration::from_secs(3))
216        .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        // .with_timeout(std::time::Duration::from_secs(3))
236        .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/// Compile-time attributes to be provided by the owning application/service.
247///
248/// Used as a set of parameters to pass to [`init()`]
249///
250/// Service attributes can be generated and passed in using the `build_attrs` macro, e.g.:
251///
252/// ```
253/// use tracing_kickstart::TracingConfig;
254///
255/// let attrs = tracing_kickstart::build_attrs!();
256/// let conf = TracingConfig::default();
257///
258/// let tracing_providers = tracing_kickstart::init(attrs, &conf).unwrap();
259/// ```
260#[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/// Generates service attributes using env! calls.
299///
300/// This is done using a macro to allow for the `env!(..)` calls to be scoped from the
301/// parent package/crate, rather than from `tracing-kickstart`.
302#[macro_export]
303macro_rules! build_attrs {
304    // This macro takes an argument of designator `ident` and
305    // creates a function named `$func_name`.
306    // The `ident` designator is used for variable/function names.
307    () => (
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
319/// Initialize tracing
320///
321/// Service attributes can be generated and passed in using the `build_attrs` macro, e.g.:
322///
323/// ```
324/// use tracing_kickstart::TracingConfig;
325///
326/// let attrs = tracing_kickstart::build_attrs!();
327/// let conf = TracingConfig::default();
328/// let custom_env_filter = None;
329/// // let custom_env_filter = "warn,example_app=debug"
330///
331/// let tracing_providers = tracing_kickstart::init(attrs, &conf, custom_env_filter).unwrap();
332///
333/// // Optionally register all configured providers globally
334/// tracing_providers.register_globally();
335///
336/// // do some work..
337///
338/// tracing_providers.shutdown();
339/// ```
340///
341/// ## `EnvFilter`
342///
343/// The EnvFilter is resolved using the first available from:
344/// - `TracingConfig::filter` (typically set from app config env var, e.g. `APP__TRACING__FILTER=app=warn`)
345/// - `RUST_LOG` env var
346/// - The `default_env_filter` parameter in this function (used to overide the default fallback)
347/// - default fallback (library defined, set to `"warn,{crate_name}=debug,tracing_kickstart=debug`)
348/// ---
349/// Regardless of how the `EnvFilter` is resolved, all required filters for `console_subscriber` will be added
350/// **if the console_subscriber** feature flag is enabled.
351// if tracing config is none, otel providers won't be handled
352pub fn init(service_attrs: ServiceAttributeStore, config: &TracingConfig, default_env_filter: Option<&str>) -> Result<TraceProviders, ExporterBuildError> {
353    // resolve the env filter in the following priority
354    let filter: EnvFilter = {
355        // config env filter
356        if let Some(filter_str) = &config.filter {
357            println!("Resolved tracing EnvFilter from provided config: {filter_str:?}");
358            filter_str.into()
359        }
360        // `RUST LOG`
361        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        // function parameter (`default_env_filter`)
366        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        // library-defined fallback env filter
371        else {
372            let filter_str = format!(
373                "warn,{}=debug,tracing_kickstart=debug", // include self in default filter
374                service_attrs.crate_name
375            );
376            println!("Using tracing-kickstart fallback EnvFilter: {filter_str:?}");
377            filter_str.into()
378        }
379    };
380
381    // add env filters for tokio console subscriber (controlled by feature flag)
382    #[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    // build base layers
394    let layer = tracing_subscriber::registry()
395    .with(filter);
396
397    // stdout layer
398    let layer = layer.with(
399        tracing_subscriber::fmt::layer()
400        .with_ansi(config.ansi_output)
401    );
402
403    // conditionally add log file layer if path is provided in config
404    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    // conditionally add tokio console layer
419    #[cfg(feature = "tokio_console")]
420    let layer = layer.with(console_subscriber::spawn());
421
422    // default has all 3 provider field options set to None
423    let mut providers_handle = TraceProviders::default();
424
425    // init open telemetry providers
426    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        // traces
433        let traces_provider =
434            init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
435        // - add tracing layer for tracing/span -> otel/trace
436        let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)));
437        providers_handle.traces = Some(traces_provider);
438
439        // logs
440        let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
441        // - add tracing layer for tracing -> otel/logs
442        let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
443        providers_handle.logs = Some(logs_provider);
444
445        // metrics
446        let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource)?;
447        // - add layer for tracing events -> otel/metrics
448        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// ---- Struct for containing otel providers
462
463// TODO: alternatively use `opentelemetry::global::set_x_provider()` fns
464#[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    /// Calls `opentelemetry::global::set_x_provider(..); for all configured providers, where applicable`
472    pub fn register_globally(&self) {
473        // register traces
474        if let Some(provider) = &self.traces {
475            tracing::info!("Traces provider registered globally");
476            opentelemetry::global::set_tracer_provider(provider.clone());
477        }
478        // register metrics
479        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    /// Triggers shutdown for each provider that has been set
486    pub fn shutdown(self) {
487        // shutdown traces
488        if let Some(provider) = self.traces && let Err(error) = provider.shutdown() {
489            println!("error shutting down traces provider: {error}");
490        }
491        // shutdown logs
492        if let Some(provider) = self.logs && let Err(error) = provider.shutdown() {
493            println!("error shutting down logs provider: {error}");
494        }
495        // shutdown metrics
496        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}