136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
package diagnostics
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"data-terminal-cloud/internal/identity"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
const (
|
|
PlatformHealthSchema = "platform-server-health-v1"
|
|
platformHealthMaxAge = 15 * time.Minute
|
|
platformHealthFuture = 2 * time.Minute
|
|
)
|
|
|
|
var platformHealthCheckName = regexp.MustCompile(`^[a-z][a-z0-9_.-]{1,79}$`)
|
|
|
|
type PlatformHealthCheck struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type PlatformHealthInput struct {
|
|
Schema string `json:"schema"`
|
|
Status string `json:"status"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
Checks []PlatformHealthCheck `json:"checks"`
|
|
}
|
|
|
|
type PlatformHealthSnapshot struct {
|
|
ID string `json:"id"`
|
|
Schema string `json:"schema"`
|
|
Status string `json:"status"`
|
|
ObservedAt time.Time `json:"observed_at"`
|
|
ReceivedAt time.Time `json:"received_at"`
|
|
Checks []PlatformHealthCheck `json:"checks"`
|
|
}
|
|
|
|
type PlatformHealthReceipt struct {
|
|
ID string `json:"id"`
|
|
Status string `json:"status"`
|
|
Duplicate bool `json:"duplicate"`
|
|
}
|
|
|
|
func (s *Service) IngestPlatformHealth(ctx context.Context, input PlatformHealthInput, rawBody []byte, now time.Time) (PlatformHealthReceipt, error) {
|
|
if !validPlatformHealth(input, now) || len(rawBody) == 0 || len(rawBody) > 64*1024 {
|
|
return PlatformHealthReceipt{}, ErrInvalid
|
|
}
|
|
checks, err := json.Marshal(input.Checks)
|
|
if err != nil {
|
|
return PlatformHealthReceipt{}, err
|
|
}
|
|
digestBytes := sha256.Sum256(rawBody)
|
|
digest := hex.EncodeToString(digestBytes[:])
|
|
publicID := uuid.NewString()
|
|
var storedID, storedStatus string
|
|
err = s.pool.QueryRow(ctx, `
|
|
INSERT INTO platform_health_snapshot(public_id,schema_name,status,observed_at,checks,body_digest)
|
|
VALUES($1::uuid,$2,$3,$4,$5::jsonb,$6)
|
|
ON CONFLICT(body_digest) DO UPDATE SET body_digest=EXCLUDED.body_digest
|
|
RETURNING public_id::text,status`, publicID, input.Schema, input.Status, input.ObservedAt, checks, digest).Scan(&storedID, &storedStatus)
|
|
if err != nil {
|
|
return PlatformHealthReceipt{}, err
|
|
}
|
|
_, _ = s.pool.Exec(ctx, `DELETE FROM platform_health_snapshot WHERE received_at < now()-interval '30 days'`)
|
|
return PlatformHealthReceipt{ID: storedID, Status: storedStatus, Duplicate: storedID != publicID}, nil
|
|
}
|
|
|
|
func (s *Service) GetLatestPlatformHealth(ctx context.Context, principal identity.Principal, tenantPublicID string) (PlatformHealthSnapshot, error) {
|
|
if _, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin"); err != nil {
|
|
return PlatformHealthSnapshot{}, err
|
|
}
|
|
var item PlatformHealthSnapshot
|
|
var checks []byte
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT public_id::text,schema_name,status,observed_at,received_at,checks
|
|
FROM platform_health_snapshot ORDER BY observed_at DESC,id DESC LIMIT 1`).Scan(
|
|
&item.ID, &item.Schema, &item.Status, &item.ObservedAt, &item.ReceivedAt, &checks,
|
|
)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return PlatformHealthSnapshot{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return PlatformHealthSnapshot{}, err
|
|
}
|
|
if err = json.Unmarshal(checks, &item.Checks); err != nil {
|
|
return PlatformHealthSnapshot{}, err
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func validPlatformHealth(input PlatformHealthInput, now time.Time) bool {
|
|
if input.Schema != PlatformHealthSchema || !platformHealthStatus(input.Status) || input.ObservedAt.IsZero() ||
|
|
input.ObservedAt.Before(now.Add(-platformHealthMaxAge)) || input.ObservedAt.After(now.Add(platformHealthFuture)) ||
|
|
len(input.Checks) == 0 || len(input.Checks) > 64 {
|
|
return false
|
|
}
|
|
seen := make(map[string]struct{}, len(input.Checks))
|
|
critical, warning := 0, 0
|
|
for _, check := range input.Checks {
|
|
if !platformHealthCheckName.MatchString(check.Name) || !platformHealthStatus(check.Status) || len([]rune(check.Value)) > 256 || strings.ContainsAny(check.Value, "\r\n\x00") {
|
|
return false
|
|
}
|
|
if _, exists := seen[check.Name]; exists {
|
|
return false
|
|
}
|
|
seen[check.Name] = struct{}{}
|
|
if check.Status == "critical" {
|
|
critical++
|
|
} else if check.Status == "warning" {
|
|
warning++
|
|
}
|
|
}
|
|
expected := "healthy"
|
|
if critical > 0 {
|
|
expected = "critical"
|
|
} else if warning > 0 {
|
|
expected = "warning"
|
|
}
|
|
return input.Status == expected
|
|
}
|
|
|
|
func platformHealthStatus(value string) bool {
|
|
return value == "healthy" || value == "warning" || value == "critical"
|
|
}
|