pub mod config;
#[cfg(any(feature = "otel", feature = "otel-grpc", feature = "otel-http"))]
mod otel;
pub struct TelemetryEvent {
pub filter_name: Option<String>,
pub command: String,
pub input_lines: u64,
pub output_lines: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub raw_tokens: u64,
pub filter_duration_secs: f64,
pub exit_code: i32,
pub pipeline: Option<String>,
}
impl TelemetryEvent {
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::too_many_arguments
)]
pub fn new(
filter_name: Option<String>,
command: String,
input_bytes: usize,
output_bytes: usize,
raw_bytes: usize,
raw_output: &str,
filtered_output: &str,
filter_duration: std::time::Duration,
exit_code: i32,
) -> Self {
Self {
filter_name,
command,
input_lines: raw_output.lines().count() as u64,
output_lines: filtered_output.lines().count() as u64,
input_tokens: (input_bytes / 4) as u64,
output_tokens: (output_bytes / 4) as u64,
raw_tokens: (raw_bytes / 4) as u64,
filter_duration_secs: filter_duration.as_secs_f64(),
exit_code,
pipeline: std::env::var("TOKF_OTEL_PIPELINE").ok(),
}
}
}
pub trait TelemetryReporter: Send + Sync {
fn report(&self, event: &TelemetryEvent);
fn shutdown(&self) -> bool;
fn endpoint_description(&self) -> Option<String> {
None
}
}
pub struct NoopReporter;
impl TelemetryReporter for NoopReporter {
fn report(&self, _event: &TelemetryEvent) {}
fn shutdown(&self) -> bool {
true
}
}
pub fn init(otel_export_requested: bool) -> Box<dyn TelemetryReporter> {
let mut cfg = config::load();
if otel_export_requested {
cfg.enabled = true;
}
if !cfg.enabled {
return Box::new(NoopReporter);
}
init_enabled(otel_export_requested, &cfg)
}
#[cfg(any(feature = "otel", feature = "otel-grpc", feature = "otel-http"))]
fn init_enabled(_requested: bool, cfg: &config::TelemetryConfig) -> Box<dyn TelemetryReporter> {
match otel::OtelReporter::new(cfg) {
Ok(reporter) => Box::new(reporter),
Err(e) => {
eprintln!("[tokf] warning: OTel init failed ({e:#}), metrics disabled");
Box::new(NoopReporter)
}
}
}
#[cfg(not(any(feature = "otel", feature = "otel-grpc", feature = "otel-http")))]
fn init_enabled(requested: bool, _cfg: &config::TelemetryConfig) -> Box<dyn TelemetryReporter> {
if requested {
eprintln!(
"[tokf] warning: OTel support not compiled in (need --features otel or otel-grpc)"
);
}
Box::new(NoopReporter)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_noop_reporter_report_and_shutdown() {
let reporter = NoopReporter;
let event = TelemetryEvent {
filter_name: Some("test/filter".to_string()),
command: "test command".to_string(),
input_lines: 100,
output_lines: 50,
input_tokens: 200,
output_tokens: 100,
raw_tokens: 200,
filter_duration_secs: 0.01,
exit_code: 0,
pipeline: None,
};
reporter.report(&event);
let _ = reporter.shutdown();
}
#[test]
fn test_noop_init_does_not_panic() {
let reporter = NoopReporter;
reporter.report(&TelemetryEvent::new(
None,
"ls".to_string(),
120,
120,
120,
"line1\nline2\n",
"line1\nline2\n",
std::time::Duration::ZERO,
0,
));
let _ = reporter.shutdown();
}
#[test]
fn test_noop_reporter_endpoint_description_is_none() {
assert!(NoopReporter.endpoint_description().is_none());
}
#[test]
fn test_noop_reporter_shutdown_returns_true() {
assert!(NoopReporter.shutdown());
}
#[test]
fn test_telemetry_event_new_computes_fields() {
let raw = "line1\nline2\nline3\n";
let filtered = "summary\n";
let event = TelemetryEvent::new(
Some("cargo/build".to_string()),
"cargo build".to_string(),
400, 100, 400, raw,
filtered,
std::time::Duration::from_millis(5),
0,
);
assert_eq!(event.input_lines, 3);
assert_eq!(event.output_lines, 1);
assert_eq!(event.input_tokens, 100); assert_eq!(event.output_tokens, 25); assert!((event.filter_duration_secs - 0.005).abs() < 0.001);
assert_eq!(event.exit_code, 0);
assert_eq!(event.filter_name, Some("cargo/build".to_string()));
assert_eq!(event.command, "cargo build");
}
#[test]
fn test_telemetry_event_new_passthrough() {
let output = "hello\nworld\n";
let event = TelemetryEvent::new(
None,
"ls".to_string(),
48,
48,
48,
output,
output,
std::time::Duration::ZERO,
0,
);
assert_eq!(event.input_lines, event.output_lines);
assert_eq!(event.input_tokens, event.output_tokens);
assert!(event.filter_duration_secs.abs() < f64::EPSILON);
assert!(event.filter_name.is_none());
}
#[cfg(not(any(feature = "otel", feature = "otel-grpc", feature = "otel-http")))]
#[test]
fn test_init_without_otel_feature_returns_noop() {
let reporter = init(true); assert!(reporter.endpoint_description().is_none());
}
}