use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::OpenOptions;
use tracing_subscriber::EnvFilter;
#[cfg(feature = "tokio_console")]
use tracing_subscriber::layer::Layer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use opentelemetry::KeyValue;
use opentelemetry_sdk::resource::{Resource, TelemetryResourceDetector};
use opentelemetry_otlp::{Protocol, WithExportConfig, WithHttpConfig};
use opentelemetry_resource_detectors::{
HostResourceDetector, OsResourceDetector, ProcessResourceDetector,
};
use opentelemetry_semantic_conventions::attribute;
use opentelemetry::trace::TracerProvider as _; use opentelemetry_otlp::SpanExporter;
use opentelemetry_sdk::trace::{Sampler, SdkTracerProvider};
use tracing_opentelemetry::OpenTelemetryLayer;
use opentelemetry_otlp::MetricExporter;
use opentelemetry_sdk::metrics::SdkMeterProvider;
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::LogExporter;
use opentelemetry_sdk::logs::SdkLoggerProvider;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TracingOtelConfig {
collector_url: String,
#[serde(default, skip_serializing)]
collector_auth_header: Option<SecretString>,
}
impl TracingOtelConfig {
pub fn new(collector_url: String, collector_auth_header: Option<SecretString>) -> Self {
Self {
collector_url,
collector_auth_header,
}
}
pub fn collector_url(&self) -> &str {
&self.collector_url
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TracingConfig {
#[serde(default)]
log_file_path: Option<String>,
#[serde(default = "TracingConfig::ansi_output_default")]
ansi_output: bool,
#[serde(default, flatten)]
otel_config: Option<TracingOtelConfig>,
}
impl TracingConfig {
pub fn new(collector_url: Option<String>, collector_auth_header: Option<SecretString>, log_file_path: Option<String>, ansi_output: Option<bool>) -> Self {
Self {
log_file_path,
ansi_output: ansi_output.unwrap_or(Self::ansi_output_default()),
otel_config: collector_url.map(|url| TracingOtelConfig {
collector_url: url,
collector_auth_header,
}),
}
}
pub fn log_file_path(&self) -> Option<&str> {
self.log_file_path.as_ref().map(|s| s.as_str())
}
pub fn otel_config(&self) -> &Option<TracingOtelConfig> {
&self.otel_config
}
pub fn ansi_output_default() -> bool {
true
}
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
ansi_output: Self::ansi_output_default(),
log_file_path: None,
otel_config: None,
}
}
}
pub mod custom_attribute {
pub const SERVICE_VERSION_MAJOR: &str = "service.version.major";
pub const SERVICE_VERSION_MINOR: &str = "service.version.minor";
pub const SERVICE_VERSION_PATCH: &str = "service.version.patch";
pub const SERVICE_ORIGIN_PACKAGE_NAME: &str = "service.origin_package.name";
}
#[rustfmt::skip]
pub fn get_build_env() -> &'static str {
#[cfg(debug_assertions)]
{ "debug" }
#[cfg(not(debug_assertions))]
{ "release" }
}
pub fn get_service_name() -> Option<String> {
std::env::var("CARGO_PKG_NAME").ok()
}
pub fn get_service_version() -> Option<String> {
std::env::var("CARGO_PKG_VERSION").ok()
}
pub fn get_service_version_major() -> Option<String> {
std::env::var("CARGO_PKG_VERSION_MAJOR").ok()
}
pub fn get_service_version_minor() -> Option<String> {
std::env::var("CARGO_PKG_VERSION_MINOR").ok()
}
pub fn get_service_version_patch() -> Option<String> {
std::env::var("CARGO_PKG_VERSION_PATCH").ok()
}
pub fn get_origin_package_name() -> Option<&'static str> {
let package_name = env!("CARGO_CRATE_NAME");
if package_name.is_empty() {
None
} else {
Some(package_name)
}
}
fn build_otel_resource() -> Resource {
let mut builder = Resource::builder_empty();
if let Some(service_name) = get_service_name() {
builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_NAME, service_name));
}
if let Some(service_version) = get_service_version() {
builder = builder.with_attribute(KeyValue::new(attribute::SERVICE_VERSION, service_version));
if let Some(version_part) = get_service_version_major() {
builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MAJOR, version_part));
if let Some(version_part) = get_service_version_minor() {
builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_MINOR, version_part));
if let Some(version_part) = get_service_version_patch() {
builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_VERSION_PATCH, version_part));
}
}
}
}
if let Some(origin_package_name) = get_origin_package_name() {
builder = builder.with_attribute(KeyValue::new(custom_attribute::SERVICE_ORIGIN_PACKAGE_NAME, origin_package_name));
}
builder
.with_attribute(KeyValue::new(attribute::DEPLOYMENT_ENVIRONMENT_NAME, get_build_env())) .with_detector(Box::new(TelemetryResourceDetector)) .with_detector(Box::new(HostResourceDetector::default())) .with_detector(Box::new(ProcessResourceDetector)) .with_detector(Box::new(OsResourceDetector)) .build()
}
fn build_otel_headers(auth_header_val: &Option<SecretString>) -> HashMap<String, String> {
let mut headers: HashMap<String, String> = HashMap::new();
if let Some(auth_header) = auth_header_val.as_ref() {
headers.insert("Authorization".into(), auth_header.expose_secret().into());
}
headers
}
fn init_otel_traces_provider(
collector_endpoint: &str,
headers: HashMap<String, String>,
resource: Resource,
) -> color_eyre::Result<SdkTracerProvider> {
let exporter = SpanExporter::builder()
.with_http()
.with_headers(headers)
.with_endpoint(format!("{collector_endpoint}/v1/traces"))
.with_protocol(Protocol::HttpBinary)
.build()?;
let provider = SdkTracerProvider::builder()
.with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(1.0))))
.with_resource(resource)
.with_batch_exporter(exporter)
.build();
Ok(provider)
}
fn init_otel_logs_provider(
collector_endpoint: &str,
headers: HashMap<String, String>,
resource: Resource,
) -> color_eyre::Result<SdkLoggerProvider> {
let exporter = LogExporter::builder()
.with_http()
.with_headers(headers)
.with_endpoint(format!("{collector_endpoint}/v1/logs"))
.with_protocol(Protocol::HttpBinary)
.build()?;
let provider = SdkLoggerProvider::builder()
.with_resource(resource)
.with_batch_exporter(exporter)
.build();
Ok(provider)
}
fn init_otel_metrics_provider(
collector_endpoint: &str,
headers: HashMap<String, String>,
resource: Resource,
) -> color_eyre::Result<SdkMeterProvider> {
let exporter = MetricExporter::builder()
.with_http()
.with_headers(headers)
.with_endpoint(format!("{collector_endpoint}/v1/metrics"))
.with_protocol(Protocol::HttpBinary)
.build()?;
let provider = SdkMeterProvider::builder()
.with_resource(resource)
.with_periodic_exporter(exporter)
.build();
Ok(provider)
}
#[allow(unused_mut)]
pub fn init(config: &TracingConfig) -> color_eyre::Result<TraceProviders> {
let crate_name = get_service_name().expect("Service name should exist from rust runtime exec");
let crate_target_name = crate_name.replace("-", "_");
let mut prepared_env_filter = format!(
"warn,{crate_target_name}=debug,tracing_kickstart=debug" );
#[cfg(feature = "tokio_console")]
{ prepared_env_filter.push_str(",tokio=trace,runtime=trace"); }
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| prepared_env_filter.clone().into());
let layer = tracing_subscriber::registry()
.with(env_filter);
let layer = layer.with(tracing_subscriber::fmt::layer()
.with_ansi(config.ansi_output)
);
let file_logging_layer = match &config.log_file_path {
Some(file_path) => {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_path)
.expect("Log file should be writable");
Some(tracing_subscriber::fmt::layer().with_ansi(false).with_writer(file))
},
None => None
};
let layer = layer.with(file_logging_layer);
#[cfg(feature = "tokio_console")]
{ let layer = layer.with(console_subscriber::spawn()); }
let mut providers_handle = TraceProviders::default();
if let Some(otel_config) = &config.otel_config {
println!("Initializing OTEL config");
let endpoint = &otel_config.collector_url;
let headers = build_otel_headers(&otel_config.collector_auth_header);
let resource = build_otel_resource();
let traces_provider =
init_otel_traces_provider(endpoint, headers.clone(), resource.clone())?;
let layer = layer.with(OpenTelemetryLayer::new(traces_provider.tracer(crate_name)));
providers_handle.traces = Some(traces_provider);
let logs_provider = init_otel_logs_provider(endpoint, headers.clone(), resource.clone())?;
let layer = layer.with(OpenTelemetryTracingBridge::new(&logs_provider));
providers_handle.logs = Some(logs_provider);
let metrics_provider = init_otel_metrics_provider(endpoint, headers, resource)?;
providers_handle.metrics = Some(metrics_provider);
layer.init();
tracing::info!("OTEL tracing configured");
} else {
layer.init();
tracing::warn!("OTEL tracing disabled");
}
tracing::debug!("Env filter: {prepared_env_filter:?}");
Ok(providers_handle)
}
pub fn dump_crate_vars() {
let service_name = get_service_name().unwrap_or("- unset -".into());
let service_version = get_service_version().unwrap_or("- unset -".into());
let service_version_major = get_service_version_major().unwrap_or("- unset -".into());
let service_version_minor = get_service_version_minor().unwrap_or("- unset -".into());
let service_version_patch = get_service_version_patch().unwrap_or("- unset -".into());
let origin_package_name = get_origin_package_name().unwrap_or("- unset -".into());
let build_env = get_build_env();
println!("");
println!("Resolved tracing attributes");
println!("--------------------");
println!("service_name: {service_name}");
println!("service_version: {service_version}");
println!("service_version_major: {service_version_major}");
println!("service_version_minor: {service_version_minor}");
println!("service_version_patch: {service_version_patch}");
println!("origin_package_name: {origin_package_name}");
println!("build_env: {build_env}");
println!("");
}
#[derive(Debug, Default, Clone)]
pub struct TraceProviders {
pub traces: Option<SdkTracerProvider>,
pub logs: Option<SdkLoggerProvider>,
pub metrics: Option<SdkMeterProvider>,
}
impl TraceProviders {
pub fn shutdown(self) {
if let Some(provider) = self.traces {
if let Err(error) = provider.shutdown() {
println!("error shutting down traces provider: {error}");
}
}
if let Some(provider) = self.logs {
if let Err(error) = provider.shutdown() {
println!("error shutting down logs provider: {error}");
}
}
if let Some(provider) = self.metrics {
if let Err(error) = provider.shutdown() {
println!("error shutting down metrics provider: {error}");
}
}
}
}