"""Pydantic models for metric label discovery MCP tools.""" import re from datetime import datetime from typing import Optional from pydantic import BaseModel, Field, field_validator MAX_LABEL_RESULTS = 100 class ListMetricLabelsRequest(BaseModel): """Input model for list_metric_labels requests.""" cluster_id: str = Field(..., description="Cluster identifier") env: str = Field( ..., description="Environment name used in biz_type resolution (e.g. prod, staging, dev)", ) deploy_type: str = Field( ..., description=( "Cluster deploy type used in biz_type resolution " "(dedicated, premium, resource_pool, starter, essential)" ), ) match: Optional[str] = Field( default=None, description=( "Optional series selector used as Prometheus match[] filter " "(e.g. '{__name__=~\"topsql_.*\"}')." ), ) start: Optional[str] = Field( default=None, description="Optional start timestamp in RFC3339 format.", ) end: Optional[str] = Field( default=None, description="Optional end timestamp in RFC3339 format.", ) limit: int = Field( default=MAX_LABEL_RESULTS, ge=1, le=MAX_LABEL_RESULTS, description=f"Maximum number of labels to return (1–{MAX_LABEL_RESULTS}).", ) @field_validator("match") @classmethod def normalize_match(cls, v: Optional[str]) -> Optional[str]: if v is None or not v.strip(): return None return v.strip() @field_validator("start", "end") @classmethod def validate_timestamp(cls, v: Optional[str]) -> Optional[str]: if v is None or not v.strip(): return None value = v.strip() try: datetime.fromisoformat(value.replace("Z", "+00:00")) return value except ValueError as e: raise ValueError(f"Invalid timestamp format: {value}. Must be RFC3339.") from e @field_validator("env", "deploy_type", mode="before") @classmethod def normalize_scope_text(cls, v: str) -> str: return v.strip().lower() @field_validator("cluster_id", mode="before") @classmethod def normalize_cluster_id(cls, v: object) -> str: if not isinstance(v, str): raise ValueError("cluster_id must be a string. Quote numeric IDs explicitly.") return str(v).strip() class ListMetricLabelValuesRequest(BaseModel): """Input model for list_metric_label_values requests.""" cluster_id: str = Field(..., description="Cluster identifier") env: str = Field( ..., description="Environment name used in biz_type resolution (e.g. prod, staging, dev)", ) deploy_type: str = Field( ..., description=( "Cluster deploy type used in biz_type resolution " "(dedicated, premium, resource_pool, starter, essential)" ), ) label: str = Field(..., description="Label name (e.g. instance, job, sql_digest)") match: Optional[str] = Field( default=None, description=( "Optional series selector used as Prometheus match[] filter (e.g. '{__name__=\"up\"}')." ), ) start: Optional[str] = Field( default=None, description="Optional start timestamp in RFC3339 format.", ) end: Optional[str] = Field( default=None, description="Optional end timestamp in RFC3339 format.", ) limit: int = Field( default=MAX_LABEL_RESULTS, ge=1, le=MAX_LABEL_RESULTS, description=f"Maximum number of values to return (1–{MAX_LABEL_RESULTS}).", ) @field_validator("label") @classmethod def validate_label(cls, v: str) -> str: value = v.strip() if not value: raise ValueError("label must not be empty") if not re.fullmatch(r"[a-zA-Z_][a-zA-Z0-9_]*", value): raise ValueError("label must be a valid Prometheus label name") return value @field_validator("match") @classmethod def normalize_match(cls, v: Optional[str]) -> Optional[str]: if v is None or not v.strip(): return None return v.strip() @field_validator("start", "end") @classmethod def validate_timestamp(cls, v: Optional[str]) -> Optional[str]: if v is None or not v.strip(): return None value = v.strip() try: datetime.fromisoformat(value.replace("Z", "+00:00")) return value except ValueError as e: raise ValueError(f"Invalid timestamp format: {value}. Must be RFC3339.") from e @field_validator("env", "deploy_type", mode="before") @classmethod def normalize_scope_text(cls, v: str) -> str: return v.strip().lower() @field_validator("cluster_id", mode="before") @classmethod def normalize_cluster_id(cls, v: object) -> str: if not isinstance(v, str): raise ValueError("cluster_id must be a string. Quote numeric IDs explicitly.") return str(v).strip()