aboutsummaryrefslogtreecommitdiff
path: root/internal/monitor
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-19 16:53:39 -0800
committerFuwn <[email protected]>2026-01-19 16:53:39 -0800
commitb83baaae6735eccca31a94e630fdeb67919733a0 (patch)
tree625deb2572ed2e8cefa607e88c764c7c21344802 /internal/monitor
parentfeat: Add reset_on_next_check flag to wipe monitor history (diff)
downloadkaze-b83baaae6735eccca31a94e630fdeb67919733a0.tar.xz
kaze-b83baaae6735eccca31a94e630fdeb67919733a0.zip
feat: Add group defaults, content checking, SSL tracking for Gemini, hide/round options
Diffstat (limited to 'internal/monitor')
-rw-r--r--internal/monitor/gemini.go52
-rw-r--r--internal/monitor/http.go88
-rw-r--r--internal/monitor/monitor.go27
-rw-r--r--internal/monitor/tcp.go32
4 files changed, 142 insertions, 57 deletions
diff --git a/internal/monitor/gemini.go b/internal/monitor/gemini.go
index d92499b..fb75b7d 100644
--- a/internal/monitor/gemini.go
+++ b/internal/monitor/gemini.go
@@ -14,12 +14,14 @@ import (
// GeminiMonitor monitors Gemini protocol endpoints
type GeminiMonitor struct {
- name string
- target string
- interval time.Duration
- timeout time.Duration
- retries int
- verifySSL bool
+ name string
+ target string
+ interval time.Duration
+ timeout time.Duration
+ retries int
+ verifySSL bool
+ hideSSLDays bool
+ roundResponseTime bool
}
// NewGeminiMonitor creates a new Gemini monitor
@@ -50,12 +52,14 @@ func NewGeminiMonitor(cfg config.MonitorConfig) (*GeminiMonitor, error) {
}
return &GeminiMonitor{
- name: cfg.Name,
- target: target,
- interval: cfg.Interval.Duration,
- timeout: cfg.Timeout.Duration,
- retries: cfg.Retries,
- verifySSL: verifySSL,
+ name: cfg.Name,
+ target: target,
+ interval: cfg.Interval.Duration,
+ timeout: cfg.Timeout.Duration,
+ retries: cfg.Retries,
+ verifySSL: verifySSL,
+ hideSSLDays: cfg.HideSSLDays,
+ roundResponseTime: cfg.RoundResponseTime,
}, nil
}
@@ -84,6 +88,16 @@ func (m *GeminiMonitor) Retries() int {
return m.retries
}
+// HideSSLDays returns whether to hide SSL days from display
+func (m *GeminiMonitor) HideSSLDays() bool {
+ return m.hideSSLDays
+}
+
+// RoundResponseTime returns whether to round response time
+func (m *GeminiMonitor) RoundResponseTime() bool {
+ return m.roundResponseTime
+}
+
// Check performs the Gemini protocol check
func (m *GeminiMonitor) Check(ctx context.Context) *Result {
result := &Result{
@@ -119,14 +133,12 @@ func (m *GeminiMonitor) Check(ctx context.Context) *Result {
}
defer conn.Close()
- // Check SSL certificate
- if m.verifySSL {
- connState := conn.ConnectionState()
- if len(connState.PeerCertificates) > 0 {
- cert := connState.PeerCertificates[0]
- result.SSLExpiry = &cert.NotAfter
- result.SSLDaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
- }
+ // Check SSL certificate (always track, even if not verifying)
+ connState := conn.ConnectionState()
+ if len(connState.PeerCertificates) > 0 {
+ cert := connState.PeerCertificates[0]
+ result.SSLExpiry = &cert.NotAfter
+ result.SSLDaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
}
// Set deadline for the entire operation
diff --git a/internal/monitor/http.go b/internal/monitor/http.go
index 5587a5f..b8a0177 100644
--- a/internal/monitor/http.go
+++ b/internal/monitor/http.go
@@ -15,18 +15,21 @@ import (
// HTTPMonitor monitors HTTP and HTTPS endpoints
type HTTPMonitor struct {
- name string
- monitorType string
- target string
- interval time.Duration
- timeout time.Duration
- retries int
- method string
- headers map[string]string
- body string
- expectedStatus int
- verifySSL bool
- client *http.Client
+ name string
+ monitorType string
+ target string
+ interval time.Duration
+ timeout time.Duration
+ retries int
+ method string
+ headers map[string]string
+ body string
+ expectedStatus int
+ expectedContent string
+ verifySSL bool
+ hideSSLDays bool
+ roundResponseTime bool
+ client *http.Client
}
// NewHTTPMonitor creates a new HTTP/HTTPS monitor
@@ -77,18 +80,21 @@ func NewHTTPMonitor(cfg config.MonitorConfig) (*HTTPMonitor, error) {
}
return &HTTPMonitor{
- name: cfg.Name,
- monitorType: cfg.Type,
- target: target,
- interval: cfg.Interval.Duration,
- timeout: cfg.Timeout.Duration,
- retries: cfg.Retries,
- method: cfg.Method,
- headers: cfg.Headers,
- body: cfg.Body,
- expectedStatus: cfg.ExpectedStatus,
- verifySSL: verifySSL,
- client: client,
+ name: cfg.Name,
+ monitorType: cfg.Type,
+ target: target,
+ interval: cfg.Interval.Duration,
+ timeout: cfg.Timeout.Duration,
+ retries: cfg.Retries,
+ method: cfg.Method,
+ headers: cfg.Headers,
+ body: cfg.Body,
+ expectedStatus: cfg.ExpectedStatus,
+ expectedContent: cfg.ExpectedContent,
+ verifySSL: verifySSL,
+ hideSSLDays: cfg.HideSSLDays,
+ roundResponseTime: cfg.RoundResponseTime,
+ client: client,
}, nil
}
@@ -117,6 +123,16 @@ func (m *HTTPMonitor) Retries() int {
return m.retries
}
+// HideSSLDays returns whether to hide SSL days from display
+func (m *HTTPMonitor) HideSSLDays() bool {
+ return m.hideSSLDays
+}
+
+// RoundResponseTime returns whether to round response time
+func (m *HTTPMonitor) RoundResponseTime() bool {
+ return m.roundResponseTime
+}
+
// Check performs the HTTP/HTTPS check
func (m *HTTPMonitor) Check(ctx context.Context) *Result {
result := &Result{
@@ -155,8 +171,20 @@ func (m *HTTPMonitor) Check(ctx context.Context) *Result {
}
defer resp.Body.Close()
- // Discard body to allow connection reuse
- io.Copy(io.Discard, resp.Body)
+ // Read body if we need to check content, otherwise discard
+ var bodyContent string
+ if m.expectedContent != "" {
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("failed to read response body: %w", err)
+ return result
+ }
+ bodyContent = string(bodyBytes)
+ } else {
+ // Discard body to allow connection reuse
+ io.Copy(io.Discard, resp.Body)
+ }
result.StatusCode = resp.StatusCode
@@ -179,6 +207,14 @@ func (m *HTTPMonitor) Check(ctx context.Context) *Result {
result.Error = fmt.Errorf("bad status code: %d", resp.StatusCode)
}
+ // Check expected content if specified
+ if m.expectedContent != "" && result.Status == StatusUp {
+ if !strings.Contains(bodyContent, m.expectedContent) {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("expected content not found in response")
+ }
+ }
+
// Check for slow response (degraded if > 2 seconds)
if result.Status == StatusUp && result.ResponseTime > 2*time.Second {
result.Status = StatusDegraded
diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go
index b0c2f4f..cbf01b3 100644
--- a/internal/monitor/monitor.go
+++ b/internal/monitor/monitor.go
@@ -34,7 +34,7 @@ type Monitor interface {
// Name returns the monitor's name
Name() string
- // Type returns the monitor type (http, https, tcp)
+ // Type returns the monitor type (http, https, tcp, gemini)
Type() string
// Target returns the monitor target (URL or host:port)
@@ -46,6 +46,12 @@ type Monitor interface {
// Retries returns the number of retry attempts
Retries() int
+ // HideSSLDays returns whether to hide SSL days from display
+ HideSSLDays() bool
+
+ // RoundResponseTime returns whether to round response time
+ RoundResponseTime() bool
+
// Check performs the monitoring check and returns the result
Check(ctx context.Context) *Result
}
@@ -89,3 +95,22 @@ func (r *Result) ToCheckResult() *storage.CheckResult {
}
return cr
}
+
+// ToCheckResultWithOptions converts a monitor Result to a storage CheckResult with display options applied
+func (r *Result) ToCheckResultWithOptions(mon Monitor) *storage.CheckResult {
+ cr := r.ToCheckResult()
+
+ // Apply rounding if enabled
+ if mon.RoundResponseTime() {
+ // Round to nearest second (1000ms)
+ cr.ResponseTime = ((cr.ResponseTime + 500) / 1000) * 1000
+ }
+
+ // Hide SSL days if enabled
+ if mon.HideSSLDays() {
+ cr.SSLDaysLeft = 0
+ cr.SSLExpiry = nil
+ }
+
+ return cr
+}
diff --git a/internal/monitor/tcp.go b/internal/monitor/tcp.go
index d315545..d14a5f1 100644
--- a/internal/monitor/tcp.go
+++ b/internal/monitor/tcp.go
@@ -11,11 +11,12 @@ import (
// TCPMonitor monitors TCP endpoints
type TCPMonitor struct {
- name string
- target string
- interval time.Duration
- timeout time.Duration
- retries int
+ name string
+ target string
+ interval time.Duration
+ timeout time.Duration
+ retries int
+ roundResponseTime bool
}
// NewTCPMonitor creates a new TCP monitor
@@ -27,11 +28,12 @@ func NewTCPMonitor(cfg config.MonitorConfig) (*TCPMonitor, error) {
}
return &TCPMonitor{
- name: cfg.Name,
- target: cfg.Target,
- interval: cfg.Interval.Duration,
- timeout: cfg.Timeout.Duration,
- retries: cfg.Retries,
+ name: cfg.Name,
+ target: cfg.Target,
+ interval: cfg.Interval.Duration,
+ timeout: cfg.Timeout.Duration,
+ retries: cfg.Retries,
+ roundResponseTime: cfg.RoundResponseTime,
}, nil
}
@@ -60,6 +62,16 @@ func (m *TCPMonitor) Retries() int {
return m.retries
}
+// HideSSLDays returns whether to hide SSL days from display
+func (m *TCPMonitor) HideSSLDays() bool {
+ return false // TCP doesn't use SSL
+}
+
+// RoundResponseTime returns whether to round response time
+func (m *TCPMonitor) RoundResponseTime() bool {
+ return m.roundResponseTime
+}
+
// Check performs the TCP connection check
func (m *TCPMonitor) Check(ctx context.Context) *Result {
result := &Result{