use crate::StorageError;
pub mod name {
pub const HTTP_REQUESTS: &str = "notedthat_http_requests_total";
pub const HTTP_REQUEST_DURATION: &str = "notedthat_http_request_duration_seconds";
pub const HTTP_IN_FLIGHT: &str = "notedthat_http_requests_in_flight";
pub const SEARCH_REQUESTS: &str = "notedthat_search_requests_total";
pub const SEARCH_DURATION: &str = "notedthat_search_duration_seconds";
pub const SEARCH_HITS: &str = "notedthat_search_hits";
pub const EMBEDDING_REQUESTS: &str = "notedthat_embedding_requests_total";
pub const EMBEDDING_DURATION: &str = "notedthat_embedding_duration_seconds";
pub const EMBEDDING_ERRORS: &str = "notedthat_embedding_errors_total";
pub const EMBEDDING_TEXTS: &str = "notedthat_embedding_texts";
pub const INDEX_QUEUE_DEPTH: &str = "notedthat_index_queue_depth";
pub const INDEX_QUEUE_CAPACITY: &str = "notedthat_index_queue_capacity";
pub const INDEX_EVENTS_ENQUEUED: &str = "notedthat_index_events_enqueued_total";
pub const INDEX_EVENTS_REFUSED: &str = "notedthat_index_events_refused_total";
pub const INDEX_EVENTS_COMPLETED: &str = "notedthat_index_events_completed_total";
pub const INDEX_STALE_MARKS: &str = "notedthat_index_stale_marks_total";
pub const INDEX_WORKER_ALIVE: &str = "notedthat_index_worker_alive";
pub const VECTOR_STORE_OPERATIONS: &str = "notedthat_vector_store_operations_total";
pub const VECTOR_STORE_DURATION: &str = "notedthat_vector_store_operation_duration_seconds";
pub const VECTOR_STORE_ERRORS: &str = "notedthat_vector_store_errors_total";
pub const EVENTS_PUBLISHED: &str = "notedthat_events_published_total";
pub const EVENTS_PUBLISH_FAILED: &str = "notedthat_events_publish_failed_total";
pub const EVENTS_SUBSCRIBERS: &str = "notedthat_events_subscribers";
pub const EVENTS_REPLAY_GONE: &str = "notedthat_events_replay_gone_total";
pub const FS_WATCH_LOST: &str = "notedthat_fs_watch_lost_total";
pub const RECONCILE_PASSES: &str = "notedthat_reconcile_passes_total";
pub const RECONCILE_DURATION: &str = "notedthat_reconcile_duration_seconds";
pub const RECONCILE_OBJECTS: &str = "notedthat_reconcile_objects_total";
pub const RECONCILE_OBJECTS_ON_DISK: &str = "notedthat_reconcile_objects_on_disk";
pub const STORAGE_OPERATIONS: &str = "notedthat_storage_operations_total";
pub const STORAGE_DURATION: &str = "notedthat_storage_operation_duration_seconds";
pub const STORAGE_ERRORS: &str = "notedthat_storage_errors_total";
pub const BUILD_INFO: &str = "notedthat_build_info";
}
pub mod label {
pub const SURFACE: &str = "surface";
pub const ROUTE: &str = "route";
pub const METHOD: &str = "method";
pub const STATUS: &str = "status";
pub const KB: &str = "kb";
pub const OUTCOME: &str = "outcome";
pub const OP: &str = "op";
pub const BACKEND: &str = "backend";
pub const PHASE: &str = "phase";
pub const REASON: &str = "reason";
pub const KIND: &str = "kind";
pub const CAUSE: &str = "cause";
pub const CLASS: &str = "class";
pub const ERROR_KIND: &str = "error_kind";
pub const VERSION: &str = "version";
pub const REVISION: &str = "revision";
}
pub const ROUTE_UNMATCHED: &str = "<unmatched>";
pub mod surface {
pub const API: &str = "api";
pub const WEBDAV: &str = "webdav";
pub const MCP: &str = "mcp";
pub const BROWSE: &str = "browse";
pub const ROOT: &str = "root";
}
pub mod outcome {
pub const OK: &str = "ok";
pub const NOT_FOUND: &str = "not_found";
pub const PRECONDITION: &str = "precondition";
pub const UNAVAILABLE: &str = "unavailable";
pub const ERROR: &str = "error";
pub const CANCELLED: &str = "cancelled";
}
pub mod phase {
pub const INDEX: &str = "index";
pub const QUERY: &str = "query";
}
pub mod reconcile_class {
pub const UNCHANGED: &str = "unchanged";
pub const CHANGED: &str = "changed";
pub const ORPHANED: &str = "orphaned";
}
pub mod index_outcome {
pub const SUCCEEDED: &str = "succeeded";
pub const FAILED: &str = "failed";
}
pub mod watch_lost_reason {
pub const MAX_FILES_WATCH: &str = "max_files_watch";
pub const ERROR: &str = "error";
}
pub const HISTOGRAM_BUCKETS: &[(&str, &[f64])] = &[
(
name::HTTP_REQUEST_DURATION,
&[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
],
),
(
name::SEARCH_DURATION,
&[0.01, 0.025, 0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4],
),
(
name::SEARCH_HITS,
&[0.0, 1.0, 2.0, 3.0, 5.0, 10.0, 20.0, 50.0, 100.0],
),
(
name::EMBEDDING_DURATION,
&[0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
),
(
name::EMBEDDING_TEXTS,
&[1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0],
),
(
name::STORAGE_DURATION,
&[
0.0005, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
],
),
(
name::VECTOR_STORE_DURATION,
&[
0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
],
),
(
name::RECONCILE_DURATION,
&[0.1, 0.5, 1.0, 5.0, 10.0, 30.0, 60.0, 300.0, 900.0],
),
];
#[must_use]
pub fn storage_outcome<T>(result: &Result<T, StorageError>) -> &'static str {
match result {
Ok(_) => outcome::OK,
Err(StorageError::NotFound { .. }) => outcome::NOT_FOUND,
Err(
StorageError::NotModified
| StorageError::PreconditionFailed
| StorageError::RangeNotSatisfiable { .. },
) => outcome::PRECONDITION,
Err(StorageError::BackendUnavailable { .. }) => outcome::UNAVAILABLE,
Err(_) => outcome::ERROR,
}
}
#[must_use]
pub fn storage_error_kind(error: &StorageError) -> Option<&'static str> {
match error {
StorageError::NotFound { .. }
| StorageError::NotModified
| StorageError::PreconditionFailed
| StorageError::RangeNotSatisfiable { .. } => None,
StorageError::BucketNotFound { .. } => Some("bucket_not_found"),
StorageError::BackendUnavailable { .. } => Some("backend_unavailable"),
StorageError::Other { .. } => Some("other"),
}
}
#[cfg(test)]
mod tests {
use super::{
HISTOGRAM_BUCKETS, StorageError, label, name, outcome, storage_error_kind, storage_outcome,
};
const ALL_NAMES: &[&str] = &[
name::HTTP_REQUESTS,
name::HTTP_REQUEST_DURATION,
name::HTTP_IN_FLIGHT,
name::SEARCH_REQUESTS,
name::SEARCH_DURATION,
name::SEARCH_HITS,
name::EMBEDDING_REQUESTS,
name::EMBEDDING_DURATION,
name::EMBEDDING_ERRORS,
name::EMBEDDING_TEXTS,
name::INDEX_QUEUE_DEPTH,
name::INDEX_QUEUE_CAPACITY,
name::INDEX_EVENTS_ENQUEUED,
name::INDEX_EVENTS_REFUSED,
name::INDEX_EVENTS_COMPLETED,
name::INDEX_STALE_MARKS,
name::INDEX_WORKER_ALIVE,
name::VECTOR_STORE_OPERATIONS,
name::VECTOR_STORE_DURATION,
name::VECTOR_STORE_ERRORS,
name::EVENTS_PUBLISHED,
name::EVENTS_PUBLISH_FAILED,
name::EVENTS_SUBSCRIBERS,
name::EVENTS_REPLAY_GONE,
name::FS_WATCH_LOST,
name::RECONCILE_PASSES,
name::RECONCILE_DURATION,
name::RECONCILE_OBJECTS,
name::RECONCILE_OBJECTS_ON_DISK,
name::STORAGE_OPERATIONS,
name::STORAGE_DURATION,
name::STORAGE_ERRORS,
name::BUILD_INFO,
];
#[test]
fn every_name_is_prefixed_and_well_formed() {
for metric in ALL_NAMES {
assert!(
metric.starts_with("notedthat_"),
"{metric} does not carry the project prefix"
);
assert!(
metric
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'),
"{metric} is not a valid Prometheus metric name"
);
assert!(
!(metric.ends_with("_total") && metric.contains("_duration_")),
"{metric} is spelled as both a counter and a duration"
);
}
}
#[test]
fn no_two_metrics_share_a_name() {
let mut seen = ALL_NAMES.to_vec();
seen.sort_unstable();
let before = seen.len();
seen.dedup();
assert_eq!(before, seen.len(), "two metrics share a name");
}
#[test]
fn every_histogram_has_buckets() {
for (metric, buckets) in HISTOGRAM_BUCKETS {
assert!(
ALL_NAMES.contains(metric),
"{metric} has buckets but is not in the catalogue"
);
assert!(!buckets.is_empty(), "{metric} has an empty bucket set");
assert!(
buckets.windows(2).all(|w| w[0] < w[1]),
"{metric}'s buckets must be strictly ascending"
);
}
for metric in ALL_NAMES.iter().filter(|m| m.ends_with("_seconds")) {
assert!(
HISTOGRAM_BUCKETS.iter().any(|(name, _)| name == metric),
"{metric} is a duration histogram with no buckets registered"
);
}
for metric in [name::SEARCH_HITS, name::EMBEDDING_TEXTS] {
assert!(
HISTOGRAM_BUCKETS.iter().any(|(name, _)| *name == metric),
"{metric} is a histogram with no buckets registered"
);
}
}
#[test]
fn label_keys_are_lowercase_words() {
for key in [
label::SURFACE,
label::ROUTE,
label::METHOD,
label::STATUS,
label::KB,
label::OUTCOME,
label::OP,
label::BACKEND,
label::PHASE,
label::CAUSE,
label::CLASS,
label::ERROR_KIND,
label::REASON,
label::KIND,
label::VERSION,
label::REVISION,
] {
assert!(
key.chars().all(|c| c.is_ascii_lowercase() || c == '_'),
"{key} is not a valid Prometheus label name"
);
}
}
#[test]
fn ordinary_control_flow_is_not_counted_as_a_storage_error() {
for error in [
StorageError::NotFound {
key: "some/key.md".to_string(),
},
StorageError::NotModified,
StorageError::PreconditionFailed,
StorageError::RangeNotSatisfiable { complete_length: 9 },
] {
let expected = if matches!(error, StorageError::NotFound { .. }) {
outcome::NOT_FOUND
} else {
outcome::PRECONDITION
};
let result: Result<(), StorageError> = Err(error);
assert_eq!(storage_outcome(&result), expected);
assert_eq!(
storage_error_kind(result.as_ref().unwrap_err()),
None,
"a conditional answer is the backend working, not failing"
);
}
}
#[test]
fn a_failing_backend_is_counted_as_an_error() {
for (error, kind, expected) in [
(
StorageError::BackendUnavailable {
message: "https://seaweed.internal:8333 refused the connection".to_string(),
},
"backend_unavailable",
outcome::UNAVAILABLE,
),
(
StorageError::BucketNotFound {
bucket: "notedthat-notes".to_string(),
},
"bucket_not_found",
outcome::ERROR,
),
] {
let result: Result<(), StorageError> = Err(error);
assert_eq!(storage_outcome(&result), expected);
assert_eq!(storage_error_kind(result.as_ref().unwrap_err()), Some(kind));
}
}
#[test]
fn an_unreachable_backend_is_its_own_outcome_not_the_catch_all() {
let result: Result<(), StorageError> = Err(StorageError::BackendUnavailable {
message: "connection refused".to_string(),
});
assert_eq!(storage_outcome(&result), outcome::UNAVAILABLE);
}
#[test]
fn an_error_kind_never_quotes_the_error() {
let error = StorageError::BackendUnavailable {
message: "https://seaweed.internal:8333 refused".to_string(),
};
let kind = storage_error_kind(&error).expect("a backend failure has a kind");
assert!(!kind.contains("seaweed"), "{kind} quotes the backend");
assert!(!kind.contains(':'), "{kind} looks like it carries a URL");
}
#[test]
fn ok_is_ok() {
assert_eq!(storage_outcome(&Ok::<_, StorageError>(())), outcome::OK);
}
}