**Progress: 1,178 → 57 test errors (95% reduction)** ## Status Summary - ✅ Production code: Compiles cleanly (0 errors) - ⚠️ Test code: 57 errors remain (massive improvement) - ⚙️ All services build successfully - 📊 Warning count: 253 (target: <20) - AGENTS WILL FIX ## Remaining Test Errors (57 total) ### Primary Issues: 1. 23× E0308 mismatched types 2. 17× E0433 undeclared Decimal 3. 15× E0433 compliance module not found 4. 6× E0624 private method access 5. Various import and type issues ## Next Phase: Wave 33-2 Launch 10+ parallel agents to: - Fix remaining 57 test compilation errors - Reduce 253 warnings to <20 - Achieve 95% test coverage - Ensure all tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
947 lines
30 KiB
Rust
947 lines
30 KiB
Rust
//! Model Performance Tracking Repository
|
|
//!
|
|
//! Tracks model performance metrics, A/B testing results, and performance
|
|
//! degradation detection for HFT ML models with PostgreSQL integration.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use uuid::Uuid;
|
|
|
|
use crate::{MlDataError, PerformanceConfig, Result};
|
|
use database::{Database, DatabaseTransaction};
|
|
use sqlx::Row;
|
|
|
|
/// Performance tracking repository for ML models
|
|
#[derive(Clone)]
|
|
pub struct PerformanceRepository {
|
|
db: Database,
|
|
config: PerformanceConfig,
|
|
}
|
|
|
|
impl PerformanceRepository {
|
|
pub async fn new(db: Database, config: PerformanceConfig) -> Result<Self> {
|
|
let repo = Self { db, config };
|
|
repo.initialize_schema().await?;
|
|
Ok(repo)
|
|
}
|
|
|
|
/// Initialize database schema for performance tracking
|
|
pub async fn initialize_schema(&self) -> Result<()> {
|
|
// Initialize schema
|
|
|
|
// Model performance metrics table
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS ml_model_performance (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
model_id UUID NOT NULL,
|
|
model_name VARCHAR NOT NULL,
|
|
model_version VARCHAR NOT NULL,
|
|
environment VARCHAR NOT NULL,
|
|
timestamp TIMESTAMPTZ NOT NULL,
|
|
metric_name VARCHAR NOT NULL,
|
|
metric_value DOUBLE PRECISION NOT NULL,
|
|
metric_metadata JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
// Performance benchmarks table
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS ml_performance_benchmarks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
benchmark_name VARCHAR NOT NULL,
|
|
model_id UUID NOT NULL,
|
|
model_name VARCHAR NOT NULL,
|
|
model_version VARCHAR NOT NULL,
|
|
environment VARCHAR NOT NULL,
|
|
started_at TIMESTAMPTZ NOT NULL,
|
|
completed_at TIMESTAMPTZ,
|
|
duration_ms BIGINT,
|
|
status benchmark_status DEFAULT 'running',
|
|
results JSONB DEFAULT '{}',
|
|
error_message TEXT,
|
|
created_by VARCHAR NOT NULL,
|
|
metadata JSONB DEFAULT '{}'
|
|
)
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
// A/B testing experiments table
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS ml_ab_experiments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
experiment_name VARCHAR NOT NULL,
|
|
description TEXT,
|
|
control_model_id UUID NOT NULL,
|
|
treatment_model_id UUID NOT NULL,
|
|
started_at TIMESTAMPTZ NOT NULL,
|
|
ended_at TIMESTAMPTZ,
|
|
status experiment_status DEFAULT 'running',
|
|
traffic_split DOUBLE PRECISION NOT NULL,
|
|
confidence_level DOUBLE PRECISION NOT NULL,
|
|
results JSONB DEFAULT '{}',
|
|
created_by VARCHAR NOT NULL,
|
|
metadata JSONB DEFAULT '{}'
|
|
)
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
// A/B experiment metrics table
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS ml_ab_experiment_metrics (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
experiment_id UUID NOT NULL REFERENCES ml_ab_experiments(id) ON DELETE CASCADE,
|
|
model_variant ab_variant NOT NULL,
|
|
timestamp TIMESTAMPTZ NOT NULL,
|
|
metric_name VARCHAR NOT NULL,
|
|
metric_value DOUBLE PRECISION NOT NULL,
|
|
sample_size INTEGER NOT NULL,
|
|
metadata JSONB DEFAULT '{}'
|
|
)
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
// Performance alerts table
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
CREATE TABLE IF NOT EXISTS ml_performance_alerts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
model_id UUID NOT NULL,
|
|
model_name VARCHAR NOT NULL,
|
|
alert_type alert_type NOT NULL,
|
|
severity alert_severity NOT NULL,
|
|
metric_name VARCHAR NOT NULL,
|
|
threshold_value DOUBLE PRECISION NOT NULL,
|
|
actual_value DOUBLE PRECISION NOT NULL,
|
|
triggered_at TIMESTAMPTZ NOT NULL,
|
|
resolved_at TIMESTAMPTZ,
|
|
status alert_status DEFAULT 'active',
|
|
message TEXT NOT NULL,
|
|
metadata JSONB DEFAULT '{}'
|
|
)
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
// Create enums
|
|
self.db.execute(r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE benchmark_status AS ENUM ('running', 'completed', 'failed', 'cancelled');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#).await?;
|
|
|
|
self.db.execute(r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE experiment_status AS ENUM ('draft', 'running', 'paused', 'completed', 'failed');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#).await?;
|
|
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE ab_variant AS ENUM ('control', 'treatment');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE alert_type AS ENUM ('degradation', 'anomaly', 'threshold', 'drift');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE alert_severity AS ENUM ('low', 'medium', 'high', 'critical');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
self.db
|
|
.execute(
|
|
r#"
|
|
DO $$ BEGIN
|
|
CREATE TYPE alert_status AS ENUM ('active', 'acknowledged', 'resolved');
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
"#,
|
|
)
|
|
.await?;
|
|
|
|
// Indexes for performance
|
|
self.db.execute("CREATE INDEX IF NOT EXISTS idx_performance_model_timestamp ON ml_model_performance(model_id, timestamp DESC)").await?;
|
|
self.db.execute("CREATE INDEX IF NOT EXISTS idx_performance_metric_name ON ml_model_performance(metric_name, timestamp DESC)").await?;
|
|
self.db.execute("CREATE INDEX IF NOT EXISTS idx_benchmarks_model ON ml_performance_benchmarks(model_id, started_at DESC)").await?;
|
|
self.db.execute("CREATE INDEX IF NOT EXISTS idx_experiments_status ON ml_ab_experiments(status, started_at DESC)").await?;
|
|
self.db.execute("CREATE INDEX IF NOT EXISTS idx_alerts_model ON ml_performance_alerts(model_id, triggered_at DESC)").await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Record model performance metrics
|
|
pub async fn record_metrics(&self, request: RecordMetricsRequest) -> Result<()> {
|
|
let mut tx = self.db.begin_transaction().await?;
|
|
|
|
let metric_count = request.metrics.len();
|
|
for metric in &request.metrics {
|
|
let query = format!(
|
|
r#"INSERT INTO ml_model_performance
|
|
(model_id, model_name, model_version, environment, timestamp,
|
|
metric_name, metric_value, metric_metadata)
|
|
VALUES ('{}', '{}', '{}', '{}', '{}', '{}', {}, '{}')"#,
|
|
request.model_id,
|
|
request.model_name,
|
|
request.model_version,
|
|
request.environment,
|
|
metric.timestamp.to_rfc3339(),
|
|
metric.name,
|
|
metric.value,
|
|
metric.metadata.to_string().replace("'", "''")
|
|
);
|
|
tx.execute(&query).await?;
|
|
// Check for performance alerts
|
|
self.check_performance_threshold(&mut tx, &request, &metric)
|
|
.await?;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
|
|
tracing::info!(
|
|
"Recorded {} metrics for model {} in {}",
|
|
metric_count,
|
|
request.model_name,
|
|
request.environment
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get performance metrics for a model
|
|
pub async fn get_performance(
|
|
&self,
|
|
model_id: Uuid,
|
|
metric_names: Option<Vec<String>>,
|
|
time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
|
limit: Option<usize>,
|
|
) -> Result<ModelPerformance> {
|
|
let mut conn = self.db.acquire().await?;
|
|
|
|
let _query = r#"
|
|
SELECT timestamp, metric_name, metric_value, metric_metadata
|
|
FROM ml_model_performance
|
|
WHERE model_id = $1
|
|
"#
|
|
.to_string();
|
|
|
|
// Using sqlx query builder pattern
|
|
let mut query_builder = sqlx::QueryBuilder::new(
|
|
"SELECT timestamp, metric_name, metric_value, metric_metadata FROM ml_model_performance WHERE model_id = "
|
|
);
|
|
query_builder.push_bind(model_id);
|
|
|
|
// Add metric name filter
|
|
if let Some(ref names) = metric_names {
|
|
query_builder.push(" AND metric_name = ANY(");
|
|
query_builder.push_bind(names);
|
|
query_builder.push(")");
|
|
}
|
|
|
|
// Add time range filter
|
|
if let Some((start, end)) = time_range {
|
|
query_builder.push(" AND timestamp >= ");
|
|
query_builder.push_bind(start);
|
|
query_builder.push(" AND timestamp <= ");
|
|
query_builder.push_bind(end);
|
|
}
|
|
|
|
query_builder.push(" ORDER BY timestamp DESC");
|
|
|
|
// Add limit
|
|
if let Some(limit_val) = limit {
|
|
query_builder.push(" LIMIT ");
|
|
query_builder.push_bind(limit_val as i64);
|
|
}
|
|
|
|
let query = query_builder.build();
|
|
let rows = query.fetch_all(&mut *conn).await?;
|
|
|
|
let mut metrics = Vec::new();
|
|
for row in rows {
|
|
metrics.push(PerformanceMetric {
|
|
timestamp: row.get("timestamp"),
|
|
name: row.get("metric_name"),
|
|
value: row.get("metric_value"),
|
|
metadata: row.get("metric_metadata"),
|
|
});
|
|
}
|
|
|
|
// Calculate summary statistics
|
|
let summary = self.calculate_performance_summary(&metrics);
|
|
let last_updated = metrics.first().map(|m| m.timestamp);
|
|
|
|
Ok(ModelPerformance {
|
|
model_id,
|
|
metrics,
|
|
summary,
|
|
last_updated,
|
|
})
|
|
}
|
|
|
|
/// Start a performance benchmark
|
|
pub async fn start_benchmark(&self, request: StartBenchmarkRequest) -> Result<BenchmarkResult> {
|
|
let benchmark_id = Uuid::new_v4();
|
|
|
|
let mut conn = self.db.acquire().await?;
|
|
sqlx::query(
|
|
r#"INSERT INTO ml_performance_benchmarks
|
|
(id, benchmark_name, model_id, model_name, model_version, environment,
|
|
started_at, created_by, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)"#,
|
|
)
|
|
.bind(&benchmark_id)
|
|
.bind(&request.benchmark_name)
|
|
.bind(&request.model_id)
|
|
.bind(&request.model_name)
|
|
.bind(&request.model_version)
|
|
.bind(&request.environment)
|
|
.bind(&request.started_at)
|
|
.bind(&request.created_by)
|
|
.bind(&request.metadata)
|
|
.execute(conn.as_mut())
|
|
.await?;
|
|
|
|
let benchmark = BenchmarkResult {
|
|
id: benchmark_id,
|
|
benchmark_name: request.benchmark_name,
|
|
model_id: request.model_id,
|
|
model_name: request.model_name,
|
|
model_version: request.model_version,
|
|
environment: request.environment,
|
|
started_at: request.started_at,
|
|
completed_at: None,
|
|
duration_ms: None,
|
|
status: BenchmarkStatus::Running,
|
|
results: serde_json::Value::Object(serde_json::Map::new()),
|
|
error_message: None,
|
|
created_by: request.created_by,
|
|
metadata: request.metadata,
|
|
};
|
|
|
|
tracing::info!(
|
|
"Started benchmark {} for model {}",
|
|
benchmark.benchmark_name,
|
|
benchmark.model_name
|
|
);
|
|
|
|
Ok(benchmark)
|
|
}
|
|
|
|
/// Complete a performance benchmark
|
|
pub async fn complete_benchmark(
|
|
&self,
|
|
benchmark_id: Uuid,
|
|
results: serde_json::Value,
|
|
error_message: Option<String>,
|
|
) -> Result<()> {
|
|
let mut conn = self.db.acquire().await?;
|
|
let completed_at = Utc::now();
|
|
|
|
// Get start time to calculate duration
|
|
let start_time: DateTime<Utc> =
|
|
sqlx::query_scalar("SELECT started_at FROM ml_performance_benchmarks WHERE id = $1")
|
|
.bind(&benchmark_id)
|
|
.fetch_one(conn.as_mut())
|
|
.await?;
|
|
|
|
let duration_ms = (completed_at - start_time).num_milliseconds();
|
|
let status = if error_message.is_some() {
|
|
BenchmarkStatus::Failed
|
|
} else {
|
|
BenchmarkStatus::Completed
|
|
};
|
|
|
|
sqlx::query(
|
|
r#"UPDATE ml_performance_benchmarks
|
|
SET completed_at = $1, duration_ms = $2, status = $3,
|
|
results = $4, error_message = $5
|
|
WHERE id = $6"#,
|
|
)
|
|
.bind(&completed_at)
|
|
.bind(&duration_ms)
|
|
.bind(&status.to_string())
|
|
.bind(&results)
|
|
.bind(&error_message)
|
|
.bind(&benchmark_id)
|
|
.execute(conn.as_mut())
|
|
.await?;
|
|
|
|
tracing::info!("Completed benchmark {} in {}ms", benchmark_id, duration_ms);
|
|
Ok(())
|
|
}
|
|
|
|
/// Create A/B testing experiment
|
|
pub async fn create_experiment(
|
|
&self,
|
|
request: CreateExperimentRequest,
|
|
) -> Result<AbTestExperiment> {
|
|
let mut conn = self.db.acquire().await?;
|
|
let experiment_id = Uuid::new_v4();
|
|
|
|
sqlx::query(
|
|
r#"INSERT INTO ml_ab_experiments
|
|
(id, experiment_name, description, control_model_id, treatment_model_id,
|
|
started_at, traffic_split, confidence_level, created_by, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)"#,
|
|
)
|
|
.bind(&experiment_id)
|
|
.bind(&request.experiment_name)
|
|
.bind(&request.description)
|
|
.bind(&request.control_model_id)
|
|
.bind(&request.treatment_model_id)
|
|
.bind(&request.started_at)
|
|
.bind(&request.traffic_split)
|
|
.bind(&request.confidence_level)
|
|
.bind(&request.created_by)
|
|
.bind(&request.metadata)
|
|
.execute(conn.as_mut())
|
|
.await?;
|
|
|
|
let experiment = AbTestExperiment {
|
|
id: experiment_id,
|
|
experiment_name: request.experiment_name,
|
|
description: request.description,
|
|
control_model_id: request.control_model_id,
|
|
treatment_model_id: request.treatment_model_id,
|
|
started_at: request.started_at,
|
|
ended_at: None,
|
|
status: ExperimentStatus::Running,
|
|
traffic_split: request.traffic_split,
|
|
confidence_level: request.confidence_level,
|
|
results: serde_json::Value::Object(serde_json::Map::new()),
|
|
created_by: request.created_by,
|
|
metadata: request.metadata,
|
|
metrics: Vec::new(),
|
|
};
|
|
|
|
tracing::info!("Created A/B experiment: {}", experiment.experiment_name);
|
|
Ok(experiment)
|
|
}
|
|
|
|
/// Record A/B experiment metrics
|
|
pub async fn record_experiment_metrics(
|
|
&self,
|
|
experiment_id: Uuid,
|
|
metrics: Vec<ExperimentMetric>,
|
|
) -> Result<()> {
|
|
let mut tx = self.db.begin_transaction().await?;
|
|
|
|
let metric_count = metrics.len();
|
|
for metric in metrics {
|
|
let query = format!(
|
|
r#"INSERT INTO ml_ab_experiment_metrics
|
|
(experiment_id, model_variant, timestamp, metric_name,
|
|
metric_value, sample_size, metadata)
|
|
VALUES ('{}', '{}', '{}', '{}', {}, {}, '{}')"#,
|
|
experiment_id,
|
|
metric.variant.to_string(),
|
|
metric.timestamp.to_rfc3339(),
|
|
metric.metric_name,
|
|
metric.metric_value,
|
|
metric.sample_size,
|
|
metric.metadata.to_string().replace("'", "''")
|
|
);
|
|
tx.execute(&query).await?;
|
|
}
|
|
|
|
tx.commit().await?;
|
|
tracing::info!("Recorded {} experiment metrics", metric_count);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get active performance alerts
|
|
pub async fn get_active_alerts(&self, model_id: Option<Uuid>) -> Result<Vec<PerformanceAlert>> {
|
|
let mut conn = self.db.acquire().await?;
|
|
|
|
// Using sqlx query builder pattern
|
|
let rows = if let Some(model_id) = model_id {
|
|
sqlx::query_as::<
|
|
_,
|
|
(
|
|
Uuid,
|
|
Uuid,
|
|
String,
|
|
String,
|
|
String,
|
|
String,
|
|
f64,
|
|
f64,
|
|
DateTime<Utc>,
|
|
String,
|
|
serde_json::Value,
|
|
),
|
|
>(
|
|
r#"SELECT id, model_id, model_name, alert_type, severity, metric_name,
|
|
threshold_value, actual_value, triggered_at, message, metadata
|
|
FROM ml_performance_alerts
|
|
WHERE model_id = $1 AND status = 'active'
|
|
ORDER BY triggered_at DESC"#,
|
|
)
|
|
.bind(&model_id)
|
|
.fetch_all(conn.as_mut())
|
|
.await?
|
|
} else {
|
|
sqlx::query_as::<
|
|
_,
|
|
(
|
|
Uuid,
|
|
Uuid,
|
|
String,
|
|
String,
|
|
String,
|
|
String,
|
|
f64,
|
|
f64,
|
|
DateTime<Utc>,
|
|
String,
|
|
serde_json::Value,
|
|
),
|
|
>(
|
|
r#"SELECT id, model_id, model_name, alert_type, severity, metric_name,
|
|
threshold_value, actual_value, triggered_at, message, metadata
|
|
FROM ml_performance_alerts
|
|
WHERE status = 'active'
|
|
ORDER BY triggered_at DESC"#,
|
|
)
|
|
.fetch_all(conn.as_mut())
|
|
.await?
|
|
};
|
|
|
|
let mut alerts = Vec::new();
|
|
for (
|
|
id,
|
|
model_id,
|
|
model_name,
|
|
alert_type,
|
|
severity,
|
|
metric_name,
|
|
threshold_value,
|
|
actual_value,
|
|
triggered_at,
|
|
message,
|
|
metadata,
|
|
) in rows
|
|
{
|
|
alerts.push(PerformanceAlert {
|
|
id,
|
|
model_id,
|
|
model_name,
|
|
alert_type: self.parse_alert_type(&alert_type)?,
|
|
severity: self.parse_alert_severity(&severity)?,
|
|
metric_name,
|
|
threshold_value,
|
|
actual_value,
|
|
triggered_at,
|
|
resolved_at: None,
|
|
status: AlertStatus::Active,
|
|
message,
|
|
metadata,
|
|
});
|
|
}
|
|
|
|
Ok(alerts)
|
|
}
|
|
|
|
/// Check performance thresholds and create alerts
|
|
async fn check_performance_threshold(
|
|
&self,
|
|
tx: &mut DatabaseTransaction,
|
|
request: &RecordMetricsRequest,
|
|
metric: &PerformanceMetric,
|
|
) -> Result<()> {
|
|
// Check for degradation based on configuration threshold
|
|
if let Some(threshold) = self.get_metric_threshold(&metric.name) {
|
|
let degradation_detected = match metric.name.as_str() {
|
|
"accuracy" | "precision" | "recall" | "f1_score" => {
|
|
metric.value < threshold - self.config.degradation_threshold
|
|
},
|
|
"latency_ms" | "error_rate" => {
|
|
metric.value > threshold + self.config.degradation_threshold
|
|
},
|
|
_ => false,
|
|
};
|
|
|
|
if degradation_detected {
|
|
let alert_id = Uuid::new_v4();
|
|
let message = format!(
|
|
"Performance degradation detected for {}: {} (threshold: {})",
|
|
metric.name, metric.value, threshold
|
|
);
|
|
let query = format!(
|
|
r#"INSERT INTO ml_performance_alerts
|
|
(id, model_id, model_name, alert_type, severity, metric_name,
|
|
threshold_value, actual_value, triggered_at, message)
|
|
VALUES ('{}', '{}', '{}', 'degradation', 'medium', '{}', {}, {}, '{}', '{}')"#,
|
|
alert_id,
|
|
request.model_id,
|
|
request.model_name,
|
|
metric.name,
|
|
threshold,
|
|
metric.value,
|
|
metric.timestamp.to_rfc3339(),
|
|
message.replace("'", "''")
|
|
);
|
|
tx.execute(&query).await?;
|
|
tracing::warn!(
|
|
"Performance alert triggered for model {} metric {}",
|
|
request.model_name,
|
|
metric.name
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get metric threshold (placeholder - would be configurable)
|
|
fn get_metric_threshold(&self, metric_name: &str) -> Option<f64> {
|
|
match metric_name {
|
|
"accuracy" => Some(0.95),
|
|
"precision" => Some(0.90),
|
|
"recall" => Some(0.90),
|
|
"f1_score" => Some(0.90),
|
|
"latency_ms" => Some(100.0),
|
|
"error_rate" => Some(0.01),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Calculate performance summary statistics
|
|
fn calculate_performance_summary(&self, metrics: &[PerformanceMetric]) -> PerformanceSummary {
|
|
let mut metric_groups: HashMap<String, Vec<f64>> = HashMap::new();
|
|
|
|
for metric in metrics {
|
|
metric_groups
|
|
.entry(metric.name.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(metric.value);
|
|
}
|
|
|
|
let mut summaries = HashMap::new();
|
|
for (name, values) in metric_groups {
|
|
if !values.is_empty() {
|
|
let sum: f64 = values.iter().sum();
|
|
let mean = sum / values.len() as f64;
|
|
let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
|
|
let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
|
|
|
|
summaries.insert(
|
|
name,
|
|
MetricSummary {
|
|
count: values.len(),
|
|
mean,
|
|
min,
|
|
max,
|
|
latest: values[0], // First value is latest due to DESC order
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
PerformanceSummary {
|
|
total_metrics: metrics.len(),
|
|
metric_summaries: summaries,
|
|
time_range: if metrics.is_empty() {
|
|
None
|
|
} else {
|
|
Some((
|
|
metrics.last().unwrap().timestamp,
|
|
metrics.first().unwrap().timestamp,
|
|
))
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Parse alert type from database
|
|
fn parse_alert_type(&self, type_str: &str) -> Result<AlertType> {
|
|
match type_str {
|
|
"degradation" => Ok(AlertType::Degradation),
|
|
"anomaly" => Ok(AlertType::Anomaly),
|
|
"threshold" => Ok(AlertType::Threshold),
|
|
"drift" => Ok(AlertType::Drift),
|
|
_ => Err(MlDataError::Validation {
|
|
message: format!("Invalid alert type: {}", type_str),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Parse alert severity from database
|
|
fn parse_alert_severity(&self, severity_str: &str) -> Result<AlertSeverity> {
|
|
match severity_str {
|
|
"low" => Ok(AlertSeverity::Low),
|
|
"medium" => Ok(AlertSeverity::Medium),
|
|
"high" => Ok(AlertSeverity::High),
|
|
"critical" => Ok(AlertSeverity::Critical),
|
|
_ => Err(MlDataError::Validation {
|
|
message: format!("Invalid alert severity: {}", severity_str),
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Health check for performance repository
|
|
pub async fn health_check(&self) -> Result<bool> {
|
|
let mut conn = self.db.acquire().await?;
|
|
let _: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM ml_model_performance")
|
|
.fetch_one(conn.as_mut())
|
|
.await?;
|
|
Ok(true)
|
|
}
|
|
}
|
|
|
|
/// Request to record performance metrics
|
|
#[derive(Debug)]
|
|
pub struct RecordMetricsRequest {
|
|
pub model_id: Uuid,
|
|
pub model_name: String,
|
|
pub model_version: String,
|
|
pub environment: String,
|
|
pub metrics: Vec<PerformanceMetric>,
|
|
}
|
|
|
|
/// Individual performance metric
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PerformanceMetric {
|
|
pub timestamp: DateTime<Utc>,
|
|
pub name: String,
|
|
pub value: f64,
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// Model performance data
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ModelPerformance {
|
|
pub model_id: Uuid,
|
|
pub metrics: Vec<PerformanceMetric>,
|
|
pub summary: PerformanceSummary,
|
|
pub last_updated: Option<DateTime<Utc>>,
|
|
}
|
|
|
|
/// Performance summary statistics
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PerformanceSummary {
|
|
pub total_metrics: usize,
|
|
pub metric_summaries: HashMap<String, MetricSummary>,
|
|
pub time_range: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
|
}
|
|
|
|
/// Summary statistics for a specific metric
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MetricSummary {
|
|
pub count: usize,
|
|
pub mean: f64,
|
|
pub min: f64,
|
|
pub max: f64,
|
|
pub latest: f64,
|
|
}
|
|
|
|
/// Performance benchmark request
|
|
#[derive(Debug)]
|
|
pub struct StartBenchmarkRequest {
|
|
pub benchmark_name: String,
|
|
pub model_id: Uuid,
|
|
pub model_name: String,
|
|
pub model_version: String,
|
|
pub environment: String,
|
|
pub started_at: DateTime<Utc>,
|
|
pub created_by: String,
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// Benchmark result
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct BenchmarkResult {
|
|
pub id: Uuid,
|
|
pub benchmark_name: String,
|
|
pub model_id: Uuid,
|
|
pub model_name: String,
|
|
pub model_version: String,
|
|
pub environment: String,
|
|
pub started_at: DateTime<Utc>,
|
|
pub completed_at: Option<DateTime<Utc>>,
|
|
pub duration_ms: Option<i64>,
|
|
pub status: BenchmarkStatus,
|
|
pub results: serde_json::Value,
|
|
pub error_message: Option<String>,
|
|
pub created_by: String,
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// Benchmark status
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum BenchmarkStatus {
|
|
Running,
|
|
Completed,
|
|
Failed,
|
|
Cancelled,
|
|
}
|
|
|
|
impl ToString for BenchmarkStatus {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
BenchmarkStatus::Running => "running".to_string(),
|
|
BenchmarkStatus::Completed => "completed".to_string(),
|
|
BenchmarkStatus::Failed => "failed".to_string(),
|
|
BenchmarkStatus::Cancelled => "cancelled".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create A/B experiment request
|
|
#[derive(Debug)]
|
|
pub struct CreateExperimentRequest {
|
|
pub experiment_name: String,
|
|
pub description: Option<String>,
|
|
pub control_model_id: Uuid,
|
|
pub treatment_model_id: Uuid,
|
|
pub started_at: DateTime<Utc>,
|
|
pub traffic_split: f64,
|
|
pub confidence_level: f64,
|
|
pub created_by: String,
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// A/B testing experiment
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AbTestExperiment {
|
|
pub id: Uuid,
|
|
pub experiment_name: String,
|
|
pub description: Option<String>,
|
|
pub control_model_id: Uuid,
|
|
pub treatment_model_id: Uuid,
|
|
pub started_at: DateTime<Utc>,
|
|
pub ended_at: Option<DateTime<Utc>>,
|
|
pub status: ExperimentStatus,
|
|
pub traffic_split: f64,
|
|
pub confidence_level: f64,
|
|
pub results: serde_json::Value,
|
|
pub created_by: String,
|
|
pub metadata: serde_json::Value,
|
|
pub metrics: Vec<ExperimentMetric>,
|
|
}
|
|
|
|
/// Experiment status
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum ExperimentStatus {
|
|
Draft,
|
|
Running,
|
|
Paused,
|
|
Completed,
|
|
Failed,
|
|
}
|
|
|
|
/// Experiment metric data
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ExperimentMetric {
|
|
pub variant: AbVariant,
|
|
pub timestamp: DateTime<Utc>,
|
|
pub metric_name: String,
|
|
pub metric_value: f64,
|
|
pub sample_size: i32,
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// A/B test variant
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum AbVariant {
|
|
Control,
|
|
Treatment,
|
|
}
|
|
|
|
impl ToString for AbVariant {
|
|
fn to_string(&self) -> String {
|
|
match self {
|
|
AbVariant::Control => "control".to_string(),
|
|
AbVariant::Treatment => "treatment".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performance alert
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PerformanceAlert {
|
|
pub id: Uuid,
|
|
pub model_id: Uuid,
|
|
pub model_name: String,
|
|
pub alert_type: AlertType,
|
|
pub severity: AlertSeverity,
|
|
pub metric_name: String,
|
|
pub threshold_value: f64,
|
|
pub actual_value: f64,
|
|
pub triggered_at: DateTime<Utc>,
|
|
pub resolved_at: Option<DateTime<Utc>>,
|
|
pub status: AlertStatus,
|
|
pub message: String,
|
|
pub metadata: serde_json::Value,
|
|
}
|
|
|
|
/// Alert type
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum AlertType {
|
|
Degradation,
|
|
Anomaly,
|
|
Threshold,
|
|
Drift,
|
|
}
|
|
|
|
/// Alert severity
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum AlertSeverity {
|
|
Low,
|
|
Medium,
|
|
High,
|
|
Critical,
|
|
}
|
|
|
|
/// Alert status
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum AlertStatus {
|
|
Active,
|
|
Acknowledged,
|
|
Resolved,
|
|
}
|
|
|
|
// TECHNICAL DEBT ELIMINATED - Use HashMap<String, Vec<PerformanceMetric>> directly
|