Public source validation / validate (push) Failing after 3m8s
183 lines
8.0 KiB
Go
183 lines
8.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/itworx/pulse/internal/probe"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type PostgresProvider struct {
|
|
Pool *pgxpool.Pool
|
|
Policy StatusPolicy
|
|
Now func() time.Time
|
|
}
|
|
|
|
func NewPostgresProvider(pool *pgxpool.Pool, policy StatusPolicy) (*PostgresProvider, error) {
|
|
if pool == nil {
|
|
return nil, errors.New("service status provider requires a database pool")
|
|
}
|
|
policy = policy.withDefaults()
|
|
if err := policy.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &PostgresProvider{Pool: pool, Policy: policy}, nil
|
|
}
|
|
|
|
func (p *PostgresProvider) Snapshot(ctx context.Context) (Snapshot, error) {
|
|
if p == nil || p.Pool == nil {
|
|
return Snapshot{}, errors.New("service status provider is unavailable")
|
|
}
|
|
if ctx == nil {
|
|
return Snapshot{}, errors.New("service status context is nil")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
policy := p.Policy.withDefaults()
|
|
if err := policy.Validate(); err != nil {
|
|
return Snapshot{}, err
|
|
}
|
|
now := time.Now().UTC()
|
|
if p.Now != nil {
|
|
now = p.Now().UTC()
|
|
}
|
|
rows, err := p.Pool.Query(ctx, `
|
|
WITH bounded_services AS (
|
|
SELECT id, entity_id, source_id, name, description, revision, archived_at, created_at, updated_at
|
|
FROM services
|
|
WHERE archived_at IS NULL
|
|
ORDER BY id ASC
|
|
LIMIT $1
|
|
), bounded_results AS (
|
|
SELECT s.id::text AS service_id, s.entity_id::text AS entity_id, s.source_id::text AS source_id,
|
|
s.name, s.description, s.revision, s.archived_at, s.created_at, s.updated_at,
|
|
COALESCE((SELECT jsonb_agg(jsonb_build_object('id', pc.id::text, 'name', pc.name, 'type', pc.probe_type, 'intervalSeconds', pc.interval_seconds, 'timeoutSeconds', pc.timeout_seconds, 'enabled', pc.enabled, 'followRedirects', pc.follow_redirects, 'verifyTls', pc.verify_tls, 'revision', pc.revision) ORDER BY pc.id) FROM probes pc WHERE pc.service_id = s.id AND pc.archived_at IS NULL), '[]'::jsonb) AS probe_configs,
|
|
p.id::text AS probe_id, pr.id::text AS result_id, pr.observed_at, pr.completed_at,
|
|
pr.state, pr.response_time_ms, pr.status_code, pr.error_class, pr.error_message, pr.attributes,
|
|
certificate.id::text AS certificate_id, certificate.service_id::text AS certificate_service_id,
|
|
certificate.endpoint_id::text AS certificate_endpoint_id, certificate.observed_at AS certificate_observed_at,
|
|
certificate.expires_at AS certificate_expires_at, certificate.issuer AS certificate_issuer,
|
|
certificate.subject AS certificate_subject, certificate.hostname_valid AS certificate_hostname_valid,
|
|
certificate.verification_state AS certificate_verification_state,
|
|
row_number() OVER (PARTITION BY s.id ORDER BY pr.observed_at DESC NULLS LAST, p.id ASC, pr.id ASC) AS result_rank
|
|
FROM bounded_services s
|
|
LEFT JOIN probes p ON p.service_id = s.id AND p.archived_at IS NULL
|
|
LEFT JOIN probe_results pr ON pr.probe_id = p.id
|
|
LEFT JOIN LATERAL (
|
|
SELECT sc.id, sc.service_id, sc.endpoint_id, sc.observed_at, sc.expires_at,
|
|
sc.issuer, sc.subject, sc.hostname_valid, sc.verification_state
|
|
FROM service_certificates sc
|
|
WHERE sc.service_id = s.id
|
|
ORDER BY sc.observed_at DESC, sc.id ASC
|
|
LIMIT 1
|
|
) certificate ON true
|
|
)
|
|
SELECT service_id, COALESCE(entity_id, ''), COALESCE(source_id, ''), name, description, revision, archived_at, created_at, updated_at, probe_configs,
|
|
probe_id, result_id, observed_at, completed_at, state, response_time_ms, status_code, error_class, error_message, attributes,
|
|
certificate_id, certificate_service_id, certificate_endpoint_id, certificate_observed_at,
|
|
certificate_expires_at, certificate_issuer, certificate_subject, certificate_hostname_valid,
|
|
certificate_verification_state
|
|
FROM bounded_results
|
|
WHERE result_rank <= $2 OR result_rank IS NULL
|
|
ORDER BY service_id ASC, observed_at DESC NULLS LAST, probe_id ASC NULLS LAST, result_id ASC NULLS LAST`, policy.MaxServices, policy.MaxHistory)
|
|
if err != nil {
|
|
return Snapshot{}, fmt.Errorf("query service status: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
inputs := make([]ServiceInput, 0, policy.MaxServices)
|
|
byID := make(map[string]int, policy.MaxServices)
|
|
for rows.Next() {
|
|
var serviceID, entityID, sourceID, name, description string
|
|
var revision int64
|
|
var archivedAt, createdAt, updatedAt *time.Time
|
|
var probeID, resultID *string
|
|
var observedAt, completedAt *time.Time
|
|
var state, errorClass, errorMessage *string
|
|
var responseTimeMS, statusCode *int
|
|
var certificateID, certificateServiceID, certificateEndpointID *string
|
|
var certificateObservedAt, certificateExpiresAt *time.Time
|
|
var certificateIssuer, certificateSubject, certificateVerificationState *string
|
|
var certificateHostnameValid *bool
|
|
var attributes []byte
|
|
var probeConfigs []byte
|
|
if err := rows.Scan(&serviceID, &entityID, &sourceID, &name, &description, &revision, &archivedAt, &createdAt, &updatedAt, &probeConfigs, &probeID, &resultID, &observedAt, &completedAt, &state, &responseTimeMS, &statusCode, &errorClass, &errorMessage, &attributes, &certificateID, &certificateServiceID, &certificateEndpointID, &certificateObservedAt, &certificateExpiresAt, &certificateIssuer, &certificateSubject, &certificateHostnameValid, &certificateVerificationState); err != nil {
|
|
return Snapshot{}, fmt.Errorf("scan service status: %w", err)
|
|
}
|
|
index, exists := byID[serviceID]
|
|
if !exists {
|
|
item := ServiceInput{Service: Service{ID: serviceID, EntityID: entityID, SourceID: sourceID, Name: name, Description: description, State: StateUnknown, Revision: revision, ArchivedAt: archivedAt, CreatedAt: timeValue(createdAt), UpdatedAt: timeValue(updatedAt)}}
|
|
inputs = append(inputs, item)
|
|
index = len(inputs) - 1
|
|
byID[serviceID] = index
|
|
if len(probeConfigs) > 0 {
|
|
if err := json.Unmarshal(probeConfigs, &inputs[index].ProbeConfigs); err != nil {
|
|
return Snapshot{}, fmt.Errorf("decode service probe configuration: %w", err)
|
|
}
|
|
}
|
|
}
|
|
input := &inputs[index]
|
|
if input.LatestCertificate == nil && certificateID != nil && certificateServiceID != nil && certificateObservedAt != nil && certificateVerificationState != nil {
|
|
input.LatestCertificate = &probe.Certificate{
|
|
ID: *certificateID,
|
|
ServiceID: *certificateServiceID,
|
|
EndpointID: stringValue(certificateEndpointID),
|
|
ObservedAt: certificateObservedAt.UTC(),
|
|
ExpiresAt: certificateExpiresAt,
|
|
Issuer: stringValue(certificateIssuer),
|
|
Subject: stringValue(certificateSubject),
|
|
HostnameValid: certificateHostnameValid,
|
|
VerificationState: *certificateVerificationState,
|
|
}
|
|
}
|
|
if probeID == nil || resultID == nil || observedAt == nil || state == nil {
|
|
continue
|
|
}
|
|
result := probe.Result{ID: *resultID, ProbeID: *probeID, ObservedAt: observedAt.UTC(), CompletedAt: timeValue(completedAt).UTC(), State: *state, ResponseTimeMS: responseTimeMS, StatusCode: statusCode}
|
|
if errorClass != nil {
|
|
result.ErrorClass = *errorClass
|
|
}
|
|
if errorMessage != nil {
|
|
result.ErrorMessage = *errorMessage
|
|
}
|
|
if len(attributes) > 0 {
|
|
if err := json.Unmarshal(attributes, &result.Attributes); err != nil {
|
|
return Snapshot{}, fmt.Errorf("decode service result attributes: %w", err)
|
|
}
|
|
}
|
|
appendProbeResult(input, *probeID, result)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return Snapshot{}, fmt.Errorf("read service status rows: %w", err)
|
|
}
|
|
return BuildSnapshot(now, inputs, policy)
|
|
}
|
|
|
|
func stringValue(value *string) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func appendProbeResult(input *ServiceInput, probeID string, result probe.Result) {
|
|
for index := range input.Probes {
|
|
if input.Probes[index].ProbeID == probeID {
|
|
input.Probes[index].Results = append(input.Probes[index].Results, result)
|
|
return
|
|
}
|
|
}
|
|
input.Probes = append(input.Probes, ProbeHistory{ProbeID: probeID, Results: []probe.Result{result}})
|
|
}
|
|
|
|
func timeValue(value *time.Time) time.Time {
|
|
if value == nil {
|
|
return time.Time{}
|
|
}
|
|
return value.UTC()
|
|
}
|