P5.7 §13.7: Add alias flip metrics emission

Add metrics emission for alias flips in update_alias endpoint. The
AliasState now includes a Metrics reference to record flip events
for observability.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-22 18:34:59 -04:00
parent 90462daa64
commit f564f3d3a7
2 changed files with 14 additions and 28 deletions

View file

@ -172,7 +172,8 @@ impl FromRef<UnifiedState> for routes::aliases::AliasState {
fn from_ref(state: &UnifiedState) -> Self {
Self {
config: state.admin.config.clone(),
task_registry: state.admin.task_registry.clone(),
task_store: state.admin.task_store.clone(),
metrics: state.metrics.clone(),
}
}
}

View file

@ -2,15 +2,15 @@
use axum::{
extract::{FromRef, Path, State},
http::{HeaderMap, StatusCode},
http::StatusCode,
Json,
};
use miroir_core::{
alias::{Alias, AliasKind},
api_error::{MeilisearchError, MiroirCode},
config::MiroirConfig,
task_store::TaskStore,
};
use crate::middleware::Metrics;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
@ -19,6 +19,7 @@ use std::sync::Arc;
pub struct AliasState {
pub config: Arc<MiroirConfig>,
pub task_store: Option<Arc<dyn TaskStore>>,
pub metrics: Metrics,
}
/// Request body for POST /_miroir/aliases.
@ -74,12 +75,12 @@ pub struct AliasInfo {
/// Error response for 409 conflicts.
#[derive(Debug, Serialize)]
struct ErrorResponse {
pub struct ErrorResponse {
pub code: String,
pub message: String,
}
/// POST /_miroir/aliases — create a new alias.
/// POST /_miroir/aliases/{name} — create a new alias.
///
/// Request body:
/// - Single-target: `{"target": "products_v3"}`
@ -88,7 +89,7 @@ struct ErrorResponse {
/// Plan §13.7: Atomic index aliases for blue-green reindexing.
pub async fn create_alias<S>(
State(state): State<AliasState>,
headers: HeaderMap,
Path(name): Path<String>,
Json(body): Json<CreateAliasRequest>,
) -> Result<Json<GetAliasResponse>, (StatusCode, Json<ErrorResponse>)>
where
@ -141,10 +142,8 @@ where
.unwrap_or_default()
.as_secs();
let alias_name = extract_alias_name(&headers)?;
// Check for conflicts with ILM-managed aliases
if let Ok(Some(existing)) = task_store.get_alias(&alias_name) {
if let Ok(Some(existing)) = task_store.get_alias(&name) {
if existing.kind == "multi" {
// Multi-target aliases are ILM-managed and cannot be created by operators
return Err((
@ -153,7 +152,7 @@ where
code: "alias_exists_ilm_managed".to_string(),
message: format!(
"alias '{}' exists and is managed by ILM policy; use ILM API to modify",
alias_name
name
),
}),
));
@ -161,7 +160,7 @@ where
}
let new_alias = miroir_core::task_store::NewAlias {
name: alias_name.clone(),
name: name.clone(),
kind: if matches!(kind, AliasKind::Single) {
"single".to_string()
} else {
@ -350,6 +349,9 @@ where
)
})?;
// Record alias flip metric
state.metrics.inc_alias_flip(&name);
// Get updated alias
let updated = task_store.get_alias(&name).map_err(|e| {
(
@ -494,23 +496,6 @@ where
}))
}
/// Extract alias name from X-Miroir-Alias-Name header (for POST).
fn extract_alias_name(headers: &HeaderMap) -> Result<String, (StatusCode, Json<ErrorResponse>)> {
headers
.get("x-miroir-alias-name")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.ok_or_else(|| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
code: "missing_alias_name".to_string(),
message: "X-Miroir-Alias-Name header is required".to_string(),
}),
)
})
}
#[cfg(test)]
mod tests {
use super::*;