use std::{num::NonZeroUsize, time::Duration};
use opentelemetry_otlp::{
ExporterBuildError, SpanExporter as OtlpSpanExporter, WithExportConfig, WithTonicConfig,
};
use opentelemetry_sdk::{
trace::{
BatchConfig, BatchConfigBuilder, BatchSpanProcessor, Sampler, SdkTracerProvider, Tracer,
},
Resource,
};
use opentelemetry_stdout::SpanExporter as StdoutSpanExporter;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{debug_span, Instrument, Level, Subscriber};
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::{
filter::{Filtered, Targets},
registry::LookupSpan,
Layer,
};
use url::Url;
use crate::{
crypto::TonicTlsConfig, errors::IoError, logging::LoggingLevel, telemetry::OtlpProtocol,
};
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TracingError {
#[error("OTel span exporter builder error: {0}")]
OpenTelemetry(#[from] ExporterBuildError),
#[error("OTel tracing error: {0}")]
Tracing(#[from] opentelemetry_sdk::trace::TraceError),
#[error("Error loading files in configuration: {0}")]
ConfigRead(IoError),
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[non_exhaustive]
pub struct TracingConfig {
#[serde(
default = "TracingConfig::default_exporters",
skip_serializing_if = "Vec::is_empty"
)]
exporters: Vec<TracingExporterConfig>,
#[serde(default)]
sample: TracingSampler,
#[serde(default)]
level: LoggingLevel,
#[serde(default, flatten)]
limits: TracingSpanLimits,
#[serde(default)]
include: TracingIncludes,
#[serde(default)]
batch: TracingBatchConfig,
#[serde(default = "crate::util::default_false")]
include_headers: bool,
#[serde(default = "TracingConfig::default_request_level")]
request_level: LoggingLevel,
#[serde(default = "TracingConfig::default_response_level")]
response_level: LoggingLevel,
}
impl Default for TracingConfig {
fn default() -> Self {
Self {
exporters: Self::default_exporters(),
sample: TracingSampler::default(),
level: LoggingLevel::default(),
limits: TracingSpanLimits::default(),
include: TracingIncludes::default(),
batch: TracingBatchConfig::default(),
include_headers: false,
request_level: Self::default_request_level(),
response_level: Self::default_response_level(),
}
}
}
impl TracingConfig {
#[must_use]
#[inline]
fn default_exporters() -> Vec<TracingExporterConfig> {
vec![TracingExporterConfig::default()]
}
fn build_batch_config(&self) -> BatchConfig {
self.batch.to_builder().build()
}
pub async fn build_provider(
&self,
resource: Resource,
) -> Result<SdkTracerProvider, TracingError> {
let span = debug_span!("build_tracing_provider");
async {
let mut provider = SdkTracerProvider::builder()
.with_resource(resource)
.with_sampler(Sampler::from(self.sample))
.with_max_events_per_span(self.limits.max_events_per_span)
.with_max_attributes_per_span(self.limits.max_attributes_per_span)
.with_max_links_per_span(self.limits.max_links_per_span)
.with_max_attributes_per_event(self.limits.max_attributes_per_event)
.with_max_attributes_per_link(self.limits.max_attributes_per_link);
for exp_cfg in &self.exporters {
match exp_cfg {
TracingExporterConfig::Otlp(cfg) => {
let exp = cfg.build_exporter().await?;
let processor = BatchSpanProcessor::builder(exp)
.with_batch_config(self.build_batch_config())
.build();
provider = provider.with_span_processor(processor);
}
TracingExporterConfig::Stdout => {
let exp = StdoutSpanExporter::default();
provider = provider.with_simple_exporter(exp);
}
}
}
Ok(provider.build())
}
.instrument(span)
.await
}
pub fn build_layer<S>(
&self,
tracer: &Tracer,
) -> Filtered<OpenTelemetryLayer<S, Tracer>, Targets, S>
where
S: Subscriber + for<'span> LookupSpan<'span>,
{
let _span = debug_span!("build_tracing_layer").entered();
tracing_opentelemetry::layer()
.with_tracer(tracer.clone())
.with_location(self.include.location)
.with_error_fields_to_exceptions(self.include.exception_from_error_fields)
.with_error_events_to_exceptions(self.include.exception_from_error_events)
.with_tracked_inactivity(self.include.inactivity)
.with_threads(self.include.thread_info)
.with_error_events_to_status(self.include.status_from_error_events)
.with_filter(
Targets::new()
.with_target("h2", Level::WARN)
.with_default(self.level),
)
}
#[must_use]
pub fn include_headers(&self) -> bool {
self.include_headers
}
#[must_use]
#[inline]
fn default_request_level() -> LoggingLevel {
LoggingLevel::Debug
}
#[must_use]
#[inline]
fn default_response_level() -> LoggingLevel {
LoggingLevel::Info
}
#[must_use]
pub fn request_level(&self) -> LoggingLevel {
self.request_level
}
#[must_use]
pub fn response_level(&self) -> LoggingLevel {
self.response_level
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(tag = "type", rename_all = "snake_case")]
enum TracingExporterConfig {
Otlp(Box<OtlpTracingExporterConfig>),
Stdout,
}
impl Default for TracingExporterConfig {
fn default() -> Self {
Self::Otlp(Box::default())
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[non_exhaustive]
struct OtlpTracingExporterConfig {
#[serde(default = "OtlpTracingExporterConfig::default_endpoint")]
endpoint: Url,
#[serde(default, alias = "format")]
protocol: OtlpProtocol,
#[serde(default = "OtlpTracingExporterConfig::default_timeout")]
timeout: Duration,
#[serde(default)]
tls: TonicTlsConfig,
}
impl Default for OtlpTracingExporterConfig {
fn default() -> Self {
Self {
endpoint: Self::default_endpoint(),
protocol: OtlpProtocol::default(),
timeout: Self::default_timeout(),
tls: TonicTlsConfig::default(),
}
}
}
impl OtlpTracingExporterConfig {
#[must_use]
#[inline]
#[allow(clippy::unwrap_used)]
fn default_endpoint() -> Url {
Url::parse("http://localhost:4317").unwrap()
}
#[must_use]
#[inline]
fn default_timeout() -> Duration {
opentelemetry_otlp::OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT
}
async fn build_exporter(&self) -> Result<OtlpSpanExporter, TracingError> {
OtlpSpanExporter::builder()
.with_tonic()
.with_endpoint(self.endpoint.to_string())
.with_protocol(self.protocol.into())
.with_timeout(self.timeout)
.with_tls_config(
self.tls
.to_tonic_config()
.await
.map_err(|err| TracingError::ConfigRead(err.into()))?,
)
.build()
.map_err(Into::into)
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
enum TracingSampler {
#[default]
Always,
Fraction(f64),
}
impl From<TracingSampler> for Sampler {
fn from(value: TracingSampler) -> Self {
match value {
TracingSampler::Always => Self::AlwaysOn,
TracingSampler::Fraction(frac) => Self::TraceIdRatioBased(frac),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
struct TracingSpanLimits {
#[serde(default = "TracingSpanLimits::default_max")]
max_events_per_span: u32,
#[serde(default = "TracingSpanLimits::default_max")]
max_attributes_per_span: u32,
#[serde(default = "TracingSpanLimits::default_max")]
max_links_per_span: u32,
#[serde(default = "TracingSpanLimits::default_max")]
max_attributes_per_event: u32,
#[serde(default = "TracingSpanLimits::default_max")]
max_attributes_per_link: u32,
}
impl Default for TracingSpanLimits {
fn default() -> Self {
Self {
max_events_per_span: Self::default_max(),
max_attributes_per_span: Self::default_max(),
max_links_per_span: Self::default_max(),
max_attributes_per_event: Self::default_max(),
max_attributes_per_link: Self::default_max(),
}
}
}
impl TracingSpanLimits {
#[must_use]
#[inline]
fn default_max() -> u32 {
128
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
struct TracingIncludes {
#[serde(default = "crate::util::default_true")]
location: bool,
#[serde(default = "crate::util::default_true")]
exception_from_error_fields: bool,
#[serde(default = "crate::util::default_true")]
exception_from_error_events: bool,
#[serde(default = "crate::util::default_true")]
status_from_error_events: bool,
#[serde(default)]
inactivity: bool,
#[serde(default = "crate::util::default_true")]
thread_info: bool,
}
impl Default for TracingIncludes {
fn default() -> Self {
Self {
location: true,
exception_from_error_fields: true,
exception_from_error_events: true,
status_from_error_events: true,
inactivity: false,
thread_info: true,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[non_exhaustive]
struct TracingBatchConfig {
#[serde(default = "TracingBatchConfig::default_max_queue_size")]
max_queue_size: NonZeroUsize,
#[serde(default = "TracingBatchConfig::default_scheduled_delay")]
scheduled_delay: Duration,
#[serde(default = "TracingBatchConfig::default_max_export_batch_size")]
max_export_batch_size: NonZeroUsize,
}
impl Default for TracingBatchConfig {
fn default() -> Self {
Self {
max_queue_size: Self::default_max_queue_size(),
scheduled_delay: Self::default_scheduled_delay(),
max_export_batch_size: Self::default_max_export_batch_size(),
}
}
}
impl TracingBatchConfig {
#[must_use]
#[inline]
fn default_max_queue_size() -> NonZeroUsize {
NonZeroUsize::new(2048).unwrap()
}
#[must_use]
#[inline]
fn default_scheduled_delay() -> Duration {
Duration::from_secs(5)
}
#[must_use]
#[inline]
fn default_max_export_batch_size() -> NonZeroUsize {
NonZeroUsize::new(512).unwrap()
}
#[must_use]
fn to_builder(&self) -> BatchConfigBuilder {
BatchConfigBuilder::default()
.with_max_queue_size(self.max_queue_size.get())
.with_scheduled_delay(self.scheduled_delay)
.with_max_export_batch_size(self.max_export_batch_size.get())
}
}