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 { id string name string group 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{ id: cfg.ID(), name: cfg.Name, group: cfg.Group, 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 } // ID returns the unique identifier for this monitor func (m *GraphQLMonitor) ID() string { return m.id } // Name returns the monitor's name func (m *GraphQLMonitor) Name() string { return m.name } // Group returns the group this monitor belongs to func (m *GraphQLMonitor) Group() string { return m.group } // 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.id, 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 }