diff options
| author | Fuwn <[email protected]> | 2026-01-19 19:41:22 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-19 19:41:22 -0800 |
| commit | 5cb6f53da0f5927edad01e854432eb0b70371b89 (patch) | |
| tree | 133824d444dc38134996eadb30a34ed63b44cfd1 /internal/monitor/graphql.go | |
| parent | feat: Add disable_ping_tooltips option to hide ping hover details (diff) | |
| download | kaze-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.go | 253 |
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 +} |