- Convert all 84 contracts from legacy [DEF:] to #region/#endregion syntax - Fix complexity tiers: 14 modules re-tiered (6 C4 route modules, 7 C4→C5 plugin services) - Remove forbidden tags: @RATIONALE/@REJECTED stripped from C1–C4 contracts - Add required tags: @PRE/@POST/@SIDE_EFFECT on C4, @RELATION on C3, @DATA_CONTRACT/@INVARIANT on C5 - Add belief runtime markers (reason/reflect/explore) to 7 service.py functions - Fix @LAYER: route files → UI, plugins → Domain, superset_executor → Infra - Fix pre-existing test mock_service fixture in test_orchestrator.py - 196/196 translation tests pass, zero regressions
165 lines
6.5 KiB
Python
165 lines
6.5 KiB
Python
# #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
|