Add DomainWithCheckStatus model and GetWorstDomainStatuses usecase to compute the most critical checker status per domain. The GET /domains endpoint now returns status alongside each domain. The frontend domain store, list components, and table row display dynamic status badges with color and icon instead of a hardcoded "OK". ZoneList is made generic (T extends HappydnsDomain) so the badges snippet preserves the caller's concrete type without unsafe casts.
370 lines
12 KiB
Go
370 lines
12 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2026 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package checker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
|
|
checkerPkg "git.happydns.org/happyDomain/internal/checker"
|
|
"git.happydns.org/happyDomain/model"
|
|
)
|
|
|
|
// CheckStatusUsecase handles aggregation of checker statuses and evaluation/execution queries.
|
|
type CheckStatusUsecase struct {
|
|
planStore CheckPlanStorage
|
|
evalStore CheckEvaluationStorage
|
|
execStore ExecutionStorage
|
|
snapStore ObservationSnapshotStorage
|
|
plannedProvider PlannedJobProvider
|
|
}
|
|
|
|
// NewCheckStatusUsecase creates a new CheckStatusUsecase.
|
|
func NewCheckStatusUsecase(planStore CheckPlanStorage, evalStore CheckEvaluationStorage, execStore ExecutionStorage, snapStore ObservationSnapshotStorage) *CheckStatusUsecase {
|
|
return &CheckStatusUsecase{
|
|
planStore: planStore,
|
|
evalStore: evalStore,
|
|
execStore: execStore,
|
|
snapStore: snapStore,
|
|
}
|
|
}
|
|
|
|
// SetPlannedJobProvider attaches an optional scheduler for planned execution queries.
|
|
func (u *CheckStatusUsecase) SetPlannedJobProvider(p PlannedJobProvider) {
|
|
u.plannedProvider = p
|
|
}
|
|
|
|
// ListPlannedExecutions returns synthetic Execution records for upcoming scheduled jobs.
|
|
// Returns nil if no PlannedJobProvider is configured.
|
|
func (u *CheckStatusUsecase) ListPlannedExecutions(checkerID string, target happydns.CheckTarget) []*happydns.Execution {
|
|
if u.plannedProvider == nil {
|
|
return nil
|
|
}
|
|
jobs := u.plannedProvider.GetPlannedJobsForChecker(checkerID, target)
|
|
result := make([]*happydns.Execution, 0, len(jobs))
|
|
for _, job := range jobs {
|
|
exec := &happydns.Execution{
|
|
CheckerID: job.CheckerID,
|
|
PlanID: job.PlanID,
|
|
Target: job.Target,
|
|
Trigger: happydns.TriggerInfo{Type: happydns.TriggerSchedule},
|
|
StartedAt: job.NextRun,
|
|
Status: happydns.ExecutionPending,
|
|
}
|
|
result = append(result, exec)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ListCheckerStatuses aggregates checkers, plans, and latest evaluations into a status list.
|
|
func (u *CheckStatusUsecase) ListCheckerStatuses(target happydns.CheckTarget) ([]happydns.CheckerStatus, error) {
|
|
checkers := checkerPkg.GetCheckers()
|
|
plans, err := u.planStore.ListCheckPlansByTarget(target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
planByChecker := make(map[string]*happydns.CheckPlan)
|
|
for _, p := range plans {
|
|
planByChecker[p.CheckerID] = p
|
|
}
|
|
|
|
var result []happydns.CheckerStatus
|
|
for _, def := range checkers {
|
|
switch target.Scope() {
|
|
case happydns.CheckScopeDomain:
|
|
if !def.Availability.ApplyToDomain {
|
|
continue
|
|
}
|
|
case happydns.CheckScopeService:
|
|
if !def.Availability.ApplyToService {
|
|
continue
|
|
}
|
|
if len(def.Availability.LimitToServices) > 0 && target.ServiceType != "" {
|
|
if !slices.Contains(def.Availability.LimitToServices, target.ServiceType) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
status := happydns.CheckerStatus{
|
|
CheckerDefinition: def,
|
|
Plan: planByChecker[def.ID],
|
|
Enabled: true,
|
|
}
|
|
|
|
enabledRules := make(map[string]bool, len(def.Rules))
|
|
for _, rule := range def.Rules {
|
|
enabledRules[rule.Name()] = true
|
|
}
|
|
if status.Plan != nil {
|
|
status.Enabled = !status.Plan.IsFullyDisabled()
|
|
for ruleName := range enabledRules {
|
|
enabledRules[ruleName] = status.Plan.IsRuleEnabled(ruleName)
|
|
}
|
|
}
|
|
status.EnabledRules = enabledRules
|
|
|
|
execs, err := u.execStore.ListExecutionsByChecker(def.ID, target, 1)
|
|
if err == nil && len(execs) > 0 {
|
|
status.LatestExecution = execs[0]
|
|
}
|
|
|
|
result = append(result, status)
|
|
}
|
|
|
|
if result == nil {
|
|
result = []happydns.CheckerStatus{}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetExecution returns a specific execution by ID.
|
|
func (u *CheckStatusUsecase) GetExecution(execID happydns.Identifier) (*happydns.Execution, error) {
|
|
return u.execStore.GetExecution(execID)
|
|
}
|
|
|
|
// ListExecutionsByChecker returns executions for a checker on a target, up to limit.
|
|
func (u *CheckStatusUsecase) ListExecutionsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]*happydns.Execution, error) {
|
|
return u.execStore.ListExecutionsByChecker(checkerID, target, limit)
|
|
}
|
|
|
|
// GetObservationsByExecution returns the observation snapshot for an execution.
|
|
func (u *CheckStatusUsecase) GetObservationsByExecution(execID happydns.Identifier) (*happydns.ObservationSnapshot, error) {
|
|
exec, err := u.execStore.GetExecution(execID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exec.EvaluationID == nil {
|
|
return nil, happydns.ErrCheckEvaluationNotFound
|
|
}
|
|
eval, err := u.evalStore.GetEvaluation(*exec.EvaluationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u.snapStore.GetSnapshot(eval.SnapshotID)
|
|
}
|
|
|
|
// DeleteExecution deletes an execution record by ID.
|
|
func (u *CheckStatusUsecase) DeleteExecution(execID happydns.Identifier) error {
|
|
return u.execStore.DeleteExecution(execID)
|
|
}
|
|
|
|
// DeleteExecutionsByChecker deletes all executions for a checker on a target.
|
|
func (u *CheckStatusUsecase) DeleteExecutionsByChecker(checkerID string, target happydns.CheckTarget) error {
|
|
return u.execStore.DeleteExecutionsByChecker(checkerID, target)
|
|
}
|
|
|
|
// GetWorstDomainStatuses fetches all executions for a user and returns the worst
|
|
// (most critical) status per domain. It keeps only the latest execution per
|
|
// (domain, checker) pair and reports the worst status among them.
|
|
func (u *CheckStatusUsecase) GetWorstDomainStatuses(userId happydns.Identifier) (map[string]*happydns.Status, error) {
|
|
execs, err := u.execStore.ListExecutionsByUser(userId, 0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
type key struct {
|
|
domainId string
|
|
checker string
|
|
}
|
|
latest := map[key]*happydns.Execution{}
|
|
for _, exec := range execs {
|
|
if exec.Target.DomainId == nil || exec.Status != happydns.ExecutionDone {
|
|
continue
|
|
}
|
|
k := key{domainId: exec.Target.DomainId.String(), checker: exec.CheckerID}
|
|
if prev, ok := latest[k]; !ok || exec.StartedAt.After(prev.StartedAt) {
|
|
latest[k] = exec
|
|
}
|
|
}
|
|
|
|
worst := map[string]*happydns.Status{}
|
|
for k, exec := range latest {
|
|
s := exec.Result.Status
|
|
if s == happydns.StatusUnknown {
|
|
continue
|
|
}
|
|
if prev, ok := worst[k.domainId]; !ok || s > *prev {
|
|
worst[k.domainId] = &s
|
|
}
|
|
}
|
|
|
|
return worst, nil
|
|
}
|
|
|
|
// GetWorstServiceStatuses returns the worst check status for each service in the zone.
|
|
// It iterates all services and all registered checkers, fetching the latest execution
|
|
// for each (service, checker) pair, and returns the worst status per service.
|
|
func (u *CheckStatusUsecase) GetWorstServiceStatuses(userId happydns.Identifier, domainId happydns.Identifier, zone *happydns.Zone) (map[string]*happydns.Status, error) {
|
|
checkers := checkerPkg.GetCheckers()
|
|
if len(checkers) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
result := make(map[string]*happydns.Status)
|
|
for subdomain := range zone.Services {
|
|
for _, svc := range zone.Services[subdomain] {
|
|
target := happydns.CheckTarget{
|
|
UserId: &userId,
|
|
DomainId: &domainId,
|
|
ServiceId: &svc.Id,
|
|
}
|
|
var worst *happydns.Status
|
|
for _, def := range checkers {
|
|
execs, err := u.execStore.ListExecutionsByChecker(def.ID, target, 1)
|
|
if err != nil || len(execs) == 0 {
|
|
continue
|
|
}
|
|
s := execs[0].Result.Status
|
|
if worst == nil || s > *worst {
|
|
worst = &s
|
|
}
|
|
}
|
|
if worst != nil {
|
|
result[svc.Id.String()] = worst
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return nil, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// GetResultsByExecution returns the evaluation (with per-rule states) for an execution.
|
|
func (u *CheckStatusUsecase) GetResultsByExecution(execID happydns.Identifier) (*happydns.CheckEvaluation, error) {
|
|
exec, err := u.execStore.GetExecution(execID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exec.EvaluationID == nil {
|
|
return nil, happydns.ErrCheckEvaluationNotFound
|
|
}
|
|
return u.evalStore.GetEvaluation(*exec.EvaluationID)
|
|
}
|
|
|
|
// extractMetricsFromExecution extracts metrics from a single execution's snapshot.
|
|
func (u *CheckStatusUsecase) extractMetricsFromExecution(exec *happydns.Execution) ([]happydns.CheckMetric, error) {
|
|
if exec.Status != happydns.ExecutionDone || exec.EvaluationID == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
eval, err := u.evalStore.GetEvaluation(*exec.EvaluationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
snap, err := u.snapStore.GetSnapshot(eval.SnapshotID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return checkerPkg.GetAllMetrics(snap)
|
|
}
|
|
|
|
// extractMetricsFromExecutions extracts metrics from a list of executions.
|
|
func (u *CheckStatusUsecase) extractMetricsFromExecutions(execs []*happydns.Execution) ([]happydns.CheckMetric, error) {
|
|
var allMetrics []happydns.CheckMetric
|
|
for _, exec := range execs {
|
|
metrics, err := u.extractMetricsFromExecution(exec)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
allMetrics = append(allMetrics, metrics...)
|
|
}
|
|
return allMetrics, nil
|
|
}
|
|
|
|
// GetMetricsByExecution extracts metrics from a single execution's snapshot.
|
|
func (u *CheckStatusUsecase) GetMetricsByExecution(execID happydns.Identifier) ([]happydns.CheckMetric, error) {
|
|
exec, err := u.execStore.GetExecution(execID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u.extractMetricsFromExecution(exec)
|
|
}
|
|
|
|
// GetMetricsByChecker extracts metrics from recent executions of a checker on a target.
|
|
func (u *CheckStatusUsecase) GetMetricsByChecker(checkerID string, target happydns.CheckTarget, limit int) ([]happydns.CheckMetric, error) {
|
|
execs, err := u.execStore.ListExecutionsByChecker(checkerID, target, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u.extractMetricsFromExecutions(execs)
|
|
}
|
|
|
|
// GetMetricsByUser extracts metrics from recent executions for a user across all checkers.
|
|
func (u *CheckStatusUsecase) GetMetricsByUser(userId happydns.Identifier, limit int) ([]happydns.CheckMetric, error) {
|
|
execs, err := u.execStore.ListExecutionsByUser(userId, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return u.extractMetricsFromExecutions(execs)
|
|
}
|
|
|
|
// GetMetricsByDomain extracts metrics from recent executions for a domain (including services).
|
|
func (u *CheckStatusUsecase) GetMetricsByDomain(userId happydns.Identifier, domainId happydns.Identifier, limit int) ([]happydns.CheckMetric, error) {
|
|
execs, err := u.execStore.ListExecutionsByUser(userId, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Filter to executions matching this domain (any service under it).
|
|
var filtered []*happydns.Execution
|
|
for _, exec := range execs {
|
|
if exec.Target.DomainId != nil && exec.Target.DomainId.Equals(domainId) {
|
|
filtered = append(filtered, exec)
|
|
}
|
|
}
|
|
|
|
return u.extractMetricsFromExecutions(filtered)
|
|
}
|
|
|
|
// GetSnapshotByExecution returns the snapshot for a given observation key from an execution.
|
|
func (u *CheckStatusUsecase) GetSnapshotByExecution(execID happydns.Identifier, obsKey string) (json.RawMessage, error) {
|
|
exec, err := u.execStore.GetExecution(execID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if exec.EvaluationID == nil {
|
|
return nil, happydns.ErrCheckEvaluationNotFound
|
|
}
|
|
|
|
eval, err := u.evalStore.GetEvaluation(*exec.EvaluationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
snap, err := u.snapStore.GetSnapshot(eval.SnapshotID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
val, ok := snap.Data[obsKey]
|
|
if !ok {
|
|
return nil, happydns.ErrSnapshotNotFound
|
|
}
|
|
|
|
return json.Marshal(val)
|
|
}
|