pub mod metrics;
pub mod prelude;
pub mod propagation;
pub mod resource;
pub mod span;
pub mod tracing;
use std::env;
use std::sync::OnceLock;
use thiserror::Error;
static SERVICE_NAME: OnceLock<String> = OnceLock::new();
pub(crate) fn configured_service_name() -> &'static str {
SERVICE_NAME
.get()
.map_or("sideways-otel-service", String::as_str)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OtlpProtocol {
#[default]
Grpc,
HttpProtobuf,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropagatorKind {
TraceContext,
Baggage,
}
#[derive(Debug, Error)]
pub enum TelemetryError {
#[error("OpenTelemetry tracing disabled via OTEL_TRACES_ENABLED=false")]
TracingDisabled,
#[error("OpenTelemetry metrics disabled via OTEL_METRICS_ENABLED=false")]
MetricsDisabled,
#[error("Failed to build OTLP exporter: {0}")]
ExporterBuild(String),
}
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
pub service_name: String,
pub resource_attributes: Vec<(String, String)>,
pub traces_enabled: bool,
pub metrics_enabled: bool,
pub logs_enabled: bool,
pub otlp_protocol: OtlpProtocol,
pub otlp_endpoint: Option<String>,
pub otlp_headers: Vec<(String, String)>,
pub rust_log: String,
pub json_logging: bool,
pub metrics_export_interval_ms: u64,
pub propagators: Vec<PropagatorKind>,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
service_name: "sideways-otel-service".to_string(),
resource_attributes: Vec::new(),
traces_enabled: true,
metrics_enabled: true,
logs_enabled: true,
otlp_protocol: OtlpProtocol::default(),
otlp_endpoint: None,
otlp_headers: Vec::new(),
rust_log: "info".to_string(),
json_logging: false,
metrics_export_interval_ms: 60_000,
propagators: vec![PropagatorKind::TraceContext, PropagatorKind::Baggage],
}
}
}
impl TelemetryConfig {
#[must_use]
pub fn from_env() -> Self {
let mut config = Self::default();
if let Ok(name) = env::var("OTEL_SERVICE_NAME") {
config.service_name = name;
}
if let Ok(attrs) = env::var("OTEL_RESOURCE_ATTRIBUTES") {
config.resource_attributes = Self::parse_pairs(&attrs, '=');
}
if let Ok(enabled) = env::var("OTEL_TRACES_ENABLED")
&& enabled.eq_ignore_ascii_case("false")
{
config.traces_enabled = false;
}
if let Ok(enabled) = env::var("OTEL_METRICS_ENABLED")
&& enabled.eq_ignore_ascii_case("false")
{
config.metrics_enabled = false;
}
if let Ok(enabled) = env::var("OTEL_LOGS_ENABLED")
&& enabled.eq_ignore_ascii_case("false")
{
config.logs_enabled = false;
}
if let Ok(protocol) = env::var("OTEL_EXPORTER_OTLP_PROTOCOL") {
match protocol.as_str() {
"grpc" => config.otlp_protocol = OtlpProtocol::Grpc,
"http/protobuf" => config.otlp_protocol = OtlpProtocol::HttpProtobuf,
other => eprintln!(
"⚠️ Unsupported OTEL_EXPORTER_OTLP_PROTOCOL '{other}' (expected 'grpc' or 'http/protobuf'), defaulting to grpc"
),
}
}
if let Ok(endpoint) = env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
config.otlp_endpoint = Some(endpoint);
}
if let Ok(headers) = env::var("OTEL_EXPORTER_OTLP_HEADERS") {
config.otlp_headers = Self::parse_pairs(&headers, '=');
}
if let Ok(rust_log) = env::var("RUST_LOG") {
config.rust_log = rust_log;
}
if let Ok(enabled) = env::var("JSON_LOGGING")
&& enabled.eq_ignore_ascii_case("true")
{
config.json_logging = true;
}
if let Ok(interval) = env::var("OTEL_METRIC_EXPORT_INTERVAL")
&& let Ok(ms) = interval.parse()
{
config.metrics_export_interval_ms = ms;
}
if let Ok(propagators) = env::var("OTEL_PROPAGATORS") {
config.propagators = propagators
.split(',')
.filter_map(|name| match name.trim() {
"tracecontext" => Some(PropagatorKind::TraceContext),
"baggage" => Some(PropagatorKind::Baggage),
"none" => None,
other => {
eprintln!(
"⚠️ Unsupported OTEL_PROPAGATORS entry '{other}' (expected 'tracecontext', 'baggage', or 'none'), skipping"
);
None
}
})
.collect();
}
config
}
fn parse_pairs(raw: &str, sep: char) -> Vec<(String, String)> {
raw.split(',')
.filter_map(|pair| {
let mut parts = pair.trim().splitn(2, sep);
let key = parts.next()?.trim();
let value = parts.next()?.trim();
if key.is_empty() {
None
} else {
Some((key.to_string(), value.to_string()))
}
})
.collect()
}
#[must_use]
pub fn builder() -> TelemetryConfigBuilder {
TelemetryConfigBuilder::default()
}
}
#[derive(Debug, Default)]
pub struct TelemetryConfigBuilder {
config: TelemetryConfig,
}
impl TelemetryConfigBuilder {
#[must_use]
pub fn service_name(mut self, name: impl Into<String>) -> Self {
self.config.service_name = name.into();
self
}
#[must_use]
pub fn resource_attributes(mut self, attributes: Vec<(String, String)>) -> Self {
self.config.resource_attributes = attributes;
self
}
#[must_use]
pub fn with_resource_attribute(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.config
.resource_attributes
.push((key.into(), value.into()));
self
}
#[must_use]
pub fn traces_enabled(mut self, enabled: bool) -> Self {
self.config.traces_enabled = enabled;
self
}
#[must_use]
pub fn metrics_enabled(mut self, enabled: bool) -> Self {
self.config.metrics_enabled = enabled;
self
}
#[must_use]
pub fn logs_enabled(mut self, enabled: bool) -> Self {
self.config.logs_enabled = enabled;
self
}
#[must_use]
pub fn otlp_protocol(mut self, protocol: OtlpProtocol) -> Self {
self.config.otlp_protocol = protocol;
self
}
#[must_use]
pub fn otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.config.otlp_endpoint = Some(endpoint.into());
self
}
#[must_use]
pub fn otlp_headers(mut self, headers: Vec<(String, String)>) -> Self {
self.config.otlp_headers = headers;
self
}
#[must_use]
pub fn with_otlp_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.config.otlp_headers.push((key.into(), value.into()));
self
}
#[must_use]
pub fn rust_log(mut self, filter: impl Into<String>) -> Self {
self.config.rust_log = filter.into();
self
}
#[must_use]
pub fn json_logging(mut self, enabled: bool) -> Self {
self.config.json_logging = enabled;
self
}
#[must_use]
pub fn metrics_export_interval_ms(mut self, ms: u64) -> Self {
self.config.metrics_export_interval_ms = ms;
self
}
#[must_use]
pub fn propagators(mut self, propagators: Vec<PropagatorKind>) -> Self {
self.config.propagators = propagators;
self
}
#[must_use]
pub fn build(self) -> TelemetryConfig {
self.config
}
}
pub struct Telemetry {
pub tracer_provider: Option<opentelemetry_sdk::trace::SdkTracerProvider>,
pub meter_provider: Option<opentelemetry_sdk::metrics::SdkMeterProvider>,
pub logger_provider: Option<opentelemetry_sdk::logs::SdkLoggerProvider>,
}
impl Telemetry {
pub fn shutdown(&self) {
if let Some(tp) = &self.tracer_provider
&& let Err(err) = tp.shutdown()
{
eprintln!("⚠️ Sideways OTel: tracer provider shutdown failed: {err}");
}
if let Some(mp) = &self.meter_provider
&& let Err(err) = mp.shutdown()
{
eprintln!("⚠️ Sideways OTel: meter provider shutdown failed: {err}");
}
if let Some(lp) = &self.logger_provider
&& let Err(err) = lp.shutdown()
{
eprintln!("⚠️ Sideways OTel: logger provider shutdown failed: {err}");
}
}
}
fn describe_endpoint(config: &TelemetryConfig) -> &str {
config.otlp_endpoint.as_deref().unwrap_or(match config.otlp_protocol {
OtlpProtocol::Grpc => "http://localhost:4317 (grpc default)",
OtlpProtocol::HttpProtobuf => "http://localhost:4318 (http/protobuf default)",
})
}
#[must_use]
pub fn init_telemetry_layer(config: &TelemetryConfig) -> (Telemetry, tracing::BoxedLayer) {
eprintln!("🦀 Sideways OTel: Initializing...");
let _ = SERVICE_NAME.set(config.service_name.clone());
propagation::init_propagator(config);
let resource = resource::build_resource(config);
let endpoint_description = describe_endpoint(config);
let (tracer_provider, logger_provider, layer) = if config.traces_enabled {
match tracing::init_otlp_tracing(config, resource.clone()) {
Ok((tp, lp, layer)) => {
eprintln!("✅ Sideways OTel: tracing initialized -> {endpoint_description}");
if lp.is_some() {
eprintln!("✅ Sideways OTel: log export initialized");
}
(Some(tp), lp, layer)
}
Err(err) => {
eprintln!("⚠️ Sideways OTel: tracing unavailable: {err}");
(None, None, tracing::console_layer(config))
}
}
} else {
eprintln!("📊 Sideways OTel: tracing disabled");
(None, None, tracing::console_layer(config))
};
let meter_provider = if config.metrics_enabled {
match metrics::init_otlp_metrics(config, resource) {
Ok(mp) => {
eprintln!("✅ Sideways OTel: metrics initialized -> {endpoint_description}");
Some(mp)
}
Err(err) => {
eprintln!("⚠️ Sideways OTel: metrics unavailable: {err}");
None
}
}
} else {
eprintln!("📊 Sideways OTel: metrics disabled");
None
};
(
Telemetry {
tracer_provider,
meter_provider,
logger_provider,
},
layer,
)
}
#[must_use]
pub fn init_telemetry(config: &TelemetryConfig) -> Telemetry {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
let (telemetry, layer) = init_telemetry_layer(config);
if let Err(err) = tracing_subscriber::registry().with(layer).try_init() {
eprintln!("❌ Sideways OTel: failed to install global tracing subscriber: {err}");
}
telemetry
}