use doku::Document;
use opentelemetry::{global, KeyValue};
use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge;
use opentelemetry_otlp::{
ExporterBuildError, LogExporter, MetricExporter, SpanExporter, WithExportConfig,
};
use opentelemetry_sdk::logs::SdkLoggerProvider;
use opentelemetry_sdk::metrics::{PeriodicReader, SdkMeterProvider};
use opentelemetry_sdk::{trace as sdktrace, Resource};
use serde::{Deserialize, Serialize};
use snafu::{ResultExt as _, Snafu};
use tracing::Subscriber;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
use crate::ServiceInfo;
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("Could not initialize logging: {source}"))]
InitLog {
source: ExporterBuildError,
},
#[snafu(display("Could not initialize metrics: {source}"))]
InitMetric {
source: ExporterBuildError,
},
#[snafu(display("Could not initialize tracing: {source}"))]
InitTrace {
source: ExporterBuildError,
},
}
#[derive(Default, Serialize, Deserialize, Document)]
pub struct MetricSettings {
#[doku(example = "http://localhost:4318/v1/metrics")]
pub endpoint: Option<String>,
}
#[derive(Default, Serialize, Deserialize, Document)]
pub struct LogSettings {
#[doku(example = "debug,yourcrate=info")]
pub console_level: String,
#[doku(example = "warn,yourcrate=debug")]
pub otel_level: String,
#[doku(example = "http://localhost:4317")]
pub endpoint: Option<String>,
}
#[derive(Default, Serialize, Deserialize, Document)]
pub struct TraceSettings {
#[doku(example = "http://localhost:4317")]
pub endpoint: Option<String>,
}
#[derive(Default, Serialize, Deserialize, Document)]
pub struct TelemetrySettings {
pub trace: TraceSettings,
pub log: LogSettings,
pub metric: MetricSettings,
}
#[derive(Debug, Default)]
pub struct TelemetryProviders {
meter: Option<SdkMeterProvider>,
tracer: Option<sdktrace::SdkTracerProvider>,
logger: Option<SdkLoggerProvider>,
}
impl Drop for TelemetryProviders {
fn drop(&mut self) {
if let Some(tracer_provider) = self.tracer.take() {
if let Err(err) = tracer_provider.shutdown() {
eprintln!("Error shutting down Telemetry tracer provider: {err}");
}
}
if let Some(logger_provider) = self.logger.take() {
if let Err(err) = logger_provider.shutdown() {
eprintln!("Error shutting down Telemetry logger provider: {err}");
}
}
if let Some(meter_provider) = self.meter.take() {
if let Err(err) = meter_provider.shutdown() {
eprintln!("Error shutting down Telemetry meter provider: {err}");
}
}
}
}
fn init_traces(
service_info: &ServiceInfo,
settings: &TraceSettings,
) -> Result<Option<sdktrace::SdkTracerProvider>, ExporterBuildError> {
match &settings.endpoint {
Some(endpoint) => {
let exporter = SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
let resource = Resource::builder()
.with_attribute(KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
service_info.name_in_metrics.clone(),
))
.build();
Ok(Some(
sdktrace::SdkTracerProvider::builder()
.with_resource(resource)
.with_batch_exporter(exporter)
.build(),
))
}
None => Ok(None),
}
}
fn init_metrics(
service_info: &ServiceInfo,
setting: &MetricSettings,
) -> Result<Option<opentelemetry_sdk::metrics::SdkMeterProvider>, ExporterBuildError> {
match &setting.endpoint {
Some(endpoint) => {
let exporter = MetricExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()?;
let reader = PeriodicReader::builder(exporter).build();
let resource = Resource::builder()
.with_attribute(KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
service_info.name_in_metrics.clone(),
))
.build();
Ok(Some(
SdkMeterProvider::builder()
.with_reader(reader)
.with_resource(resource)
.build(),
))
}
None => Ok(None),
}
}
fn init_otel_logs<S>(
service_info: &ServiceInfo,
settings: &LogSettings,
) -> Result<
(
Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
Option<impl tracing_subscriber::layer::Layer<S> + use<S>>,
),
Error,
>
where
S: Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
match &settings.endpoint {
None => Ok((None, None)),
Some(endpoint) => {
let builder = init_otel_logs_builder(service_info, endpoint)?;
let logger_provider = builder.build();
let otel_layer = OpenTelemetryTracingBridge::new(&logger_provider);
let filter_otel = EnvFilter::new(&settings.otel_level)
.add_directive("hyper=off".parse().unwrap())
.add_directive("opentelemetry=off".parse().unwrap())
.add_directive("tonic=off".parse().unwrap())
.add_directive("h2=off".parse().unwrap())
.add_directive("reqwest=off".parse().unwrap());
let otel_layer = otel_layer.with_filter(filter_otel);
Ok((Some(logger_provider), Some(otel_layer)))
}
}
}
fn init_otel_logs_builder(
service_info: &ServiceInfo,
endpoint: &String,
) -> Result<opentelemetry_sdk::logs::LoggerProviderBuilder, Error> {
let builder = SdkLoggerProvider::builder();
let exporter = LogExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.with_context(|_| InitLogSnafu {})?;
let resource = Resource::builder()
.with_attribute(KeyValue::new(
opentelemetry_semantic_conventions::resource::SERVICE_NAME,
service_info.name_in_metrics.clone(),
))
.build();
let builder = builder
.with_resource(resource)
.with_batch_exporter(exporter);
Ok(builder)
}
fn init_logs(
service_info: &ServiceInfo,
settings: &LogSettings,
) -> Result<Option<opentelemetry_sdk::logs::SdkLoggerProvider>, Error> {
let (logger_provider, otel_layer) = init_otel_logs(service_info, settings)?;
let filter_fmt = EnvFilter::new(&settings.console_level);
let fmt_layer = tracing_subscriber::fmt::layer()
.with_thread_names(true)
.with_filter(filter_fmt);
tracing_subscriber::registry()
.with(otel_layer)
.with(fmt_layer)
.init();
Ok(logger_provider)
}
pub fn init(
service_info: &ServiceInfo,
settings: &TelemetrySettings,
) -> Result<TelemetryProviders, Error> {
let logger_provider = init_logs(service_info, &settings.log)?;
let tracer_provider =
init_traces(service_info, &settings.trace).with_context(|_| InitTraceSnafu {})?;
if let Some(tracer_provider) = &tracer_provider {
global::set_tracer_provider(tracer_provider.clone());
}
let meter_provider =
init_metrics(service_info, &settings.metric).with_context(|_| InitMetricSnafu {})?;
if let Some(meter_provider) = &meter_provider {
global::set_meter_provider(meter_provider.clone());
}
Ok(TelemetryProviders {
meter: meter_provider,
tracer: tracer_provider,
logger: logger_provider,
})
}