Files
ss-tools/backend/src/plugins/translate/metrics.py
busya 9f995f22ae refactor: migrate translate engine to GRACE-Poly v2.6 semantic protocol
- 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
2026-05-12 14:32:28 +03:00

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