76 lines
3.1 KiB
Go
76 lines
3.1 KiB
Go
package externalprobe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
var (
|
|
serviceNamePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{1,31}$`)
|
|
dependencyPattern = regexp.MustCompile(`^[a-z][a-z0-9_]{1,63}$`)
|
|
)
|
|
|
|
func PostgresObserver(pool *pgxpool.Pool, serviceName string) (Observer, error) {
|
|
if pool == nil || !serviceNamePattern.MatchString(serviceName) {
|
|
return nil, errors.New("external probe database and service name are required")
|
|
}
|
|
return func(ctx context.Context, observation Observation) error {
|
|
if !dependencyPattern.MatchString(observation.Dependency) {
|
|
return errors.New("external probe dependency name is invalid")
|
|
}
|
|
if observation.Interval <= 0 {
|
|
return errors.New("external probe interval is required")
|
|
}
|
|
latencyMS := observation.Latency.Milliseconds()
|
|
if latencyMS < 0 {
|
|
latencyMS = 0
|
|
}
|
|
intervalMS := observation.Interval.Milliseconds()
|
|
if intervalMS < 1 {
|
|
intervalMS = 1
|
|
}
|
|
checkedAt := observation.CheckedAt.UTC()
|
|
if checkedAt.IsZero() {
|
|
checkedAt = time.Now().UTC()
|
|
}
|
|
status := "AVAILABLE"
|
|
if !observation.Available {
|
|
status = "UNAVAILABLE"
|
|
if observation.ErrorCode == "" {
|
|
observation.ErrorCode = "PROBE_FAILED"
|
|
}
|
|
}
|
|
_, err := pool.Exec(ctx, `
|
|
INSERT INTO external_dependency_probe_state(service_name,dependency,status,checked_at,last_success_at,last_failure_at,last_error_code,latency_ms,probe_interval_ms,consecutive_failures)
|
|
VALUES($1,$2,$3::varchar,$4::timestamptz,CASE WHEN $3::varchar='AVAILABLE' THEN $4::timestamptz END,CASE WHEN $3::varchar='UNAVAILABLE' THEN $4::timestamptz END,NULLIF($5::varchar,''),$6,$7,CASE WHEN $3::varchar='UNAVAILABLE' THEN 1 ELSE 0 END)
|
|
ON CONFLICT(service_name,dependency) DO UPDATE SET
|
|
status=EXCLUDED.status,
|
|
checked_at=EXCLUDED.checked_at,
|
|
last_success_at=CASE WHEN EXCLUDED.status='AVAILABLE' THEN EXCLUDED.checked_at ELSE external_dependency_probe_state.last_success_at END,
|
|
last_failure_at=CASE WHEN EXCLUDED.status='UNAVAILABLE' THEN EXCLUDED.checked_at ELSE external_dependency_probe_state.last_failure_at END,
|
|
last_error_code=EXCLUDED.last_error_code,
|
|
latency_ms=EXCLUDED.latency_ms,
|
|
probe_interval_ms=EXCLUDED.probe_interval_ms,
|
|
consecutive_failures=CASE WHEN EXCLUDED.status='AVAILABLE' THEN 0 ELSE external_dependency_probe_state.consecutive_failures+1 END,
|
|
updated_at=now()`, serviceName, observation.Dependency, status, checkedAt, observation.ErrorCode, latencyMS, intervalMS)
|
|
return err
|
|
}, nil
|
|
}
|
|
|
|
func ReconcilePostgresState(ctx context.Context, pool *pgxpool.Pool, serviceName string, dependencies []string) error {
|
|
if pool == nil || !serviceNamePattern.MatchString(serviceName) {
|
|
return errors.New("external probe database and service name are required")
|
|
}
|
|
for _, dependency := range dependencies {
|
|
if !dependencyPattern.MatchString(dependency) {
|
|
return errors.New("external probe dependency name is invalid")
|
|
}
|
|
}
|
|
_, err := pool.Exec(ctx, `DELETE FROM external_dependency_probe_state WHERE service_name=$1 AND NOT (dependency=ANY(COALESCE($2::text[],ARRAY[]::text[])))`, serviceName, dependencies)
|
|
return err
|
|
}
|