use axum::http::{Request, Response};
use opentelemetry::trace::{Status, TracerProvider};
use opentelemetry::{KeyValue, Value, global};
use opentelemetry_sdk::{
Resource,
metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider},
trace::{RandomIdGenerator, Sampler, SdkTracerProvider},
};
use opentelemetry_semantic_conventions::{
SCHEMA_URL,
attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_VERSION},
};
use serde::{Deserialize, Serialize};
use std::env;
use std::time::Duration;
use tower_http::trace::{MakeSpan, OnResponse, TraceLayer};
use tracing::Span;
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer, OpenTelemetrySpanExt};
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum TelemetryProtocol {
#[serde(rename = "http/protobuf")]
HTTP,
#[serde(rename = "grpc")]
GRPC,
}
impl TelemetryProtocol {
pub fn from_env() -> Option<Self> {
let is_enabled = env::var("OTEL_EXPORTER_OTLP_ENDPOINT").is_ok()
|| env::var("OTEL_EXPORTER_OTLP_HEADERS").is_ok()
|| env::var("OTEL_EXPORTER_OTLP_PROTOCOL").is_ok();
if is_enabled {
let protocol = match env::var("OTEL_EXPORTER_OTLP_PROTOCOL") {
Ok(string) => match string.as_str() {
"http/protobuf" | "http" => TelemetryProtocol::HTTP,
"grpc" => TelemetryProtocol::GRPC,
_ => TelemetryProtocol::HTTP,
},
Err(_) => TelemetryProtocol::HTTP,
};
Some(protocol)
} else {
None
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Telemetry {
pub name: Option<Value>,
pub version: Option<Value>,
pub deployment: Option<Value>,
}
impl Telemetry {
pub fn new() -> Self {
Self::default()
}
#[allow(dead_code)]
pub fn with_name(&self, name: impl Into<Value>) -> Self {
let mut this = self.clone();
this.name = Some(name.into());
this
}
#[allow(dead_code)]
pub fn with_version(&self, version: impl Into<Value>) -> Self {
let mut this = self.clone();
this.version = Some(version.into());
this
}
#[allow(dead_code)]
pub fn with_deployment(&self, deployment: impl Into<Value>) -> Self {
let mut this = self.clone();
this.deployment = Some(deployment.into());
this
}
pub fn name(&self) -> Option<Value> {
env::var("OTEL_SERVICE_NAME")
.ok()
.filter(|value| !value.trim().is_empty())
.map(Value::from)
.or_else(|| self.name.clone())
}
pub fn version(&self) -> Option<Value> {
env::var("OTEL_SERVICE_VERSION")
.ok()
.filter(|value| !value.trim().is_empty())
.map(Value::from)
.or_else(|| self.version.clone())
}
pub fn deployment(&self) -> Option<Value> {
env::var("OTEL_SERVICE_DEPLOYMENT")
.ok()
.filter(|value| !value.trim().is_empty())
.map(Value::from)
.or_else(|| self.deployment.clone())
}
pub fn resource(&self) -> Resource {
let mut builder = Resource::builder();
if let Some(name) = self.name() {
builder = builder.with_service_name(name)
}
let mut attributes = Vec::<KeyValue>::with_capacity(2);
if let Some(version) = self.version() {
attributes.push(KeyValue::new(SERVICE_VERSION, version));
}
if let Some(deployment) = self.deployment() {
attributes.push(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, deployment));
}
if !attributes.is_empty() {
builder = builder.with_schema_url(attributes, SCHEMA_URL);
}
builder.build()
}
fn init_tracer_provider(&self, telemetry_protocol: &TelemetryProtocol) -> SdkTracerProvider {
let exporter = opentelemetry_otlp::SpanExporter::builder();
let exporter = match telemetry_protocol {
TelemetryProtocol::HTTP => exporter.with_http().build(),
TelemetryProtocol::GRPC => exporter.with_tonic().build(),
};
let exporter = exporter.expect("Failed to build OTLP span exporter");
SdkTracerProvider::builder()
.with_sampler(Sampler::ParentBased(Box::new(Sampler::TraceIdRatioBased(
1.0,
))))
.with_id_generator(RandomIdGenerator::default())
.with_resource(self.resource())
.with_batch_exporter(exporter)
.build()
}
fn init_meter_provider(&self, telemetry_protocol: &TelemetryProtocol) -> SdkMeterProvider {
let exporter = opentelemetry_otlp::MetricExporter::builder();
let exporter = match telemetry_protocol {
TelemetryProtocol::HTTP => exporter
.with_http()
.with_temporality(opentelemetry_sdk::metrics::Temporality::default())
.build(),
TelemetryProtocol::GRPC => exporter
.with_tonic()
.with_temporality(opentelemetry_sdk::metrics::Temporality::default())
.build(),
};
let exporter = exporter.expect("Failed to build OTLP metric exporter");
let reader = PeriodicReader::builder(exporter)
.with_interval(std::time::Duration::from_secs(30))
.build();
let stdout_reader =
PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build();
let meter_provider = MeterProviderBuilder::default()
.with_resource(self.resource())
.with_reader(reader)
.with_reader(stdout_reader)
.build();
global::set_meter_provider(meter_provider.clone());
meter_provider
}
pub fn register(&self) -> TelemetryProviders {
let telemetry_protocol = TelemetryProtocol::from_env();
match telemetry_protocol {
Some(telemetry_protocol) => {
let tracer_provider = self.init_tracer_provider(&telemetry_protocol);
let meter_provider = self.init_meter_provider(&telemetry_protocol);
let tracer = tracer_provider.tracer("tracing-otel-subscriber");
tracing_subscriber::registry()
.with(tracing_subscriber::filter::LevelFilter::INFO)
.with(tracing_subscriber::fmt::layer())
.with(MetricsLayer::new(meter_provider.clone()))
.with(OpenTelemetryLayer::new(tracer))
.init();
tracing::info!(
"OpenTelemetry tracing and metrics exporter is enabled via {:?}",
telemetry_protocol
);
TelemetryProviders {
tracer_provider: Some(tracer_provider),
meter_provider: Some(meter_provider),
}
}
None => {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "trace".into()))
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!("OpenTelemetry is not enabled");
TelemetryProviders {
tracer_provider: None,
meter_provider: None,
}
}
}
}
}
pub struct TelemetryProviders {
pub tracer_provider: Option<SdkTracerProvider>,
pub meter_provider: Option<SdkMeterProvider>,
}
impl Drop for TelemetryProviders {
fn drop(&mut self) {
if let Some(tracer_provider) = self.tracer_provider.as_ref()
&& let Err(err) = tracer_provider.shutdown()
{
eprintln!("{err:?}");
}
if let Some(meter_provider) = self.meter_provider.as_ref()
&& let Err(err) = meter_provider.shutdown()
{
eprintln!("{err:?}");
}
}
}
impl TelemetryProviders {
pub fn http_tracing(
&self,
) -> TraceLayer<
tower_http::classify::SharedClassifier<tower_http::classify::ServerErrorsAsFailures>,
FacilitatorHttpMakeSpan,
tower_http::trace::DefaultOnRequest,
FacilitatorHttpOnResponse,
> {
TraceLayer::new_for_http()
.make_span_with(FacilitatorHttpMakeSpan)
.on_response(FacilitatorHttpOnResponse)
}
}
#[derive(Clone, Debug)]
pub struct FacilitatorHttpMakeSpan;
impl<A> MakeSpan<A> for FacilitatorHttpMakeSpan {
fn make_span(&mut self, request: &Request<A>) -> Span {
tracing::info_span!(
"http_request",
otel.kind = "server",
otel.name = %format!("{} {}", request.method(), request.uri()),
method = %request.method(),
uri = %request.uri(),
version = ?request.version(),
)
}
}
#[derive(Clone, Debug)]
pub struct FacilitatorHttpOnResponse;
impl<A> OnResponse<A> for FacilitatorHttpOnResponse {
fn on_response(self, response: &Response<A>, latency: Duration, span: &Span) {
span.record("status", tracing::field::display(response.status()));
span.record("latency", tracing::field::display(latency.as_millis()));
span.record(
"http.status_code",
tracing::field::display(response.status().as_u16()),
);
if response.status().is_success() {
span.set_status(Status::Ok);
} else {
span.set_status(Status::error(
response
.status()
.canonical_reason()
.unwrap_or("unknown")
.to_string(),
));
}
tracing::info!(
"status={} elapsed={}ms",
response.status().as_u16(),
latency.as_millis()
);
}
}