tracing_kickstart/
trace.rs

1use secrecy::{ExposeSecret, SecretString};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::fs::OpenOptions;
5use tracing_subscriber::EnvFilter;
6#[cfg(feature = "tokio_console")]
7use tracing_subscriber::layer::Layer;
8use tracing_subscriber::layer::SubscriberExt;
9use tracing_subscriber::util::SubscriberInitExt;
10
11// opentelemetry - base
12use opentelemetry::KeyValue;
13use opentelemetry_sdk::resource::{Resource, TelemetryResourceDetector};
14// use opentelemetry_sdk::propagation::TraceContextPropagator; // ?
15use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
16use opentelemetry_resource_detectors::{
17    HostResourceDetector, OsResourceDetector, ProcessResourceDetector,
18};
19use opentelemetry_semantic_conventions::attribute;
20
21// opentelemetry - traces
22use opentelemetry::trace::TracerProvider as _; // for tracer trait
23use opentelemetry_otlp::SpanExporter;
24use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
25use tracing_opentelemetry::OpenTelemetryLayer;
26
27// opentelemetry - metrics
28use opentelemetry_otlp::MetricExporter;
29use opentelemetry_sdk::metrics::SdkMeterProvider;
30
31// opentelemetry - logs
32use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
33use opentelemetry_otlp::LogExporter;
34use opentelemetry_sdk::logs::SdkLoggerProvider;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct TracingOtelConfig {
38    collector_url: String,
39
40    #[serde(default, skip_serializing)]
41    collector_auth_header: Option<SecretString>,
42}
43impl TracingOtelConfig {
44    pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
45        Self {
46            collector_url,
47            collector_auth_header,
48        }
49    }
50    pub fn collector_url(&self) -> &str {
51        &self.collector_url
52    }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TracingConfig {
57    #[serde(default)]
58    log_file_path: Option<String>,
59
60    #[serde(default = "TracingConfig::ansi_output_default")]
61    ansi_output: bool,
62
63    #[serde(default, flatten)]
64    otel_config: Option<TracingOtelConfig>,
65}
66impl TracingConfig {
67    pub fn new(collector_url: Option<String>, collector_auth_header: Option<SecretString>, log_file_path: Option<String>, ansi_output: Option<bool>) -> Self {
68        Self {
69            log_file_path,
70            ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
71            otel_config: collector_url.map(|url| TracingOtelConfig {
72                collector_url: url,
73                collector_auth_header,
74            }),
75        }
76    }
77    pub fn log_file_path(&self) -> Option<&str> {
78        self.log_file_path.as_ref().map(|s| s.as_str())
79    }
80    pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
81        &self.otel_config
82    }
83    pub fn ansi_output_default() -> bool {
84        true
85    }
86}
87impl Default for TracingConfig {
88    fn default() -> Self {
89        Self {
90            ansi_output: Self::ansi_output_default(),
91            log_file_path: None,
92            otel_config: None,
93        }
94    }
95}
96
97// -- custom attributes + attribute helpers
98
99pub mod custom_attribute {
100    pub const SERVICE_CRATE_NAME: &str = "service.crate.name";
101    pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
102    pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
103    pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
104    pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin.package_name";
105    pub const SERVICE_ORIGIN_CRATE_NAME: &str = "service.origin.crate_name";
106}
107#[rustfmt::skip]
108pub fn get_build_env() -> &'static str {
109    #[cfg(debug_assertions)]
110    { "debug" }
111    #[cfg(not(debug_assertions))]
112    { "release" }
113}
114pub fn get_service_version() -> Option<String> {
115    std::env::var("CARGO_PKG_VERSION").ok()
116}
117pub fn get_service_version_major() -> Option<String> {
118    std::env::var("CARGO_PKG_VERSION_MAJOR").ok()
119}
120pub fn get_service_version_minor() -> Option<String> {
121    std::env::var("CARGO_PKG_VERSION_MINOR").ok()
122}
123pub fn get_service_version_patch() -> Option<String> {
124    std::env::var("CARGO_PKG_VERSION_PATCH").ok()
125}
126pub fn get_origin_package_name() -> Option<&'static str> {
127    let package_name = env!("CARGO_PKG_NAME");
128    if package_name.is_empty() {
129        None
130    } else {
131        Some(package_name)
132    }
133}
134pub fn get_origin_crate_name() -> Option<&'static str> {
135    let package_name = env!("CARGO_CRATE_NAME");
136    if package_name.is_empty() {
137        None
138    } else {
139        Some(package_name)
140    }
141}
142
143fn build_otel_resource(service_attrs: &ServiceAttributeStore) -> Resource {
144    let mut builder = Resource::builder_empty();
145    // root/primary service name + package name
146    builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_attrs.pkg_name));
147    builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_CRATE_NAME, service_attrs.crate_name));
148
149    // version
150    if let Some(service_version) = get_service_version() {
151        builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_version));
152        if let Some(version_part) = get_service_version_major() {
153            builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, version_part));
154            if let Some(version_part) = get_service_version_minor() {
155                builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, version_part));
156                if let Some(version_part) = get_service_version_patch() {
157                    builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, version_part));
158                }
159            }
160        }
161    }
162    // returns the name of the package that contains the associated tracing call
163    if let Some(origin_package_name) = get_origin_package_name() {
164        builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
165    }
166    if let Some(origin_crate_name) = get_origin_crate_name() {
167        builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_CRATE_NAME, origin_crate_name));
168    }
169
170    builder
171        .with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, get_build_env())) // build mode: release/debug
172        .with_detector(Box::new(TelemetryResourceDetector)) // telemetry sdk stack attrs
173        .with_detector(Box::new(HostResourceDetector::default())) // host id, host arch
174        .with_detector(Box::new(ProcessResourceDetector)) // process args, pid
175        .with_detector(Box::new(OsResourceDetector)) // os
176        .build()
177}
178
179fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
180    let mut headers: HashMap<String, String> = HashMap::new();
181
182    // add auth headers if provided
183    if let Some(auth_header) = auth_header_val.as_ref() {
184        headers.insert("Authorization".into(), auth_header.expose_secret().into());
185    }
186
187    headers
188}
189
190// Construct TracerProvider for OpenTelemetryLayer
191fn init_otel_traces_provider(
192    collector_endpoint: &str,
193    headers: HashMap<String, String>,
194    resource: Resource,
195) -> color_eyre::Result<SdkTracerProvider> {
196    let exporter = SpanExporter::builder()
197        .with_http()
198        .with_headers(headers)
199        .with_endpoint(format!("{collector_endpoint}/v1/traces"))
200        .with_protocol(Protocol::HttpBinary)
201        // .with_timeout(std::time::Duration::from_secs(3))
202        .build()?;
203
204    let provider = SdkTracerProvider::builder()
205        // Customize sampling strategy
206        .with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
207        .with_resource(resource)
208        .with_batch_exporter(exporter)
209        .build();
210
211    Ok(provider)
212}
213fn init_otel_logs_provider(
214    collector_endpoint: &str,
215    headers: HashMap<String, String>,
216    resource: Resource,
217) -> color_eyre::Result<SdkLoggerProvider> {
218    let exporter = LogExporter::builder()
219        .with_http()
220        .with_headers(headers)
221        .with_endpoint(format!("{collector_endpoint}/v1/logs"))
222        .with_protocol(Protocol::HttpBinary)
223        // .with_timeout(std::time::Duration::from_secs(3))
224        .build()?;
225
226    let provider = SdkLoggerProvider::builder()
227        .with_resource(resource)
228        .with_batch_exporter(exporter)
229        .build();
230
231    Ok(provider)
232}
233fn init_otel_metrics_provider(
234    collector_endpoint: &str,
235    headers: HashMap<String, String>,
236    resource: Resource,
237) -> color_eyre::Result<SdkMeterProvider> {
238    let exporter = MetricExporter::builder()
239        .with_http()
240        .with_headers(headers)
241        .with_endpoint(format!("{collector_endpoint}/v1/metrics"))
242        .with_protocol(Protocol::HttpBinary)
243        // .with_timeout(std::time::Duration::from_secs(3))
244        .build()?;
245
246    let provider = SdkMeterProvider::builder()
247        .with_resource(resource)
248        .with_periodic_exporter(exporter)
249        .build();
250
251    Ok(provider)
252}
253
254/// Compile-time attributes to be provided by the owning application/service.
255///
256/// Used as a set of parameters to pass to [`init()`]
257#[derive(Debug, Clone)]
258pub struct ServiceAttributeStore {
259    pub crate_name: &'static str,
260    pub pkg_name: &'static str,
261}
262impl ServiceAttributeStore {
263    /// Initializes a new [`ServiceAttributeStore`].
264    ///
265    /// Typically the args passed should be `env!("CARGO_CRATE_NAME")` and `env!("CARGO_PKG_NAME")`, respectively.
266    ///
267    /// ### Example
268    ///
269    /// ```rust
270    /// use tracing_kickstart::ServiceAttributeStore;
271    /// let attrs = ServiceAttributeStore::new(env!("CARGO_CRATE_NAME"), env!("CARGO_PKG_NAME"));
272    /// ```
273    pub fn new(crate_name: &'static str, pkg_name: &'static str) -> Self {
274        Self {
275            crate_name,
276            pkg_name,
277        }
278    }
279}
280
281// if tracing config is none, otel providers won't be handled
282#[allow(unused_mut)]
283pub fn init(service_attrs: ServiceAttributeStore, config: &TracingConfig) -> color_eyre::Result<TraceProviders> {
284    let mut prepared_env_filter = format!(
285        "warn,{}=debug,tracing_kickstart=debug", // include self in default filter
286        service_attrs.crate_name
287    );
288
289    // add env filters for tokio console subscriber (controller by feature flag)
290    #[cfg(feature = "tokio_console")]
291    { prepared_env_filter.push_str(",tokio=trace,runtime=trace"); }
292
293    let env_filter =
294        EnvFilter::try_from_default_env().unwrap_or_else(|_| prepared_env_filter.clone().into());
295
296    // build base layers
297    let layer = tracing_subscriber::registry()
298        .with(env_filter);
299    // stdout layer
300    let layer = layer.with(tracing_subscriber::fmt::layer()
301        .with_ansi(config.ansi_output)
302    );
303    // conditionally add log file layer if path is provided in config
304    let file_logging_layer = match &config.log_file_path {
305        Some(file_path) => {
306            let file = OpenOptions::new()
307            .write(true)
308            .create(true)
309            .truncate(true)
310            .open(file_path)
311            .expect("Log file should be writable");
312            Some(tracing_subscriber::fmt::layer().with_ansi(false).with_writer(file))
313        },
314        None => None
315    };
316    let layer = layer.with(file_logging_layer);
317
318    // conditionally add tokio console layer
319    #[cfg(feature = "tokio_console")]
320    { let layer = layer.with(console_subscriber::spawn()); }
321
322    // default has all 3 provider field options set to None
323    let mut providers_handle = TraceProviders::default();
324
325    // init open telemetry providers
326    if let Some(otel_config) = &config.otel_config {
327        println!("Initializing OTEL config");
328        let endpoint = &otel_config.collector_url;
329        let headers = build_otel_headers(&otel_config.collector_auth_header);
330        let resource = build_otel_resource(&service_attrs);
331
332        // traces
333        let traces_provider =
334            init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
335        // - add tracing layer for tracing/span -> otel/trace
336        let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(service_attrs.crate_name)));
337        providers_handle.traces = Some(traces_provider);
338
339        // logs
340        let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
341        // - add tracing layer for tracing -> otel/logs
342        let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
343        providers_handle.logs = Some(logs_provider);
344
345        // metrics
346        let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource)?;
347        providers_handle.metrics = Some(metrics_provider);
348
349        layer.init();
350        tracing::info!("OTEL tracing configured");
351    } else {
352        layer.init();
353        tracing::warn!("OTEL tracing disabled");
354    }
355
356    tracing::debug!("Env filter:  {prepared_env_filter:?}");
357
358    Ok(providers_handle)
359}
360
361pub fn dump_crate_vars(attrs: &ServiceAttributeStore) {
362    let service_name = attrs.pkg_name;
363    let crate_name = attrs.crate_name;
364    let service_version = get_service_version().unwrap_or("- unset -".into());
365    let service_version_major = get_service_version_major().unwrap_or("- unset -".into());
366    let service_version_minor = get_service_version_minor().unwrap_or("- unset -".into());
367    let service_version_patch = get_service_version_patch().unwrap_or("- unset -".into());
368    let origin_pkg_name = get_origin_package_name().unwrap_or("- unset -".into());
369    let origin_crate_name = get_origin_crate_name().unwrap_or("- unset -".into());
370    let build_env = get_build_env();
371
372    println!("");
373    println!("Resolved tracing attributes");
374    println!("--------------------");
375    println!("service_name (pkg_name): {service_name}");
376    println!("service_crate_name:      {crate_name}");
377    println!("service_version:         {service_version}");
378    println!("service_version_major:   {service_version_major}");
379    println!("service_version_minor:   {service_version_minor}");
380    println!("service_version_patch:   {service_version_patch}");
381    println!("origin_pkg_name:         {origin_pkg_name}");
382    println!("origin_crate_name:       {origin_crate_name}");
383    println!("build_env:               {build_env}");
384    println!("");
385}
386
387// ---- Struct for containing otel providers
388
389// TODO: alternatively use `opentelemetry::global::set_x_provider()` fns
390#[derive(Debug, Default, Clone)]
391pub struct TraceProviders {
392    pub traces: Option<SdkTracerProvider>,
393    pub logs: Option<SdkLoggerProvider>,
394    pub metrics: Option<SdkMeterProvider>,
395}
396impl TraceProviders {
397    pub fn shutdown(self) {
398        // shutdown traces
399        if let Some(provider) = self.traces {
400            if let Err(error) = provider.shutdown() {
401                println!("error shutting down traces provider: {error}");
402            }
403        }
404        // shutdown logs
405        if let Some(provider) = self.logs {
406            if let Err(error) = provider.shutdown() {
407                println!("error shutting down logs provider: {error}");
408            }
409        }
410        // shutdown metrics
411        if let Some(provider) = self.metrics {
412            if let Err(error) = provider.shutdown() {
413                println!("error shutting down metrics provider: {error}");
414            }
415        }
416    }
417}