Files
cmbone/internal/services/metrics_service.go
T

145 lines
3.6 KiB
Go

package services
import (
"cmbone/internal/models"
"database/sql"
"time"
)
const (
defaultTrendDays = 7
maxTrendDays = 30
imageTaskStatusFailed = "failed"
)
type MetricsService struct {
store *SuiStore
}
func NewMetricsService(store *SuiStore) *MetricsService {
return &MetricsService{store: store}
}
func (s *MetricsService) GetDashboardMetrics() (models.DashboardMetrics, error) {
start := localDayStart(time.Now())
end := start.AddDate(0, 0, 1)
taskMetric, err := s.queryTaskMetric(start, end)
if err != nil {
return models.DashboardMetrics{}, err
}
auditLogs, err := s.countAuditLogs(start, end)
if err != nil {
return models.DashboardMetrics{}, err
}
return models.DashboardMetrics{
TodayProcessedImages: taskMetric.ProcessedImages,
SuccessRate: successRate(taskMetric.SucceededTasks, taskMetric.ProcessedImages),
AverageDurationMs: taskMetric.AverageDuration,
FailedTasks: taskMetric.FailedTasks,
TodayAuditLogs: auditLogs,
}, nil
}
func (s *MetricsService) GetProcessingTrend(days int) ([]models.DailyMetric, error) {
days = normalizeTrendDays(days)
today := localDayStart(time.Now())
start := today.AddDate(0, 0, -days+1)
trend := make([]models.DailyMetric, 0, days)
for i := 0; i < days; i++ {
dayStart := start.AddDate(0, 0, i)
dayEnd := dayStart.AddDate(0, 0, 1)
taskMetric, err := s.queryTaskMetric(dayStart, dayEnd)
if err != nil {
return nil, err
}
auditLogs, err := s.countAuditLogs(dayStart, dayEnd)
if err != nil {
return nil, err
}
trend = append(trend, models.DailyMetric{
Date: dayStart.Format("2006-01-02"),
ProcessedImages: taskMetric.ProcessedImages,
SucceededTasks: taskMetric.SucceededTasks,
FailedTasks: taskMetric.FailedTasks,
AverageDurationMs: taskMetric.AverageDuration,
AuditLogs: auditLogs,
})
}
return trend, nil
}
type taskMetric struct {
ProcessedImages int64
SucceededTasks int64
FailedTasks int64
AverageDuration float64
}
func (s *MetricsService) queryTaskMetric(start time.Time, end time.Time) (taskMetric, error) {
var metric taskMetric
var average sql.NullFloat64
err := s.store.DB.QueryRow(`
SELECT
COUNT(*),
COALESCE(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END), 0),
COALESCE(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END), 0),
AVG(CASE WHEN duration_ms > 0 THEN duration_ms END)
FROM image_tasks
WHERE created_at >= ? AND created_at < ?
`, imageTaskStatusSucceeded, imageTaskStatusFailed, formatMetricTime(start), formatMetricTime(end)).Scan(
&metric.ProcessedImages,
&metric.SucceededTasks,
&metric.FailedTasks,
&average,
)
if err != nil {
return taskMetric{}, err
}
if average.Valid {
metric.AverageDuration = average.Float64
}
return metric, nil
}
func (s *MetricsService) countAuditLogs(start time.Time, end time.Time) (int64, error) {
var count int64
err := s.store.DB.QueryRow(`
SELECT COUNT(*)
FROM audit_logs
WHERE created_at >= ? AND created_at < ?
`, formatMetricTime(start), formatMetricTime(end)).Scan(&count)
return count, err
}
func normalizeTrendDays(days int) int {
if days <= 0 {
return defaultTrendDays
}
if days > maxTrendDays {
return maxTrendDays
}
return days
}
func localDayStart(now time.Time) time.Time {
local := now.In(time.Local)
return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, local.Location())
}
func formatMetricTime(value time.Time) string {
return value.UTC().Format(time.RFC3339)
}
func successRate(succeeded int64, total int64) float64 {
if total <= 0 {
return 0
}
return float64(succeeded) * 100 / float64(total)
}