Import source: internal/checker/checker
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
// Package checker implements the four probe types: http, tcp, tls, icmp.
|
||||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
|
||||
"dash/internal/config"
|
||||
)
|
||||
|
||||
// Result is the outcome of a single probe.
|
||||
type Result struct {
|
||||
TS time.Time
|
||||
OK bool
|
||||
Degraded bool // ok but worrying (slow, cert nearing expiry)
|
||||
LatencyMS float64 // round-trip time
|
||||
Detail string // machine-readable extra: http status, cert days left
|
||||
Err string
|
||||
}
|
||||
|
||||
func fail(start time.Time, format string, args ...any) Result {
|
||||
return Result{
|
||||
TS: start,
|
||||
LatencyMS: float64(time.Since(start).Microseconds()) / 1000.0,
|
||||
Err: fmt.Sprintf(format, args...),
|
||||
}
|
||||
}
|
||||
|
||||
// Run executes the probe described by t once.
|
||||
func Run(ctx context.Context, t config.Target) Result {
|
||||
ctx, cancel := context.WithTimeout(ctx, t.Timeout.Duration)
|
||||
defer cancel()
|
||||
|
||||
var res Result
|
||||
switch t.Type {
|
||||
case config.CheckHTTP:
|
||||
res = checkHTTP(ctx, t)
|
||||
case config.CheckTCP:
|
||||
res = checkTCP(ctx, t)
|
||||
case config.CheckTLS:
|
||||
res = checkTLS(ctx, t)
|
||||
case config.CheckICMP:
|
||||
res = checkICMP(ctx, t)
|
||||
case config.CheckDocker:
|
||||
res = checkDocker(ctx, t)
|
||||
default:
|
||||
res = fail(time.Now(), "unknown check type %q", t.Type)
|
||||
}
|
||||
if res.OK && t.DegradedLatencyMS > 0 && res.LatencyMS > t.DegradedLatencyMS {
|
||||
res.Degraded = true
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkHTTP(ctx context.Context, t config.Target) Result {
|
||||
start := time.Now()
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, t.URL, nil)
|
||||
if err != nil {
|
||||
return fail(start, "bad url: %v", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", "dash-monitor/1.0")
|
||||
client := &http.Client{
|
||||
// Each probe measures a full fresh connection, not a pooled one.
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fail(start, "%v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
lat := float64(time.Since(start).Microseconds()) / 1000.0
|
||||
|
||||
ok := resp.StatusCode < 400
|
||||
if t.ExpectStatus != 0 {
|
||||
ok = resp.StatusCode == t.ExpectStatus
|
||||
}
|
||||
res := Result{TS: start, OK: ok, LatencyMS: lat, Detail: strconv.Itoa(resp.StatusCode)}
|
||||
if !ok {
|
||||
res.Err = fmt.Sprintf("unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func checkTCP(ctx context.Context, t config.Target) Result {
|
||||
start := time.Now()
|
||||
var d net.Dialer
|
||||
conn, err := d.DialContext(ctx, "tcp", t.Address)
|
||||
if err != nil {
|
||||
return fail(start, "%v", err)
|
||||
}
|
||||
conn.Close()
|
||||
return Result{TS: start, OK: true, LatencyMS: float64(time.Since(start).Microseconds()) / 1000.0}
|
||||
}
|
||||
|
||||
func checkTLS(ctx context.Context, t config.Target) Result {
|
||||
start := time.Now()
|
||||
d := tls.Dialer{Config: &tls.Config{}}
|
||||
conn, err := d.DialContext(ctx, "tcp", t.Address)
|
||||
if err != nil {
|
||||
return fail(start, "%v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
lat := float64(time.Since(start).Microseconds()) / 1000.0
|
||||
|
||||
state := conn.(*tls.Conn).ConnectionState()
|
||||
if len(state.PeerCertificates) == 0 {
|
||||
return fail(start, "no peer certificates")
|
||||
}
|
||||
leaf := state.PeerCertificates[0]
|
||||
days := int(time.Until(leaf.NotAfter).Hours() / 24)
|
||||
res := Result{TS: start, OK: true, LatencyMS: lat, Detail: strconv.Itoa(days)}
|
||||
switch {
|
||||
case days < 0:
|
||||
res.OK = false
|
||||
res.Err = fmt.Sprintf("certificate expired %d days ago", -days)
|
||||
case days < t.WarnDays:
|
||||
res.Degraded = true
|
||||
res.Err = fmt.Sprintf("certificate expires in %d days", days)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// dockerInspect is the subset of the container-inspect response we read.
|
||||
type dockerInspect struct {
|
||||
State struct {
|
||||
Status string `json:"Status"`
|
||||
Running bool `json:"Running"`
|
||||
Health *struct {
|
||||
Status string `json:"Status"` // starting | healthy | unhealthy
|
||||
} `json:"Health"`
|
||||
} `json:"State"`
|
||||
}
|
||||
|
||||
func checkDocker(ctx context.Context, t config.Target) Result {
|
||||
start := time.Now()
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", t.Socket)
|
||||
},
|
||||
},
|
||||
}
|
||||
url := "http://docker/containers/" + t.Container + "/json"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fail(start, "bad request: %v", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fail(start, "docker socket: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
lat := float64(time.Since(start).Microseconds()) / 1000.0
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return fail(start, "container %q not found", t.Container)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fail(start, "docker api status %d", resp.StatusCode)
|
||||
}
|
||||
var info dockerInspect
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return fail(start, "decode: %v", err)
|
||||
}
|
||||
|
||||
res := Result{TS: start, LatencyMS: lat, Detail: info.State.Status}
|
||||
if !info.State.Running {
|
||||
res.Err = fmt.Sprintf("container %s", info.State.Status)
|
||||
return res
|
||||
}
|
||||
res.OK = true
|
||||
if h := info.State.Health; h != nil {
|
||||
res.Detail = h.Status
|
||||
switch h.Status {
|
||||
case "unhealthy":
|
||||
res.OK = false
|
||||
res.Err = "healthcheck failing"
|
||||
case "starting":
|
||||
res.Degraded = true
|
||||
res.Err = "healthcheck starting"
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
var icmpSeq uint32
|
||||
|
||||
func checkICMP(ctx context.Context, t config.Target) Result {
|
||||
start := time.Now()
|
||||
dst, err := net.ResolveIPAddr("ip4", t.Host)
|
||||
if err != nil {
|
||||
return fail(start, "resolve: %v", err)
|
||||
}
|
||||
|
||||
// Prefer unprivileged datagram ICMP; fall back to a raw socket (needs
|
||||
// root or CAP_NET_RAW).
|
||||
var peer net.Addr = &net.UDPAddr{IP: dst.IP}
|
||||
conn, err := icmp.ListenPacket("udp4", "0.0.0.0")
|
||||
if err != nil {
|
||||
conn, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
return fail(start, "icmp socket: %v (needs CAP_NET_RAW or net.ipv4.ping_group_range)", err)
|
||||
}
|
||||
peer = dst
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
icmpSeq++
|
||||
msg := icmp.Message{
|
||||
Type: ipv4.ICMPTypeEcho,
|
||||
Body: &icmp.Echo{
|
||||
ID: os.Getpid() & 0xffff,
|
||||
Seq: int(icmpSeq & 0xffff),
|
||||
Data: []byte("dash-monitor-ping"),
|
||||
},
|
||||
}
|
||||
wire, err := msg.Marshal(nil)
|
||||
if err != nil {
|
||||
return fail(start, "marshal: %v", err)
|
||||
}
|
||||
|
||||
deadline, _ := ctx.Deadline()
|
||||
conn.SetDeadline(deadline)
|
||||
|
||||
sent := time.Now()
|
||||
if _, err := conn.WriteTo(wire, peer); err != nil {
|
||||
return fail(start, "send: %v", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1500)
|
||||
for {
|
||||
n, _, err := conn.ReadFrom(buf)
|
||||
if err != nil {
|
||||
return fail(start, "recv: %v", err)
|
||||
}
|
||||
reply, err := icmp.ParseMessage(1, buf[:n]) // 1 = ICMPv4 protocol number
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if reply.Type == ipv4.ICMPTypeEchoReply {
|
||||
lat := float64(time.Since(sent).Microseconds()) / 1000.0
|
||||
return Result{TS: start, OK: true, LatencyMS: lat}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user