use impulse_utils::prelude::*;
use serde::Deserialize;
use tracing_appender::non_blocking::WorkerGuard as TracingFileGuard;
use tracing_rfc_5424::transport::{
Error as SyslogError, TcpTransport, Transport, UdpTransport, UnixSocket, UnixSocketStream,
};
#[derive(Clone, Deserialize, Default)]
pub struct TracingOptions {
pub enable_io_logs: Option<bool>,
pub io_log_level: Option<String>,
pub enable_file_logs: Option<bool>,
pub file_log_level: Option<String>,
pub file_log_rotation: Option<String>,
pub file_log_max_rolling_files: Option<u32>,
pub enable_syslog_logs: Option<bool>,
pub syslog_addr: Option<String>,
pub syslog_log_level: Option<String>,
pub enable_ecs_logs: Option<bool>,
pub ecs_log_level: Option<String>,
pub ecs_rotation: Option<String>,
pub ecs_max_rolling_files: Option<u32>,
#[cfg(feature = "otel")]
pub otel_grpc_endpoint: Option<String>,
#[cfg(feature = "otel")]
pub otel_http_endpoint: Option<String>,
#[cfg(feature = "otel")]
pub otel_log_level: Option<String>,
}
#[derive(Default)]
#[allow(dead_code)]
pub struct TracingGuards {
file_log_guard: Option<TracingFileGuard>,
ecs_log_guard: Option<TracingFileGuard>,
}
fn match_log_level(log_level: &Option<String>) -> MResult<tracing::Level> {
if log_level.is_some() {
Ok(match log_level.as_ref().unwrap().as_str() {
"error" => tracing::Level::ERROR,
"warn" => tracing::Level::WARN,
"info" => tracing::Level::INFO,
"debug" => tracing::Level::DEBUG,
"trace" => tracing::Level::TRACE,
_ => ServerError::from_public("Incorrect logging level.").with_500().bail()?,
})
} else if cfg!(debug_assertions) {
Ok(tracing::Level::DEBUG)
} else {
ServerError::from_public("Logging is disabled").with_500().bail()
}
}
fn match_log_file_rolling(log_rolling: &Option<String>) -> MResult<tracing_appender::rolling::Rotation> {
if let Some(log_rolling) = log_rolling {
Ok(match log_rolling.as_str() {
"never" => tracing_appender::rolling::Rotation::NEVER,
"daily" => tracing_appender::rolling::Rotation::DAILY,
"hourly" => tracing_appender::rolling::Rotation::HOURLY,
"minutely" => tracing_appender::rolling::Rotation::MINUTELY,
_ => ServerError::from_public(
"Incorrect level of log rotation. Choose one of the options: `never`, `daily`, `hourly`, `minutely`.",
)
.with_500()
.bail()?,
})
} else {
Ok(tracing_appender::rolling::Rotation::NEVER)
}
}
enum SyslogTransportWrapper {
Udp(UdpTransport),
Tcp(TcpTransport),
Unix(UnixSocket),
UnixStream(UnixSocketStream),
}
impl<F: tracing_rfc_5424::formatter::SyslogFormatter> Transport<F> for SyslogTransportWrapper {
type Error = SyslogError;
fn send(&self, buf: F::Output) -> Result<(), Self::Error> {
match self {
SyslogTransportWrapper::Udp(t) => {
<tracing_rfc_5424::transport::UdpTransport as tracing_rfc_5424::transport::Transport<F>>::send(t, buf)
}
SyslogTransportWrapper::Tcp(t) => {
<tracing_rfc_5424::transport::TcpTransport as tracing_rfc_5424::transport::Transport<F>>::send(t, buf)
}
SyslogTransportWrapper::Unix(t) => {
<tracing_rfc_5424::transport::UnixSocket as tracing_rfc_5424::transport::Transport<F>>::send(t, buf)
}
SyslogTransportWrapper::UnixStream(t) => {
<tracing_rfc_5424::transport::UnixSocketStream as tracing_rfc_5424::transport::Transport<F>>::send(t, buf)
}
}
}
}
fn match_syslog_addr(addr: &Option<String>) -> MResult<SyslogTransportWrapper> {
use tracing_rfc_5424::transport::*;
let addr = addr
.as_ref()
.ok_or(ServerError::from_public("Syslog export address is empty!").with_500())?;
match addr.as_str() {
s if s.starts_with("udp://") => UdpTransport::new(&s[6..])
.map(SyslogTransportWrapper::Udp)
.map_err(ServerError::from_private),
s if s.starts_with("tcp://") => TcpTransport::new(&s[6..])
.map(SyslogTransportWrapper::Tcp)
.map_err(ServerError::from_private),
s if s.starts_with("unix://") => UnixSocket::new(&s[7..])
.map(SyslogTransportWrapper::Unix)
.map_err(ServerError::from_private),
s if s.starts_with("ustream://") => UnixSocketStream::new(&s[10..])
.map(SyslogTransportWrapper::UnixStream)
.map_err(ServerError::from_private),
_ => Err(ServerError::from_public(
"Can't init syslog because of incorrect address; your address should start with `udp://`, `tcp://`, `unix://` or `ustream://`.",
)),
}
}
#[allow(dead_code)]
fn log_filter(metadata: &tracing::Metadata) -> bool {
#[cfg(not(feature = "log-without-filtering"))]
{
metadata.module_path().is_none_or(|p| {
!(p.contains("salvo")
|| p.contains("hyper_util")
|| p.contains("tower")
|| p.contains("quinn")
|| p.contains("h2"))
})
}
#[cfg(feature = "log-without-filtering")]
{
true
}
}
impl TracingOptions {
pub fn init(&self, app_name: &str) -> MResult<TracingGuards> {
use tracing_appender::rolling;
#[allow(unused_imports)]
use tracing_subscriber::filter::{LevelFilter, filter_fn};
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{fmt, registry};
#[cfg(feature = "otel")]
use crate::otel::api::trace::TracerProvider;
#[cfg(feature = "otel")]
use crate::otel::exporter::WithExportConfig;
#[cfg(feature = "otel")]
use crate::otel::sdk::{Resource, trace::RandomIdGenerator};
let format = fmt::format()
.with_level(true)
.with_target(true)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(false)
.with_line_number(true)
.compact();
let io_tracer = if self.enable_io_logs.is_some_and(|v| v) {
let io_log_level = match_log_level(&self.io_log_level)?;
let io_tracer = fmt::layer()
.event_format(format.clone())
.with_writer(std::io::stdout)
.with_span_events(FmtSpan::CLOSE)
.with_filter(LevelFilter::from_level(io_log_level))
.with_filter(filter_fn(log_filter));
Some(io_tracer)
} else {
None
};
let (file_tracer, file_log_guard) = if self.enable_file_logs.is_some_and(|v| v) {
let file_log_level = match_log_level(&self.file_log_level).or_else(|_| match_log_level(&self.io_log_level))?;
let file_log_rotation = match_log_file_rolling(&self.file_log_rotation)?;
let file_log_max_rolling_files = self.file_log_max_rolling_files.unwrap_or(5) as usize;
let file_appender = rolling::RollingFileAppender::builder()
.rotation(file_log_rotation)
.filename_suffix(app_name)
.max_log_files(file_log_max_rolling_files)
.build("logs")
.map_err(|e| {
ServerError::from_private(e)
.with_public("Failed to initialize logging to file!")
.with_500()
})?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let file_tracer = fmt::layer()
.event_format(format)
.with_writer(non_blocking)
.with_ansi(false)
.with_span_events(FmtSpan::CLOSE)
.with_filter(LevelFilter::from_level(file_log_level))
.with_filter(filter_fn(log_filter));
(Some(file_tracer), Some(guard))
} else {
(None, None)
};
let syslog_tracer = if self.enable_syslog_logs.is_some_and(|v| v) {
let syslog_log_level =
match_log_level(&self.syslog_log_level).or_else(|_| match_log_level(&self.io_log_level))?;
let transport = match_syslog_addr(&self.syslog_addr)?;
let format = tracing_rfc_5424::rfc5424::Rfc5424::builder()
.appname_as_string(app_name.to_string())
.map_err(ServerError::from_private)?
.facility(tracing_rfc_5424::facility::Facility::LOG_USER)
.build();
let syslog_tracer = tracing_rfc_5424::layer::Layer::with_transport_and_syslog_formatter(transport, format)
.with_filter(LevelFilter::from_level(syslog_log_level))
.with_filter(filter_fn(log_filter));
Some(syslog_tracer)
} else {
None
};
let (ecs_tracer, ecs_log_guard) = if self.enable_ecs_logs.is_some_and(|v| v) {
let ecs_log_level = match_log_level(&self.ecs_log_level).or_else(|_| match_log_level(&self.io_log_level))?;
let ecs_log_rotation = match_log_file_rolling(&self.ecs_rotation)?;
let ecs_log_max_rolling_files = self.ecs_max_rolling_files.unwrap_or(5) as usize;
let file_appender = rolling::RollingFileAppender::builder()
.rotation(ecs_log_rotation)
.filename_suffix(app_name)
.max_log_files(ecs_log_max_rolling_files)
.build("ecs-logs")
.map_err(|e| {
ServerError::from_private(e)
.with_public("Failed to initialize ECS logging to file!")
.with_500()
})?;
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let ecs_tracer = tracing_ecs::ECSLayerBuilder::default()
.normalize_json(false)
.with_span_events(FmtSpan::CLOSE)
.build_with_writer(non_blocking)
.with_filter(LevelFilter::from_level(ecs_log_level))
.with_filter(filter_fn(log_filter));
(Some(ecs_tracer), Some(guard))
} else {
(None, None)
};
#[cfg(feature = "otel")]
let otel_tracer = if let Some(otel_grpc_endpoint) = &self.otel_grpc_endpoint {
let otel_log_level = match_log_level(&self.otel_log_level).or_else(|_| match_log_level(&self.io_log_level))?;
let otel_span_exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_protocol(opentelemetry_otlp::Protocol::Grpc)
.with_endpoint(otel_grpc_endpoint.as_str())
.with_timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| {
ServerError::from_private(e)
.with_public("Failed to initialize OTEL gRPC telemetry!")
.with_500()
})?;
let otel_tracer_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
.with_batch_exporter(otel_span_exporter)
.with_id_generator(RandomIdGenerator::default())
.with_max_events_per_span(32)
.with_max_attributes_per_span(64)
.with_resource(Resource::builder().with_service_name(app_name.to_string()).build())
.build()
.tracer(app_name.to_owned());
let opentelemetry = tracing_opentelemetry::layer()
.with_tracer(otel_tracer_provider)
.with_filter(LevelFilter::from_level(otel_log_level))
.with_filter(filter_fn(log_filter));
Some(opentelemetry)
} else {
None
};
#[cfg(feature = "otel")]
if let Some(otel_http_endpoint) = &self.otel_http_endpoint {
use opentelemetry_otlp::WithHttpConfig;
let otel_metric_exporter = opentelemetry_otlp::MetricExporter::builder()
.with_http()
.with_http_client(reqwest::blocking::Client::new())
.with_protocol(opentelemetry_otlp::Protocol::HttpBinary)
.with_endpoint(otel_http_endpoint.as_str())
.with_timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| {
ServerError::from_private(e)
.with_public("Failed to initialize OTEL HTTP telemetry!")
.with_500()
})?;
let otel_metric_reader = opentelemetry_sdk::metrics::PeriodicReader::builder(otel_metric_exporter)
.with_interval(std::time::Duration::from_secs(5))
.build();
let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
.with_resource(Resource::builder().with_service_name(app_name.to_string()).build())
.with_reader(otel_metric_reader)
.build();
opentelemetry::global::set_meter_provider(meter_provider.clone());
}
let collector = registry()
.with(file_tracer)
.with(io_tracer)
.with(syslog_tracer)
.with(ecs_tracer);
#[cfg(feature = "otel")]
let collector = collector.with(otel_tracer);
tracing::subscriber::set_global_default(collector).map_err(|e| {
ServerError::from_private(e)
.with_public("Can't init global default log collector!")
.with_500()
})?;
Ok(TracingGuards {
file_log_guard,
ecs_log_guard,
})
}
}