|
| 1 | +pub mod errors; |
| 2 | +use std::error::Error; |
| 3 | + |
| 4 | +use opentelemetry::{KeyValue, global, trace::TracerProvider as _}; |
| 5 | +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; |
| 6 | +use opentelemetry_otlp::WithExportConfig; |
| 7 | +use opentelemetry_sdk::{ |
| 8 | + Resource, |
| 9 | + logs::SdkLoggerProvider, |
| 10 | + metrics::{PeriodicReader, SdkMeterProvider}, |
| 11 | + propagation::TraceContextPropagator, |
| 12 | + trace::SdkTracerProvider, |
| 13 | +}; |
| 14 | +use serde::{Deserialize, Serialize}; |
| 15 | +use tracing_subscriber::{ |
| 16 | + EnvFilter, Layer, filter::filter_fn, layer::SubscriberExt, util::SubscriberInitExt, |
| 17 | +}; |
| 18 | + |
| 19 | +#[derive(Clone, Debug, Deserialize, Serialize)] |
| 20 | +#[serde(default)] |
| 21 | +pub struct OpenTelemetry { |
| 22 | + pub enabled: bool, |
| 23 | + pub service_name: String, |
| 24 | + pub logs_endpoint: Option<String>, |
| 25 | + pub metrics_endpoint: Option<String>, |
| 26 | + pub traces_endpoint: Option<String>, |
| 27 | +} |
| 28 | + |
| 29 | +impl OpenTelemetry { |
| 30 | + pub fn logs_endpoint(&self) -> Option<&str> { |
| 31 | + non_empty_url(&self.logs_endpoint) |
| 32 | + } |
| 33 | + |
| 34 | + pub fn metrics_endpoint(&self) -> Option<&str> { |
| 35 | + non_empty_url(&self.metrics_endpoint) |
| 36 | + } |
| 37 | + |
| 38 | + pub fn traces_endpoint(&self) -> Option<&str> { |
| 39 | + non_empty_url(&self.traces_endpoint) |
| 40 | + } |
| 41 | + |
| 42 | + pub fn has_enabled_exporter(&self) -> bool { |
| 43 | + self.logs_endpoint().is_some() |
| 44 | + || self.metrics_endpoint().is_some() |
| 45 | + || self.traces_endpoint().is_some() |
| 46 | + } |
| 47 | +} |
| 48 | + |
| 49 | +impl Default for OpenTelemetry { |
| 50 | + fn default() -> Self { |
| 51 | + Self { |
| 52 | + enabled: false, |
| 53 | + service_name: env!("CARGO_PKG_NAME").into(), |
| 54 | + logs_endpoint: None, |
| 55 | + metrics_endpoint: None, |
| 56 | + traces_endpoint: None, |
| 57 | + } |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +pub struct TelemetrySettings<'a> { |
| 62 | + pub environment: &'a str, |
| 63 | + pub default_log_level: &'a str, |
| 64 | + pub service_version: &'a str, |
| 65 | + pub instrumentation_name: &'a str, |
| 66 | + pub initialize_metrics: Option<fn()>, |
| 67 | +} |
| 68 | + |
| 69 | +pub struct Telemetry { |
| 70 | + logger_provider: Option<SdkLoggerProvider>, |
| 71 | + meter_provider: Option<SdkMeterProvider>, |
| 72 | + tracer_provider: Option<SdkTracerProvider>, |
| 73 | +} |
| 74 | + |
| 75 | +impl Telemetry { |
| 76 | + pub fn initialize( |
| 77 | + config: &OpenTelemetry, |
| 78 | + settings: TelemetrySettings<'_>, |
| 79 | + ) -> Result<Self, Box<dyn Error + Send + Sync>> { |
| 80 | + let filter = EnvFilter::try_from_default_env() |
| 81 | + .or_else(|_| EnvFilter::try_new(settings.default_log_level))?; |
| 82 | + let fmt_layer = tracing_subscriber::fmt::layer() |
| 83 | + .compact() |
| 84 | + .with_target(false) |
| 85 | + .with_filter(filter_fn(|metadata| { |
| 86 | + metadata.target() != errors::EXCEPTION_TARGET |
| 87 | + })); |
| 88 | + |
| 89 | + if !config.enabled || !config.has_enabled_exporter() { |
| 90 | + tracing_subscriber::registry() |
| 91 | + .with(filter) |
| 92 | + .with(fmt_layer) |
| 93 | + .init(); |
| 94 | + return Ok(Self { |
| 95 | + logger_provider: None, |
| 96 | + meter_provider: None, |
| 97 | + tracer_provider: None, |
| 98 | + }); |
| 99 | + } |
| 100 | + |
| 101 | + let resource = Resource::builder() |
| 102 | + .with_service_name(config.service_name.clone()) |
| 103 | + .with_attributes([ |
| 104 | + KeyValue::new("service.version", settings.service_version.to_owned()), |
| 105 | + KeyValue::new( |
| 106 | + "deployment.environment.name", |
| 107 | + settings.environment.to_owned(), |
| 108 | + ), |
| 109 | + ]) |
| 110 | + .build(); |
| 111 | + |
| 112 | + let tracer_provider = if let Some(endpoint) = config.traces_endpoint() { |
| 113 | + let exporter = opentelemetry_otlp::SpanExporter::builder() |
| 114 | + .with_tonic() |
| 115 | + .with_endpoint(endpoint.to_owned()) |
| 116 | + .build()?; |
| 117 | + let provider = SdkTracerProvider::builder() |
| 118 | + .with_resource(resource.clone()) |
| 119 | + .with_batch_exporter(exporter) |
| 120 | + .build(); |
| 121 | + global::set_text_map_propagator(TraceContextPropagator::new()); |
| 122 | + Some(provider) |
| 123 | + } else { |
| 124 | + None |
| 125 | + }; |
| 126 | + |
| 127 | + let logger_provider = if let Some(endpoint) = config.logs_endpoint() { |
| 128 | + let exporter = opentelemetry_otlp::LogExporter::builder() |
| 129 | + .with_tonic() |
| 130 | + .with_endpoint(endpoint.to_owned()) |
| 131 | + .build()?; |
| 132 | + Some( |
| 133 | + SdkLoggerProvider::builder() |
| 134 | + .with_resource(resource.clone()) |
| 135 | + .with_batch_exporter(exporter) |
| 136 | + .build(), |
| 137 | + ) |
| 138 | + } else { |
| 139 | + None |
| 140 | + }; |
| 141 | + |
| 142 | + let meter_provider = if let Some(endpoint) = config.metrics_endpoint() { |
| 143 | + let exporter = opentelemetry_otlp::MetricExporter::builder() |
| 144 | + .with_tonic() |
| 145 | + .with_endpoint(endpoint.to_owned()) |
| 146 | + .build()?; |
| 147 | + let reader = PeriodicReader::builder(exporter).build(); |
| 148 | + let provider = SdkMeterProvider::builder() |
| 149 | + .with_resource(resource) |
| 150 | + .with_reader(reader) |
| 151 | + .build(); |
| 152 | + global::set_meter_provider(provider.clone()); |
| 153 | + if let Some(initialize_metrics) = settings.initialize_metrics { |
| 154 | + initialize_metrics(); |
| 155 | + } |
| 156 | + Some(provider) |
| 157 | + } else { |
| 158 | + None |
| 159 | + }; |
| 160 | + |
| 161 | + if config.logs_endpoint().is_some() || config.traces_endpoint().is_some() { |
| 162 | + errors::enable_backtraces(); |
| 163 | + } |
| 164 | + |
| 165 | + let trace_layer = tracer_provider.as_ref().map(|provider| { |
| 166 | + tracing_opentelemetry::layer() |
| 167 | + .with_tracer(provider.tracer(settings.instrumentation_name.to_owned())) |
| 168 | + .with_error_records_to_exceptions(true) |
| 169 | + .with_error_events_to_status(true) |
| 170 | + .with_filter(filter_fn(|metadata| { |
| 171 | + metadata.target() != errors::SUMMARY_TARGET |
| 172 | + })) |
| 173 | + }); |
| 174 | + let log_layer = logger_provider.as_ref().map(|provider| { |
| 175 | + OpenTelemetryTracingBridge::new(provider).with_filter(filter_fn(|metadata| { |
| 176 | + metadata.target() != errors::SUMMARY_TARGET |
| 177 | + })) |
| 178 | + }); |
| 179 | + |
| 180 | + tracing_subscriber::registry() |
| 181 | + .with(filter) |
| 182 | + .with(fmt_layer) |
| 183 | + .with(trace_layer) |
| 184 | + .with(log_layer) |
| 185 | + .init(); |
| 186 | + |
| 187 | + Ok(Self { |
| 188 | + logger_provider, |
| 189 | + meter_provider, |
| 190 | + tracer_provider, |
| 191 | + }) |
| 192 | + } |
| 193 | + |
| 194 | + pub fn shutdown(self) { |
| 195 | + if let Some(provider) = self.logger_provider { |
| 196 | + let _ = provider.shutdown(); |
| 197 | + } |
| 198 | + if let Some(provider) = self.meter_provider { |
| 199 | + let _ = provider.shutdown(); |
| 200 | + } |
| 201 | + if let Some(provider) = self.tracer_provider { |
| 202 | + let _ = provider.shutdown(); |
| 203 | + } |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +fn non_empty_url(url: &Option<String>) -> Option<&str> { |
| 208 | + url.as_deref().filter(|value| !value.trim().is_empty()) |
| 209 | +} |
0 commit comments