aboutsummaryrefslogtreecommitdiff
path: root/internal/monitor/graphql.go
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-19 19:41:22 -0800
committerFuwn <[email protected]>2026-01-19 19:41:22 -0800
commit5cb6f53da0f5927edad01e854432eb0b70371b89 (patch)
tree133824d444dc38134996eadb30a34ed63b44cfd1 /internal/monitor/graphql.go
parentfeat: Add disable_ping_tooltips option to hide ping hover details (diff)
downloadkaze-5cb6f53da0f5927edad01e854432eb0b70371b89.tar.xz
kaze-5cb6f53da0f5927edad01e854432eb0b70371b89.zip
feat: Add ICMP, DNS, and GraphQL monitor types
Add three new monitor types with full support: - ICMP: Ping monitoring with configurable packet count, tracks packet loss and average RTT. Marks degraded on partial packet loss. - DNS: DNS resolution monitoring supporting A, AAAA, CNAME, MX, and TXT records. Optional custom DNS server and validation of expected IPs/CNAME. - GraphQL: GraphQL endpoint monitoring with query execution, variable support, error detection, and content validation. All new monitors include retry support, response time tracking, and integrate with existing display options (round_response_time, etc). GraphQL monitors also support SSL certificate tracking.
Diffstat (limited to 'internal/monitor/graphql.go')
-rw-r--r--internal/monitor/graphql.go253
1 files changed, 253 insertions, 0 deletions
diff --git a/internal/monitor/graphql.go b/internal/monitor/graphql.go
new file mode 100644
index 0000000..5b1fc91
--- /dev/null
+++ b/internal/monitor/graphql.go
@@ -0,0 +1,253 @@
+package monitor
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/Fuwn/kaze/internal/config"
+)
+
+// GraphQLMonitor monitors GraphQL endpoints
+type GraphQLMonitor struct {
+ name string
+ target string
+ interval time.Duration
+ timeout time.Duration
+ retries int
+ expectedStatus int
+ expectedContent string
+ verifySSL bool
+ hideSSLDays bool
+ roundResponseTime bool
+ roundUptime bool
+ query string
+ variables map[string]string
+ headers map[string]string
+ userAgent string
+ client *http.Client
+}
+
+// GraphQLRequest represents a GraphQL request payload
+type GraphQLRequest struct {
+ Query string `json:"query"`
+ Variables map[string]interface{} `json:"variables,omitempty"`
+}
+
+// GraphQLResponse represents a GraphQL response
+type GraphQLResponse struct {
+ Data interface{} `json:"data,omitempty"`
+ Errors []GraphQLError `json:"errors,omitempty"`
+}
+
+// GraphQLError represents a GraphQL error
+type GraphQLError struct {
+ Message string `json:"message"`
+}
+
+// NewGraphQLMonitor creates a new GraphQL monitor
+func NewGraphQLMonitor(cfg config.MonitorConfig) (*GraphQLMonitor, error) {
+ if cfg.GraphQLQuery == "" {
+ return nil, fmt.Errorf("graphql_query is required for GraphQL monitors")
+ }
+
+ verifySSL := true
+ if cfg.VerifySSL != nil {
+ verifySSL = *cfg.VerifySSL
+ }
+
+ expectedStatus := cfg.ExpectedStatus
+ if expectedStatus == 0 {
+ expectedStatus = 200
+ }
+
+ userAgent := cfg.UserAgent
+ if userAgent == "" {
+ userAgent = "Kaze-Monitor/1.0"
+ }
+
+ // Create HTTP client with custom transport
+ transport := &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: !verifySSL,
+ },
+ }
+
+ client := &http.Client{
+ Timeout: cfg.Timeout.Duration,
+ Transport: transport,
+ }
+
+ return &GraphQLMonitor{
+ name: cfg.Name,
+ target: cfg.Target,
+ interval: cfg.Interval.Duration,
+ timeout: cfg.Timeout.Duration,
+ retries: cfg.Retries,
+ expectedStatus: expectedStatus,
+ expectedContent: cfg.ExpectedContent,
+ verifySSL: verifySSL,
+ hideSSLDays: cfg.HideSSLDays,
+ roundResponseTime: cfg.RoundResponseTime,
+ roundUptime: cfg.RoundUptime,
+ query: cfg.GraphQLQuery,
+ variables: cfg.GraphQLVariables,
+ headers: cfg.Headers,
+ userAgent: userAgent,
+ client: client,
+ }, nil
+}
+
+// Name returns the monitor's name
+func (m *GraphQLMonitor) Name() string {
+ return m.name
+}
+
+// Type returns the monitor type
+func (m *GraphQLMonitor) Type() string {
+ return "graphql"
+}
+
+// Target returns the monitor target
+func (m *GraphQLMonitor) Target() string {
+ return m.target
+}
+
+// Interval returns the check interval
+func (m *GraphQLMonitor) Interval() time.Duration {
+ return m.interval
+}
+
+// Retries returns the number of retry attempts
+func (m *GraphQLMonitor) Retries() int {
+ return m.retries
+}
+
+// HideSSLDays returns whether to hide SSL days from display
+func (m *GraphQLMonitor) HideSSLDays() bool {
+ return m.hideSSLDays
+}
+
+// RoundResponseTime returns whether to round response time
+func (m *GraphQLMonitor) RoundResponseTime() bool {
+ return m.roundResponseTime
+}
+
+// RoundUptime returns whether to round uptime percentage
+func (m *GraphQLMonitor) RoundUptime() bool {
+ return m.roundUptime
+}
+
+// Check performs the GraphQL endpoint check
+func (m *GraphQLMonitor) Check(ctx context.Context) *Result {
+ result := &Result{
+ MonitorName: m.name,
+ Timestamp: time.Now(),
+ }
+
+ // Prepare GraphQL request
+ variables := make(map[string]interface{})
+ for k, v := range m.variables {
+ variables[k] = v
+ }
+
+ gqlReq := GraphQLRequest{
+ Query: m.query,
+ Variables: variables,
+ }
+
+ jsonData, err := json.Marshal(gqlReq)
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("failed to marshal GraphQL request: %w", err)
+ return result
+ }
+
+ // Create HTTP request
+ req, err := http.NewRequestWithContext(ctx, "POST", m.target, bytes.NewBuffer(jsonData))
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("failed to create request: %w", err)
+ return result
+ }
+
+ // Set headers
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("User-Agent", m.userAgent)
+ for key, value := range m.headers {
+ req.Header.Set(key, value)
+ }
+
+ // Perform the request
+ start := time.Now()
+ resp, err := m.client.Do(req)
+ result.ResponseTime = time.Since(start)
+
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("request failed: %w", err)
+ return result
+ }
+ defer resp.Body.Close()
+
+ result.StatusCode = resp.StatusCode
+
+ // Check if we need to track SSL certificate
+ if strings.HasPrefix(m.target, "https://") && resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
+ cert := resp.TLS.PeerCertificates[0]
+ result.SSLExpiry = &cert.NotAfter
+ daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
+ result.SSLDaysLeft = daysLeft
+ }
+
+ // Read response body
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("failed to read response: %w", err)
+ return result
+ }
+
+ // Parse GraphQL response
+ var gqlResp GraphQLResponse
+ if err := json.Unmarshal(body, &gqlResp); err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("failed to parse GraphQL response: %w", err)
+ return result
+ }
+
+ // Check for GraphQL errors
+ if len(gqlResp.Errors) > 0 {
+ errorMessages := make([]string, len(gqlResp.Errors))
+ for i, gqlErr := range gqlResp.Errors {
+ errorMessages[i] = gqlErr.Message
+ }
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("GraphQL errors: %s", strings.Join(errorMessages, "; "))
+ return result
+ }
+
+ // Check HTTP status code
+ if resp.StatusCode != m.expectedStatus {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("unexpected status code: got %d, expected %d", resp.StatusCode, m.expectedStatus)
+ return result
+ }
+
+ // Check for expected content in response
+ if m.expectedContent != "" && !strings.Contains(string(body), m.expectedContent) {
+ result.Status = StatusDegraded
+ result.Error = fmt.Errorf("expected content not found in response")
+ return result
+ }
+
+ result.Status = StatusUp
+
+ return result
+}