package servermonitoring import ( "bytes" "encoding/json" "fmt" "log" "net/http" "net/url" "time" "service-operation/pocketbase" ) // ServerMetrics represents server metrics record in PocketBase type ServerMetrics struct { ID string `json:"id"` ServerID string `json:"server_id"` Timestamp string `json:"timestamp"` // Changed to string for custom parsing RAMTotal string `json:"ram_total"` RAMUsed string `json:"ram_used"` RAMFree string `json:"ram_free"` CPUCores string `json:"cpu_cores"` CPUUsage string `json:"cpu_usage"` CPUFree string `json:"cpu_free"` DiskTotal string `json:"disk_total"` DiskUsed string `json:"disk_used"` DiskFree string `json:"disk_free"` Status string `json:"status"` NetworkRxBytes int64 `json:"network_rx_bytes"` NetworkTxBytes int64 `json:"network_tx_bytes"` NetworkRxSpeed int64 `json:"network_rx_speed"` NetworkTxSpeed int64 `json:"network_tx_speed"` Created string `json:"created"` // Changed to string for custom parsing Updated string `json:"updated"` // Changed to string for custom parsing } // ParsedServerMetrics represents ServerMetrics with parsed time fields type ParsedServerMetrics struct { ServerMetrics CreatedTime time.Time UpdatedTime time.Time TimestampTime time.Time } // ParseServerMetrics converts ServerMetrics to ParsedServerMetrics with proper time parsing func ParseServerMetrics(sm ServerMetrics) (ParsedServerMetrics, error) { psm := ParsedServerMetrics{ServerMetrics: sm} var err error // Parse Created time if sm.Created != "" { psm.CreatedTime, err = parsePocketBaseTime(sm.Created) if err != nil { log.Printf("Warning: Failed to parse Created time '%s': %v", sm.Created, err) } } // Parse Updated time if sm.Updated != "" { psm.UpdatedTime, err = parsePocketBaseTime(sm.Updated) if err != nil { log.Printf("Warning: Failed to parse Updated time '%s': %v", sm.Updated, err) } } // Parse Timestamp if sm.Timestamp != "" { psm.TimestampTime, err = parsePocketBaseTime(sm.Timestamp) if err != nil { log.Printf("Warning: Failed to parse Timestamp '%s': %v", sm.Timestamp, err) } } return psm, nil } // parsePocketBaseTime parses PocketBase time format "2025-08-11 13:09:13.243Z" func parsePocketBaseTime(timeStr string) (time.Time, error) { // First try the PocketBase format with space separator if t, err := time.Parse("2006-01-02 15:04:05.000Z", timeStr); err == nil { return t, nil } // Fallback to RFC3339 format with T separator if t, err := time.Parse(time.RFC3339, timeStr); err == nil { return t, nil } // Fallback to RFC3339Nano format if t, err := time.Parse(time.RFC3339Nano, timeStr); err == nil { return t, nil } return time.Time{}, fmt.Errorf("unable to parse time string: %s", timeStr) } // ServerPocketBaseClient is a wrapper around the PocketBase client for server monitoring type ServerPocketBaseClient struct { client *pocketbase.PocketBaseClient } // NewServerPocketBaseClient creates a new server PocketBase client func NewServerPocketBaseClient(client *pocketbase.PocketBaseClient) *ServerPocketBaseClient { return &ServerPocketBaseClient{ client: client, } } // GetServerThreshold fetches threshold configuration for a server func (spc *ServerPocketBaseClient) GetServerThreshold(thresholdID string) (*ServerThreshold, error) { if thresholdID == "" { log.Printf("No threshold ID provided") _ = thresholdID return nil, nil } url := fmt.Sprintf("%s/api/collections/server_threshold_templates/records/%s", spc.client.GetBaseURL(), thresholdID) //log.Printf("🔍 Fetching server threshold from: %s", url) resp, err := http.Get(url) if err != nil { //log.Printf("❌ HTTP error fetching server threshold: %v", err) _ = err return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { //log.Printf("❌ Failed to fetch server threshold, status: %d", resp.StatusCode) return nil, fmt.Errorf("failed to fetch server threshold, status: %d", resp.StatusCode) } var threshold ServerThreshold if err := json.NewDecoder(resp.Body).Decode(&threshold); err != nil { //log.Printf("❌ Error decoding server threshold JSON: %v", err) _ = err return nil, err } //log.Printf("✅ Successfully fetched server threshold: %+v", threshold) _ = url _ = threshold return &threshold, nil } // GetAllServers retrieves all servers from PocketBase func (spc *ServerPocketBaseClient) GetAllServers() ([]Server, error) { url := fmt.Sprintf("%s/api/collections/servers/records?perPage=500", spc.client.GetBaseURL()) //log.Printf("🌐 Fetching all servers from: %s", url) resp, err := http.Get(url) if err != nil { //log.Printf("❌ HTTP error fetching servers: %v", err) _ = err return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { //log.Printf("❌ Failed to fetch servers, status: %d", resp.StatusCode) return nil, fmt.Errorf("failed to fetch servers, status: %d", resp.StatusCode) } var response struct { Items []Server `json:"items"` } if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { //log.Printf("❌ Error decoding servers JSON: %v", err) _ = err return nil, err } //log.Printf("✅ Successfully fetched %d servers", len(response.Items)) _ = url return response.Items, nil } // GetLatestServerMetrics retrieves the latest server metrics from PocketBase func (spc *ServerPocketBaseClient) GetLatestServerMetrics(serverID string, timeout time.Duration) ([]ParsedServerMetrics, error) { // Calculate the time before which metrics are considered too old cutoff := time.Now().Add(-timeout).UTC().Format("2006-01-02 15:04:05.000Z") // Construct the filter string with proper formatting (no spaces around operators) filter := fmt.Sprintf("server_id='%s'&&created>'%s'", serverID, cutoff) // URL encode the filter parameter encodedFilter := url.QueryEscape(filter) // Construct the URL with properly encoded parameters requestURL := fmt.Sprintf("%s/api/collections/server_metrics/records?filter=%s&sort=-created&perPage=1", spc.client.GetBaseURL(), encodedFilter) //log.Printf("🌐 Fetching latest server metrics from: %s", requestURL) //log.Printf("🔍 Filter used: %s", filter) //log.Printf("🔍 Cutoff time: %s", cutoff) resp, err := http.Get(requestURL) if err != nil { //log.Printf("❌ HTTP error fetching server metrics: %v", err) _ = err return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { //log.Printf("❌ Failed to fetch server metrics, status: %d", resp.StatusCode) return nil, fmt.Errorf("failed to fetch server metrics, status: %d", resp.StatusCode) } var response struct { Items []ServerMetrics `json:"items"` } if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { //log.Printf("❌ Error decoding server metrics JSON: %v", err) _ = err return nil, err } // Convert to ParsedServerMetrics var parsedMetrics []ParsedServerMetrics for _, metric := range response.Items { parsed, err := ParseServerMetrics(metric) if err != nil { //log.Printf("❌ Error parsing server metric: %v", err) _ = err continue } parsedMetrics = append(parsedMetrics, parsed) } //log.Printf("✅ Successfully fetched and parsed %d server metrics", len(parsedMetrics)) _ = requestURL _ = filter _ = cutoff return parsedMetrics, nil } // UpdateServerStatus updates the server status in PocketBase func (spc *ServerPocketBaseClient) UpdateServerStatus(serverID string, status string) error { url := fmt.Sprintf("%s/api/collections/servers/records/%s", spc.client.GetBaseURL(), serverID) //log.Printf("🌐 Updating server status at: %s", url) payload := map[string]interface{}{ "status": status, "last_checked": time.Now().UTC().Format("2006-01-02 15:04:05.000Z"), } payloadBytes, err := json.Marshal(payload) if err != nil { //log.Printf("❌ Error marshaling payload: %v", err) _ = err return err } //log.Printf("📝 Update payload: %s", string(payloadBytes)) req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(payloadBytes)) if err != nil { //log.Printf("❌ Error creating request: %v", err) _ = err return err } req.Header.Set("Content-Type", "application/json") httpClient := &http.Client{Timeout: 10 * time.Second} resp, err := httpClient.Do(req) if err != nil { //log.Printf("❌ HTTP error updating server status: %v", err) _ = err return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { //log.Printf("❌ Failed to update server status, status: %d", resp.StatusCode) return fmt.Errorf("failed to update server status, status: %d", resp.StatusCode) } //log.Printf("✅ Successfully updated server status to %s", status) _ = url _ = payloadBytes _ = status return nil }