feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
378
checks/cping/ping.go
Обычный файл
378
checks/cping/ping.go
Обычный файл
@@ -0,0 +1,378 @@
|
||||
// Package cping provides ICMP echo (ping) check functionality for RSMon.
|
||||
//
|
||||
// The implementation targets minimal-reuse logic inspired by
|
||||
// github.com/go-ping/ping, but rewritten inline so the project does not
|
||||
// pick up an external dependency. Concretely it uses
|
||||
// golang.org/x/net/icmp to send an Echo Request and waits for a single
|
||||
// Echo Reply within the configured timeout.
|
||||
//
|
||||
// Privileges / CAP_NET_RAW:
|
||||
//
|
||||
// - On Linux, the "ip4:icmp" listener needs either CAP_NET_RAW on the
|
||||
// binary OR the net.ipv4.ping_group_range sysctl to be widened
|
||||
// (see "unprivileged ICMP sockets"). When neither is available, we
|
||||
// fall back to "udp4" which works on Linux only when the same
|
||||
// sysctl is widened; on macOS the "udp4" mode is unprivileged by
|
||||
// default.
|
||||
// - On Windows the privileged (raw) ICMP listener is required.
|
||||
//
|
||||
// The check returns FAIL when neither listener can be opened so the
|
||||
// caller knows the operator needs to enable the capability. OK/ERR are
|
||||
// reported when the listener works but the host is unreachable or times
|
||||
// out.
|
||||
package cping
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
)
|
||||
|
||||
const (
|
||||
stateOK = "OK"
|
||||
stateERR = "ERR"
|
||||
stateFail = "FAIL"
|
||||
|
||||
defaultCount = 1
|
||||
defaultTimeout = 5 * time.Second
|
||||
minTimeout = 1 * time.Second
|
||||
defaultPacketSz = 56
|
||||
|
||||
icmpProtoIP4 = "ip4:icmp"
|
||||
icmpProtoUDP = "udp4"
|
||||
)
|
||||
|
||||
// Pinger is the abstraction the package uses for ICMP echo. The
|
||||
// production code path uses runPinger, but it is interface-typed so
|
||||
// tests can substitute a fake without touching raw sockets.
|
||||
type Pinger interface {
|
||||
Run(host string, count int, timeout time.Duration, payloadSize int) Stats
|
||||
}
|
||||
|
||||
// Stats is the per-run summary produced by a Pinger.
|
||||
type Stats struct {
|
||||
PacketsSent int
|
||||
PacketsRecv int
|
||||
AvgRtt time.Duration
|
||||
Err error
|
||||
}
|
||||
|
||||
var (
|
||||
defaultPinger Pinger = &realPinger{}
|
||||
|
||||
// pingerMu guards the swap of defaultPinger in tests.
|
||||
pingerMu sync.RWMutex
|
||||
)
|
||||
|
||||
// SetPinger overrides the default Pinger. It is intended for tests
|
||||
// that need to inject fakes without granting CAP_NET_RAW to the test
|
||||
// binary.
|
||||
func SetPinger(p Pinger) {
|
||||
pingerMu.Lock()
|
||||
defaultPinger = p
|
||||
pingerMu.Unlock()
|
||||
}
|
||||
|
||||
func currentPinger() Pinger {
|
||||
pingerMu.RLock()
|
||||
defer pingerMu.RUnlock()
|
||||
return defaultPinger
|
||||
}
|
||||
|
||||
// Perform executes a single ping check for the supplied Check.
|
||||
//
|
||||
// Settings consumed from models.CheckSettings:
|
||||
// - count (int): number of echo requests to send. Defaults to 1 to
|
||||
// keep intervals short; clamped to [1, 5].
|
||||
// - timeout (int, seconds): total budget for the check; clamped to
|
||||
// at least 1s.
|
||||
// - packet_size (int): ICMP payload size in bytes; clamped to
|
||||
// [0, 1400].
|
||||
// - host (string): optional override of the monitor host.
|
||||
func Perform(c *models.Check) *Result {
|
||||
r := &Result{}
|
||||
settings := c.GetSettings()
|
||||
|
||||
host := c.Monitor.Host
|
||||
if settings.Host != "" {
|
||||
host = settings.Host
|
||||
}
|
||||
if host == "" {
|
||||
r.State = stateFail
|
||||
r.Error = errors.New("ping: empty host")
|
||||
return r
|
||||
}
|
||||
|
||||
count := settings.Count
|
||||
if count <= 0 {
|
||||
count = defaultCount
|
||||
}
|
||||
if count > 5 {
|
||||
count = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(settings.Timeout) * time.Second
|
||||
if timeout <= 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
if timeout < minTimeout {
|
||||
timeout = minTimeout
|
||||
}
|
||||
|
||||
payloadSize := settings.PacketSize
|
||||
if payloadSize == 0 {
|
||||
payloadSize = defaultPacketSz
|
||||
}
|
||||
if payloadSize < 0 {
|
||||
payloadSize = 0
|
||||
}
|
||||
if payloadSize > 1400 {
|
||||
payloadSize = 1400
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
stats := currentPinger().Run(host, count, timeout, payloadSize)
|
||||
r.Duration = time.Since(start)
|
||||
r.PacketsSent = stats.PacketsSent
|
||||
r.PacketsRecv = stats.PacketsRecv
|
||||
if stats.PacketsRecv > 0 {
|
||||
r.AvgRttMs = float64(stats.AvgRtt.Microseconds()) / 1000.0
|
||||
}
|
||||
|
||||
if stats.Err != nil {
|
||||
// Distinguish "could not run at all" (no privileges) from
|
||||
// "ran but failed" so the operator can fix the environment.
|
||||
if isUnsupported(stats.Err) {
|
||||
r.State = stateFail
|
||||
} else {
|
||||
r.State = stateERR
|
||||
}
|
||||
r.Error = stats.Err
|
||||
return r
|
||||
}
|
||||
|
||||
if stats.PacketsRecv == 0 {
|
||||
r.State = stateERR
|
||||
r.Error = fmt.Errorf("no reply from %s (sent %d)", host, stats.PacketsSent)
|
||||
return r
|
||||
}
|
||||
|
||||
r.State = stateOK
|
||||
r.Infos = append(r.Infos, fmt.Sprintf("rtt=%.2fms sent=%d recv=%d", r.AvgRttMs, stats.PacketsSent, stats.PacketsRecv))
|
||||
return r
|
||||
}
|
||||
|
||||
// isUnsupported reports whether err looks like a permission problem
|
||||
// rather than a runtime failure.
|
||||
func isUnsupported(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
msg := err.Error()
|
||||
if runtime.GOOS == "windows" {
|
||||
return msg != ""
|
||||
}
|
||||
// Linux/Darwin permission flavors.
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return true
|
||||
}
|
||||
if msg == "" {
|
||||
return false
|
||||
}
|
||||
if containsAny(msg, "operation not permitted", "permission denied", "cap_net_raw", "ping_group_range") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsAny(s string, needles ...string) bool {
|
||||
for _, n := range needles {
|
||||
if n == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(s, n) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// realPinger sends and receives ICMP echo packets using the standard
|
||||
// library plus golang.org/x/net/icmp.
|
||||
type realPinger struct{}
|
||||
|
||||
// Run is the production pinger entrypoint. It resolves host, opens an
|
||||
// ICMP listener (preferring the unprivileged UDP path on Linux when
|
||||
// available, falling back to raw IP), and waits up to timeout for at
|
||||
// least one Echo Reply.
|
||||
func (r *realPinger) Run(host string, count int, timeout time.Duration, payloadSize int) Stats {
|
||||
stats := Stats{PacketsSent: 0, PacketsRecv: 0}
|
||||
|
||||
dst, err := net.ResolveIPAddr("ip4", host)
|
||||
if err != nil {
|
||||
stats.Err = fmt.Errorf("resolve %s: %w", host, err)
|
||||
return stats
|
||||
}
|
||||
|
||||
conn, network, err := openICMP()
|
||||
if err != nil {
|
||||
stats.Err = err
|
||||
return stats
|
||||
}
|
||||
defer conn.Close() //nolint:errcheck // accepted lint exception: best-effort close
|
||||
|
||||
// Build an "echo and wait for reply" function keyed by network so
|
||||
// the IPv4-only logic stays close to where it is used.
|
||||
sendAndRecv := func(seq int) (time.Duration, error) {
|
||||
msg := icmp.Message{
|
||||
Type: ipv4.ICMPTypeEcho, Code: 0,
|
||||
Body: &icmp.Echo{
|
||||
ID: os.Getpid() & 0xffff, Seq: seq,
|
||||
Data: makeBytes(payloadSize),
|
||||
},
|
||||
}
|
||||
bin, err := msg.Marshal(nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("marshal icmp: %w", err)
|
||||
}
|
||||
|
||||
sentAt := time.Now()
|
||||
if _, err := conn.WriteTo(bin, dst); err != nil {
|
||||
return 0, fmt.Errorf("write icmp: %w", err)
|
||||
}
|
||||
|
||||
// Per-packet deadline = remaining budget / remaining attempts
|
||||
// (or 1s minimum). We reuse the single shared conn for all
|
||||
// count iterations so replies may arrive slightly out of order.
|
||||
deadline := time.Now().Add(timeout / time.Duration(count))
|
||||
if remaining := time.Until(deadline); remaining < time.Second {
|
||||
deadline = time.Now().Add(time.Second)
|
||||
}
|
||||
if err := conn.SetReadDeadline(deadline); err != nil {
|
||||
return 0, fmt.Errorf("set deadline: %w", err)
|
||||
}
|
||||
|
||||
reply, peer, err := readOne(conn, network)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_ = peer // peer would be useful for response-time per hop; not needed for MVP.
|
||||
if reply == nil {
|
||||
return 0, errors.New("nil reply")
|
||||
}
|
||||
return time.Since(sentAt), nil
|
||||
}
|
||||
|
||||
var total time.Duration
|
||||
for i := 1; i <= count; i++ {
|
||||
stats.PacketsSent++
|
||||
rtt, err := sendAndRecv(i)
|
||||
if err != nil {
|
||||
// First packet failed because of read timeout: report
|
||||
// host as unreachable. Keep iterating up to count so the
|
||||
// reported packet loss is accurate (>= 50%).
|
||||
if i == 1 {
|
||||
stats.Err = fmt.Errorf("icmp %s: %w", host, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
stats.PacketsRecv++
|
||||
total += rtt
|
||||
}
|
||||
if stats.PacketsRecv > 0 {
|
||||
stats.AvgRtt = total / time.Duration(stats.PacketsRecv)
|
||||
}
|
||||
// When at least one packet succeeded, drop the underlying error so
|
||||
// Perform() reports OK.
|
||||
if stats.PacketsRecv > 0 {
|
||||
stats.Err = nil
|
||||
}
|
||||
return stats
|
||||
}
|
||||
|
||||
// openICMP returns an ICMP packet connection. On Linux the code prefers
|
||||
// the "udp4" (unprivileged) listener because the raw "ip4:icmp"
|
||||
// listener needs CAP_NET_RAW unless the sysctl
|
||||
// net.ipv4.ping_group_range is widened. On other OSes we fall back to
|
||||
// the raw listener.
|
||||
func openICMP() (*icmp.PacketConn, string, error) {
|
||||
// Try the unprivileged path first; if it fails, fall back to raw.
|
||||
conn, err := icmp.ListenPacket(icmpProtoUDP, "0.0.0.0")
|
||||
if err == nil {
|
||||
return conn, icmpProtoUDP, nil
|
||||
}
|
||||
rawErr := err
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows must use the raw (privileged) listener.
|
||||
conn, err = icmp.ListenPacket(icmpProtoIP4, "0.0.0.0")
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("icmp listen: %w", rawErr)
|
||||
}
|
||||
return conn, icmpProtoIP4, nil
|
||||
}
|
||||
// Linux/Darwin: try raw as a fallback. Production binaries
|
||||
// shipping with cap_net_raw=+ep will succeed here; test/CI
|
||||
// runners without the capability will surface a clear
|
||||
// permission error.
|
||||
conn, err = icmp.ListenPacket(icmpProtoIP4, "0.0.0.0")
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("icmp listen (udp4=%v; ip4=%v)", rawErr, err)
|
||||
}
|
||||
return conn, icmpProtoIP4, nil
|
||||
}
|
||||
|
||||
// readOne reads a single packet, validates it is an Echo Reply (or a
|
||||
// TTL-exceeded reply from a router along the path), and returns it.
|
||||
func readOne(conn *icmp.PacketConn, network string) (*icmp.Message, net.Addr, error) {
|
||||
buf := make([]byte, 1500)
|
||||
n, peer, err := conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("read icmp: %w", err)
|
||||
}
|
||||
parsed, err := icmp.ParseMessage(icmpProtoToInt(network), buf[:n])
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("parse icmp: %w", err)
|
||||
}
|
||||
// We accept both Echo Reply (the destination) and Time Exceeded
|
||||
// (intermediate router hop) because some networks filter Echo
|
||||
// Replies but still return traceroute-style Time Exceeded packets,
|
||||
// which proves the host is reachable.
|
||||
switch parsed.Type {
|
||||
case ipv4.ICMPTypeEchoReply, ipv4.ICMPTypeTimeExceeded:
|
||||
return parsed, peer, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("unexpected icmp type %v", parsed.Type)
|
||||
}
|
||||
|
||||
// icmpProtoToInt maps our internal "ip4:icmp"/"udp4" tag to the
|
||||
// protocol number expected by icmp.ParseMessage. icmp.DefaultPacketProtocol
|
||||
// would re-derive this but we want the value stable.
|
||||
func icmpProtoToInt(probe string) int {
|
||||
if probe == icmpProtoUDP {
|
||||
return 1 // udp4
|
||||
}
|
||||
return 0 // ip4:icmp
|
||||
}
|
||||
|
||||
// makeBytes returns a deterministic payload of the requested size so
|
||||
// packet sizes are stable across runs.
|
||||
func makeBytes(n int) []byte {
|
||||
if n <= 0 {
|
||||
return []byte{}
|
||||
}
|
||||
b := make([]byte, n)
|
||||
for i := range b {
|
||||
b[i] = byte('a' + (i % 26))
|
||||
}
|
||||
return b
|
||||
}
|
||||
159
checks/cping/ping_test.go
Обычный файл
159
checks/cping/ping_test.go
Обычный файл
@@ -0,0 +1,159 @@
|
||||
package cping
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
)
|
||||
|
||||
// settings mirrors models.CheckSettings so the test can build a
|
||||
// CheckSettings JSON without pulling in the entire model package.
|
||||
type settings struct {
|
||||
Host string `json:"host,omitempty"`
|
||||
Count int `json:"count,omitempty"`
|
||||
Timeout int `json:"timeout,omitempty"`
|
||||
PacketSize int `json:"packet_size,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
}
|
||||
|
||||
func newCheck(t *testing.T, s settings, host string) *models.Check {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal settings: %v", err)
|
||||
}
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return &models.Check{
|
||||
Kind: "ping",
|
||||
Monitor: &models.Monitor{Host: host},
|
||||
Settings: datatypes.JSON(raw),
|
||||
}
|
||||
}
|
||||
|
||||
// fakePinger returns canned stats without touching the network so the
|
||||
// state machine in Perform() can be exercised in CI environments
|
||||
// without CAP_NET_RAW.
|
||||
type fakePinger struct {
|
||||
stats Stats
|
||||
}
|
||||
|
||||
func (f *fakePinger) Run(host string, count int, timeout time.Duration, payloadSize int) Stats {
|
||||
return f.stats
|
||||
}
|
||||
|
||||
func TestPerformOK(t *testing.T) {
|
||||
SetPinger(&fakePinger{stats: Stats{PacketsSent: 3, PacketsRecv: 3, AvgRtt: 4 * time.Millisecond}})
|
||||
defer SetPinger(&realPinger{})
|
||||
|
||||
c := newCheck(t, settings{Count: 3}, "")
|
||||
r := Perform(c)
|
||||
if r.State != stateOK {
|
||||
t.Fatalf("expected OK, got %s (err=%v)", r.State, r.Error)
|
||||
}
|
||||
if r.PacketsSent != 3 || r.PacketsRecv != 3 {
|
||||
t.Fatalf("packet counts wrong: sent=%d recv=%d", r.PacketsSent, r.PacketsRecv)
|
||||
}
|
||||
if r.AvgRttMs < 1 {
|
||||
t.Fatalf("expected RTT > 0, got %v", r.AvgRttMs)
|
||||
}
|
||||
if len(r.Infos) == 0 || !strings.Contains(r.Infos[0], "rtt=") {
|
||||
t.Fatalf("expected rtt info line, got %v", r.Infos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformNoReply(t *testing.T) {
|
||||
SetPinger(&fakePinger{stats: Stats{PacketsSent: 3, PacketsRecv: 0, Err: errors.New("i/o timeout")}})
|
||||
defer SetPinger(&realPinger{})
|
||||
|
||||
c := newCheck(t, settings{}, "")
|
||||
r := Perform(c)
|
||||
if r.State != stateERR {
|
||||
t.Fatalf("expected ERR, got %s", r.State)
|
||||
}
|
||||
if r.PacketsSent == 0 {
|
||||
t.Fatalf("expected PacketsSent to be incremented even on failure")
|
||||
}
|
||||
if r.Error == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformUnsupported(t *testing.T) {
|
||||
SetPinger(&fakePinger{stats: Stats{Err: errors.New("socket: operation not permitted (cap_net_raw)")}})
|
||||
defer SetPinger(&realPinger{})
|
||||
|
||||
c := newCheck(t, settings{}, "")
|
||||
r := Perform(c)
|
||||
if r.State != stateFail {
|
||||
t.Fatalf("expected FAIL when raw ICMP is not allowed, got %s", r.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPerformEmptyHost(t *testing.T) {
|
||||
c := &models.Check{
|
||||
Kind: "ping",
|
||||
Monitor: &models.Monitor{Host: ""},
|
||||
Settings: datatypes.JSON("{}"),
|
||||
}
|
||||
r := Perform(c)
|
||||
if r.State != stateFail {
|
||||
t.Fatalf("expected FAIL on empty host, got %s", r.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUnsupported(t *testing.T) {
|
||||
cases := []struct {
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{err: nil, want: false},
|
||||
{err: errors.New(""), want: false},
|
||||
{err: errors.New("permission denied"), want: true},
|
||||
{err: errors.New("icmp listen: cap_net_raw required"), want: true},
|
||||
{err: errors.New("i/o timeout"), want: false},
|
||||
{err: errors.New("no route to host"), want: false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := isUnsupported(tc.err); got != tc.want {
|
||||
t.Errorf("isUnsupported(%q) = %v, want %v", tc.err, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLossPercent(t *testing.T) {
|
||||
cases := []struct {
|
||||
sent, recv int
|
||||
want int64
|
||||
}{
|
||||
{sent: 0, recv: 0, want: 0},
|
||||
{sent: 5, recv: 5, want: 0},
|
||||
{sent: 5, recv: 3, want: 40},
|
||||
{sent: 5, recv: 0, want: 100},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := lossPercent(tc.sent, tc.recv); got != tc.want {
|
||||
t.Errorf("lossPercent(%d,%d) = %d, want %d", tc.sent, tc.recv, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeBytes(t *testing.T) {
|
||||
if got := makeBytes(0); len(got) != 0 {
|
||||
t.Fatalf("expected empty slice, got %d bytes", len(got))
|
||||
}
|
||||
if got := makeBytes(-1); len(got) != 0 {
|
||||
t.Fatalf("expected empty slice for negative size, got %d bytes", len(got))
|
||||
}
|
||||
got := makeBytes(3)
|
||||
if len(got) != 3 || string(got) != "abc" {
|
||||
t.Fatalf("expected 'abc', got %q", string(got))
|
||||
}
|
||||
}
|
||||
60
checks/cping/result.go
Обычный файл
60
checks/cping/result.go
Обычный файл
@@ -0,0 +1,60 @@
|
||||
package cping
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
"rsgit.ru/rsmon/rsmon/internal/checkresult"
|
||||
)
|
||||
|
||||
// Result is the outcome of a single ping check. It embeds
|
||||
// checkresult.CheckResult for the standard fields (State, Error,
|
||||
// Duration, Warnings, Infos) and adds Ping-specific metrics consumed
|
||||
// by the metrics writers when the check produces influx telemetry.
|
||||
type Result struct {
|
||||
checkresult.CheckResult
|
||||
PacketsSent int
|
||||
PacketsRecv int
|
||||
AvgRttMs float64
|
||||
}
|
||||
|
||||
// InfluxFields reports ping metrics in the same shape as the rest of
|
||||
// the check packages: "took" is elapsed ms (for graphs and alerts).
|
||||
func (r *Result) InfluxFields() map[string]interface{} {
|
||||
ret := make(map[string]interface{}, 0)
|
||||
ret["took"] = int64(r.Duration / time.Millisecond)
|
||||
ret["packets_sent"] = r.PacketsSent
|
||||
ret["packets_recv"] = r.PacketsRecv
|
||||
ret["packet_loss"] = lossPercent(r.PacketsSent, r.PacketsRecv)
|
||||
if r.AvgRttMs > 0 {
|
||||
ret["rtt_ms"] = int64(r.AvgRttMs)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// InfluxTags returns the standard set of tags used by the chttp/cdns
|
||||
// packages. The "state" tag is taken from the embedded CheckResult
|
||||
// after Perform() has populated it so the writer sees the actual
|
||||
// final state (OK/ERR/FAIL/WARN).
|
||||
func (r *Result) InfluxTags(c models.Check) map[string]string { //nolint:gocritic // hugeParam: accepted for interface compatibility
|
||||
ret := make(map[string]string, 0)
|
||||
ret["check"] = strconv.FormatInt(c.ID, 10)
|
||||
ret["state"] = r.State
|
||||
if r.Error != nil {
|
||||
ret["error"] = r.Error.Error()
|
||||
}
|
||||
ret["warnings"] = strings.Join(r.Warnings, ",")
|
||||
return ret
|
||||
}
|
||||
|
||||
func lossPercent(sent, recv int) int64 {
|
||||
if sent <= 0 {
|
||||
return 0
|
||||
}
|
||||
if recv >= sent {
|
||||
return 0
|
||||
}
|
||||
return int64(100 * (sent - recv) / sent)
|
||||
}
|
||||
Ссылка в новой задаче
Block a user