Documentation
¶
Overview ¶
Package checkerdef defines the core interfaces for health checkers.
Index ¶
- Constants
- Variables
- func AssertConfig[T Config](config Config) (T, error)
- func BuildHTTPTransport(dialer ContextDialer, skipTLSVerify bool, version IPVersion) http.RoundTripper
- func ClassifyDialError(err error, timedOut bool) string
- func ClassifyTLSHandshakeError(err error, timedOut bool) string
- func DialEgressProbe(version IPVersion, ip net.IP) error
- func DropNetworkFailure(r *Result)
- func ExtractTargetHost(configMap map[string]any) *string
- func FamilyDialContext(version IPVersion) func(context.Context, string, string) (net.Conn, error)
- func LocateNetworkFailure(result *Result, host, address string, port int)
- func LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error)
- func MatchesIPVersion(ip net.IP, version IPVersion) bool
- func NewConfigError(parameter, message string) error
- func NewConfigErrorf(parameter, format string, args ...any) error
- func NormalizeConfigFor(cfg any, configMap map[string]any) (map[string]any, error)
- func ProbeEgress() map[IPVersion]bool
- func ProbeEgressWith(probe EgressProbe) map[IPVersion]bool
- func SampleConfigWithIPVersion(cfg map[string]any, version IPVersion) map[string]any
- func SelectIPAddr(host string, addrs []net.IPAddr, version IPVersion) (net.IP, error)
- func SelectIPAddrWithProbe(host string, addrs []net.IPAddr, version IPVersion, probe EgressProbe) (net.IP, error)
- func TunnelCheckUIDFrom(configMap map[string]any) (string, bool)
- func WithIPVersion(ctx context.Context, version IPVersion) context.Context
- func WithSMTPJobIdentity(ctx context.Context, identity SMTPJobIdentity) context.Context
- func WithTunnelDialer(ctx context.Context, dialer ContextDialer) context.Context
- type ActivationResolver
- type CheckSpec
- type CheckType
- type CheckTypeMeta
- type CheckTypeStatus
- type Checker
- type CheckerSamplesProvider
- type Config
- type ConfigError
- type ConfigNormalizer
- type ContextDialer
- type Diagnostics
- type DialerFunc
- type EgressCache
- type EgressProbe
- type FailureResponse
- type IPVersion
- type ListSampleOptionType
- type ListSampleOptions
- type NetworkFailure
- type Result
- type SMTPJobIdentity
- type Screenshot
- type Status
Examples ¶
Constants ¶
const ( // SeverityNone means the target is not near expiry (StatusUp). SeverityNone = "" // SeverityWarning means expiry is approaching the warning window // (StatusWarning — amber, counts as up, no incident). SeverityWarning = "warning" // SeverityCritical means expiry is at/inside the critical window // (StatusDown — pages, as a hard failure). SeverityCritical = "critical" )
Expiry severity labels emitted in check output. They let the dashboard render a consistent badge regardless of which checker produced the result.
const ( // IPVersionConfigKey is the canonical (camelCase) check-config key holding // the requested address family. Like `timeout` and `tunnelCheckUid` it is a // shared, well-known key read generically off the raw config map rather // than a field on every checker's config struct — so a checker gaining // support needs no config-struct change, and address-family selection has // exactly one implementation. IPVersionConfigKey = "ipVersion" // IPVersionConfigKeyLegacy is the snake_case spelling, accepted on read for // configs written by hand or by an older client. camelCase is canonical. IPVersionConfigKeyLegacy = "ip_version" // OutputKeyIPVersion is the result-output key reporting the family a check // actually used. It predates the input option (tcp/udp/icmp already // reported it) and keeps its shape: the string "ipv4" or "ipv6". OutputKeyIPVersion = "ip_version" )
const ( // NetFailureConnectTimeout is a connect that never completed: no SYN-ACK, // no refusal, nothing — the classic silent drop of a firewall or a black // hole somewhere on the path. NetFailureConnectTimeout = "connect-timeout" // NetFailureConnectionRefused is an RST: something answered, and said no. // The path is reachable; the trace still tells you which hops it crossed. NetFailureConnectionRefused = "connection-refused" // NetFailureNetworkUnreachable is ICMP/errno "network unreachable" — a // routing failure, usually local or one hop out. NetFailureNetworkUnreachable = "network-unreachable" // NetFailureHostUnreachable is ICMP/errno "host unreachable". NetFailureHostUnreachable = "host-unreachable" // NetFailureICMPTimeout is an ICMP check whose echoes all went unanswered. NetFailureICMPTimeout = "icmp-timeout" // NetFailureICMPUnreachable is an ICMP check that got a destination // unreachable back rather than silence. NetFailureICMPUnreachable = "icmp-unreachable" // NetFailureTLSHandshakeTimeout is a TCP connection that came up and then // stalled inside the TLS handshake — a middlebox, an MTU black hole, or a // server that accepts and never speaks. NetFailureTLSHandshakeTimeout = "tls-handshake-timeout" )
Network-reachability failure classes (spec 2026-08-21-10).
These name the ways a probe can fail to REACH its target — the cases where a path trace has something to say. They deliberately do NOT cover application-level failures (an HTTP 500, a keyword mismatch, a certificate about to expire): the target answered, the path is fine, and a traceroute would only add noise to the incident.
const ( OutputKeyError = "error" OutputKeyHost = "host" OutputKeyPort = "port" OutputKeyMethod = "method" OutputKeyTimeout = "timeout" OutputKeyCount = "count" OutputKeyOID = "oid" OutputKeyURL = "url" OutputKeyStatusCode = "status_code" OutputKeyDurationMs = "duration_ms" OutputKeyDomain = "domain" OutputKeyRecordType = "record_type" // OutputKeyTLSVerifySkipped marks a result whose request ran with TLS // certificate verification disabled (checkhttp's verifySsl: false), so // operators can see the reduced trust from the result details alone. OutputKeyTLSVerifySkipped = "tls_verify_skipped" )
Common output and config map keys used across checker implementations.
const GlobalMinPeriod = 10 * time.Second
GlobalMinPeriod is the smallest period the API accepts for any check type that does not declare its own (stricter) MinPeriod. It matches the smallest period in real-world use; sub-10s checks are out of scope for the results/aggregation model (spec 2026-07-01-04). The synthetic `sleep` type and internal checks are exempt at the validation site, not here.
const MetricKeyTunnelSetupMs = "tunnel_setup_ms"
MetricKeyTunnelSetupMs is the result-metric key holding how long establishing the tunnel took. Tunnel setup is deliberately kept OUT of the check's measured Duration (each checker times its own probe, which starts after the dialer is handed to it) — otherwise every tunneled check's latency graph would be dominated by SSH handshakes rather than by the target's response.
const OutputKeyTunnelFailed = "tunnel_failed"
OutputKeyTunnelFailed is the machine-readable marker set on a result whose tunnel could not be established (resolve, dial, auth, host-key mismatch, forward rejected). It distinguishes "the bastion is broken" from "the target behind the bastion is down" — groundwork for dependency-aware suppression.
const TunnelCheckUIDConfigKey = "tunnelCheckUid"
TunnelCheckUIDConfigKey is the well-known check-config key referencing the SSH check whose connection the probe is dialed through. It follows the `timeout` precedent (see checkworker.checkTimeoutConfigKey): the worker reads it generically off the raw config map and the per-checker `FromMap` implementations simply ignore the unknown key, so no checker config struct grows a field for it.
The name is deliberately tunnel-specific rather than a generic `dependsOn`: the semantic is "dial through this". A generic dependency concept can arrive later and subsume the edge.
Variables ¶
var ErrInvalidConfigType = errors.New("invalid config type")
ErrInvalidConfigType is returned when a config value doesn't match the expected type.
var ErrInvalidIPVersion = errors.New("invalid ipVersion")
ErrInvalidIPVersion is returned for a value that is not auto/ipv4/ipv6.
var ErrNoAddressForFamily = errors.New("no address of the requested IP version")
ErrNoAddressForFamily is the cataloged failure for "the target has no address of the requested family" — usually a missing AAAA record. It is deliberately distinct from a dial failure: the target may be perfectly healthy, it simply is not reachable over the family this check pins.
var ErrWorkerNoEgress = errors.New("worker has no egress for the requested IP version")
ErrWorkerNoEgress is the cataloged failure for "this worker cannot send traffic over the requested family at all" — the node running the check has no IPv6 route, not the target being down. Kept separate from ErrNoAddressForFamily and from any dial error so the message can point the user at their region/worker instead of at their own service.
Functions ¶
func AssertConfig ¶
AssertConfig performs a type assertion on a Config value, returning a typed config or an error with descriptive type information.
func BuildHTTPTransport ¶
func BuildHTTPTransport( dialer ContextDialer, skipTLSVerify bool, version IPVersion, ) http.RoundTripper
BuildHTTPTransport returns the http.Transport an HTTP-speaking check needs, or nil when it needs none.
Returning nil matters: with a nil Transport net/http uses the shared http.DefaultTransport, which keeps its connection pool across executions. Only a check that actually needs a tunnel dialer, a relaxed TLS config, or a pinned address family pays for a private transport.
It lives in checkerdef (rather than in checkhttp, where it was born) so every checker that speaks plain HTTP — checkhttp, checkprometheus — honors `tunnelCheckUid` and `ipVersion` through exactly the same code path instead of each growing its own near-copy.
func ClassifyDialError ¶
ClassifyDialError maps a failed dial to a reachability class, or "" when the error is not one.
timedOut is the caller's own answer to "did MY deadline fire?" — a probe context that expired is a connect timeout even when the underlying error is a bare `context.DeadlineExceeded` with no syscall attached.
DNS failures classify as "" on purpose. A name that does not resolve has no address to trace to, and resolution diagnostics are a different capture with its own spec.
func ClassifyTLSHandshakeError ¶
ClassifyTLSHandshakeError maps a failure that happened AFTER the TCP connection came up.
Only the stall is a reachability class. A certificate that is expired, self-signed, or for the wrong name is an application-level answer — the server is right there and talking — and must never trigger a trace.
func DialEgressProbe ¶
DialEgressProbe is the production EgressProbe: it opens a *connected* UDP socket towards ip. No packet is ever sent — connect(2) on a datagram socket only performs a route lookup — so this costs microseconds and never touches the target. A missing route (ENETUNREACH) or an unsupported family (EAFNOSUPPORT) surfaces here, which is precisely the "this worker has no IPv6" case we must not report as the target being down.
It is best-effort by construction: a host with a default v6 route into a blackhole passes the probe and the check then fails on the real dial. That is acceptable — the probe only ever upgrades an error message, never downgrades one.
func DropNetworkFailure ¶
func DropNetworkFailure(r *Result)
DropNetworkFailure removes a marker a lower layer recorded.
Its one caller is the tunneled probe path: the failure is real, but it happened on the far side of a bastion, so a trace run from this worker would describe a route the probe never took. Better no evidence than misleading evidence.
func ExtractTargetHost ¶
ExtractTargetHost derives the "target host" a check probes from its public (non-secret) config, independent of check type: the config's `host` field when present, else the hostname parsed from `url`, else `target`; nil when none apply (e.g. heartbeat/email passive checks, which carry only a `token`).
This is deliberately a flat, type-agnostic fallback chain rather than a per-CheckType switch: across every checker's config schema (see server/internal/checkers/check*/config.go), the field that names "the thing being probed" already converges on host/url/target — tcp/ssl/smtp/ssh/… use `host`, http/browser/websocket use `url`, dnsbl uses `target`. A new checker type gets a correct targetHost for free as long as it follows the same naming convention; no registry entry is required. Types with neither field (heartbeat, email, domain, kubernetes, sleep, …) correctly resolve to nil — a by-host view buckets them under "no host" rather than guessing.
One documented exception: checkdocker's `host` is a Docker daemon connection URI (`unix:///var/run/docker.sock` by default, or `tcp://host:port` when customized), not a bare hostname — resolveHostField normalizes it before it's treated as a hostname. Every other checker's `host` field was verified (as of this writing) to already be a bare hostname/IP validated alongside a separate `port` field, so this is not a general "host may be a URI" rule, just a targeted fix for the one type that doesn't follow the convention.
The computation is read-time only and not persisted: renaming a host in a check's config changes this value on the next read, with no migration.
func FamilyDialContext ¶
FamilyDialContext returns a DialContext pinned to one address family.
It resolves and selects explicitly (rather than just handing "tcp6" to net.Dialer) so that a target with no address of the requested family fails with checkerdef's cataloged error naming the host and the family, instead of the stdlib's opaque "no suitable address found" — and so the worker-has-no-v6 case is told apart from the target-has-no-AAAA case. The error travels out through *url.Error, which unwraps, so errors.Is still matches at the top.
func LocateNetworkFailure ¶
LocateNetworkFailure fills in the endpoint on a marker a lower layer already classified.
The split exists because the two facts are known in different places: the dial helper sees the error, and only the caller that did the name resolution knows which address it handed that helper. A result with no marker is left alone — this never invents one.
func LookupIPAddr ¶
LookupIPAddr resolves host like net.Resolver.LookupIPAddr, but retries transient failures (timeouts, temporary server errors) with short attempts instead of letting one lost DNS packet consume the caller's entire budget. Non-transient failures (NXDOMAIN, cancellation, ...) return immediately. IP literals resolve without any DNS traffic, exactly like the stdlib.
func MatchesIPVersion ¶
MatchesIPVersion reports whether ip belongs to the requested family. auto matches everything.
func NewConfigError ¶
NewConfigError creates a new ConfigError for a specific parameter.
Example:
return checkerdef.NewConfigError("url", "must be a valid HTTP or HTTPS URL")
Example ¶
ExampleNewConfigError demonstrates how to create a config error for a specific parameter.
package main
import (
"fmt"
"github.com/fclairamb/solidping/server/internal/checkers/checkerdef"
)
func main() {
err := checkerdef.NewConfigError("url", "must be a valid HTTP or HTTPS URL")
fmt.Println(err)
}
Output: url: must be a valid HTTP or HTTPS URL
func NewConfigErrorf ¶
NewConfigErrorf creates a new ConfigError with a formatted message.
Example:
return checkerdef.NewConfigErrorf("timeout", "must be between %d and %d seconds", minTimeout, maxTimeout)
Example ¶
ExampleNewConfigErrorf demonstrates how to create a config error with formatted message.
package main
import (
"fmt"
"github.com/fclairamb/solidping/server/internal/checkers/checkerdef"
)
func main() {
minPort := 1
maxPort := 65535
err := checkerdef.NewConfigErrorf("port", "must be between %d and %d", minPort, maxPort)
fmt.Println(err)
}
Output: port: must be between 1 and 65535
func NormalizeConfigFor ¶
NormalizeConfigFor normalizes a config map through cfg's optional ConfigNormalizer, returning the map untouched when cfg does not implement it.
func ProbeEgress ¶
ProbeEgress reports, per address family, whether this host can originate traffic at all. It is the proactive half of the ErrWorkerNoEgress story: the same question the per-run pre-flight answers, asked before a check is ever created rather than after one has already failed.
It is deliberately built on DialEgressProbe so the codebase holds exactly ONE route-lookup implementation — a second, subtly different one would be free to disagree with the authority, which is the failure this whole feature exists to avoid.
The result is a HINT, advertised so a user can pick a region that works. It must never gate execution: SelectIPAddrWithProbe's per-run probe remains the only authority, so a host that gains v6 runs immediately (no stale flag blocks it) and a host that loses v6 still fails with ErrWorkerNoEgress rather than a false DOWN.
func ProbeEgressWith ¶
func ProbeEgressWith(probe EgressProbe) map[IPVersion]bool
ProbeEgressWith is ProbeEgress with the route lookup injectable, the same seam SelectIPAddrWithProbe exposes: tests drive the v4-only and dual-stack hosts deterministically instead of depending on the CI runner's networking.
func SampleConfigWithIPVersion ¶
SampleConfigWithIPVersion returns cfg with the shared `ipVersion` key set. It exists because `ipVersion` is deliberately NOT a field on any checker's config struct — samples are built through those structs, so a sample that demonstrates the option needs this one seam rather than nine new fields.
func SelectIPAddr ¶
SelectIPAddr picks the single address a check dials, out of the addresses the resolver returned. It is THE address-family decision for every checker that resolves before dialing — no checker keeps its own preference loop, because nine copies of that loop is exactly how this option would rot back into per-type inconsistency.
Behavior:
- auto reproduces the historical pick byte-for-byte: the first IPv4 address, normalized to its 4-byte form (checkicmp relied on that), and otherwise the first address the resolver returned. No egress probe runs, so an auto check performs exactly the syscalls it always did.
- ipv4/ipv6 filter to that family and never fall back — falling back is what makes an "IPv6 check" silently an IPv4 check. With no address of the family, the check fails with ErrNoAddressForFamily naming the host and the family, not a generic dial error.
- ipv4/ipv6 additionally pre-flight local egress, so a worker with no IPv6 route reports ErrWorkerNoEgress ("look at your region") instead of a network-unreachable dial error that reads as "your target is down".
func SelectIPAddrWithProbe ¶
func SelectIPAddrWithProbe( host string, addrs []net.IPAddr, version IPVersion, probe EgressProbe, ) (net.IP, error)
SelectIPAddrWithProbe is SelectIPAddr with the local-egress pre-flight injectable. Production code calls SelectIPAddr; tests use this to exercise the worker-has-no-IPv6 branch deterministically. A nil probe disables the pre-flight.
func TunnelCheckUIDFrom ¶
TunnelCheckUIDFrom extracts the tunnel check reference from a raw check config map. Returns ("", false) when absent, not a string, or empty.
func WithIPVersion ¶
WithIPVersion returns a context carrying the address family every outbound connection of the check must use. The worker sets it once per execution from the check's config; checkers only consume it. Mirrors WithTunnelDialer.
func WithSMTPJobIdentity ¶
func WithSMTPJobIdentity(ctx context.Context, identity SMTPJobIdentity) context.Context
WithSMTPJobIdentity returns a context marking a real, dispatched SMTP check job. The worker sets it once per execution for every SMTP job (send mode or not — see applySMTPDeliveryContext); checkers only consume it to decide whether send mode is allowed to run at all.
func WithTunnelDialer ¶
func WithTunnelDialer(ctx context.Context, dialer ContextDialer) context.Context
WithTunnelDialer returns a context carrying the dialer every outbound connection of the check should be made through. The worker sets it once per execution after establishing the tunnel; checkers only consume it.
Types ¶
type ActivationResolver ¶
type ActivationResolver struct {
// contains filtered or unexported fields
}
ActivationResolver determines which check types are enabled based on server config and org overrides.
func NewActivationResolver ¶
func NewActivationResolver(cfg *config.CheckersConfig) *ActivationResolver
NewActivationResolver creates a resolver from the server-level checkers configuration.
func (*ActivationResolver) IsTypeEnabled ¶
func (r *ActivationResolver) IsTypeEnabled(checkType CheckType, orgDisabled []string) bool
IsTypeEnabled returns true if the check type is enabled at both server and org level.
func (*ActivationResolver) ListAllWithStatus ¶
func (r *ActivationResolver) ListAllWithStatus(orgDisabled []string) []CheckTypeStatus
ListAllWithStatus returns all check type metadata annotated with enabled status and reason.
func (*ActivationResolver) ListEnabledTypes ¶
func (r *ActivationResolver) ListEnabledTypes(orgDisabled []string) []CheckTypeMeta
ListEnabledTypes returns metadata for all types that are enabled (server minus org-disabled).
type CheckSpec ¶
type CheckSpec struct {
// Name is the human-readable name for the sample check.
Name string
// Slug is the URL-friendly identifier for the sample check.
Slug string
// Period is the check frequency interval.
Period time.Duration
// Config is the actual checker configuration.
Config map[string]any
}
CheckSpec represents a sample check configuration with metadata.
type CheckType ¶
type CheckType string
CheckType represents the type of a check.
const ( // CheckTypeHTTP performs HTTP/HTTPS endpoint monitoring. CheckTypeHTTP CheckType = "http" // CheckTypeTCP performs TCP port connectivity checks. CheckTypeTCP CheckType = "tcp" // CheckTypeICMP performs ICMP ping checks. CheckTypeICMP CheckType = "icmp" // CheckTypeDNS performs DNS record resolution checks. CheckTypeDNS CheckType = "dns" // CheckTypeSSL performs SSL/TLS certificate validation checks. CheckTypeSSL CheckType = "ssl" // CheckTypeHeartbeat monitors via incoming pings (passive check). CheckTypeHeartbeat CheckType = "heartbeat" // CheckTypeEmail monitors via incoming emails to a unique address (passive check). CheckTypeEmail CheckType = "email" // CheckTypeDomain monitors domain name expiration. CheckTypeDomain CheckType = "domain" // CheckTypeSMTP performs SMTP server health checks. CheckTypeSMTP CheckType = "smtp" // CheckTypeUDP performs UDP port reachability checks. CheckTypeUDP CheckType = "udp" // CheckTypeSSH performs SSH server health checks. CheckTypeSSH CheckType = "ssh" // CheckTypePOP3 performs POP3 server health checks. CheckTypePOP3 CheckType = "pop3" // CheckTypeIMAP performs IMAP server health checks. CheckTypeIMAP CheckType = "imap" // CheckTypeWebSocket performs WebSocket connectivity checks. CheckTypeWebSocket CheckType = "websocket" // CheckTypePostgreSQL performs PostgreSQL database health checks. CheckTypePostgreSQL CheckType = "postgresql" // CheckTypeFTP performs FTP server health checks. CheckTypeFTP CheckType = "ftp" // CheckTypeSFTP performs SFTP server health checks. CheckTypeSFTP CheckType = "sftp" // CheckTypeJS runs custom JavaScript monitoring scripts. CheckTypeJS CheckType = "js" // CheckTypeMySQL performs MySQL/MariaDB database health checks. CheckTypeMySQL CheckType = "mysql" // CheckTypeRedis performs Redis health checks. CheckTypeRedis CheckType = "redis" // CheckTypeMongoDB performs MongoDB database health checks. CheckTypeMongoDB CheckType = "mongodb" // CheckTypeMSSQL performs Microsoft SQL Server health checks. CheckTypeMSSQL CheckType = "mssql" // CheckTypeOracle performs Oracle Database health checks. CheckTypeOracle CheckType = "oracle" // CheckTypeClickHouse performs ClickHouse health checks over the native // (binary) protocol. CheckTypeClickHouse CheckType = "clickhouse" // CheckTypeGRPC performs gRPC health checks. CheckTypeGRPC CheckType = "grpc" // CheckTypeKafka performs Kafka cluster health checks. CheckTypeKafka CheckType = "kafka" // CheckTypeMQTT performs MQTT broker health checks. CheckTypeMQTT CheckType = "mqtt" // CheckTypeA2S performs Source engine game server health checks via the A2S query protocol. CheckTypeA2S CheckType = "a2s" // CheckTypeMinecraft performs Minecraft server health checks (Java + Bedrock editions). CheckTypeMinecraft CheckType = "minecraft" // CheckTypeRabbitMQ performs RabbitMQ health checks. CheckTypeRabbitMQ CheckType = "rabbitmq" // CheckTypeSNMP performs SNMP health checks. CheckTypeSNMP CheckType = "snmp" // CheckTypeDocker performs Docker container health checks. CheckTypeDocker CheckType = "docker" // CheckTypeBrowser performs headless Chrome browser health checks. CheckTypeBrowser CheckType = "browser" // CheckTypeFreeboxLine monitors xDSL/FTTH line quality via the Freebox OS API. CheckTypeFreeboxLine CheckType = "freebox_line" // CheckTypeDNSBL checks whether an IP/domain is listed on DNS blocklists. CheckTypeDNSBL CheckType = "dnsbl" // CheckTypeSIP checks SIP server reachability (OPTIONS) and registration (REGISTER). CheckTypeSIP CheckType = "sip" // CheckTypeKubernetes monitors a Kubernetes workload's replica health // (Deployment / ReplicaSet ready vs desired replicas). CheckTypeKubernetes CheckType = "kubernetes" // CheckTypeNTP monitors an NTP time server: reachability plus the server's // self-reported health (stratum, leap indicator, root distance), with // optional clock-offset and max-stratum thresholds. CheckTypeNTP CheckType = "ntp" // CheckTypeRDP monitors Remote Desktop Protocol servers via the pre-auth // X.224 negotiation handshake (MS-RDPBCGR): service liveness, negotiated // security protocol (optionally enforcing NLA), and certificate expiry // when a TLS-based protocol is selected. No credentials are used. CheckTypeRDP CheckType = "rdp" // CheckTypePrometheus reads one numeric value out of a Prometheus // metrics endpoint (scrape mode) or a Prometheus server (promql mode) // and grades it against warning/critical thresholds. It is the first // check type that inspects a value rather than a service. CheckTypePrometheus CheckType = "prometheus" // CheckTypeSleep is a synthetic/testing check that sleeps for a configured // duration. It performs no network I/O and exists as a deterministic load // generator for the scheduler. It is NOT a customer-facing check type and // must not be counted in the customer "N check types" tally. CheckTypeSleep CheckType = "sleep" )
Supported check types.
func ListCheckTypes ¶
func ListCheckTypes(_ *ListSampleOptions) []CheckType
ListCheckTypes returns a list of supported check types based on the provided options.
func (CheckType) IsPassive ¶
IsPassive reports whether the check type is passive — driven by an inbound signal (an HTTP heartbeat, an incoming email) rather than by an outbound probe the worker makes.
This matters beyond the worker loop: a passive check returns before the per-org MaxChecksPerMinute token gate, so it never consumes execution budget and must be excluded when computing an org's scheduled demand against that cap (spec 2026-08-26-03). Keep this the single definition — a second copy would silently drift the gate and the demand figure apart.
type CheckTypeMeta ¶
type CheckTypeMeta struct {
Type CheckType `json:"type"`
Labels []string `json:"labels"`
Description string `json:"description"`
MinPeriod time.Duration `json:"-"` // Minimum allowed check period (0 = use global default)
MaxPeriod time.Duration `json:"-"` // Maximum allowed check period (0 = no limit)
DefaultPeriod time.Duration `json:"-"` // Default check period (0 = use global default)
// SupportsTunnel reports whether the type honors a tunnel dialer
// (TunnelDialerFrom) and can therefore carry a `tunnelCheckUid` in its
// config. Declarative metadata rather than a hand-maintained list
// elsewhere: the API serves it and the dashboard gates its selector on it.
// Enabled for every TCP-dialing type that routes its probe through the
// context dialer: http, tcp, the mail protocols (smtp/imap/pop3), ssl, the
// database drivers (postgres/mysql/mssql/oracle), and the client-library
// types (redis/mongodb/rabbitmq/kafka/grpc/websocket/ftp/mqtt). UDP/ICMP
// types cannot — SSH direct-tcpip forwards TCP only.
SupportsTunnel bool `json:"supportsTunnel"`
// SupportsIPVersion reports whether the type honors the shared `ipVersion`
// config key (auto/ipv4/ipv6). Declarative metadata for the same reason as
// SupportsTunnel: the API serves it and the dashboard gates its selector on
// it, instead of a hand-maintained type list that would drift.
//
// Enabled for the types that resolve a hostname and pick one address
// themselves (tcp, udp, icmp, ssl, ssh, smtp, imap, pop3, dnsbl) plus http,
// which pins the family on its transport instead. Deliberately NOT enabled
// for `dns`: for a DNS check "ipVersion" could mean either which record
// types to assert on or which transport to reach the nameserver over —
// different features, neither implemented here, so the option is rejected
// rather than silently ignored. Everything else either has no network
// target (heartbeat, email, sleep) or dials by name through a client
// library that exposes no address-family seam.
SupportsIPVersion bool `json:"supportsIpVersion"`
}
CheckTypeMeta holds metadata and labels for a check type.
func GetCheckTypeMeta ¶
func GetCheckTypeMeta(ct CheckType) *CheckTypeMeta
GetCheckTypeMeta returns the metadata for a given check type, or nil if not found.
func ListCheckTypeMetas ¶
func ListCheckTypeMetas() []CheckTypeMeta
ListCheckTypeMetas returns all registered check type metadata.
func (*CheckTypeMeta) MatchesLabels ¶
func (m *CheckTypeMeta) MatchesLabels(labels []string) bool
MatchesLabels returns true if the check type has any of the given labels.
type CheckTypeStatus ¶
type CheckTypeStatus struct {
CheckTypeMeta
Enabled bool `json:"enabled"`
DisabledReason string `json:"disabledReason,omitempty"`
}
CheckTypeStatus extends CheckTypeMeta with activation status.
type Checker ¶
type Checker interface {
// Type returns the check type identifier this checker handles (e.g., "http", "tcp").
Type() CheckType
// Validate checks if the configuration is valid.
// It shall not perform any network operations.
// Returns nil if valid, or an error describing what's wrong.
Validate(spec *CheckSpec) error
// Execute performs the check and returns the result.
// The context should be used for cancellation and timeout control.
// The config is already validated before being passed to Execute.
// Returns a pointer to Result and an error. If error is not nil, Result will be nil.
Execute(ctx context.Context, config Config) (*Result, error)
}
Checker is the interface that all protocol checkers must implement.
type CheckerSamplesProvider ¶
type CheckerSamplesProvider interface {
// GetSampleConfigs returns a slice of sample configurations with metadata.
GetSampleConfigs(opts *ListSampleOptions) []CheckSpec
}
CheckerSamplesProvider is an optional interface that provides sample configurations.
type Config ¶
type Config interface {
// FromMap populates the configuration from a map.
// Returns an error if the map contains invalid values.
// TODO: Remove it
FromMap(configMap map[string]any) error
// GetConfig returns the configuration as a map.
// TODO: Support it through the `models.Result` so that we can pass it directly to the plugins
GetConfig() map[string]any
}
Config is the interface that all check configurations must implement. Each checker defines its own config struct with protocol-specific fields.
type ConfigError ¶
type ConfigError struct {
// Parameter is the name of the configuration parameter that failed validation.
// This should match the field name in the configuration map/struct.
Parameter string
// Message is the human-readable error message describing what's wrong.
Message string
}
ConfigError represents an error specific to a configuration parameter. It provides structured information about which parameter failed validation and why, making it easier for API clients to display field-specific errors.
Example (Validate) ¶
ExampleConfigError demonstrates typical usage in a Validate method.
package main
import (
"fmt"
"github.com/fclairamb/solidping/server/internal/checkers/checkerdef"
)
func main() {
// This would typically be in a checker's Validate method
validateURL := func(url string) error {
if url == "" {
return checkerdef.NewConfigError("url", "cannot be empty")
}
if len(url) > 2048 {
return checkerdef.NewConfigErrorf("url", "cannot exceed %d characters", 2048)
}
return nil
}
// Test empty URL
if err := validateURL(""); err != nil {
fmt.Println(err)
}
// Test too long URL
longURL := string(make([]byte, 3000))
if err := validateURL(longURL); err != nil {
fmt.Println(err)
}
}
Output: url: cannot be empty url: cannot exceed 2048 characters
func IsConfigError ¶
func IsConfigError(err error) *ConfigError
IsConfigError checks if an error is a ConfigError and returns it. Returns nil if the error is not a ConfigError.
Example ¶
ExampleIsConfigError demonstrates how to check if an error is a ConfigError.
package main
import (
"fmt"
"github.com/fclairamb/solidping/server/internal/checkers/checkerdef"
)
func main() {
// Simulate a validation function returning a ConfigError
err := checkerdef.NewConfigError("timeout", "must be positive")
// Check if it's a ConfigError
if configErr := checkerdef.IsConfigError(err); configErr != nil {
fmt.Printf("Parameter: %s, Message: %s\n", configErr.Parameter, configErr.Message)
}
}
Output: Parameter: timeout, Message: must be positive
func (*ConfigError) Error ¶
func (e *ConfigError) Error() string
Error implements the error interface.
type ConfigNormalizer ¶
type ConfigNormalizer interface {
NormalizeConfig(configMap map[string]any) (map[string]any, error)
}
ConfigNormalizer is an *optional* interface a checker config can implement to rewrite an effective (post-merge) config map into its canonical stored shape — e.g. folding HTTP's legacy `username`/`password` pair into a single reserved `basicAuth` key that `SecretFields` can then encrypt as a whole.
It is probed exactly like credentials.SecretFielder: an optional interface rather than a method on Config, so the 30+ checker types that need no normalization stay untouched.
Implementations MUST NOT mutate the input map (the caller may still hold it) and MUST be idempotent — normalizing an already-normalized map must be a no-op, since the same manifest can be applied repeatedly.
An error returned here MUST be a *ConfigError so handlers map it to a 400: on the PATCH path normalization is the only validation the config sees.
type ContextDialer ¶
type ContextDialer interface {
DialContext(ctx context.Context, network, addr string) (net.Conn, error)
}
ContextDialer is the dialer seam a tunnel-capable checker consumes: the `DialContext(ctx, network, addr) (net.Conn, error)` shape rather than the concrete *net.Dialer struct, so any transport (an SSH port-forward today, a SOCKS proxy or an agent-side relay tomorrow) can satisfy it. It is deliberately compatible with golang.org/x/net/proxy.ContextDialer and with what *ssh.Client already exposes.
`addr` is the raw `host:port` the check targets — NOT a pre-resolved IP. Tunneling implies remote-side name resolution: the SSH direct-tcpip request carries the hostname and the bastion resolves it, which is the whole point for private hostnames a worker's resolver can never see. Checkers must therefore skip their own local lookup when a tunnel dialer is present.
func TunnelDialerFrom ¶
func TunnelDialerFrom(ctx context.Context) ContextDialer
TunnelDialerFrom returns the tunnel dialer carried by ctx, or nil when the check is not tunneled (the overwhelmingly common case — callers must keep their untunneled path byte-for-byte unchanged).
type Diagnostics ¶
type Diagnostics struct {
// FailureResponse is the captured response of a FAILED probe. Nil unless
// the check opted in (HTTP: `capture_failure_response`) AND a response
// actually existed — a timeout, DNS failure or TLS error produces no
// response, so the capture degrades to absent and the existing error
// output remains the only evidence.
FailureResponse *FailureResponse `json:"failureResponse,omitempty"`
// Screenshot is the capture of what a FAILING browser check's page looked
// like. Nil unless the check opted in (browser: `screenshot`) and the
// capture succeeded.
//
// See Screenshot's own doc comment for why its bytes never cross the agent
// WS control channel.
Screenshot *Screenshot `json:"screenshot,omitempty"`
// NetworkFailure states that this probe failed to REACH its target, and
// names the endpoint it was dialing (spec 2026-08-21-10). It is set at
// transport-error sites only, which is what makes "an application-level
// failure never triggers a path trace" a property of where the field is
// written rather than of a string match on an error message.
//
// It is the only member of Diagnostics that is not opt-in per check: it
// costs a few dozen bytes and only ever exists on a failing probe. The
// opt-in is on the ACTION it enables (`tracerouteOnFailure`), decided
// server-side at the incident transition — see NetworkFailure's doc.
NetworkFailure *NetworkFailure `json:"networkFailure,omitempty"`
}
Diagnostics carries optional, opt-in operational evidence about a check execution — what the probe actually saw at the moment it decided the target was unhealthy.
It is DELIBERATELY NOT part of Result.Output. Output is persisted verbatim as the `results.output` JSONB column on every single execution, and raw result rows are reaped by the aggregation job 24 h later; writing a response body there would cost thousands of kilobyte-scale rows per flapping check per day for evidence nobody keeps. Diagnostics instead rides the wire next to Output and is persisted ONLY when the incident pipeline decides this result opened (or reopened) an incident — the object a human actually opens three days later.
Everything here is `omitempty` in both directions, so an agent that predates the field simply sends nothing and a new agent talking to an older server loses nothing but the capture.
type DialerFunc ¶
DialerFunc adapts a plain function to ContextDialer.
func (DialerFunc) DialContext ¶
DialContext implements ContextDialer.
type EgressCache ¶
type EgressCache struct {
// contains filtered or unexported fields
}
EgressCache memoizes a ProbeEgress result for a short TTL. A worker reports its egress on every heartbeat and (for a deported agent) on every claim frame, and a route lookup per claim would be wasteful churn for an answer that changes approximately never. The TTL is what keeps "probed at report time, not at process start" true: a host that gains or loses v6 still converges within one TTL plus one heartbeat, with no restart.
func NewEgressCache ¶
func NewEgressCache(ttl time.Duration) *EgressCache
NewEgressCache builds a cache over ProbeEgress with the given TTL. A non-positive TTL probes on every call.
func NewEgressCacheWith ¶
func NewEgressCacheWith(ttl time.Duration, probe func() map[IPVersion]bool) *EgressCache
NewEgressCacheWith is NewEgressCache with the prober injectable (tests).
func (*EgressCache) Get ¶
func (c *EgressCache) Get() map[IPVersion]bool
Get returns the cached families, re-probing when the entry has expired.
type EgressProbe ¶
EgressProbe reports whether this worker can originate traffic of the given family towards ip. It is the seam SelectIPAddr uses so tests can drive the "worker has no IPv6" branch without an actual v6-less host.
type FailureResponse ¶
type FailureResponse struct {
// URL is the URL the probe requested.
URL string `json:"url,omitempty"`
// StatusLine is the response status line, e.g. "HTTP/2.0 503 Service Unavailable".
StatusLine string `json:"statusLine,omitempty"`
// StatusCode is the numeric response status.
StatusCode int `json:"statusCode,omitempty"`
// Headers are the RESPONSE headers with sensitive values replaced by the
// redaction marker. Multi-valued headers are joined with ", ".
Headers map[string]string `json:"headers,omitempty"`
// Body is the captured response body, truncated to the capture cap. Empty
// when Binary is true — a non-text or non-UTF-8 body is never stored raw.
Body string `json:"body,omitempty"`
// Truncated reports that Body holds only the leading bytes of a larger body.
Truncated bool `json:"truncated,omitempty"`
// ContentLength is the response's declared Content-Length, or -1 when the
// response did not declare one (chunked). It is what tells a reader how
// much a truncated Body is missing.
ContentLength int64 `json:"contentLength,omitempty"`
// ContentType is the raw Content-Type response header.
ContentType string `json:"contentType,omitempty"`
// BodyBytes is the size in bytes of the body the probe actually read.
BodyBytes int `json:"bodyBytes,omitempty"`
// BodySHA256 is the hex SHA-256 of the bytes the probe read. It is the
// only identity a binary body gets, and it lets two captures be compared
// without storing either.
BodySHA256 string `json:"bodySha256,omitempty"`
// Binary reports that the body was not text-like or not valid UTF-8, so
// only metadata (ContentType, BodyBytes, BodySHA256) was kept.
Binary bool `json:"binary,omitempty"`
// CapturedAt is when the probe captured this response.
CapturedAt time.Time `json:"capturedAt,omitzero"`
// RemoteAddr is the address the probe actually connected to, when the
// transport reported one.
RemoteAddr string `json:"remoteAddr,omitempty"`
// Region is the probing region. Filled SERVER-SIDE from the persisted
// result row rather than by the checker, so it cannot be influenced by a
// deported agent's own idea of where it runs.
Region string `json:"region,omitempty"`
}
FailureResponse is the textual capture of the response a failing probe received. It is bounded by construction (see the caps in the capturing checker) so no storage quota work is needed.
SECURITY: this may carry internal hostnames, stack traces or PII from the monitored service, which is exactly why it is opt-in per check and why it must never reach a public surface (status pages, subscriber payloads). The REQUEST side is never captured at all — request headers carry the check's own credentials.
type IPVersion ¶
type IPVersion string
IPVersion is the address family a check is pinned to.
It exists because "the check is up" over IPv4 says nothing about whether the same target answers over IPv6: a dual-stack host with a broken AAAA path (missing firewall rule, dead v6 route, load balancer listening on v4 only) keeps reporting up while every IPv6 user is down. Pinning a check to a family is the only way to express "monitor this host over IPv6" without hardcoding a literal address and losing DNS coverage.
const ( // IPVersionAuto is the zero value and the default: pick one address // exactly the way the checker did before this option existed (IPv4-first // for the address-picking checkers, Happy Eyeballs for HTTP). It // deliberately does NOT mean "probe both families" — that would need two // probes and a rollup status; a user wanting both coverage creates two // checks. This is the one place SolidPing knowingly diverges from Better // Stack, whose null value means "use both". IPVersionAuto IPVersion = "auto" // IPVersionIPv4 pins the check to an A record / IPv4 address. IPVersionIPv4 IPVersion = "ipv4" // IPVersionIPv6 pins the check to an AAAA record / IPv6 address. IPVersionIPv6 IPVersion = "ipv6" )
Address families a check can be pinned to.
func IPVersionFrom ¶
IPVersionFrom returns the family carried by ctx, or IPVersionAuto when the check did not pin one (the overwhelmingly common case — callers must keep their auto path byte-for-byte unchanged).
func IPVersionFromConfig ¶
IPVersionFromConfig reads the requested family off a raw check-config map, accepting the canonical camelCase key and the snake_case fallback. A missing key yields IPVersionAuto; a present-but-wrong value is an error, never a silent fallback to auto.
func IPVersionOf ¶
IPVersionOf reports the family an address belongs to, for the `ip_version` result-output field. Every checker that reports the family goes through this rather than rolling its own `To4()` test, so the reported strings can never drift apart between check types.
func ParseIPVersion ¶
ParseIPVersion normalizes a user-supplied value. Empty, "auto" and unset all yield IPVersionAuto. Comparison is case-insensitive and surrounding whitespace is tolerated, but no aliases are invented: "v4", "4", "inet" and friends are rejected so a typo fails loudly at write time rather than silently monitoring the wrong family.
func (IPVersion) Explicit ¶
Explicit reports whether the check pinned a specific family. Everything in this package treats "not explicit" as "behave exactly as before the option existed".
func (IPVersion) Label ¶
Label renders the family the way users write it in prose and error messages.
func (IPVersion) Network ¶
Network qualifies a base network name ("tcp", "udp", "ip") with the address family, yielding the "tcp4"/"tcp6" forms net.Dial understands. auto returns the base name unchanged, which is what keeps the default path byte-for-byte identical.
type ListSampleOptionType ¶
type ListSampleOptionType uint8
ListSampleOptionType represents the type of sample configuration to retrieve.
const ( // Default represents standard sample configurations for normal operation. Default ListSampleOptionType = iota // Demo represents sample configurations optimized for demonstration purposes. Demo ListSampleOptionType = iota // Test represents sample configurations for testing scenarios. Test ListSampleOptionType = iota )
Sample option types.
type ListSampleOptions ¶
type ListSampleOptions struct {
Type ListSampleOptionType
BaseURL string // Base URL for self-referencing checks (e.g., fake API)
}
ListSampleOptions represents options for listing check types.
type NetworkFailure ¶
type NetworkFailure struct {
// Class is one of the NetFailure* constants.
Class string `json:"class"`
// Host is the configured hostname, for display. May be empty, and may be
// the same as Address when the check was configured with a literal IP.
Host string `json:"host,omitempty"`
// Address is the resolved IP the probe dialed.
Address string `json:"address,omitempty"`
// Port is the TCP/UDP port the probe dialed. Zero for ICMP checks, which
// is exactly what makes the TCP fallback prober unusable for them.
Port int `json:"port,omitempty"`
}
NetworkFailure is the checker's statement that this failure was about REACHING the target, plus the exact endpoint it was trying to reach.
It is what makes "trace only network failures" a structural property rather than a string match on an error message: it is set at transport-error sites and nowhere else, so a check that got a response — any response — never carries one.
UNLIKE Screenshot's bytes, this DOES ride the agent WS result frame. It is a few dozen bytes of scalars, and the server needs it to decide whether to ask for a trace at all.
Address is the IP the probe actually dialed, not the configured hostname. That is deliberate: it is what lets the trace follow the same IP family the check pinned via `ipVersion`, and what stops a round-robin DNS record from producing a trace to a different machine than the one that failed.
func NewNetworkFailure ¶
func NewNetworkFailure(class, host, address string, port int) *NetworkFailure
NewNetworkFailure builds a marker, or nil when class is empty.
Returning nil for an unclassified error is the whole point: a caller wires this into its error path unconditionally and gets a marker only for the classes that warrant a trace.
type Result ¶
type Result struct {
Status Status // The check status
Duration time.Duration // Time taken to execute the check
Metrics map[string]any // Numerical metrics that can be aggregated (e.g., ttfb, dns_time)
Output map[string]any // Diagnostic output (error messages, status text, etc.)
// Diagnostics carries opt-in, incident-only evidence (today: the captured
// failing HTTP response). It is deliberately NOT part of Output: Output is
// persisted per raw result row as JSONB, and this payload is kilobytes.
// See the Diagnostics doc comment for the full rationale. Nil on every
// path that did not opt in — which is all of them by default.
Diagnostics *Diagnostics
}
Result represents the outcome of executing a check.
func (*Result) SetNetworkFailure ¶
func (r *Result) SetNetworkFailure(failure *NetworkFailure)
SetNetworkFailure hangs a marker on a result, allocating Diagnostics only when there is something to hang. A nil failure is a no-op, so callers can pass the result of NewNetworkFailure straight through.
type SMTPJobIdentity ¶
type SMTPJobIdentity struct {
CheckUID string
}
SMTPJobIdentity is the context-only marker that distinguishes a real, dispatched SMTP check job from any other way SMTPChecker.Execute can be reached (revised design, 2026-08-19).
Before this revision, send mode stored a delivery_check_uid REFERENCE and resolved it to a concrete recipient server-side at claim time, threading the resolved address through this same context-only seam — which structurally stopped a `js` check's sub-check helper (checkjs/checker.go:307, a verified second caller of Checker.Execute alongside worker.go's real job dispatch) from ever reaching a resolved recipient, since nothing populates the context for that path.
The revised design stores the recipient directly as a plain `delivery_to` config field instead (so send mode works on every worker/agent with no claim-time resolution or sealing question — see checksmtp.SMTPConfig's DeliveryTo doc comment), which removes that structural protection: a JS sub-check config can now supply `send_email: true` and a valid inbox-domain `delivery_to` directly. This marker replaces the old recipient-carrying context value with a narrower one that exists for exactly this gate: only worker.go's real job-dispatch path sets it (see applySMTPDeliveryContext), so a JS sub-check's execCtx — inherited from the OUTER job's context, which is never of type smtp for a JS check's own job — never carries it, regardless of what the sub-check's config map requests. CheckUID additionally carries the sending check's own UID for the X-SolidPing-Check attribution header (never itself security-sensitive — it is not authentication, just attribution).
func SMTPJobIdentityFrom ¶
func SMTPJobIdentityFrom(ctx context.Context) (SMTPJobIdentity, bool)
SMTPJobIdentityFrom returns the job identity carried by ctx, or (zero, false) when absent. Absence means this Execute call did not come from worker.go's real job dispatch — the checkjs sub-check path being the verified example — which send mode must treat as a loud error rather than silently sending anyway.
type Screenshot ¶
type Screenshot struct {
// PNG is the raw image. NEVER SERIALIZED — see the type doc.
PNG []byte `json:"-"`
// CapturedAt is when the screenshot was taken.
CapturedAt time.Time `json:"capturedAt,omitzero"`
// Available is the agent-side MARKER: "I hold a capture for this result".
// It is what crosses the wire in place of the bytes, so the server can ask
// for the upload if (and only if) this result opens an incident.
Available bool `json:"available,omitempty"`
// CaptureID names the capture in the agent's local LRU, so the server's
// upload request can identify which one it wants.
CaptureID string `json:"captureId,omitempty"`
// Region is the probing region. Filled SERVER-SIDE from the persisted
// result row, never by the checker — a deported agent must not be the
// authority on where it ran.
Region string `json:"region,omitempty"`
}
Screenshot is a PNG capture of the page a failing browser check was looking at, taken before the browser context is disposed.
HONESTY ABOUT WHAT THIS IS: it is what the page looked like a moment AFTER the check decided the target was unhealthy, not the frame at the instant of failure. Every surface that renders it must say so — presenting it as "the failure" invites an operator to conclude the wrong thing from a page that finished loading half a second later.
THE BYTES NEVER CROSS THE CONTROL CHANNEL. Diagnostics is serialized onto the agent WebSocket result frame (internal/agents/protocol.go), which is JSON: a megabyte PNG would become a multi-megabyte base64 blob on the socket every agent uses to claim work. PNG is therefore `json:"-"` — a deported agent uploads its bytes out-of-band to POST /api/v1/agent/attachments and advertises only the marker fields below. The IN-PROCESS worker path keeps the bytes in memory and never serializes them at all, which is why the field works there with no wire representation.
type Status ¶
type Status int
Status represents the outcome of a check execution.
const ( StatusRunning Status = 2 // Check process started but not yet completed StatusUp Status = 3 // Check succeeded StatusDown Status = 4 // Check failed (target unreachable or unhealthy) StatusTimeout Status = 5 // Check timed out StatusError Status = 6 // Internal error during check execution // StatusDegraded is the aggregated rollup status: a window contained // warning(s) but no dominating failure. It is produced ONLY by the // aggregation job, never returned by a checker's Execute. It is declared // here so Severity() can rank the value the rollup writes. StatusDegraded Status = 7 // StatusWarning indicates the target is up but there is something to // report (e.g. a certificate nearing expiry, a flapping container). It // counts as up for availability and is neutral for incidents; the // aggregation job promotes any window containing a raw Warning to the // aggregated Degraded status. Checkers never emit Degraded directly. StatusWarning Status = 8 )
Check status constants — values match models.ResultStatus.
func GradedExpiryStatus ¶
GradedExpiryStatus maps a days-remaining value onto a two-tier expiry policy, returning the Status to report and a severity label for output. It is the single source of truth for the tiering rule shared by expiry-aware checkers (checkssl and checkdomain) so the comparison literal is not duplicated across checkers.
Tiering (callers pass the MINIMUM days-remaining across whatever they monitor, e.g. the whole certificate chain):
- daysRemaining <= criticalDays → StatusDown, SeverityCritical
- criticalDays < daysRemaining <= warningDays → StatusWarning, SeverityWarning
- otherwise → StatusUp, SeverityNone
Callers are expected to validate warningDays >= criticalDays >= 0; when that holds the warning band is non-empty only when warningDays > criticalDays.
func IPVersionFailureStatus ¶
IPVersionFailureStatus maps an address-selection failure to the status the check should report.
"The target has no AAAA record" is a genuine failure of the thing being monitored — an IPv6 user cannot reach it — so it is StatusDown. "This worker has no IPv6 egress" is our own infrastructure gap and must not be recorded as the target being down, so it is StatusError.
Use this at sites where the error can only ever come from SelectIPAddr. Where the same error path also carries genuine resolve failures (a DNS lookup that failed, a name with no addresses at all), use ResolveFailureStatus so those keep the status the checker has always reported for them.
func ResolveFailureStatus ¶
ResolveFailureStatus maps a resolve-or-select failure to a status, falling back to the checker's historical status for anything that is not an address-family failure.
This exists so the SAME user-visible condition reports the SAME status on every check type. "example.com has no AAAA record" must not be Down on a tcp check and Error on an ssl check — Down and Error account differently for availability, so a user's uptime number would depend on which check type they happened to pick. The fallback keeps genuine resolve failures (NXDOMAIN, a dead resolver) reporting exactly what that checker reported before.
func (Status) Severity ¶
Severity ranks statuses so the aggregation job can resolve a dominant status by gravity rather than by raw numeric value (the numbers do not encode severity once Degraded=7 / Warning=8 exist). Higher = more severe.
Hard failures (Down/Timeout/Error) outrank everything; the aggregated Degraded status (7, produced only by rollups) sits below failures but above Up; Warning ranks at Up level for availability (it counts as up), while its promotion to Degraded in a rollup is handled separately in the aggregation job, not here. Created/Running are lifecycle markers and rank lowest.