use opentelemetry::trace::TracerProvider as _;
use opentelemetry::KeyValue;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::trace::SdkTracerProvider;
use opentelemetry_sdk::Resource;
use std::error::Error;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
#[derive(Debug, Clone)]
pub struct TracingConfig {
pub service_name: String,
pub service_version: String,
pub otlp_endpoint: Option<String>,
pub json_logs: bool,
pub log_level: String,
}
impl TracingConfig {
pub fn new(service_name: String) -> Self {
Self {
service_name,
service_version: env!("CARGO_PKG_VERSION").to_string(),
otlp_endpoint: None,
json_logs: false,
log_level: "info".to_string(),
}
}
pub fn with_otlp_endpoint(mut self, endpoint: String) -> Self {
self.otlp_endpoint = Some(endpoint);
self
}
pub fn with_json_logs(mut self, enabled: bool) -> Self {
self.json_logs = enabled;
self
}
pub fn with_log_level(mut self, level: String) -> Self {
self.log_level = level;
self
}
}
impl Default for TracingConfig {
fn default() -> Self {
Self::new("ipfrs".to_string())
}
}
pub fn init_tracing(config: TracingConfig) -> Result<TracingGuard, Box<dyn Error>> {
let resource = Resource::builder()
.with_attributes(vec![
KeyValue::new("service.name", config.service_name.clone()),
KeyValue::new("service.version", config.service_version.clone()),
])
.build();
let tracer_provider = if let Some(endpoint) = &config.otlp_endpoint {
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_http()
.with_endpoint(endpoint.clone())
.build()?;
let batch_processor =
opentelemetry_sdk::trace::BatchSpanProcessor::builder(exporter).build();
SdkTracerProvider::builder()
.with_resource(resource.clone())
.with_span_processor(batch_processor)
.build()
} else {
SdkTracerProvider::builder().with_resource(resource).build()
};
let telemetry_layer =
tracing_opentelemetry::layer().with_tracer(tracer_provider.tracer("ipfrs"));
let env_filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log_level));
if config.json_logs {
let fmt_layer = tracing_subscriber::fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true);
tracing_subscriber::registry()
.with(env_filter)
.with(telemetry_layer)
.with(fmt_layer)
.init();
} else {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_line_number(true);
tracing_subscriber::registry()
.with(env_filter)
.with(telemetry_layer)
.with(fmt_layer)
.init();
}
tracing::info!(
service_name = %config.service_name,
service_version = %config.service_version,
otlp_endpoint = ?config.otlp_endpoint,
"Distributed tracing initialized"
);
Ok(TracingGuard {
tracer_provider: Some(tracer_provider),
})
}
pub struct TracingGuard {
tracer_provider: Option<SdkTracerProvider>,
}
impl Drop for TracingGuard {
fn drop(&mut self) {
if let Some(provider) = self.tracer_provider.take() {
let _ = provider.shutdown();
}
}
}
pub mod attributes {
use opentelemetry::KeyValue;
pub fn block_op(operation: &str, cid: &str) -> Vec<KeyValue> {
vec![
KeyValue::new("ipfrs.operation", operation.to_string()),
KeyValue::new("ipfrs.block.cid", cid.to_string()),
]
}
pub fn semantic_search(k: usize, results: usize) -> Vec<KeyValue> {
vec![
KeyValue::new("ipfrs.semantic.k", k as i64),
KeyValue::new("ipfrs.semantic.results", results as i64),
]
}
pub fn logic_op(operation: &str, predicate: &str) -> Vec<KeyValue> {
vec![
KeyValue::new("ipfrs.logic.operation", operation.to_string()),
KeyValue::new("ipfrs.logic.predicate", predicate.to_string()),
]
}
pub fn network_op(operation: &str, peer_count: usize) -> Vec<KeyValue> {
vec![
KeyValue::new("ipfrs.network.operation", operation.to_string()),
KeyValue::new("ipfrs.network.peers", peer_count as i64),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tracing_config_creation() {
let config = TracingConfig::new("test-service".to_string());
assert_eq!(config.service_name, "test-service");
assert_eq!(config.service_version, env!("CARGO_PKG_VERSION"));
assert_eq!(config.otlp_endpoint, None);
assert!(!config.json_logs);
assert_eq!(config.log_level, "info");
}
#[test]
fn test_tracing_config_builder() {
let config = TracingConfig::new("test-service".to_string())
.with_otlp_endpoint("http://localhost:4317".to_string())
.with_json_logs(true)
.with_log_level("debug".to_string());
assert_eq!(config.service_name, "test-service");
assert_eq!(
config.otlp_endpoint,
Some("http://localhost:4317".to_string())
);
assert!(config.json_logs);
assert_eq!(config.log_level, "debug");
}
#[test]
fn test_tracing_config_default() {
let config = TracingConfig::default();
assert_eq!(config.service_name, "ipfrs");
assert_eq!(config.service_version, env!("CARGO_PKG_VERSION"));
}
#[test]
fn test_block_op_attributes() {
let attrs = attributes::block_op("put", "QmTest123");
assert_eq!(attrs.len(), 2);
}
#[test]
fn test_semantic_search_attributes() {
let attrs = attributes::semantic_search(10, 8);
assert_eq!(attrs.len(), 2);
}
#[test]
fn test_logic_op_attributes() {
let attrs = attributes::logic_op("infer", "parent(X, Y)");
assert_eq!(attrs.len(), 2);
}
#[test]
fn test_network_op_attributes() {
let attrs = attributes::network_op("connect", 5);
assert_eq!(attrs.len(), 2);
}
}