use crate::utils::error::Result;
#[cfg(feature = "otel")]
use crate::utils::error::Error;
#[cfg(feature = "otel")]
use opentelemetry::{global, KeyValue};
#[cfg(feature = "otel")]
use opentelemetry_sdk::trace::SdkTracerProvider;
#[cfg(feature = "otel")]
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry};
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
pub endpoint: String,
pub service_name: String,
pub console_output: bool,
}
impl Default for TelemetryConfig {
fn default() -> Self {
#[cfg(feature = "otel")]
{
Self {
endpoint: std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
.unwrap_or_else(|_| "http://localhost:4317".to_string()),
service_name: "ggen".to_string(),
console_output: true,
}
}
#[cfg(not(feature = "otel"))]
{
Self {
endpoint: String::new(),
service_name: "ggen".to_string(),
console_output: false,
}
}
}
}
#[cfg(feature = "otel")]
pub struct TelemetryGuard {
_provider: SdkTracerProvider,
}
#[cfg(feature = "otel")]
impl Drop for TelemetryGuard {
fn drop(&mut self) {
let _ = self._provider.shutdown();
}
}
#[cfg(feature = "otel")]
pub fn init_telemetry(config: TelemetryConfig) -> Result<TelemetryGuard> {
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(&config.endpoint)
.build()
.map_err(|e| Error::with_context("Failed to create OTLP exporter", &e.to_string()))?;
let resource = Resource::builder_empty()
.with_attributes([
KeyValue::new("service.name", config.service_name.clone()),
KeyValue::new("service.version", env!("CARGO_PKG_VERSION")),
])
.build();
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(resource)
.build();
global::set_tracer_provider(provider.clone());
let tracer = global::tracer("ggen");
let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = Registry::default()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")))
.with(telemetry_layer);
if config.console_output {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_level(true);
subscriber.with(fmt_layer).init();
} else {
subscriber.init();
}
tracing::info!(
endpoint = %config.endpoint,
service = %config.service_name,
"OpenTelemetry initialized"
);
Ok(TelemetryGuard {
_provider: provider,
})
}
#[cfg(not(feature = "otel"))]
pub fn init_telemetry(_config: TelemetryConfig) -> Result<()> {
Ok(())
}
#[cfg(feature = "otel")]
pub fn shutdown_telemetry() {
tracing::info!("OpenTelemetry shutdown requested — use TelemetryGuard for automatic flush");
}
#[cfg(not(feature = "otel"))]
pub fn shutdown_telemetry() {
}
#[macro_export]
macro_rules! telemetry_context {
($($key:expr => $value:expr),* $(,)?) => {
{
let span = tracing::Span::current();
$(
span.record($key, &tracing::field::display($value));
)*
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(feature = "otel")]
fn test_telemetry_config_default() {
let config = TelemetryConfig::default();
assert_eq!(config.service_name, "ggen");
assert!(config.console_output);
}
#[test]
#[cfg(not(feature = "otel"))]
fn test_telemetry_config_default_no_otel() {
let config = TelemetryConfig::default();
assert_eq!(config.service_name, "ggen");
assert!(!config.console_output);
}
#[test]
fn test_telemetry_config_custom() {
let config = TelemetryConfig {
endpoint: "http://custom:4317".to_string(),
service_name: "test-service".to_string(),
console_output: false,
};
assert_eq!(config.endpoint, "http://custom:4317");
assert_eq!(config.service_name, "test-service");
assert!(!config.console_output);
}
}