diff --git a/checker/evaluate.go b/checker/evaluate.go index 8cef1aa..f64b2f1 100644 --- a/checker/evaluate.go +++ b/checker/evaluate.go @@ -23,7 +23,6 @@ package checker import ( "fmt" - "strings" "time" ) @@ -53,48 +52,43 @@ type Metric struct { Timestamp time.Time `json:"timestamp"` } -// EvaluateResult holds the evaluation outcome. +// EvaluateResult holds the evaluation outcome for a single target. type EvaluateResult struct { + Address string `json:"address"` Status int `json:"status"` Message string `json:"message"` Code string `json:"code"` } const ( - StatusOK = 1 - StatusWarn = 3 - StatusCrit = 4 + StatusUnknown = 0 + StatusOK = 1 + StatusWarn = 3 + StatusCrit = 4 ) -// Evaluate checks the ping data against the given thresholds. -// StatusUnknown indicates the check could not be performed. -const StatusUnknown = 0 - -func Evaluate(data *PingData, warningRTT, criticalRTT, warningPacketLoss, criticalPacketLoss float64) EvaluateResult { +// Evaluate checks the ping data against the given thresholds and returns one +// result per target. +func Evaluate(data *PingData, warningRTT, criticalRTT, warningPacketLoss, criticalPacketLoss float64) []EvaluateResult { if len(data.Targets) == 0 { - return EvaluateResult{ - Status: StatusUnknown, - Message: "No targets to ping", - Code: "ping_no_targets", - } + return nil } - overallStatus := StatusOK - var summaryParts []string - + results := make([]EvaluateResult, 0, len(data.Targets)) for _, target := range data.Targets { + status := StatusOK if target.PacketLoss >= criticalPacketLoss || target.RTTAvg >= criticalRTT { - overallStatus = StatusCrit - } else if (target.PacketLoss >= warningPacketLoss || target.RTTAvg >= warningRTT) && overallStatus < StatusWarn { - overallStatus = StatusWarn + status = StatusCrit + } else if target.PacketLoss >= warningPacketLoss || target.RTTAvg >= warningRTT { + status = StatusWarn } - summaryParts = append(summaryParts, fmt.Sprintf("%s: %.1fms avg, %.0f%% loss", target.Address, target.RTTAvg, target.PacketLoss)) - } - - return EvaluateResult{ - Status: overallStatus, - Message: strings.Join(summaryParts, " | "), - Code: "ping_result", + results = append(results, EvaluateResult{ + Address: target.Address, + Status: status, + Message: fmt.Sprintf("%.1fms avg, %.0f%% loss", target.RTTAvg, target.PacketLoss), + Code: "ping_result", + }) } + return results } diff --git a/checker/interactive.go b/checker/interactive.go new file mode 100644 index 0000000..784db24 --- /dev/null +++ b/checker/interactive.go @@ -0,0 +1,109 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2020-2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package checker + +import ( + "errors" + "net/http" + "strconv" + "strings" + + sdk "git.happydns.org/checker-sdk-go/checker" +) + +// RenderForm implements sdk.CheckerInteractive. It exposes a minimal form +// letting a human submit one or more ping targets (hostnames or IPs) along +// with the usual threshold knobs. +func (p *pingProvider) RenderForm() []sdk.CheckerOptionField { + return []sdk.CheckerOptionField{ + { + Id: "addresses", + Type: "string", + Label: "Targets", + Placeholder: "example.com, 192.0.2.1", + Description: "Comma- or newline-separated list of hostnames or IP addresses.", + Required: true, + }, + { + Id: "count", + Type: "uint", + Label: "Number of pings to send", + Default: float64(5), + }, + { + Id: "warningRTT", + Type: "number", + Label: "Warning RTT threshold (ms)", + Default: float64(100), + }, + { + Id: "criticalRTT", + Type: "number", + Label: "Critical RTT threshold (ms)", + Default: float64(500), + }, + { + Id: "warningPacketLoss", + Type: "number", + Label: "Warning packet loss threshold (%)", + Default: float64(10), + }, + { + Id: "criticalPacketLoss", + Type: "number", + Label: "Critical packet loss threshold (%)", + Default: float64(50), + }, + } +} + +// ParseForm implements sdk.CheckerInteractive. It converts the HTML form +// inputs into a CheckerOptions that Collect can consume directly — pinging +// resolves hostnames on its own, so no extra lookups are needed here. +func (p *pingProvider) ParseForm(r *http.Request) (sdk.CheckerOptions, error) { + raw := strings.TrimSpace(r.FormValue("addresses")) + if raw == "" { + return nil, errors.New("at least one target is required") + } + + var addresses []string + for _, part := range strings.FieldsFunc(raw, func(c rune) bool { + return c == ',' || c == '\n' || c == '\r' || c == ' ' || c == '\t' || c == ';' + }) { + if part = strings.TrimSpace(part); part != "" { + addresses = append(addresses, part) + } + } + if len(addresses) == 0 { + return nil, errors.New("at least one target is required") + } + + opts := sdk.CheckerOptions{"addresses": addresses} + for _, k := range []string{"count", "warningRTT", "criticalRTT", "warningPacketLoss", "criticalPacketLoss"} { + if v := strings.TrimSpace(r.FormValue(k)); v != "" { + if n, err := strconv.ParseFloat(v, 64); err == nil { + opts[k] = n + } + } + } + return opts, nil +} diff --git a/checker/provider.go b/checker/provider.go index ab6b7e8..3b7a411 100644 --- a/checker/provider.go +++ b/checker/provider.go @@ -53,9 +53,9 @@ func (p *pingProvider) Definition() *happydns.CheckerDefinition { } // ExtractMetrics implements happydns.CheckerMetricsReporter. -func (p *pingProvider) ExtractMetrics(raw json.RawMessage, collectedAt time.Time) ([]happydns.CheckMetric, error) { +func (p *pingProvider) ExtractMetrics(ctx happydns.ReportContext, collectedAt time.Time) ([]happydns.CheckMetric, error) { var data PingData - if err := json.Unmarshal(raw, &data); err != nil { + if err := json.Unmarshal(ctx.Data(), &data); err != nil { return nil, err } diff --git a/checker/rule.go b/checker/rule.go index 5932cb1..f54d76a 100644 --- a/checker/rule.go +++ b/checker/rule.go @@ -106,14 +106,14 @@ func (r *pingRule) ValidateOptions(opts sdk.CheckerOptions) error { return nil } -func (r *pingRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState { +func (r *pingRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState { var data PingData if err := obs.Get(ctx, ObservationKeyPing, &data); err != nil { - return sdk.CheckState{ + return []sdk.CheckState{{ Status: sdk.StatusError, Message: fmt.Sprintf("Failed to get ping data: %v", err), Code: "ping_error", - } + }} } warningRTT := sdk.GetFloatOption(opts, "warningRTT", 100) @@ -121,26 +121,44 @@ func (r *pingRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts warningPacketLoss := sdk.GetFloatOption(opts, "warningPacketLoss", 10) criticalPacketLoss := sdk.GetFloatOption(opts, "criticalPacketLoss", 50) - result := Evaluate(&data, warningRTT, criticalRTT, warningPacketLoss, criticalPacketLoss) - - var status sdk.Status - switch result.Status { - case StatusOK: - status = sdk.StatusOK - case StatusWarn: - status = sdk.StatusWarn - case StatusCrit: - status = sdk.StatusCrit - default: - status = sdk.StatusUnknown + results := Evaluate(&data, warningRTT, criticalRTT, warningPacketLoss, criticalPacketLoss) + if len(results) == 0 { + return []sdk.CheckState{{ + Status: sdk.StatusInfo, + Message: "No targets to ping", + Code: "ping_no_targets", + }} } - return sdk.CheckState{ - Status: status, - Message: result.Message, - Code: result.Code, - Meta: map[string]any{ - "targets": data.Targets, - }, + targetByAddr := make(map[string]PingTargetResult, len(data.Targets)) + for _, t := range data.Targets { + targetByAddr[t.Address] = t } + + out := make([]sdk.CheckState, 0, len(results)) + for _, r := range results { + var status sdk.Status + switch r.Status { + case StatusOK: + status = sdk.StatusOK + case StatusWarn: + status = sdk.StatusWarn + case StatusCrit: + status = sdk.StatusCrit + default: + status = sdk.StatusUnknown + } + + state := sdk.CheckState{ + Status: status, + Subject: r.Address, + Message: r.Message, + Code: r.Code, + } + if t, ok := targetByAddr[r.Address]; ok { + state.Meta = map[string]any{"target": t} + } + out = append(out, state) + } + return out } diff --git a/go.mod b/go.mod index 02a7c39..8e1e592 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,12 @@ module git.happydns.org/checker-ping go 1.25.0 require ( + git.happydns.org/checker-sdk-go v1.2.0 git.happydns.org/happyDomain v0.7.0 github.com/prometheus-community/pro-bing v0.8.0 ) require ( - git.happydns.org/checker-sdk-go v0.0.1 github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic v1.15.0 // indirect github.com/bytedance/sonic/loader v0.5.0 // indirect diff --git a/go.sum b/go.sum index 4a5626b..630d66c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.happydns.org/checker-sdk-go v0.0.1 h1:4RxCJr73HWKxjOyU/6NJMO8lXJmH0gMLA68EzTqLbQI= -git.happydns.org/checker-sdk-go v0.0.1/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= +git.happydns.org/checker-sdk-go v1.2.0 h1:v4MpKAz0W3PwP+bxx3pya8w893sVH5xTD1of1cc0TV8= +git.happydns.org/checker-sdk-go v1.2.0/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI= git.happydns.org/happyDomain v0.7.0 h1:NV82/NbcSeRm0+IUZqaK3Vu9Ovl5+vv4AigUJZMdwws= git.happydns.org/happyDomain v0.7.0/go.mod h1:5tgkmqFE65kK359rY49V++49wgZ0gco+Gh9X6tbL+bY= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=