use std::ffi::OsString;
pub(super) const SOURCE_PROCESS: &str = "process";
pub(super) const SOURCE_INJECTED: &str = "injected";
pub(super) const SELECTOR_NOT_CONFIGURED: &str = "not_configured";
pub(super) const SELECTOR_UNSET: &str = "unset";
pub(super) const SELECTOR_EMPTY: &str = "empty";
pub(super) const SELECTOR_ACCEPTED: &str = "accepted";
pub(super) const PRESENCE_ABSENT: &str = "absent";
pub(super) const PRESENCE_EMPTY: &str = "empty";
pub(super) const PRESENCE_PRESENT: &str = "present";
pub(super) const XDG_RESOLUTION_DEFAULT: &str = "default";
pub(super) const XDG_RESOLUTION_LIST: &str = "list";
pub(super) const HOME_FROM_HOME: &str = "home";
pub(super) const HOME_FROM_USERPROFILE: &str = "userprofile";
pub(super) const HOME_FROM_FALLBACK: &str = "fallback";
pub(super) const HOME_NONE: &str = "none";
pub(super) const CANDIDATE_REQUIRED_EXPLICIT: &str = "required_explicit";
pub(super) const CANDIDATE_EXPLICIT: &str = "explicit";
pub(super) const CANDIDATE_SELECTOR: &str = "selector";
pub(super) const CANDIDATE_XDG: &str = "xdg";
pub(super) const CANDIDATE_WINDOWS: &str = "windows";
pub(super) const CANDIDATE_HOME: &str = "home";
pub(super) const CANDIDATE_PROJECT: &str = "project";
pub(super) const CATEGORY_FILE: &str = "file";
pub(super) const CATEGORY_CYCLIC_EXTENDS: &str = "cyclic_extends";
pub(super) const CATEGORY_GATHERING: &str = "gathering";
pub(super) const CATEGORY_VALIDATION: &str = "validation";
pub(super) const CATEGORY_OTHER: &str = "other";
pub(super) const OPERATION_DISCOVER_FIRST: &str = "discover_first";
pub(super) const OPERATION_COMPOSE_LAYERS: &str = "compose_layers";
pub(super) const OUTCOME_SUCCESS: &str = "success";
pub(super) const OUTCOME_NOT_FOUND: &str = "not_found";
pub(super) const OUTCOME_OPTIONAL_FAILURE: &str = "optional_failure";
pub(super) const OUTCOME_REQUIRED_FAILURE: &str = "required_failure";
pub(super) fn presence(value: Option<&OsString>) -> &'static str {
match value {
None => PRESENCE_ABSENT,
Some(inner) if inner.is_empty() => PRESENCE_EMPTY,
Some(_) => PRESENCE_PRESENT,
}
}
pub(super) fn source_selected(source: &'static str) {
tracing::debug!(
event = "discovery.source_selected",
source,
"configuration discovery environment source selected"
);
}
pub(super) fn selector_decision(state: &'static str) {
tracing::debug!(
event = "discovery.selector",
state,
"configuration path selector resolved"
);
}
pub(super) fn xdg_decision(
config_home: &'static str,
dirs: &'static str,
resolution: &'static str,
) {
tracing::debug!(
event = "discovery.xdg",
config_home,
dirs,
resolution,
"XDG base directories resolved"
);
}
pub(super) fn home_decision(source: &'static str) {
tracing::debug!(
event = "discovery.home",
source,
"home directory for discovery resolved"
);
}
pub(super) fn attempt(operation: &'static str) {
tracing::debug!(
event = "discovery.attempt",
operation,
"configuration discovery started"
);
count_attempt(operation);
}
pub(super) const fn error_category(err: &crate::OrthoError) -> &'static str {
match err {
crate::OrthoError::File { .. } => CATEGORY_FILE,
crate::OrthoError::CyclicExtends { .. } => CATEGORY_CYCLIC_EXTENDS,
crate::OrthoError::Gathering(_) => CATEGORY_GATHERING,
crate::OrthoError::Validation { .. } => CATEGORY_VALIDATION,
_ => CATEGORY_OTHER,
}
}
pub(super) fn candidate_failure(
operation: &'static str,
required: bool,
source: &'static str,
category: &'static str,
) {
let outcome = if required {
OUTCOME_REQUIRED_FAILURE
} else {
OUTCOME_OPTIONAL_FAILURE
};
tracing::debug!(
event = "discovery.candidate",
operation,
outcome,
required,
source,
category,
"configuration candidate rejected"
);
count_outcome(operation, outcome);
count_candidate_failure(operation, source, category);
}
pub(super) fn project_root_cwd_unavailable() {
tracing::debug!(
event = "discovery.project_root",
state = "cwd_unavailable",
"working directory unavailable; no default project root added"
);
}
pub(super) fn load_outcome(
operation: &'static str,
outcome: &'static str,
source: Option<&'static str>,
) {
if let Some(rung) = source {
tracing::debug!(
event = "discovery.load",
operation,
outcome,
source = rung,
"configuration discovery finished"
);
} else {
tracing::debug!(
event = "discovery.load",
operation,
outcome,
"configuration discovery finished"
);
}
count_outcome(operation, outcome);
}
#[cfg(feature = "metrics")]
fn count_attempt(operation: &'static str) {
metrics::counter!("ortho_config.discovery.attempts", "operation" => operation).increment(1);
}
#[cfg(not(feature = "metrics"))]
const fn count_attempt(_operation: &'static str) {}
#[cfg(feature = "metrics")]
fn count_outcome(operation: &'static str, outcome: &'static str) {
metrics::counter!(
"ortho_config.discovery.outcomes",
"operation" => operation,
"outcome" => outcome,
)
.increment(1);
}
#[cfg(not(feature = "metrics"))]
const fn count_outcome(_operation: &'static str, _outcome: &'static str) {}
#[cfg(feature = "metrics")]
fn count_candidate_failure(operation: &'static str, source: &'static str, category: &'static str) {
metrics::counter!(
"ortho_config.discovery.candidate_failures",
"operation" => operation,
"source" => source,
"category" => category,
)
.increment(1);
}
#[cfg(not(feature = "metrics"))]
const fn count_candidate_failure(
_operation: &'static str,
_source: &'static str,
_category: &'static str,
) {
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn error_category_covers_every_variant() {
use crate::OrthoError;
let file = OrthoError::File {
path: std::path::PathBuf::from("demo.toml"),
source: Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, "gone")),
};
assert_eq!(error_category(&file), CATEGORY_FILE);
let cyclic = OrthoError::CyclicExtends {
cycle: String::from("a -> b -> a"),
};
assert_eq!(error_category(&cyclic), CATEGORY_CYCLIC_EXTENDS);
let gathering = OrthoError::Gathering(Box::new(figment::Error::from(String::from(
"gathering failed",
))));
assert_eq!(error_category(&gathering), CATEGORY_GATHERING);
let validation = OrthoError::Validation {
key: String::from("port"),
message: String::from("out of range"),
};
assert_eq!(error_category(&validation), CATEGORY_VALIDATION);
let other = OrthoError::CliParsing(Box::new(clap::Error::raw(
clap::error::ErrorKind::InvalidValue,
"bad flag",
)));
assert_eq!(error_category(&other), CATEGORY_OTHER);
}
#[test]
fn presence_distinguishes_absent_empty_and_present() {
assert_eq!(presence(None), PRESENCE_ABSENT);
assert_eq!(presence(Some(&OsString::new())), PRESENCE_EMPTY);
assert_eq!(presence(Some(&OsString::from("/xdg"))), PRESENCE_PRESENT);
}
}