init work of uptime
This commit is contained in:
69
internal/monitor/dns.go
Normal file
69
internal/monitor/dns.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"arclineit/arcline-uptime/internal/config"
|
||||
)
|
||||
|
||||
// DNSChecker resolves a hostname and optionally asserts a specific IP is returned.
|
||||
type DNSChecker struct {
|
||||
cfg config.MonitorConfig
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewDNSChecker(cfg config.MonitorConfig, timeout time.Duration) *DNSChecker {
|
||||
return &DNSChecker{cfg: cfg, timeout: timeout}
|
||||
}
|
||||
|
||||
func (d *DNSChecker) Name() string { return d.cfg.Name }
|
||||
func (d *DNSChecker) Interval() time.Duration { return time.Duration(d.cfg.Interval) * time.Second }
|
||||
|
||||
func (d *DNSChecker) Check() Result {
|
||||
start := time.Now()
|
||||
result := Result{
|
||||
MonitorName: d.cfg.Name,
|
||||
CheckedAt: start,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), d.timeout)
|
||||
defer cancel()
|
||||
|
||||
addrs, err := net.DefaultResolver.LookupHost(ctx, d.cfg.Host)
|
||||
result.ResponseTime = time.Since(start)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
|
||||
if d.cfg.ExpectedIP != "" {
|
||||
want := normalizeIP(d.cfg.ExpectedIP)
|
||||
found := false
|
||||
for _, addr := range addrs {
|
||||
if normalizeIP(addr) == want {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
result.Error = fmt.Sprintf("expected IP %q not found in results %v", d.cfg.ExpectedIP, addrs)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result.Up = true
|
||||
applyThreshold(&result, d.cfg.MaxResponseMS)
|
||||
return result
|
||||
}
|
||||
|
||||
// normalizeIP parses and re-serialises an IP string so different representations
|
||||
// of the same address compare equal (e.g. "::1" and "0:0:0:0:0:0:0:1").
|
||||
func normalizeIP(s string) string {
|
||||
if ip := net.ParseIP(s); ip != nil {
|
||||
return ip.String()
|
||||
}
|
||||
return s
|
||||
}
|
||||
101
internal/monitor/http.go
Normal file
101
internal/monitor/http.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"arclineit/arcline-uptime/internal/config"
|
||||
)
|
||||
|
||||
type HTTPChecker struct {
|
||||
cfg config.MonitorConfig
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewHTTPChecker(cfg config.MonitorConfig, timeout time.Duration) *HTTPChecker {
|
||||
return &HTTPChecker{
|
||||
cfg: cfg,
|
||||
client: &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: redirectPolicy(cfg.ExpectedStatus),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// redirectPolicy stops at the first redirect when the expected status is 3xx so
|
||||
// the raw redirect code is returned. Otherwise follows up to 10 redirects.
|
||||
func redirectPolicy(expected int) func(*http.Request, []*http.Request) error {
|
||||
if expected >= 300 && expected < 400 {
|
||||
return func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *HTTPChecker) Name() string { return h.cfg.Name }
|
||||
func (h *HTTPChecker) Interval() time.Duration { return time.Duration(h.cfg.Interval) * time.Second }
|
||||
|
||||
func (h *HTTPChecker) Check() Result {
|
||||
start := time.Now()
|
||||
result := Result{
|
||||
MonitorName: h.cfg.Name,
|
||||
CheckedAt: start,
|
||||
}
|
||||
|
||||
var bodyReader io.Reader
|
||||
if h.cfg.Body != "" {
|
||||
bodyReader = strings.NewReader(h.cfg.Body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(h.cfg.Method, h.cfg.URL, bodyReader)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("build request: %v", err)
|
||||
result.ResponseTime = time.Since(start)
|
||||
return result
|
||||
}
|
||||
req.Header.Set("User-Agent", "arcline-uptime/1.0")
|
||||
for k, v := range h.cfg.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
resp, err := h.client.Do(req)
|
||||
result.ResponseTime = time.Since(start)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
result.StatusCode = resp.StatusCode
|
||||
|
||||
// Response time threshold check — before reading body to fail fast.
|
||||
applyThreshold(&result, h.cfg.MaxResponseMS)
|
||||
if !result.Up && result.Error != "" {
|
||||
return result
|
||||
}
|
||||
|
||||
if resp.StatusCode != h.cfg.ExpectedStatus {
|
||||
result.Error = fmt.Sprintf("expected status %d, got %d", h.cfg.ExpectedStatus, resp.StatusCode)
|
||||
return result
|
||||
}
|
||||
|
||||
if h.cfg.Contains != "" {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("read body: %v", err)
|
||||
return result
|
||||
}
|
||||
if !strings.Contains(string(body), h.cfg.Contains) {
|
||||
result.Error = fmt.Sprintf("body does not contain %q", h.cfg.Contains)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result.Up = true
|
||||
applyThreshold(&result, h.cfg.MaxResponseMS)
|
||||
return result
|
||||
}
|
||||
59
internal/monitor/monitor.go
Normal file
59
internal/monitor/monitor.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"arclineit/arcline-uptime/internal/config"
|
||||
)
|
||||
|
||||
// Result is returned by every Checker after a single probe.
|
||||
type Result struct {
|
||||
MonitorName string
|
||||
Up bool
|
||||
StatusCode int // HTTP only; 0 for TCP/TLS/DNS
|
||||
ResponseTime time.Duration
|
||||
Error string // empty when Up == true
|
||||
CheckedAt time.Time
|
||||
}
|
||||
|
||||
// Checker probes one endpoint.
|
||||
type Checker interface {
|
||||
Name() string
|
||||
Check() Result
|
||||
Interval() time.Duration // per-monitor interval; 0 means use the global default
|
||||
}
|
||||
|
||||
// BuildCheckers constructs one Checker per MonitorConfig entry.
|
||||
// globalTimeout is the fallback when a monitor's own Timeout is 0.
|
||||
func BuildCheckers(monitors []config.MonitorConfig, globalTimeout time.Duration) ([]Checker, error) {
|
||||
checkers := make([]Checker, 0, len(monitors))
|
||||
for _, m := range monitors {
|
||||
timeout := globalTimeout
|
||||
if m.Timeout > 0 {
|
||||
timeout = time.Duration(m.Timeout) * time.Second
|
||||
}
|
||||
switch m.Type {
|
||||
case "http":
|
||||
checkers = append(checkers, NewHTTPChecker(m, timeout))
|
||||
case "tcp":
|
||||
checkers = append(checkers, NewTCPChecker(m, timeout))
|
||||
case "tls":
|
||||
checkers = append(checkers, NewTLSChecker(m, timeout))
|
||||
case "dns":
|
||||
checkers = append(checkers, NewDNSChecker(m, timeout))
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown monitor type %q for %q", m.Type, m.Name)
|
||||
}
|
||||
}
|
||||
return checkers, nil
|
||||
}
|
||||
|
||||
// applyThreshold marks a successful result as failed if response time exceeds max.
|
||||
func applyThreshold(r *Result, maxResponseMS int64) {
|
||||
if maxResponseMS > 0 && r.Up && r.ResponseTime.Milliseconds() > maxResponseMS {
|
||||
r.Up = false
|
||||
r.Error = fmt.Sprintf("response time %dms exceeded threshold %dms",
|
||||
r.ResponseTime.Milliseconds(), maxResponseMS)
|
||||
}
|
||||
}
|
||||
46
internal/monitor/tcp.go
Normal file
46
internal/monitor/tcp.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"arclineit/arcline-uptime/internal/config"
|
||||
)
|
||||
|
||||
type TCPChecker struct {
|
||||
cfg config.MonitorConfig
|
||||
addr string
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewTCPChecker(cfg config.MonitorConfig, timeout time.Duration) *TCPChecker {
|
||||
return &TCPChecker{
|
||||
cfg: cfg,
|
||||
addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TCPChecker) Name() string { return t.cfg.Name }
|
||||
func (t *TCPChecker) Interval() time.Duration { return time.Duration(t.cfg.Interval) * time.Second }
|
||||
|
||||
func (t *TCPChecker) Check() Result {
|
||||
start := time.Now()
|
||||
result := Result{
|
||||
MonitorName: t.cfg.Name,
|
||||
CheckedAt: start,
|
||||
}
|
||||
|
||||
conn, err := net.DialTimeout("tcp", t.addr, t.timeout)
|
||||
result.ResponseTime = time.Since(start)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
result.Up = true
|
||||
applyThreshold(&result, t.cfg.MaxResponseMS)
|
||||
return result
|
||||
}
|
||||
68
internal/monitor/tls.go
Normal file
68
internal/monitor/tls.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"arclineit/arcline-uptime/internal/config"
|
||||
)
|
||||
|
||||
// TLSChecker dials a TLS endpoint and alerts when the certificate is about
|
||||
// to expire (within ExpiryWarningDays days).
|
||||
type TLSChecker struct {
|
||||
cfg config.MonitorConfig
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func NewTLSChecker(cfg config.MonitorConfig, timeout time.Duration) *TLSChecker {
|
||||
return &TLSChecker{cfg: cfg, timeout: timeout}
|
||||
}
|
||||
|
||||
func (t *TLSChecker) Name() string { return t.cfg.Name }
|
||||
func (t *TLSChecker) Interval() time.Duration { return time.Duration(t.cfg.Interval) * time.Second }
|
||||
|
||||
func (t *TLSChecker) Check() Result {
|
||||
start := time.Now()
|
||||
result := Result{
|
||||
MonitorName: t.cfg.Name,
|
||||
CheckedAt: start,
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", t.cfg.Host, t.cfg.Port)
|
||||
dialer := &tls.Dialer{
|
||||
NetDialer: &net.Dialer{Timeout: t.timeout},
|
||||
Config: &tls.Config{ServerName: t.cfg.Host},
|
||||
}
|
||||
|
||||
conn, err := dialer.Dial("tcp", addr)
|
||||
result.ResponseTime = time.Since(start)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
tlsConn := conn.(*tls.Conn)
|
||||
state := tlsConn.ConnectionState()
|
||||
|
||||
// Find the earliest-expiring certificate in the chain.
|
||||
var earliest time.Time
|
||||
for _, cert := range state.PeerCertificates {
|
||||
if earliest.IsZero() || cert.NotAfter.Before(earliest) {
|
||||
earliest = cert.NotAfter
|
||||
}
|
||||
}
|
||||
|
||||
daysLeft := int(time.Until(earliest).Hours() / 24)
|
||||
if daysLeft <= t.cfg.ExpiryWarningDays {
|
||||
result.Error = fmt.Sprintf("certificate expires in %d day(s) on %s",
|
||||
daysLeft, earliest.UTC().Format("2006-01-02"))
|
||||
return result
|
||||
}
|
||||
|
||||
result.Up = true
|
||||
applyThreshold(&result, t.cfg.MaxResponseMS)
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user