use std::{
io,
sync::OnceLock,
time::{Duration, Instant},
};
use anyhow::Error;
use camino::Utf8Path;
use mdtablefix::report::FileReport;
use metrics::{Unit, counter, describe_counter, describe_histogram, histogram};
use tracing::{Span, debug, field};
use crate::driver::{ExitStatus, Mode};
pub fn describe_metrics() {
static DESCRIPTIONS: OnceLock<()> = OnceLock::new();
DESCRIPTIONS.get_or_init(register_metrics);
}
pub(super) fn register_metrics() {
describe_counter!(
"mdtablefix_run_total",
"Runs of the tool, by mode and outcome"
);
describe_counter!(
"mdtablefix_file_total",
"Files analysed by a run, by mode and outcome"
);
describe_histogram!(
"mdtablefix_file_duration_seconds",
Unit::Seconds,
"Duration of one file's analysis, by mode and outcome"
);
describe_counter!(
"mdtablefix_file_error_total",
"Files that could not be analysed, by error category"
);
}
pub enum FileOutcome<'a> {
Changed,
Unchanged,
Failed(&'a Error),
}
impl<'a> FileOutcome<'a> {
#[must_use]
pub fn of(result: &'a anyhow::Result<(FileReport, String)>) -> Self {
match result {
Ok((report, _)) if report.is_changed => Self::Changed,
Ok(_) => Self::Unchanged,
Err(error) => Self::Failed(error),
}
}
}
pub fn record_run(mode: Mode, status: ExitStatus) {
counter!(
"mdtablefix_run_total",
"mode" => mode_label(mode),
"outcome" => status_label(status)
)
.increment(1);
}
fn mode_label(mode: Mode) -> &'static str {
match mode {
Mode::Print => "print",
Mode::InPlace => "in_place",
Mode::Check => "check",
Mode::Diff => "diff",
Mode::ListFiles => "list_files",
}
}
fn status_label(status: ExitStatus) -> &'static str {
match status {
ExitStatus::Success => "success",
ExitStatus::Drift => "drift",
ExitStatus::Error => "error",
}
}
fn file_outcome_label(outcome: &FileOutcome<'_>) -> &'static str {
match outcome {
FileOutcome::Changed => "changed",
FileOutcome::Unchanged => "unchanged",
FileOutcome::Failed(_) => "error",
}
}
#[tracing::instrument(
level = "debug",
skip(analyse, mode, display_path),
fields(
mode = mode_label(mode),
path = %display_path,
outcome = field::Empty,
elapsed_seconds = field::Empty
)
)]
pub fn record_analysis(
mode: Mode,
display_path: &Utf8Path,
analyse: impl FnOnce() -> anyhow::Result<(FileReport, String)>,
) -> anyhow::Result<(FileReport, String)> {
let started = Instant::now();
let result = analyse();
let outcome = FileOutcome::of(&result);
let elapsed = started.elapsed();
let span = Span::current();
span.record("outcome", file_outcome_label(&outcome));
span.record("elapsed_seconds", elapsed.as_secs_f64());
if let FileOutcome::Failed(error) = &outcome {
debug!(error_category = category(error), "analysis failed");
}
record_file(mode, &outcome, elapsed);
result
}
pub fn record_file(mode: Mode, outcome: &FileOutcome<'_>, elapsed: Duration) {
let mode_label = mode_label(mode);
let outcome_label = file_outcome_label(outcome);
counter!(
"mdtablefix_file_total",
"mode" => mode_label,
"outcome" => outcome_label
)
.increment(1);
histogram!(
"mdtablefix_file_duration_seconds",
"mode" => mode_label,
"outcome" => outcome_label
)
.record(elapsed.as_secs_f64());
if let FileOutcome::Failed(error) = outcome {
counter!("mdtablefix_file_error_total", "category" => category(error)).increment(1);
}
}
fn category(error: &Error) -> &'static str {
match error
.chain()
.find_map(|cause| cause.downcast_ref::<io::Error>())
.map(io::Error::kind)
{
Some(io::ErrorKind::NotFound) => "not_found",
Some(io::ErrorKind::PermissionDenied) => "permission_denied",
Some(io::ErrorKind::InvalidInput) => "declined",
Some(_) | None => "other",
}
}
#[cfg(test)]
#[path = "metrics_tests.rs"]
mod metrics_tests;
#[cfg(test)]
#[path = "metrics_file_tests.rs"]
mod metrics_file_tests;