use anyhow::Result;
use serde::{Deserialize, Serialize};
#[cfg(feature = "otel")]
pub use opentelemetry;
#[cfg(feature = "otel")]
pub use tracing_opentelemetry;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OtelConfig {
pub service_name: String,
pub service_version: String,
pub otlp_endpoint: Option<String>,
pub tracing_enabled: bool,
pub metrics_enabled: bool,
pub sampling_ratio: f64,
pub export_timeout_secs: u64,
}
impl Default for OtelConfig {
fn default() -> Self {
Self {
service_name: "reasonkit".to_string(),
service_version: env!("CARGO_PKG_VERSION").to_string(),
otlp_endpoint: None,
tracing_enabled: true,
metrics_enabled: true,
sampling_ratio: 1.0,
export_timeout_secs: 30,
}
}
}
pub fn init_otel(config: &OtelConfig) -> Result<OtelGuard> {
use opentelemetry::global;
use opentelemetry::KeyValue;
use opentelemetry_sdk::trace::TracerProvider;
use opentelemetry_sdk::Resource;
let resource = Resource::new(vec![
KeyValue::new("service.name", config.service_name.clone()),
KeyValue::new("service.version", config.service_version.clone()),
]);
let provider = TracerProvider::builder().with_resource(resource).build();
global::set_tracer_provider(provider);
tracing::info!(
service = %config.service_name,
endpoint = ?config.otlp_endpoint,
"OpenTelemetry initialized (basic mode)"
);
if config.otlp_endpoint.is_some() {
tracing::warn!(
"OTLP endpoint configured but export requires additional setup. \
Set OTEL_EXPORTER_OTLP_ENDPOINT env var for automatic export."
);
}
Ok(OtelGuard { _private: () })
}
pub struct OtelGuard {
_private: (),
}
impl Drop for OtelGuard {
fn drop(&mut self) {
opentelemetry::global::shutdown_tracer_provider();
tracing::info!("OpenTelemetry shut down");
}
}
pub mod attributes {
use opentelemetry::KeyValue;
pub fn thinktool_name(name: &str) -> KeyValue {
KeyValue::new("reasonkit.thinktool.name", name.to_string())
}
pub fn thinktool_profile(profile: &str) -> KeyValue {
KeyValue::new("reasonkit.thinktool.profile", profile.to_string())
}
pub fn reasoning_confidence(confidence: f32) -> KeyValue {
KeyValue::new("reasonkit.reasoning.confidence", confidence as f64)
}
pub fn token_count(tokens: usize) -> KeyValue {
KeyValue::new("reasonkit.tokens.total", tokens as i64)
}
pub fn llm_provider(provider: &str) -> KeyValue {
KeyValue::new("reasonkit.llm.provider", provider.to_string())
}
pub fn llm_model(model: &str) -> KeyValue {
KeyValue::new("reasonkit.llm.model", model.to_string())
}
pub fn cache_hit(hit: bool) -> KeyValue {
KeyValue::new("reasonkit.cache.hit", hit)
}
pub fn document_count(count: usize) -> KeyValue {
KeyValue::new("reasonkit.rag.document_count", count as i64)
}
}
pub mod metrics {
use std::sync::OnceLock;
static THINKTOOL_EXECUTIONS: OnceLock<()> = OnceLock::new();
static REASONING_LATENCY: OnceLock<()> = OnceLock::new();
static CACHE_OPERATIONS: OnceLock<()> = OnceLock::new();
pub fn record_thinktool_execution(tool: &str, success: bool) {
let _ = THINKTOOL_EXECUTIONS.get_or_init(|| ());
tracing::debug!(
tool = tool,
success = success,
"ThinkTool execution recorded"
);
}
pub fn record_reasoning_latency(tool: &str, latency_ms: u64) {
let _ = REASONING_LATENCY.get_or_init(|| ());
tracing::debug!(
tool = tool,
latency_ms = latency_ms,
"Reasoning latency recorded"
);
}
pub fn record_cache_operation(cache: &str, hit: bool) {
let _ = CACHE_OPERATIONS.get_or_init(|| ());
tracing::debug!(cache = cache, hit = hit, "Cache operation recorded");
}
}
#[macro_export]
macro_rules! thinktool_span {
($name:expr) => {
tracing::info_span!("thinktool", otel.name = $name, thinktool.name = $name,)
};
($name:expr, $profile:expr) => {
tracing::info_span!(
"thinktool",
otel.name = $name,
thinktool.name = $name,
thinktool.profile = $profile,
)
};
}
#[macro_export]
macro_rules! reasoning_span {
($operation:expr) => {
tracing::info_span!(
"reasoning",
otel.name = $operation,
reasoning.operation = $operation,
)
};
}
pub fn init_subscriber(config: &OtelConfig) -> Result<SubscriberGuard> {
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(true);
if config.tracing_enabled {
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::trace::TracerProvider;
let provider = TracerProvider::builder().build();
let tracer = provider.tracer("reasonkit");
let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
let subscriber = tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.with(otel_layer);
tracing::subscriber::set_global_default(subscriber)
.map_err(|e| anyhow::anyhow!("Failed to set subscriber: {}", e))?;
} else {
let subscriber = tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer);
tracing::subscriber::set_global_default(subscriber)
.map_err(|e| anyhow::anyhow!("Failed to set subscriber: {}", e))?;
}
Ok(SubscriberGuard { _private: () })
}
pub struct SubscriberGuard {
_private: (),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = OtelConfig::default();
assert_eq!(config.service_name, "reasonkit");
assert!(config.tracing_enabled);
assert!(config.metrics_enabled);
}
#[test]
fn test_attributes() {
let attr = attributes::thinktool_name("GigaThink");
assert_eq!(attr.key.as_str(), "reasonkit.thinktool.name");
}
}