use metrics_exporter_prometheus::PrometheusBuilder;
use regex::Regex;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::Instant;
use tracing::Instrument;
use tracing::{error, info, info_span, Span};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
pub const MAX_LOG_ENTRIES: usize = 100_000;
static SAMPLING_RATE_MICRO: AtomicU64 = AtomicU64::new(1_000_000);
static SAMPLE_COUNTER: AtomicU64 = AtomicU64::new(0);
pub fn set_sampling_rate(rate: f64) {
let clamped = rate.clamp(0.0, 1.0);
SAMPLING_RATE_MICRO.store((clamped * 1_000_000.0) as u64, Ordering::Relaxed);
}
pub fn sampling_rate() -> f64 {
SAMPLING_RATE_MICRO.load(Ordering::Relaxed) as f64 / 1_000_000.0
}
pub fn should_sample() -> bool {
let rate_micro = SAMPLING_RATE_MICRO.load(Ordering::Relaxed);
if rate_micro >= 1_000_000 {
return true;
}
if rate_micro == 0 {
return false;
}
let count = SAMPLE_COUNTER.fetch_add(1, Ordering::Relaxed);
(count % 1_000_000) < rate_micro
}
static TRACING_GUARD: OnceLock<Mutex<Option<tracing_appender::non_blocking::WorkerGuard>>> =
OnceLock::new();
static LOG_ENTRY_COUNT: AtomicUsize = AtomicUsize::new(0);
pub fn increment_log_count() -> usize {
LOG_ENTRY_COUNT.fetch_add(1, Ordering::Relaxed) + 1
}
pub fn log_entry_count() -> usize {
LOG_ENTRY_COUNT.load(Ordering::Relaxed)
}
pub fn rotate_if_needed() -> bool {
let mut count = LOG_ENTRY_COUNT.load(Ordering::Relaxed);
loop {
if count >= MAX_LOG_ENTRIES {
match LOG_ENTRY_COUNT.compare_exchange_weak(
count,
count / 2,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => {
info!(
"Telemetry log rotation triggered: {} entries exceeded limit, reset to {}",
count,
count / 2
);
return true;
}
Err(actual) => count = actual,
}
} else {
return false;
}
}
}
pub fn sanitize_for_log(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\x0b' => out.push_str("\\v"),
'\x0c' => out.push_str("\\f"),
'\x1b' => out.push_str("\\e"),
'\x00' => out.push_str("\\0"),
c if c.is_control() => out.push_str(&format!("\\u{:04x}", c as u32)),
_ => out.push(c),
}
}
out
}
static SECRET_PATTERNS: OnceLock<Vec<Regex>> = OnceLock::new();
fn secret_patterns() -> &'static Vec<Regex> {
SECRET_PATTERNS.get_or_init(|| {
vec![
Regex::new(r"(?i)(sk-|key-|token-)[A-Za-z0-9_\-]{8,}").expect("invalid secret regex"),
Regex::new(r"(?i)Bearer\s+[A-Za-z0-9_\-\.]{8,}").expect("invalid bearer regex"),
Regex::new(r"(?i)(password|passwd|pwd)\s*=\s*\S+").expect("invalid password regex"),
]
})
}
pub fn redact_secrets(input: &str) -> String {
let mut result = input.to_string();
for pattern in secret_patterns() {
result = pattern.replace_all(&result, "[REDACTED]").to_string();
}
result
}
struct RedactingMakeWriter<M> {
inner: M,
}
impl<M> RedactingMakeWriter<M> {
fn new(inner: M) -> Self {
Self { inner }
}
}
impl<'a, M> tracing_subscriber::fmt::MakeWriter<'a> for RedactingMakeWriter<M>
where
M: tracing_subscriber::fmt::MakeWriter<'a>,
{
type Writer = RedactingWriter<M::Writer>;
fn make_writer(&'a self) -> Self::Writer {
RedactingWriter {
inner: self.inner.make_writer(),
buf: Vec::new(),
}
}
}
struct RedactingWriter<W: std::io::Write> {
inner: W,
buf: Vec<u8>,
}
impl<W: std::io::Write> RedactingWriter<W> {
fn flush_redacted(&mut self) -> std::io::Result<()> {
if !self.buf.is_empty() {
let text = String::from_utf8_lossy(&self.buf);
let redacted = redact_secrets(&text);
self.inner.write_all(redacted.as_bytes())?;
self.buf.clear();
}
self.inner.flush()
}
}
impl<W: std::io::Write> std::io::Write for RedactingWriter<W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buf.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.flush_redacted()
}
}
impl<W: std::io::Write> Drop for RedactingWriter<W> {
fn drop(&mut self) {
let _ = self.flush_redacted();
}
}
pub fn init_tracing() {
let filter = std::env::var("RUST_LOG").or_else(|_| std::env::var("SELFWARE_LOG_LEVEL"));
if let Ok(f) = filter {
init_tracing_with_filter(&f);
}
}
pub fn init_tracing_verbose() {
init_tracing_with_filter("info")
}
pub fn init_tracing_with_filter(filter: &str) {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let filter_layer = EnvFilter::try_new(filter).unwrap_or_else(|_| EnvFilter::new("warn"));
let fmt_layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_thread_ids(false)
.with_thread_names(false)
.with_file(false)
.with_line_number(false)
.with_level(true)
.compact()
.with_writer(RedactingMakeWriter::new(std::io::stderr));
let log_dir = dirs::data_local_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("selfware")
.join("logs");
let _ = std::fs::create_dir_all(&log_dir);
let file_appender = tracing_appender::rolling::daily(log_dir, "selfware.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let _ = TRACING_GUARD.set(Mutex::new(Some(guard)));
let file_layer = tracing_subscriber::fmt::layer()
.with_writer(RedactingMakeWriter::new(non_blocking))
.with_ansi(false)
.with_file(true)
.with_line_number(true);
let subscriber = tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(file_layer);
if let Ok(endpoint) = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") {
use opentelemetry_otlp::WithExportConfig;
if let Ok(tracer) = opentelemetry_otlp::new_pipeline()
.tracing()
.with_exporter(
opentelemetry_otlp::new_exporter()
.tonic()
.with_endpoint(endpoint),
)
.install_batch(opentelemetry_sdk::runtime::Tokio)
{
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let _ = subscriber.with(telemetry).try_init();
return; }
}
let _ = subscriber.try_init();
});
}
pub fn shutdown_tracing() {
if let Some(guard_slot) = TRACING_GUARD.get() {
if let Ok(mut slot) = guard_slot.lock() {
drop(slot.take()); }
}
}
pub struct Metrics {
pub api_requests: AtomicU64,
pub api_errors: AtomicU64,
pub tool_executions: AtomicU64,
pub tool_errors: AtomicU64,
pub tokens_processed: AtomicU64,
}
static METRICS: Metrics = Metrics {
api_requests: AtomicU64::new(0),
api_errors: AtomicU64::new(0),
tool_executions: AtomicU64::new(0),
tool_errors: AtomicU64::new(0),
tokens_processed: AtomicU64::new(0),
};
pub fn increment_api_requests() {
METRICS.api_requests.fetch_add(1, Ordering::Relaxed);
metrics::increment_counter!("selfware_api_requests_total");
}
pub fn increment_api_errors() {
METRICS.api_errors.fetch_add(1, Ordering::Relaxed);
metrics::increment_counter!("selfware_api_errors_total");
}
pub fn increment_tool_executions() {
METRICS.tool_executions.fetch_add(1, Ordering::Relaxed);
metrics::increment_counter!("selfware_tool_executions_total");
}
pub fn increment_tool_errors() {
METRICS.tool_errors.fetch_add(1, Ordering::Relaxed);
metrics::increment_counter!("selfware_tool_errors_total");
}
pub fn add_tokens_processed(count: u64) {
METRICS.tokens_processed.fetch_add(count, Ordering::Relaxed);
metrics::counter!("selfware_tokens_processed_total", count);
}
pub fn get_metrics() -> &'static Metrics {
&METRICS
}
pub fn record_workflow_run(
workflow_name: &str,
status: &str,
duration_ms: u64,
llm_calls: u64,
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
estimated_cost_usd: f64,
) {
metrics::counter!(
"selfware_workflow_runs_total",
1,
"workflow" => workflow_name.to_string(),
"status" => status.to_string()
);
metrics::histogram!(
"selfware_workflow_duration_ms",
duration_ms as f64,
"workflow" => workflow_name.to_string(),
"status" => status.to_string()
);
metrics::counter!(
"selfware_workflow_llm_calls_total",
llm_calls,
"workflow" => workflow_name.to_string()
);
metrics::counter!(
"selfware_workflow_prompt_tokens_total",
prompt_tokens,
"workflow" => workflow_name.to_string()
);
metrics::counter!(
"selfware_workflow_completion_tokens_total",
completion_tokens,
"workflow" => workflow_name.to_string()
);
metrics::counter!(
"selfware_workflow_total_tokens_total",
total_tokens,
"workflow" => workflow_name.to_string()
);
metrics::histogram!(
"selfware_workflow_estimated_cost_usd",
estimated_cost_usd,
"workflow" => workflow_name.to_string()
);
}
pub fn record_workflow_llm_call(
workflow_name: &str,
model: &str,
latency_ms: u64,
prompt_tokens: u64,
completion_tokens: u64,
total_tokens: u64,
estimated_cost_usd: f64,
) {
metrics::counter!(
"selfware_workflow_llm_requests_total",
1,
"workflow" => workflow_name.to_string(),
"model" => model.to_string()
);
metrics::histogram!(
"selfware_workflow_llm_latency_ms",
latency_ms as f64,
"workflow" => workflow_name.to_string(),
"model" => model.to_string()
);
metrics::counter!(
"selfware_workflow_prompt_tokens_total",
prompt_tokens,
"workflow" => workflow_name.to_string(),
"model" => model.to_string()
);
metrics::counter!(
"selfware_workflow_completion_tokens_total",
completion_tokens,
"workflow" => workflow_name.to_string(),
"model" => model.to_string()
);
metrics::counter!(
"selfware_workflow_total_tokens_total",
total_tokens,
"workflow" => workflow_name.to_string(),
"model" => model.to_string()
);
metrics::histogram!(
"selfware_workflow_llm_estimated_cost_usd",
estimated_cost_usd,
"workflow" => workflow_name.to_string(),
"model" => model.to_string()
);
}
pub fn start_prometheus_exporter(bind_addr: std::net::SocketAddr) -> anyhow::Result<()> {
PrometheusBuilder::new()
.with_http_listener(bind_addr)
.install()
.map_err(|e| anyhow::anyhow!("Failed to start Prometheus exporter: {}", e))?;
metrics::describe_counter!(
"selfware_api_requests_total",
"Total number of LLM API requests made"
);
metrics::describe_counter!(
"selfware_api_errors_total",
"Total number of LLM API errors"
);
metrics::describe_counter!(
"selfware_tool_executions_total",
"Total number of tool executions"
);
metrics::describe_counter!(
"selfware_tool_errors_total",
"Total number of tool execution errors"
);
metrics::describe_counter!(
"selfware_tokens_processed_total",
"Total number of tokens processed"
);
metrics::describe_counter!(
"selfware_workflow_runs_total",
"Total number of workflow executions"
);
metrics::describe_histogram!(
"selfware_workflow_duration_ms",
"Workflow execution duration in milliseconds"
);
metrics::describe_counter!(
"selfware_workflow_llm_calls_total",
"Total number of LLM calls made during workflow execution"
);
metrics::describe_counter!(
"selfware_workflow_llm_requests_total",
"Total number of workflow LLM requests"
);
metrics::describe_histogram!(
"selfware_workflow_llm_latency_ms",
"Workflow LLM request latency in milliseconds"
);
metrics::describe_counter!(
"selfware_workflow_prompt_tokens_total",
"Total prompt tokens consumed by workflows"
);
metrics::describe_counter!(
"selfware_workflow_completion_tokens_total",
"Total completion tokens generated by workflows"
);
metrics::describe_counter!(
"selfware_workflow_total_tokens_total",
"Total tokens consumed by workflows"
);
metrics::describe_histogram!(
"selfware_workflow_estimated_cost_usd",
"Estimated workflow execution cost in USD"
);
metrics::describe_histogram!(
"selfware_workflow_llm_estimated_cost_usd",
"Estimated workflow LLM request cost in USD"
);
metrics::describe_counter!(
"swl_guardrail_checks_total",
"Total number of guardrail checks performed"
);
metrics::describe_counter!(
"swl_guardrail_violations_total",
"Total number of guardrail violations detected"
);
Ok(())
}
#[macro_export]
macro_rules! tool_span {
($tool_name:expr) => {
tracing::info_span!(
"tool_execution",
tool_name = $tool_name,
duration_ms = tracing::field::Empty,
success = tracing::field::Empty,
error = tracing::field::Empty,
)
};
}
pub async fn track_tool_execution<F, Fut, T, E>(tool_name: &str, f: F) -> Result<T, E>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
let start = Instant::now();
let safe_name = redact_secrets(&sanitize_for_log(tool_name));
let span = info_span!(
"tool.execute",
tool_name = safe_name.as_str(),
duration_ms = tracing::field::Empty,
success = tracing::field::Empty,
error = tracing::field::Empty,
);
increment_tool_executions();
async {
info!("Starting tool execution");
match f().await {
Ok(result) => {
let duration = start.elapsed().as_millis() as u64;
span.record("duration_ms", duration);
span.record("success", true);
info!(
duration_ms = duration,
"Tool execution completed successfully"
);
Ok(result)
}
Err(e) => {
increment_tool_errors();
let duration = start.elapsed().as_millis() as u64;
let safe_err = redact_secrets(&sanitize_for_log(&e.to_string()));
span.record("duration_ms", duration);
span.record("success", false);
span.record("error", safe_err.as_str());
error!(
duration_ms = duration,
error = safe_err.as_str(),
"Tool execution failed"
);
Err(e)
}
}
}
.instrument(span.clone())
.await
}
pub fn record_success() {
Span::current().record("success", true);
if should_sample() {
info!("Operation completed successfully");
}
increment_log_count();
}
pub fn record_failure(error: &str) {
let safe_err = redact_secrets(&sanitize_for_log(error));
Span::current().record("success", false);
Span::current().record("error", safe_err.as_str());
error!(error = safe_err.as_str(), "Operation failed");
}
pub fn enter_agent_step(state: &str, step: usize) -> tracing::span::Span {
let safe_state = sanitize_for_log(state);
let span = info_span!("agent.step", state = safe_state.as_str(), step = step,);
span
}
pub fn record_state_transition(from: &str, to: &str) {
let safe_from = sanitize_for_log(from);
let safe_to = sanitize_for_log(to);
if should_sample() {
info!(
from = safe_from.as_str(),
to = safe_to.as_str(),
"Agent state transition"
);
}
increment_log_count();
}
#[cfg(test)]
pub fn init_test_tracing() {
let _ = tracing_subscriber::fmt()
.with_test_writer()
.with_max_level(tracing::Level::DEBUG)
.try_init();
}
#[cfg(test)]
#[path = "../../tests/unit/observability/telemetry/telemetry_test.rs"]
mod tests;