Implement CheckCle Microservice Operation

A Go-based microservice for service operations including ICMP ping, DNS resolution, and TCP connectivity
This commit is contained in:
Tola Leng
2025-06-19 17:49:30 +07:00
parent 89572c44b7
commit d3ea5dc77c
36 changed files with 2895 additions and 0 deletions
@@ -0,0 +1,122 @@
package monitoring
import (
"log"
"strings"
"time"
"service-operation/operations"
"service-operation/pocketbase"
"service-operation/shared/savers"
"service-operation/types"
)
func (ms *MonitoringService) performCheck(service pocketbase.Service) {
// First, fetch the latest service status from PocketBase to ensure we have current data
latestService, err := ms.pbClient.GetService(service.ID)
if err != nil {
log.Printf("Failed to fetch latest service status for %s: %v", service.Name, err)
return
}
// Respect the service status - don't check paused services
if latestService.Status == "paused" {
return // Silently skip paused services
}
timeout := 10 * time.Second // Default timeout
var result *types.OperationResult
serviceType := strings.ToLower(latestService.ServiceType)
// Single log message for check start
//log.Printf("Checking %s (%s)", latestService.Name, serviceType)
switch serviceType {
case "ping", "icmp":
pingOp := operations.NewPingOperation(timeout)
host := latestService.Host
if host == "" {
host = latestService.URL
}
result, err = pingOp.Execute(host, 1) // Single ping for monitoring
case "dns":
dnsOp := operations.NewDNSOperation(timeout)
host := latestService.Host
if host == "" {
host = latestService.Domain
}
// Default to A record, but could be made configurable
queryType := "A"
result, err = dnsOp.Execute(host, queryType)
case "tcp":
tcpOp := operations.NewTCPOperation(timeout)
host := latestService.Host
if host == "" {
host = latestService.URL
}
port := latestService.Port
if port <= 0 {
port = 80 // Default port
}
result, err = tcpOp.Execute(host, port)
case "http", "https":
httpOp := operations.NewHTTPOperation(timeout)
url := latestService.URL
if url == "" {
url = latestService.Host
}
result, err = httpOp.Execute(url, "GET")
default:
log.Printf("Unknown service type: %s for service %s", latestService.ServiceType, latestService.Name)
return
}
// Determine status based on result
status := "down"
errorMessage := ""
responseTime := int64(0)
if err != nil {
errorMessage = err.Error()
log.Printf("❌ %s failed: %v", latestService.Name, err)
} else if result != nil {
responseTime = result.ResponseTime.Milliseconds()
if result.Success {
status = "up"
//log.Printf("✅ %s: %.0fms", latestService.Name, float64(responseTime))
} else {
status = "down"
errorMessage = result.Error
log.Printf("❌ %s failed: %s", latestService.Name, errorMessage)
}
}
// Only update service status if the service is not paused
// Check one more time before updating to prevent race conditions
currentService, err := ms.pbClient.GetService(latestService.ID)
if err != nil {
log.Printf("Failed to verify service status before update for %s: %v", latestService.Name, err)
return
}
if currentService.Status == "paused" {
return // Silently skip status update for paused services
}
// Update service status in PocketBase only if not paused
if err := ms.pbClient.UpdateServiceStatus(latestService.ID, status, responseTime, errorMessage); err != nil {
log.Printf("Failed to update service status for %s: %v", latestService.Name, err)
}
// Save metrics data in ONE place to prevent duplicates
if result != nil {
metricsSaver := savers.NewMetricsSaver(ms.pbClient)
metricsSaver.SaveMetricsForService(*latestService, result)
}
}
@@ -0,0 +1,33 @@
package monitoring
import (
"log"
"time"
"service-operation/pocketbase"
)
// Simplified metrics saver for basic service status updates only
func (ms *MonitoringService) saveMetrics(service pocketbase.Service, status string, responseTime int64, errorMessage string) {
// Save general metrics using the new structure
metrics := pocketbase.MetricsRecord{
ServiceName: service.Name,
Host: service.Host,
Uptime: 0, // This would need to be calculated based on your requirements
ResponseTime: responseTime,
LastChecked: time.Now().Format(time.RFC3339),
Port: service.Port,
ServiceType: service.ServiceType,
Status: status,
ErrorMessage: errorMessage,
CheckedAt: time.Now().Format(time.RFC3339),
}
if err := ms.pbClient.SaveMetrics(metrics); err != nil {
log.Printf("Failed to save metrics for %s: %v", service.Name, err)
}
}
// This method is now removed to prevent duplicate saves
// The shared savers handle everything in SaveMetricsForService
@@ -0,0 +1,52 @@
package monitoring
import (
"log"
"time"
"service-operation/pocketbase"
)
type ServiceMonitor struct {
service pocketbase.Service
ticker *time.Ticker
stopChan chan bool
}
func (ms *MonitoringService) startMonitor(service pocketbase.Service) {
if service.HeartbeatInterval <= 0 {
service.HeartbeatInterval = 60 // Default to 60 seconds
}
monitor := &ServiceMonitor{
service: service,
ticker: time.NewTicker(time.Duration(service.HeartbeatInterval) * time.Second),
stopChan: make(chan bool),
}
ms.activeServices[service.ID] = monitor
log.Printf("Starting monitor for service: %s (%s)", service.Name, service.ServiceType)
go func() {
// Perform initial check
ms.performCheck(service)
for {
select {
case <-monitor.ticker.C:
ms.performCheck(service)
case <-monitor.stopChan:
monitor.ticker.Stop()
return
}
}
}()
}
func (ms *MonitoringService) stopMonitor(serviceID string, monitor *ServiceMonitor) {
log.Printf("Stopping monitor for service: %s", serviceID)
monitor.stopChan <- true
delete(ms.activeServices, serviceID)
}
@@ -0,0 +1,112 @@
package monitoring
import (
"log"
"sync"
"time"
"service-operation/pocketbase"
)
type MonitoringService struct {
pbClient *pocketbase.PocketBaseClient
activeServices map[string]*ServiceMonitor
mu sync.RWMutex
stopChan chan bool
isRunning bool
}
func NewMonitoringService(pbClient *pocketbase.PocketBaseClient) *MonitoringService {
return &MonitoringService{
pbClient: pbClient,
activeServices: make(map[string]*ServiceMonitor),
stopChan: make(chan bool),
isRunning: false,
}
}
func (ms *MonitoringService) Start() {
ms.mu.Lock()
defer ms.mu.Unlock()
if ms.isRunning {
log.Println("Monitoring service is already running")
return
}
ms.isRunning = true
log.Println("Starting monitoring service...")
// Start monitoring all services from PocketBase
go ms.monitoringLoop()
}
func (ms *MonitoringService) Stop() {
ms.mu.Lock()
defer ms.mu.Unlock()
if !ms.isRunning {
return
}
log.Println("Stopping monitoring service...")
ms.isRunning = false
// Stop all active monitors
for serviceID, monitor := range ms.activeServices {
ms.stopMonitor(serviceID, monitor)
}
ms.stopChan <- true
}
func (ms *MonitoringService) monitoringLoop() {
ticker := time.NewTicker(30 * time.Second) // Check for new services every 30 seconds
defer ticker.Stop()
// Initial load of services
ms.loadAndStartServices()
for {
select {
case <-ticker.C:
ms.loadAndStartServices()
case <-ms.stopChan:
return
}
}
}
func (ms *MonitoringService) loadAndStartServices() {
// Only get services that are NOT paused
services, err := ms.pbClient.GetActiveServices()
if err != nil {
log.Printf("Failed to load services: %v", err)
return
}
ms.mu.Lock()
defer ms.mu.Unlock()
// Filter out paused services and start monitoring for active ones
activeServiceIDs := make(map[string]bool)
for _, service := range services {
if service.Status != "paused" {
activeServiceIDs[service.ID] = true
// Start monitoring if not already active
if _, exists := ms.activeServices[service.ID]; !exists {
ms.startMonitor(service)
}
}
}
// Stop monitoring for paused or removed services
for serviceID, monitor := range ms.activeServices {
if !activeServiceIDs[serviceID] {
log.Printf("Stopping monitoring for service %s (paused or removed)", serviceID)
ms.stopMonitor(serviceID, monitor)
}
}
}
@@ -0,0 +1,10 @@
package monitoring
// This file previously contained saveDetailedDataByType which was causing duplicate saves
// All detailed data saving is now handled by the shared savers in a single place
// to prevent duplicate records in PocketBase
// The monitoring service now uses only:
// 1. SaveMetricsForService from shared/savers for complete data saving
// 2. Direct service status updates for the services table