// Package probes implements the Checker port for each check kind. // Each file exports a Checker constructor (e.g. NewHTTPChecker) that // returns ports.Checker wrapping the scheduler's probe logic. package probes import ( "context" "crypto/tls" "encoding/json" "fmt" "io" "net" "net/http" "time" "github.com/dtoro/oikos/internal/core/ports" ) // httpChecker is the HTTP reachability probe. type httpChecker struct { client *http.Client } // NewHTTPChecker builds an HTTP probe with a connection-scoped client. func NewHTTPChecker() ports.Checker { return &httpChecker{ client: &http.Client{ Timeout: 10 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, }, }, } } func (c *httpChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult { cfg := configMap(def) url, _ := parseStr(cfg, "url") if url == "" { return ports.CheckResult{State: "unknown", Message: "no url in config"} } maxStatus := int(parseFloat(cfg, "max_status", 500)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return ports.CheckResult{State: "unknown", Message: err.Error()} } resp, err := c.client.Do(req) if err != nil { return ports.CheckResult{State: "unknown", Message: err.Error()} } defer resp.Body.Close() io.Copy(io.Discard, resp.Body) code := resp.StatusCode if code > maxStatus { return ports.CheckResult{ State: "critical", Value: float64(code), Message: fmt.Sprintf("HTTP %d exceeds max_status %d", code, maxStatus), } } return ports.CheckResult{State: "ok", Value: float64(code)} } // tcpChecker checks TCP port reachability. type tcpChecker struct{} func NewTCPChecker() ports.Checker { return &tcpChecker{} } func (c *tcpChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult { cfg := configMap(def) host, _ := parseStr(cfg, "host") port := int(parseFloat(cfg, "port", 0)) timeout := time.Duration(parseFloat(cfg, "timeout", 5)) * time.Second addr := fmt.Sprintf("%s:%d", host, port) d := &net.Dialer{Timeout: timeout} conn, err := d.DialContext(ctx, "tcp", addr) if err != nil { return ports.CheckResult{State: "critical", Message: err.Error()} } conn.Close() return ports.CheckResult{State: "ok", Value: float64(port)} } // pingChecker is the ICMP/connectivity probe. Falls back to TCP ping on // systems without raw socket access. type pingChecker struct{} func NewPingChecker() ports.Checker { return &pingChecker{} } func (c *pingChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult { cfg := configMap(def) host, _ := parseStr(cfg, "host") if host == "" { return ports.CheckResult{State: "unknown", Message: "no host in config"} } port := int(parseFloat(cfg, "port", 80)) timeout := time.Duration(parseFloat(cfg, "timeout", 5)) * time.Second d := &net.Dialer{Timeout: timeout} addr := fmt.Sprintf("%s:%d", host, port) conn, err := d.DialContext(ctx, "tcp", addr) if err != nil { return ports.CheckResult{State: "critical", Message: err.Error(), Value: -1} } conn.Close() return ports.CheckResult{State: "ok", Value: 0} } // dnsChecker resolves DNS names. type dnsChecker struct{} func NewDNSChecker() ports.Checker { return &dnsChecker{} } func (c *dnsChecker) Check(ctx context.Context, def ports.CheckDef, _ ports.Target) ports.CheckResult { cfg := configMap(def) name, _ := parseStr(cfg, "name") if name == "" { return ports.CheckResult{State: "unknown", Message: "no name in config"} } var r net.Resolver addrs, err := r.LookupHost(ctx, name) if err != nil { return ports.CheckResult{State: "critical", Message: err.Error()} } return ports.CheckResult{State: "ok", Value: float64(len(addrs))} } // configMap unmarshals a check's JSON config into a map. func configMap(def ports.CheckDef) map[string]any { var m map[string]any if len(def.Config) > 0 { json.Unmarshal(def.Config, &m) } if m == nil { m = map[string]any{} } return m } func parseStr(m map[string]any, key string) (string, bool) { v, ok := m[key] if !ok { return "", false } s, ok := v.(string) return s, ok } func parseFloat(m map[string]any, key string, def float64) float64 { v, ok := m[key] if !ok { return def } switch n := v.(type) { case float64: return n case int: return float64(n) case json.Number: f, _ := n.Float64() return f } return def }