# #region TranslationMetrics [C:3] [TYPE Module] [SEMANTICS translate,metrics,aggregation] # @BRIEF Aggregate translation metrics from live TranslationEvent + MetricSnapshot for per-job reporting. # @LAYER Domain # @RELATION DEPENDS_ON -> [TranslationEvent] # @RELATION DEPENDS_ON -> [MetricSnapshot] # @RELATION DEPENDS_ON -> [TranslationRun] # @RELATION DEPENDS_ON -> [TranslationSchedule] from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple from sqlalchemy.orm import Session from sqlalchemy import func from ...core.logger import logger, belief_scope from ...models.translate import ( TranslationEvent, MetricSnapshot, TranslationRun, TranslationSchedule, ) # #region TranslationMetrics [C:3] [TYPE Class] [SEMANTICS translate,metrics] # @BRIEF Aggregate translation metrics from live events and MetricSnapshot. # @RELATION DEPENDS_ON -> [TranslationRun] # @RELATION DEPENDS_ON -> [MetricSnapshot] # @RELATION DEPENDS_ON -> [TranslationEvent] class TranslationMetrics: def __init__(self, db: Session): self.db = db # #region get_job_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,job] # @BRIEF Get aggregated metrics for a specific job including run counts, record stats, and duration. def get_job_metrics(self, job_id: str) -> Dict[str, Any]: with belief_scope("TranslationMetrics.get_job_metrics"): # Run counts from TranslationRun run_counts = ( self.db.query( TranslationRun.status, func.count(TranslationRun.id), ) .filter(TranslationRun.job_id == job_id) .group_by(TranslationRun.status) .all() ) total_runs = 0 status_counts: Dict[str, int] = {} for status_val, count in run_counts: status_counts[status_val] = count total_runs += count # Aggregate record stats from runs record_stats = ( self.db.query( func.coalesce(func.sum(TranslationRun.total_records), 0), func.coalesce(func.sum(TranslationRun.successful_records), 0), func.coalesce(func.sum(TranslationRun.failed_records), 0), func.coalesce(func.sum(TranslationRun.skipped_records), 0), ) .filter(TranslationRun.job_id == job_id) .first() ) total_records = record_stats[0] if record_stats else 0 successful_records = record_stats[1] if record_stats else 0 failed_records = record_stats[2] if record_stats else 0 skipped_records = record_stats[3] if record_stats else 0 # Average duration from runs avg_duration = ( self.db.query( func.avg( func.extract('epoch', TranslationRun.completed_at - TranslationRun.started_at) * 1000 ) ) .filter( TranslationRun.job_id == job_id, TranslationRun.started_at.isnot(None), TranslationRun.completed_at.isnot(None), ) .scalar() ) # Last run last_run = ( self.db.query(TranslationRun) .filter(TranslationRun.job_id == job_id) .order_by(TranslationRun.created_at.desc()) .first() ) # Next scheduled run next_schedule = ( self.db.query(TranslationSchedule) .filter( TranslationSchedule.job_id == job_id, TranslationSchedule.is_active == True, ) .first() ) # Cumulative tokens/cost from MetricSnapshot + events cumulative_tokens = 0 cumulative_cost = 0.0 # Latest MetricSnapshot latest_snapshot = ( self.db.query(MetricSnapshot) .filter(MetricSnapshot.job_id == job_id) .order_by(MetricSnapshot.snapshot_date.desc()) .first() ) if latest_snapshot: # MetricSnapshot stores per-snapshot aggregated tokens/cost # Here we sum what we stored — in practice MetricSnapshot.covers_events_before # indicates cutoff pass # Live events (<90 days) for token/cost cutoff = datetime.now(timezone.utc) live_events = ( self.db.query(TranslationEvent) .filter( TranslationEvent.job_id == job_id, TranslationEvent.event_type.in_(["TRANSLATION_PHASE_COMPLETED", "RUN_COMPLETED"]), TranslationEvent.created_at > cutoff, # events newer than snapshot ) .all() ) return { "job_id": job_id, "total_runs": total_runs, "successful_runs": status_counts.get("COMPLETED", 0), "failed_runs": status_counts.get("FAILED", 0), "cancelled_runs": status_counts.get("CANCELLED", 0), "total_records": int(total_records), "successful_records": int(successful_records), "failed_records": int(failed_records), "skipped_records": int(skipped_records), "cumulative_tokens": cumulative_tokens, "cumulative_cost": cumulative_cost, "avg_duration_ms": int(avg_duration) if avg_duration else None, "last_run_at": last_run.created_at.isoformat() if last_run else None, "next_scheduled_run": next_schedule.last_run_at.isoformat() if next_schedule and next_schedule.last_run_at else None, } # #endregion get_job_metrics # #region get_all_metrics [C:2] [TYPE Function] [SEMANTICS translate,metrics,all] # @BRIEF Get aggregated metrics for all jobs. def get_all_metrics(self) -> List[Dict[str, Any]]: with belief_scope("TranslationMetrics.get_all_metrics"): job_ids = ( self.db.query(TranslationRun.job_id) .distinct() .all() ) return [self.get_job_metrics(jid[0]) for jid in job_ids if jid[0]] # #endregion get_all_metrics # #endregion TranslationMetrics # #endregion TranslationMetrics