933e70a3f6
- Add robust notification system supporting 7+ communication channels with intelligent message templating, resource monitoring, and SSL certificate alerts. Notification Channels: (Telegram, Discord, Slack, Signal, Email, Google Chat, Webhooks). - This notification system provides enterprise-grade alerting capabilities with extensive customization options and multi-channel redundancy for critical service monitoring.
287 lines
8.7 KiB
Go
287 lines
8.7 KiB
Go
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
|
|
} |