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,74 @@
package savers
import (
"fmt"
"strings"
"time"
"service-operation/pocketbase"
"service-operation/types"
)
func (ms *MetricsSaver) SaveDNSDataToPocketBase(result *types.OperationResult, serviceID string) {
// Create a short, professional status message
var details string
if result.Success {
// Success message with record count and query info
recordCount := len(result.DNSRecords)
details = fmt.Sprintf("✅ DNS %s Query OK - %d records found",
strings.ToUpper(result.DNSType), recordCount)
// Add response time
details += fmt.Sprintf(" | Response time: %.2fms",
float64(result.ResponseTime.Nanoseconds())/1000000)
// Add first few records for context
if recordCount > 0 {
if recordCount <= 2 {
details += fmt.Sprintf(" | Records: %s", strings.Join(result.DNSRecords, ", "))
} else {
details += fmt.Sprintf(" | Records: %s... (+%d more)",
strings.Join(result.DNSRecords[:2], ", "), recordCount-2)
}
}
} else {
// Error message with query type
details = fmt.Sprintf("❌ DNS %s Query Failed - %s",
strings.ToUpper(result.DNSType),
GetShortErrorMessage(result.Error))
// Add response time if available
if result.ResponseTime > 0 {
details += fmt.Sprintf(" | Response time: %.2fms",
float64(result.ResponseTime.Nanoseconds())/1000000)
}
}
dnsData := pocketbase.DNSDataRecord{
ServiceID: serviceID,
Timestamp: time.Now(),
ResponseTime: result.ResponseTime.Milliseconds(),
Status: GetStatusString(result.Success),
QueryType: result.DNSType,
ResolveIP: strings.Join(result.DNSRecords, ","),
MsgSize: fmt.Sprintf("%d", len(result.DNSRecords)),
Question: result.Host,
Answer: strings.Join(result.DNSRecords, ","),
Authority: "", // Not available in current implementation
ErrorMessage: result.Error,
Details: details, // Short, clean message
RegionName: "default", // You can make this configurable
AgentID: "1", // You can make this configurable
}
if err := ms.pbClient.SaveDNSData(dnsData); err != nil {
println("Failed to save DNS data to PocketBase:", err.Error())
}
}
// Method for monitoring service usage
func (ms *MetricsSaver) SaveDNSDataForService(service pocketbase.Service, result *types.OperationResult) {
ms.SaveDNSDataToPocketBase(result, service.ID)
}
@@ -0,0 +1,90 @@
package savers
import (
"time"
"service-operation/pocketbase"
"service-operation/types"
)
type MetricsSaver struct {
pbClient *pocketbase.PocketBaseClient
}
func NewMetricsSaver(pbClient *pocketbase.PocketBaseClient) *MetricsSaver {
return &MetricsSaver{
pbClient: pbClient,
}
}
func (ms *MetricsSaver) SaveMetricsToPocketBase(result *types.OperationResult, serviceID string) {
// Save general metrics using the new structure
metrics := pocketbase.MetricsRecord{
ServiceName: result.Host,
Host: result.Host,
Uptime: 0, // This would need to be calculated based on your requirements
ResponseTime: result.ResponseTime.Milliseconds(),
LastChecked: time.Now().Format(time.RFC3339),
Port: result.Port,
ServiceType: string(result.Type),
Status: GetStatusString(result.Success),
ErrorMessage: result.Error,
Details: FormatResultDetails(result),
CheckedAt: time.Now().Format(time.RFC3339),
}
if err := ms.pbClient.SaveMetrics(metrics); err != nil {
// Log error but don't fail the operation
println("Failed to save metrics to PocketBase:", err.Error())
}
// Save detailed data based on operation type - only once per check
if serviceID != "" {
switch result.Type {
case types.OperationPing:
ms.SavePingDataToPocketBase(result, serviceID)
case types.OperationHTTP:
ms.SaveUptimeDataToPocketBase(result, serviceID)
case types.OperationDNS:
ms.SaveDNSDataToPocketBase(result, serviceID)
case types.OperationTCP:
ms.SaveTCPDataToPocketBase(result, serviceID)
}
}
}
// Primary method for monitoring service usage - this prevents duplicates
func (ms *MetricsSaver) SaveMetricsForService(service pocketbase.Service, result *types.OperationResult) {
// Save general metrics first - reduced logging
metrics := pocketbase.MetricsRecord{
ServiceName: service.Name,
Host: service.Host,
Uptime: 0, // This would need to be calculated based on your requirements
ResponseTime: result.ResponseTime.Milliseconds(),
LastChecked: time.Now().Format(time.RFC3339),
Port: service.Port,
ServiceType: service.ServiceType,
Status: GetStatusString(result.Success),
ErrorMessage: result.Error,
Details: FormatResultDetails(result),
CheckedAt: time.Now().Format(time.RFC3339),
}
if err := ms.pbClient.SaveMetrics(metrics); err != nil {
// Silent error - no logging to reduce output
return
}
// Save detailed data based on service type - only once per service with minimal logging
switch service.ServiceType {
case "ping", "icmp":
ms.SavePingDataToPocketBase(result, service.ID)
case "dns":
ms.SaveDNSDataToPocketBase(result, service.ID)
case "http", "https":
ms.SaveUptimeDataToPocketBase(result, service.ID)
case "tcp":
ms.SaveTCPDataToPocketBase(result, service.ID)
}
}
@@ -0,0 +1,72 @@
package savers
import (
"fmt"
"time"
"service-operation/pocketbase"
"service-operation/types"
)
func (ms *MetricsSaver) SavePingDataToPocketBase(result *types.OperationResult, serviceID string) {
// Create a short, professional status message
var details string
if result.Success {
// Success message with packet stats
details = fmt.Sprintf("✅ Ping OK - %d/%d packets received",
result.PacketsRecv, result.PacketsSent)
// Add loss percentage if there was any loss
if result.PacketLoss > 0 {
details += fmt.Sprintf(" (%.1f%% loss)", result.PacketLoss)
}
// Add response time info
details += fmt.Sprintf(" | Avg: %.2fms",
float64(result.AvgRTT.Nanoseconds())/1000000)
// Add min/max if different from average (significant variance)
if result.MinRTT != result.MaxRTT {
details += fmt.Sprintf(", Min: %.2fms, Max: %.2fms",
float64(result.MinRTT.Nanoseconds())/1000000,
float64(result.MaxRTT.Nanoseconds())/1000000)
}
} else {
// Error message
if result.PacketLoss >= 100 {
details = fmt.Sprintf("❌ Ping Failed - 100%% packet loss (%s)",
GetShortErrorMessage(result.Error))
} else {
details = fmt.Sprintf("⚠️ Ping Partial - %.1f%% packet loss (%s)",
result.PacketLoss, GetShortErrorMessage(result.Error))
}
}
pingData := pocketbase.PingDataRecord{
ServiceID: serviceID,
Timestamp: time.Now(),
ResponseTime: result.ResponseTime.Milliseconds(),
Status: GetStatusString(result.Success),
PacketsSent: fmt.Sprintf("%d", result.PacketsSent),
PacketsRecv: fmt.Sprintf("%d", result.PacketsRecv),
PacketLoss: fmt.Sprintf("%.1f%%", result.PacketLoss),
MinRTT: fmt.Sprintf("%.2fms", float64(result.MinRTT.Nanoseconds())/1000000),
MaxRTT: fmt.Sprintf("%.2fms", float64(result.MaxRTT.Nanoseconds())/1000000),
AvgRTT: fmt.Sprintf("%.2fms", float64(result.AvgRTT.Nanoseconds())/1000000),
RTTs: "", // Not currently tracked
Latency: fmt.Sprintf("%.2fms", float64(result.AvgRTT.Nanoseconds())/1000000),
ErrorMessage: result.Error,
Details: details, // Short, clean message
}
if err := ms.pbClient.SavePingData(pingData); err != nil {
println("Failed to save ping data to PocketBase:", err.Error())
}
}
// Method for monitoring service usage
func (ms *MetricsSaver) SavePingDataForService(service pocketbase.Service, result *types.OperationResult) {
ms.SavePingDataToPocketBase(result, service.ID)
}
@@ -0,0 +1,66 @@
package savers
import (
"fmt"
"strconv"
"time"
"service-operation/pocketbase"
"service-operation/types"
)
func (ms *MetricsSaver) SaveTCPDataToPocketBase(result *types.OperationResult, serviceID string) {
// Create a short, professional status message
var details string
if result.Success && result.TCPConnected {
// Success message with connection info
details = fmt.Sprintf("✅ TCP Connection OK - Port %d accessible", result.Port)
// Add response time
details += fmt.Sprintf(" | Connection time: %.2fms",
float64(result.ResponseTime.Nanoseconds())/1000000)
} else {
// Error message with port info
details = fmt.Sprintf("❌ TCP Connection Failed - Port %d unreachable", result.Port)
if result.Error != "" {
details += fmt.Sprintf(" (%s)", GetShortErrorMessage(result.Error))
}
// Add response time if available
if result.ResponseTime > 0 {
details += fmt.Sprintf(" | Timeout: %.2fms",
float64(result.ResponseTime.Nanoseconds())/1000000)
}
}
connectionStatus := "disconnected"
if result.TCPConnected {
connectionStatus = "connected"
}
tcpData := pocketbase.TCPDataRecord{
ServiceID: serviceID,
Timestamp: time.Now(),
ResponseTime: result.ResponseTime.Milliseconds(),
Status: GetStatusString(result.Success),
Connection: connectionStatus,
Latency: fmt.Sprintf("%.2fms", float64(result.ResponseTime.Nanoseconds())/1000000),
Port: strconv.Itoa(result.Port),
ErrorMessage: result.Error,
Details: details,
RegionName: "default",
AgentID: "1",
}
if err := ms.pbClient.SaveTCPData(tcpData); err != nil {
fmt.Printf("Failed to save TCP data to PocketBase: %v\n", err)
}
}
// Method for monitoring service usage
func (ms *MetricsSaver) SaveTCPDataForService(service pocketbase.Service, result *types.OperationResult) {
ms.SaveTCPDataToPocketBase(result, service.ID)
}
@@ -0,0 +1,72 @@
package savers
import (
"fmt"
"time"
"service-operation/pocketbase"
"service-operation/types"
)
func (ms *MetricsSaver) SaveUptimeDataToPocketBase(result *types.OperationResult, serviceID string) {
// Create a short, professional status message
var details string
if result.Success {
// Success message with basic info
details = fmt.Sprintf("✅ HTTP %d OK - Response time: %.2fms",
result.HTTPStatusCode,
float64(result.ResponseTime.Nanoseconds())/1000000)
// Add content info if available
if result.ContentLength > 0 {
details += fmt.Sprintf(" | Content: %s", FormatBytes(result.ContentLength))
}
// Add server info if available
if server, exists := result.HTTPHeaders["Server"]; exists {
details += fmt.Sprintf(" | Server: %s", server)
}
} else {
// Error message with status code if available
if result.HTTPStatusCode > 0 {
details = fmt.Sprintf("❌ HTTP %d Error - %s",
result.HTTPStatusCode,
GetShortErrorMessage(result.Error))
} else {
details = fmt.Sprintf("🔌 Connection Error - %s",
GetShortErrorMessage(result.Error))
}
// Add response time if available
if result.ResponseTime > 0 {
details += fmt.Sprintf(" | Response time: %.2fms",
float64(result.ResponseTime.Nanoseconds())/1000000)
}
}
uptimeData := pocketbase.UptimeDataRecord{
ServiceID: serviceID,
Timestamp: time.Now(),
ResponseTime: result.ResponseTime.Milliseconds(),
Status: GetStatusString(result.Success),
Packets: "N/A", // Not applicable for HTTP
Latency: fmt.Sprintf("%.2fms", float64(result.ResponseTime.Nanoseconds())/1000000),
StatusCodes: fmt.Sprintf("%d", result.HTTPStatusCode),
Keyword: "", // Can be populated later if needed
ErrorMessage: result.Error,
Details: details, // Short, clean message
Region: "default", // You can make this configurable
RegionID: "1", // You can make this configurable
}
if err := ms.pbClient.SaveUptimeData(uptimeData); err != nil {
println("Failed to save uptime data to PocketBase:", err.Error())
}
}
// Method for monitoring service usage
func (ms *MetricsSaver) SaveUptimeDataForService(service pocketbase.Service, result *types.OperationResult) {
ms.SaveUptimeDataToPocketBase(result, service.ID)
}
@@ -0,0 +1,89 @@
package savers
import (
"fmt"
"strings"
"service-operation/types"
)
// Helper function to format bytes in a readable way
func FormatBytes(bytes int64) string {
if bytes < 1024 {
return fmt.Sprintf("%d B", bytes)
} else if bytes < 1024*1024 {
return fmt.Sprintf("%.1f KB", float64(bytes)/1024)
} else {
return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024))
}
}
// Helper function to create short error messages
func GetShortErrorMessage(errorMessage string) string {
if errorMessage == "" {
return "Unknown error"
}
errorLower := strings.ToLower(errorMessage)
if strings.Contains(errorLower, "timeout") {
return "Request timeout"
} else if strings.Contains(errorLower, "connection refused") {
return "Connection refused"
} else if strings.Contains(errorLower, "dns") || strings.Contains(errorLower, "no such host") {
return "DNS resolution failed"
} else if strings.Contains(errorLower, "certificate") || strings.Contains(errorLower, "ssl") || strings.Contains(errorLower, "tls") {
return "SSL certificate error"
} else if strings.Contains(errorLower, "server error") || strings.Contains(errorLower, "internal server error") {
return "Internal server error"
} else if strings.Contains(errorLower, "not found") {
return "Page not found"
} else if strings.Contains(errorLower, "unauthorized") {
return "Unauthorized access"
} else if strings.Contains(errorLower, "forbidden") {
return "Access forbidden"
}
// For other errors, take first 50 characters and clean it up
shortMsg := errorMessage
if len(shortMsg) > 50 {
shortMsg = shortMsg[:50] + "..."
}
return shortMsg
}
func GetStatusString(success bool) string {
if success {
return "up"
}
return "down"
}
func FormatResultDetails(result *types.OperationResult) string {
// This can be expanded based on operation type
if result.Details != "" {
return result.Details
}
switch result.Type {
case types.OperationPing:
if result.Success {
return fmt.Sprintf("Ping successful - %d packets sent, %d received", result.PacketsSent, result.PacketsRecv)
}
return fmt.Sprintf("Ping failed - %s", result.Error)
case types.OperationHTTP:
if result.Success {
return fmt.Sprintf("HTTP %d - Response time: %.2fms", result.HTTPStatusCode, float64(result.ResponseTime.Nanoseconds())/1000000)
}
return fmt.Sprintf("HTTP failed - %s", result.Error)
case types.OperationDNS:
if result.Success {
return fmt.Sprintf("DNS %s query successful - %d records found", result.DNSType, len(result.DNSRecords))
}
return fmt.Sprintf("DNS query failed - %s", result.Error)
default:
return "Operation completed"
}
}