Implement CheckCle Microservice Operation
A Go-based microservice for service operations including ICMP ping, DNS resolution, and TCP connectivity
This commit is contained in:
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# Port configuration
|
||||||
|
PORT=8091
|
||||||
|
|
||||||
|
# Operation defaults
|
||||||
|
DEFAULT_COUNT=4
|
||||||
|
DEFAULT_TIMEOUT=3s
|
||||||
|
MAX_COUNT=20
|
||||||
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
ENABLE_LOGGING=true
|
||||||
|
|
||||||
|
# PocketBase integration (no authentication required)
|
||||||
|
POCKETBASE_ENABLED=true
|
||||||
|
POCKETBASE_URL=http://localhost:8090
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
# Port configuration
|
||||||
|
PORT=8091
|
||||||
|
|
||||||
|
# Operation defaults
|
||||||
|
DEFAULT_COUNT=4
|
||||||
|
DEFAULT_TIMEOUT=3s
|
||||||
|
MAX_COUNT=20
|
||||||
|
MAX_TIMEOUT=30s
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
ENABLE_LOGGING=true
|
||||||
|
|
||||||
|
# PocketBase integration (no authentication required)
|
||||||
|
POCKETBASE_ENABLED=true
|
||||||
|
POCKETBASE_URL=https://pb-api.k8sops.asia
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
|
||||||
|
# CheckCle Microservice Operation
|
||||||
|
|
||||||
|
A Go-based microservice for service operations including ICMP ping, DNS resolution, and TCP connectivity testing.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **ICMP Ping**: Full ping functionality with packet statistics
|
||||||
|
- **DNS Resolution**: A, AAAA, MX, and TXT record lookups
|
||||||
|
- **TCP Connectivity**: Port connectivity testing
|
||||||
|
- REST API endpoints
|
||||||
|
- Health check endpoint
|
||||||
|
- Configurable via environment variables
|
||||||
|
- Docker support
|
||||||
|
- Comprehensive operation statistics
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### POST /operation
|
||||||
|
Perform various network operations (ping, dns, tcp).
|
||||||
|
|
||||||
|
**Ping Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping",
|
||||||
|
"host": "google.com",
|
||||||
|
"count": 4,
|
||||||
|
"timeout": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**DNS Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"host": "google.com",
|
||||||
|
"query": "A",
|
||||||
|
"timeout": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**TCP Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "tcp",
|
||||||
|
"host": "google.com",
|
||||||
|
"port": 443,
|
||||||
|
"timeout": 3
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "ping",
|
||||||
|
"host": "google.com",
|
||||||
|
"success": true,
|
||||||
|
"response_time": "20ms",
|
||||||
|
"packets_sent": 4,
|
||||||
|
"packets_recv": 4,
|
||||||
|
"packet_loss": 0,
|
||||||
|
"min_rtt": "15ms",
|
||||||
|
"max_rtt": "25ms",
|
||||||
|
"avg_rtt": "20ms",
|
||||||
|
"rtts": ["15ms", "20ms", "25ms", "18ms"],
|
||||||
|
"start_time": "2023-12-01T10:00:00Z",
|
||||||
|
"end_time": "2023-12-01T10:00:03Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /operation/quick
|
||||||
|
Quick operation test with query parameters.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
- `/operation/quick?type=ping&host=google.com&count=1`
|
||||||
|
- `/operation/quick?type=dns&host=google.com&query=A`
|
||||||
|
- `/operation/quick?type=tcp&host=google.com&port=443`
|
||||||
|
|
||||||
|
### GET /health
|
||||||
|
Health check endpoint.
|
||||||
|
|
||||||
|
### Legacy Endpoints
|
||||||
|
- `POST /ping` - Legacy ping endpoint (backward compatibility)
|
||||||
|
- `GET /ping/quick` - Legacy quick ping endpoint
|
||||||
|
|
||||||
|
## Operation Types
|
||||||
|
|
||||||
|
### Ping (ICMP)
|
||||||
|
- **Type**: `ping`
|
||||||
|
- **Parameters**: `host`, `count`, `timeout`
|
||||||
|
- **Features**: Packet loss calculation, RTT statistics, multiple packets
|
||||||
|
|
||||||
|
### DNS Resolution
|
||||||
|
- **Type**: `dns`
|
||||||
|
- **Parameters**: `host`, `query` (A, AAAA, MX, TXT), `timeout`
|
||||||
|
- **Features**: Multiple record types, resolution time tracking
|
||||||
|
|
||||||
|
### TCP Connectivity
|
||||||
|
- **Type**: `tcp`
|
||||||
|
- **Parameters**: `host`, `port`, `timeout`
|
||||||
|
- **Features**: Connection testing, response time measurement
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
- `PORT` - Service port (default: 8080)
|
||||||
|
- `DEFAULT_COUNT` - Default ping count (default: 4)
|
||||||
|
- `DEFAULT_TIMEOUT` - Default timeout (default: 3s)
|
||||||
|
- `MAX_COUNT` - Maximum ping count (default: 20)
|
||||||
|
- `MAX_TIMEOUT` - Maximum timeout (default: 30s)
|
||||||
|
- `ENABLE_LOGGING` - Enable logging (default: true)
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
### Local Development
|
||||||
|
```bash
|
||||||
|
go run main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
```bash
|
||||||
|
docker build -t service-operation .
|
||||||
|
docker run -p 8080:8080 service-operation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Go 1.21+
|
||||||
|
- Root privileges for ICMP (on Linux)
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
This service requires elevated privileges to send ICMP packets. Run with sudo on Linux systems or use capabilities:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo setcap cap_net_raw=+ep ./service-operation
|
||||||
|
```
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
DefaultCount int
|
||||||
|
DefaultTimeout time.Duration
|
||||||
|
MaxCount int
|
||||||
|
MaxTimeout time.Duration
|
||||||
|
EnableLogging bool
|
||||||
|
|
||||||
|
// PocketBase configuration (no auth required)
|
||||||
|
PocketBaseEnabled bool
|
||||||
|
PocketBaseURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
cfg := &Config{
|
||||||
|
Port: getEnv("PORT", "8091"),
|
||||||
|
DefaultCount: getEnvInt("DEFAULT_COUNT", 4),
|
||||||
|
DefaultTimeout: getEnvDuration("DEFAULT_TIMEOUT", 3*time.Second),
|
||||||
|
MaxCount: getEnvInt("MAX_COUNT", 20),
|
||||||
|
MaxTimeout: getEnvDuration("MAX_TIMEOUT", 30*time.Second),
|
||||||
|
EnableLogging: getEnvBool("ENABLE_LOGGING", true),
|
||||||
|
|
||||||
|
// PocketBase settings (no credentials needed)
|
||||||
|
PocketBaseEnabled: getEnvBool("POCKETBASE_ENABLED", true),
|
||||||
|
PocketBaseURL: getEnv("POCKETBASE_URL", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if intValue, err := strconv.Atoi(value); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if duration, err := time.ParseDuration(value); err == nil {
|
||||||
|
return duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvBool(key string, defaultValue bool) bool {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
if boolValue, err := strconv.ParseBool(value); err == nil {
|
||||||
|
return boolValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
module service-operation
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gorilla/mux v1.8.1
|
||||||
|
golang.org/x/net v0.17.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.13.0 // indirect
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||||
|
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||||
|
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||||
|
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *OperationHandler) HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||||
|
health := map[string]interface{}{
|
||||||
|
"status": "healthy",
|
||||||
|
"service": "service-operation",
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
"version": "1.0.0",
|
||||||
|
"operations": []string{"ping", "dns", "tcp", "http"},
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(health)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"service-operation/shared/savers"
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *OperationHandler) saveMetricsToPocketBase(result *types.OperationResult, serviceID string) {
|
||||||
|
metricsSaver := savers.NewMetricsSaver(h.pbClient)
|
||||||
|
metricsSaver.SaveMetricsToPocketBase(result, serviceID)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"service-operation/config"
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OperationHandler struct {
|
||||||
|
config *config.Config
|
||||||
|
pbClient *pocketbase.PocketBaseClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOperationHandler(cfg *config.Config, pbClient *pocketbase.PocketBaseClient) *OperationHandler {
|
||||||
|
return &OperationHandler{
|
||||||
|
config: cfg,
|
||||||
|
pbClient: pbClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-operation/operations"
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *OperationHandler) HandleOperation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req types.OperationRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Host == "" && req.URL == "" {
|
||||||
|
http.Error(w, "Host or URL is required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set defaults
|
||||||
|
if req.Count <= 0 {
|
||||||
|
req.Count = h.config.DefaultCount
|
||||||
|
}
|
||||||
|
if req.Count > h.config.MaxCount {
|
||||||
|
req.Count = h.config.MaxCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Timeout <= 0 {
|
||||||
|
req.Timeout = int(h.config.DefaultTimeout.Seconds())
|
||||||
|
}
|
||||||
|
if time.Duration(req.Timeout)*time.Second > h.config.MaxTimeout {
|
||||||
|
req.Timeout = int(h.config.MaxTimeout.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := time.Duration(req.Timeout) * time.Second
|
||||||
|
var result *types.OperationResult
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch req.Type {
|
||||||
|
case types.OperationPing:
|
||||||
|
pingOp := operations.NewPingOperation(timeout)
|
||||||
|
result, err = pingOp.Execute(req.Host, req.Count)
|
||||||
|
|
||||||
|
case types.OperationDNS:
|
||||||
|
dnsOp := operations.NewDNSOperation(timeout)
|
||||||
|
query := req.Query
|
||||||
|
if query == "" {
|
||||||
|
query = "A"
|
||||||
|
}
|
||||||
|
result, err = dnsOp.Execute(req.Host, query)
|
||||||
|
|
||||||
|
case types.OperationTCP:
|
||||||
|
if req.Port <= 0 {
|
||||||
|
http.Error(w, "Port is required for TCP operations", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tcpOp := operations.NewTCPOperation(timeout)
|
||||||
|
result, err = tcpOp.Execute(req.Host, req.Port)
|
||||||
|
|
||||||
|
case types.OperationHTTP:
|
||||||
|
httpOp := operations.NewHTTPOperation(timeout)
|
||||||
|
url := req.URL
|
||||||
|
if url == "" {
|
||||||
|
url = req.Host
|
||||||
|
}
|
||||||
|
method := req.Method
|
||||||
|
if method == "" {
|
||||||
|
method = "GET"
|
||||||
|
}
|
||||||
|
result, err = httpOp.Execute(url, method)
|
||||||
|
|
||||||
|
default:
|
||||||
|
http.Error(w, "Invalid operation type", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result = &types.OperationResult{
|
||||||
|
Type: req.Type,
|
||||||
|
Host: req.Host,
|
||||||
|
Port: req.Port,
|
||||||
|
Success: false,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save metrics to PocketBase if available
|
||||||
|
if h.pbClient != nil && h.pbClient.IsAuthenticated() {
|
||||||
|
go h.saveMetricsToPocketBase(result, req.ServiceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (h *OperationHandler) HandleQuickOperation(w http.ResponseWriter, r *http.Request) {
|
||||||
|
opType := r.URL.Query().Get("type")
|
||||||
|
host := r.URL.Query().Get("host")
|
||||||
|
|
||||||
|
if host == "" || opType == "" {
|
||||||
|
http.Error(w, "Type and host parameters are required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req := types.OperationRequest{
|
||||||
|
Type: types.OperationType(opType),
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse optional parameters
|
||||||
|
if countStr := r.URL.Query().Get("count"); countStr != "" {
|
||||||
|
if c, err := strconv.Atoi(countStr); err == nil && c > 0 && c <= h.config.MaxCount {
|
||||||
|
req.Count = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if portStr := r.URL.Query().Get("port"); portStr != "" {
|
||||||
|
if p, err := strconv.Atoi(portStr); err == nil && p > 0 && p <= 65535 {
|
||||||
|
req.Port = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if query := r.URL.Query().Get("query"); query != "" {
|
||||||
|
req.Query = query
|
||||||
|
}
|
||||||
|
|
||||||
|
if url := r.URL.Query().Get("url"); url != "" {
|
||||||
|
req.URL = url
|
||||||
|
}
|
||||||
|
|
||||||
|
if method := r.URL.Query().Get("method"); method != "" {
|
||||||
|
req.Method = method
|
||||||
|
}
|
||||||
|
|
||||||
|
if serviceID := r.URL.Query().Get("service_id"); serviceID != "" {
|
||||||
|
req.ServiceID = serviceID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward to main handler
|
||||||
|
reqBody, _ := json.Marshal(req)
|
||||||
|
r.Body = http.NoBody
|
||||||
|
r.Method = http.MethodPost
|
||||||
|
r.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Create a new request with the body
|
||||||
|
newReq := r.Clone(r.Context())
|
||||||
|
newReq.Body = http.NoBody
|
||||||
|
newReq.ContentLength = int64(len(reqBody))
|
||||||
|
|
||||||
|
h.HandleOperation(w, newReq)
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getStatusString(success bool) string {
|
||||||
|
if success {
|
||||||
|
return "up"
|
||||||
|
}
|
||||||
|
return "down"
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatResultDetails(result *types.OperationResult) string {
|
||||||
|
details := map[string]interface{}{
|
||||||
|
"type": result.Type,
|
||||||
|
"response_time": result.ResponseTime,
|
||||||
|
"start_time": result.StartTime,
|
||||||
|
"end_time": result.EndTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add type-specific details
|
||||||
|
switch result.Type {
|
||||||
|
case types.OperationPing:
|
||||||
|
details["packets_sent"] = result.PacketsSent
|
||||||
|
details["packets_recv"] = result.PacketsRecv
|
||||||
|
details["packet_loss"] = result.PacketLoss
|
||||||
|
details["avg_rtt"] = result.AvgRTT
|
||||||
|
case types.OperationHTTP:
|
||||||
|
details["status_code"] = result.HTTPStatusCode
|
||||||
|
details["method"] = result.HTTPMethod
|
||||||
|
details["content_length"] = result.ContentLength
|
||||||
|
case types.OperationTCP:
|
||||||
|
details["tcp_connected"] = result.TCPConnected
|
||||||
|
case types.OperationDNS:
|
||||||
|
details["dns_records"] = result.DNSRecords
|
||||||
|
details["dns_type"] = result.DNSType
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, _ := json.Marshal(details)
|
||||||
|
return string(jsonData)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"service-operation/config"
|
||||||
|
"service-operation/handlers"
|
||||||
|
"service-operation/monitoring"
|
||||||
|
"service-operation/pocketbase"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
// Initialize PocketBase client (no credentials required)
|
||||||
|
var pbClient *pocketbase.PocketBaseClient
|
||||||
|
var monitoringService *monitoring.MonitoringService
|
||||||
|
|
||||||
|
if cfg.PocketBaseEnabled {
|
||||||
|
var err error
|
||||||
|
pbClient, err = pocketbase.NewPocketBaseClient(cfg.PocketBaseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Warning: Failed to initialize PocketBase client: %v", err)
|
||||||
|
} else {
|
||||||
|
if err := pbClient.TestConnection(); err != nil {
|
||||||
|
log.Printf("Warning: PocketBase connection test failed: %v", err)
|
||||||
|
} else {
|
||||||
|
// Initialize and start monitoring service
|
||||||
|
monitoringService = monitoring.NewMonitoringService(pbClient)
|
||||||
|
go monitoringService.Start()
|
||||||
|
log.Println("Monitoring service started (public access mode)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := handlers.NewOperationHandler(cfg, pbClient)
|
||||||
|
|
||||||
|
router := mux.NewRouter()
|
||||||
|
|
||||||
|
// Main operation endpoint
|
||||||
|
router.HandleFunc("/operation", handler.HandleOperation).Methods("POST")
|
||||||
|
|
||||||
|
// Quick operation endpoint with query parameters
|
||||||
|
router.HandleFunc("/operation/quick", handler.HandleQuickOperation).Methods("GET")
|
||||||
|
|
||||||
|
// Legacy ping endpoint for backward compatibility
|
||||||
|
router.HandleFunc("/ping", handler.HandleOperation).Methods("POST")
|
||||||
|
router.HandleFunc("/ping/quick", handler.HandleQuickOperation).Methods("GET")
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
router.HandleFunc("/health", handler.HandleHealth).Methods("GET")
|
||||||
|
|
||||||
|
log.Printf("Service Operation starting on port %s", cfg.Port)
|
||||||
|
if pbClient != nil {
|
||||||
|
log.Printf("PocketBase integration enabled at %s (public access)", pbClient.GetBaseURL())
|
||||||
|
}
|
||||||
|
if monitoringService != nil {
|
||||||
|
log.Printf("Automatic service monitoring enabled")
|
||||||
|
}
|
||||||
|
log.Printf("Endpoints:")
|
||||||
|
log.Printf(" POST /operation - Full operation test (ping, dns, tcp, http)")
|
||||||
|
log.Printf(" GET /operation/quick?type=<type>&host=<host> - Quick operation test")
|
||||||
|
log.Printf(" POST /ping - Legacy ping endpoint")
|
||||||
|
log.Printf(" GET /ping/quick?host=<host> - Legacy quick ping test")
|
||||||
|
log.Printf(" GET /health - Health check")
|
||||||
|
log.Printf("Supported operations: ping, dns, tcp, http")
|
||||||
|
|
||||||
|
// Setup graceful shutdown
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
log.Println("Shutting down monitoring service...")
|
||||||
|
if monitoringService != nil {
|
||||||
|
monitoringService.Stop()
|
||||||
|
}
|
||||||
|
log.Println("Service stopped")
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := http.ListenAndServe(":"+cfg.Port, router); err != nil {
|
||||||
|
log.Fatal("Failed to start server:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
package operations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSOperation struct {
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDNSOperation(timeout time.Duration) *DNSOperation {
|
||||||
|
return &DNSOperation{timeout: timeout}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) Execute(host, query string) (*types.OperationResult, error) {
|
||||||
|
// Validate inputs
|
||||||
|
if host == "" {
|
||||||
|
return nil, fmt.Errorf("host cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if query == "" {
|
||||||
|
query = "A" // Default to A record
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &types.OperationResult{
|
||||||
|
Type: types.OperationDNS,
|
||||||
|
Host: host,
|
||||||
|
DNSType: query,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
// Resolve the host first to get detailed info
|
||||||
|
var resolvedIPs []string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch strings.ToUpper(query) {
|
||||||
|
case "A":
|
||||||
|
resolvedIPs, err = d.performARecordLookup(host)
|
||||||
|
|
||||||
|
case "AAAA":
|
||||||
|
resolvedIPs, err = d.performAAAARecordLookup(host)
|
||||||
|
|
||||||
|
case "MX":
|
||||||
|
resolvedIPs, err = d.performMXRecordLookup(host)
|
||||||
|
|
||||||
|
case "TXT":
|
||||||
|
resolvedIPs, err = d.performTXTRecordLookup(host)
|
||||||
|
|
||||||
|
case "CNAME":
|
||||||
|
resolvedIPs, err = d.performCNAMERecordLookup(host)
|
||||||
|
|
||||||
|
case "NS":
|
||||||
|
resolvedIPs, err = d.performNSRecordLookup(host)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Default to A record lookup for unknown types
|
||||||
|
resolvedIPs, err = d.performARecordLookup(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.ResponseTime = time.Since(start)
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
result.Success = false
|
||||||
|
result.Details = d.createDetailedErrorMessage(err.Error(), host, query)
|
||||||
|
} else {
|
||||||
|
result.DNSRecords = resolvedIPs
|
||||||
|
result.Success = len(resolvedIPs) > 0
|
||||||
|
|
||||||
|
if result.Success {
|
||||||
|
result.Details = d.createDetailedSuccessMessage(result, host, query, resolvedIPs)
|
||||||
|
} else {
|
||||||
|
result.Error = "No DNS records found"
|
||||||
|
result.Details = d.createDetailedErrorMessage("no records found", host, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) performARecordLookup(host string) ([]string, error) {
|
||||||
|
ips, err := net.LookupIP(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipv4Records []string
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
|
ipv4Records = append(ipv4Records, ipv4.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipv4Records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) performAAAARecordLookup(host string) ([]string, error) {
|
||||||
|
ips, err := net.LookupIP(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipv6Records []string
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ipv6 := ip.To16(); ipv6 != nil && ip.To4() == nil {
|
||||||
|
ipv6Records = append(ipv6Records, ipv6.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipv6Records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) performMXRecordLookup(host string) ([]string, error) {
|
||||||
|
mxRecords, err := net.LookupMX(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []string
|
||||||
|
for _, mx := range mxRecords {
|
||||||
|
records = append(records, fmt.Sprintf("%s (priority: %d)", mx.Host, mx.Pref))
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) performTXTRecordLookup(host string) ([]string, error) {
|
||||||
|
txtRecords, err := net.LookupTXT(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return txtRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) performCNAMERecordLookup(host string) ([]string, error) {
|
||||||
|
cname, err := net.LookupCNAME(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{cname}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) performNSRecordLookup(host string) ([]string, error) {
|
||||||
|
nsRecords, err := net.LookupNS(host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var records []string
|
||||||
|
for _, ns := range nsRecords {
|
||||||
|
records = append(records, ns.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) createDetailedSuccessMessage(result *types.OperationResult, host, queryType string, records []string) string {
|
||||||
|
var details strings.Builder
|
||||||
|
|
||||||
|
// Success indicator with basic info
|
||||||
|
details.WriteString(fmt.Sprintf("🟢 DNS SUCCESS - %s query for %s",
|
||||||
|
strings.ToUpper(queryType), host))
|
||||||
|
|
||||||
|
// Response time
|
||||||
|
details.WriteString(fmt.Sprintf(" | Response time: %.2fms",
|
||||||
|
float64(result.ResponseTime.Nanoseconds())/1000000))
|
||||||
|
|
||||||
|
// Record count
|
||||||
|
details.WriteString(fmt.Sprintf(" | Records found: %d", len(records)))
|
||||||
|
|
||||||
|
// Show first few records for context
|
||||||
|
if len(records) > 0 {
|
||||||
|
details.WriteString(" | ")
|
||||||
|
if len(records) <= 3 {
|
||||||
|
details.WriteString(fmt.Sprintf("Results: %s", strings.Join(records, ", ")))
|
||||||
|
} else {
|
||||||
|
details.WriteString(fmt.Sprintf("Results: %s... (+%d more)",
|
||||||
|
strings.Join(records[:3], ", "), len(records)-3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) createDetailedErrorMessage(errorMsg, host, queryType string) string {
|
||||||
|
var details strings.Builder
|
||||||
|
|
||||||
|
errorLower := strings.ToLower(errorMsg)
|
||||||
|
|
||||||
|
if strings.Contains(errorLower, "timeout") {
|
||||||
|
details.WriteString("⏱️ DNS TIMEOUT - Query timed out")
|
||||||
|
} else if strings.Contains(errorLower, "no such host") || strings.Contains(errorLower, "host not found") {
|
||||||
|
details.WriteString("🔍 HOST NOT FOUND - DNS resolution failed")
|
||||||
|
} else if strings.Contains(errorLower, "no answer") || strings.Contains(errorLower, "no records found") {
|
||||||
|
details.WriteString("📝 NO RECORDS - No DNS records of requested type")
|
||||||
|
} else if strings.Contains(errorLower, "server failure") {
|
||||||
|
details.WriteString("🔧 SERVER FAILURE - DNS server error")
|
||||||
|
} else if strings.Contains(errorLower, "refused") {
|
||||||
|
details.WriteString("🚫 QUERY REFUSED - DNS server refused query")
|
||||||
|
} else {
|
||||||
|
details.WriteString("❌ DNS FAILED - Query error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add query details
|
||||||
|
details.WriteString(fmt.Sprintf(" | Query: %s %s",
|
||||||
|
strings.ToUpper(queryType), host))
|
||||||
|
|
||||||
|
// Add specific error details
|
||||||
|
if errorMsg != "" {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Error: %s", d.getShortErrorMessage(errorMsg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DNSOperation) getShortErrorMessage(errorMessage string) string {
|
||||||
|
if errorMessage == "" {
|
||||||
|
return "Unknown error"
|
||||||
|
}
|
||||||
|
|
||||||
|
errorLower := strings.ToLower(errorMessage)
|
||||||
|
|
||||||
|
if strings.Contains(errorLower, "timeout") {
|
||||||
|
return "Query timeout"
|
||||||
|
} else if strings.Contains(errorLower, "no such host") {
|
||||||
|
return "Host not found"
|
||||||
|
} else if strings.Contains(errorLower, "no answer") {
|
||||||
|
return "No records found"
|
||||||
|
} else if strings.Contains(errorLower, "server failure") {
|
||||||
|
return "DNS server failure"
|
||||||
|
} else if strings.Contains(errorLower, "refused") {
|
||||||
|
return "Query refused"
|
||||||
|
} else if strings.Contains(errorLower, "network unreachable") {
|
||||||
|
return "Network unreachable"
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, take first 50 characters and clean it up
|
||||||
|
shortMsg := errorMessage
|
||||||
|
if len(shortMsg) > 50 {
|
||||||
|
shortMsg = shortMsg[:50] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortMsg
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
|
||||||
|
package operations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HTTPOperation struct {
|
||||||
|
timeout time.Duration
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPOperation(timeout time.Duration) *HTTPOperation {
|
||||||
|
return &HTTPOperation{
|
||||||
|
timeout: timeout,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: timeout,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPOperation) Execute(url, method string) (*types.OperationResult, error) {
|
||||||
|
result := &types.OperationResult{
|
||||||
|
Type: types.OperationHTTP,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
HTTPMethod: method,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to GET if no method specified
|
||||||
|
if method == "" {
|
||||||
|
method = "GET"
|
||||||
|
result.HTTPMethod = "GET"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure URL has protocol
|
||||||
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
|
url = "https://" + url
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(method, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = fmt.Sprintf("Failed to create request: %v", err)
|
||||||
|
result.Success = false
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a user agent
|
||||||
|
req.Header.Set("User-Agent", "ServiceOperation/1.0")
|
||||||
|
|
||||||
|
resp, err := h.client.Do(req)
|
||||||
|
|
||||||
|
result.ResponseTime = time.Since(start)
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// More detailed error messages
|
||||||
|
if strings.Contains(err.Error(), "timeout") {
|
||||||
|
result.Error = fmt.Sprintf("🕐 Request timeout after %.2fs - Server did not respond within the expected time", h.timeout.Seconds())
|
||||||
|
} else if strings.Contains(err.Error(), "connection refused") {
|
||||||
|
result.Error = "🚫 Connection refused - Server is not accepting connections on this port"
|
||||||
|
} else if strings.Contains(err.Error(), "no such host") {
|
||||||
|
result.Error = "🌐 DNS resolution failed - Host not found"
|
||||||
|
} else if strings.Contains(err.Error(), "certificate") {
|
||||||
|
result.Error = "🔒 SSL/TLS certificate error - Certificate verification failed"
|
||||||
|
} else {
|
||||||
|
result.Error = fmt.Sprintf("🔌 Connection error: %v", err)
|
||||||
|
}
|
||||||
|
result.Success = false
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
result.HTTPStatusCode = resp.StatusCode
|
||||||
|
result.ContentLength = resp.ContentLength
|
||||||
|
result.Success = resp.StatusCode >= 200 && resp.StatusCode < 400
|
||||||
|
|
||||||
|
// Capture important headers
|
||||||
|
result.HTTPHeaders = make(map[string]string)
|
||||||
|
for key, values := range resp.Header {
|
||||||
|
if len(values) > 0 {
|
||||||
|
switch strings.ToLower(key) {
|
||||||
|
case "content-type", "server", "cache-control", "content-encoding", "x-powered-by":
|
||||||
|
result.HTTPHeaders[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response body for keyword checking and additional details
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err == nil && len(body) > 0 {
|
||||||
|
result.ResponseBody = string(body)
|
||||||
|
// Update content length if not set by server
|
||||||
|
if result.ContentLength <= 0 {
|
||||||
|
result.ContentLength = int64(len(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create detailed status message with emoji
|
||||||
|
if !result.Success {
|
||||||
|
switch {
|
||||||
|
case resp.StatusCode >= 500:
|
||||||
|
result.Error = fmt.Sprintf("🔥 Server Error (HTTP %d): %s - The server encountered an internal error", resp.StatusCode, resp.Status)
|
||||||
|
case resp.StatusCode >= 400:
|
||||||
|
result.Error = fmt.Sprintf("❌ Client Error (HTTP %d): %s - The request was invalid or unauthorized", resp.StatusCode, resp.Status)
|
||||||
|
case resp.StatusCode >= 300:
|
||||||
|
result.Error = fmt.Sprintf("↩️ Redirect (HTTP %d): %s - Resource has moved", resp.StatusCode, resp.Status)
|
||||||
|
default:
|
||||||
|
result.Error = fmt.Sprintf("⚠️ Unexpected Status (HTTP %d): %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Success message with emoji
|
||||||
|
switch resp.StatusCode {
|
||||||
|
case 200:
|
||||||
|
result.Error = "✅ OK - Request successful"
|
||||||
|
case 201:
|
||||||
|
result.Error = "🆕 Created - Resource created successfully"
|
||||||
|
case 202:
|
||||||
|
result.Error = "⏳ Accepted - Request accepted for processing"
|
||||||
|
case 204:
|
||||||
|
result.Error = "📭 No Content - Request successful, no content returned"
|
||||||
|
default:
|
||||||
|
result.Error = fmt.Sprintf("✅ Success (HTTP %d): %s", resp.StatusCode, resp.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
package operations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PingOperation struct {
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPingOperation(timeout time.Duration) *PingOperation {
|
||||||
|
return &PingOperation{timeout: timeout}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) Execute(host string, count int) (*types.OperationResult, error) {
|
||||||
|
// Validate host/IP
|
||||||
|
if host == "" {
|
||||||
|
return nil, fmt.Errorf("host cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always try system ping first for better reliability
|
||||||
|
result, err := p.executeSystemPing(host, count)
|
||||||
|
if err == nil && result.PacketsRecv > 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If system ping fails, try raw ICMP as fallback
|
||||||
|
fmt.Printf("System ping failed (%v), trying raw ICMP\n", err)
|
||||||
|
rawResult, rawErr := p.executeRawICMP(host, count)
|
||||||
|
if rawErr == nil && rawResult.PacketsRecv > 0 {
|
||||||
|
return rawResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both fail, return the system ping result with error info
|
||||||
|
if result != nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("both system ping and raw ICMP failed: system_err=%v, raw_err=%v", err, rawErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) executeSystemPing(host string, count int) (*types.OperationResult, error) {
|
||||||
|
result := &types.OperationResult{
|
||||||
|
Type: types.OperationPing,
|
||||||
|
Host: host,
|
||||||
|
PacketsSent: count,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve host to get IP address for better details
|
||||||
|
ips, err := net.LookupIP(host)
|
||||||
|
var resolvedIP string
|
||||||
|
if err == nil && len(ips) > 0 {
|
||||||
|
resolvedIP = ips[0].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build ping command based on OS
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
timeoutSeconds := int(p.timeout.Seconds())
|
||||||
|
if timeoutSeconds < 1 {
|
||||||
|
timeoutSeconds = 10 // Minimum 10 seconds timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
// Linux ping: -c count -W timeout_in_seconds
|
||||||
|
cmd = exec.Command("ping", "-c", fmt.Sprintf("%d", count), "-W", fmt.Sprintf("%d", timeoutSeconds), host)
|
||||||
|
case "darwin":
|
||||||
|
// macOS ping: -c count -W timeout_in_milliseconds
|
||||||
|
cmd = exec.Command("ping", "-c", fmt.Sprintf("%d", count), "-W", fmt.Sprintf("%d", timeoutSeconds*1000), host)
|
||||||
|
case "windows":
|
||||||
|
// Windows ping: -n count -w timeout_in_milliseconds
|
||||||
|
cmd = exec.Command("ping", "-n", fmt.Sprintf("%d", count), "-w", fmt.Sprintf("%d", timeoutSeconds*1000), host)
|
||||||
|
default:
|
||||||
|
// Default to Linux-style
|
||||||
|
cmd = exec.Command("ping", "-c", fmt.Sprintf("%d", count), "-W", fmt.Sprintf("%d", timeoutSeconds), host)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set command timeout slightly longer than ping timeout
|
||||||
|
cmdTimeout := time.Duration(timeoutSeconds+5) * time.Second
|
||||||
|
done := make(chan error, 1)
|
||||||
|
var output []byte
|
||||||
|
var cmdErr error
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
output, cmdErr = cmd.Output()
|
||||||
|
done <- cmdErr
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// Even if command fails, try to parse partial output
|
||||||
|
result.PacketLoss = 100.0
|
||||||
|
if len(output) > 0 {
|
||||||
|
p.parseSystemPingOutput(result, string(output), count)
|
||||||
|
}
|
||||||
|
// Set detailed error information
|
||||||
|
result.Error = p.createDetailedErrorMessage(err.Error(), host, resolvedIP)
|
||||||
|
return result, nil // Don't return error, return result with failure info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse successful output
|
||||||
|
if len(output) > 0 {
|
||||||
|
p.parseSystemPingOutput(result, string(output), count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create detailed success message
|
||||||
|
if result.Success {
|
||||||
|
result.Details = p.createDetailedSuccessMessage(result, host, resolvedIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
|
||||||
|
case <-time.After(cmdTimeout):
|
||||||
|
// Command timed out
|
||||||
|
if cmd.Process != nil {
|
||||||
|
cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
result.PacketLoss = 100.0
|
||||||
|
result.Error = fmt.Sprintf("Ping request timed out after %v", cmdTimeout)
|
||||||
|
result.Details = p.createDetailedErrorMessage("timeout", host, resolvedIP)
|
||||||
|
return result, fmt.Errorf("ping command timed out after %v", cmdTimeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) createDetailedSuccessMessage(result *types.OperationResult, host, resolvedIP string) string {
|
||||||
|
var details strings.Builder
|
||||||
|
|
||||||
|
// Success indicator with basic info
|
||||||
|
details.WriteString(fmt.Sprintf("🟢 PING SUCCESS - %d/%d packets received",
|
||||||
|
result.PacketsRecv, result.PacketsSent))
|
||||||
|
|
||||||
|
// Host and IP information
|
||||||
|
if resolvedIP != "" && resolvedIP != host {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Host: %s (%s)", host, resolvedIP))
|
||||||
|
} else {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Host: %s", host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timing information
|
||||||
|
if result.AvgRTT > 0 {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Avg RTT: %.2fms",
|
||||||
|
float64(result.AvgRTT.Nanoseconds())/1000000))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.MinRTT > 0 && result.MaxRTT > 0 {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Range: %.2f-%.2fms",
|
||||||
|
float64(result.MinRTT.Nanoseconds())/1000000,
|
||||||
|
float64(result.MaxRTT.Nanoseconds())/1000000))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Packet loss information
|
||||||
|
if result.PacketLoss > 0 {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Packet Loss: %.1f%%", result.PacketLoss))
|
||||||
|
} else {
|
||||||
|
details.WriteString(" | No packet loss")
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) createDetailedErrorMessage(errorMsg, host, resolvedIP string) string {
|
||||||
|
var details strings.Builder
|
||||||
|
|
||||||
|
errorLower := strings.ToLower(errorMsg)
|
||||||
|
|
||||||
|
if strings.Contains(errorLower, "timeout") {
|
||||||
|
details.WriteString("⏱️ PING TIMEOUT - Host did not respond within timeout period")
|
||||||
|
} else if strings.Contains(errorLower, "unreachable") {
|
||||||
|
details.WriteString("🚫 HOST UNREACHABLE - Network path to host is blocked")
|
||||||
|
} else if strings.Contains(errorLower, "no route") {
|
||||||
|
details.WriteString("🛤️ NO ROUTE - No network route to destination")
|
||||||
|
} else if strings.Contains(errorLower, "name resolution") || strings.Contains(errorLower, "unknown host") {
|
||||||
|
details.WriteString("🔍 DNS RESOLUTION FAILED - Unable to resolve hostname")
|
||||||
|
} else if strings.Contains(errorLower, "permission denied") {
|
||||||
|
details.WriteString("🔐 PERMISSION DENIED - Insufficient privileges for ICMP")
|
||||||
|
} else {
|
||||||
|
details.WriteString("❌ PING FAILED - Connection error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add host information
|
||||||
|
if resolvedIP != "" && resolvedIP != host {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Target: %s (%s)", host, resolvedIP))
|
||||||
|
} else {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Target: %s", host))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add specific error details
|
||||||
|
if errorMsg != "" {
|
||||||
|
details.WriteString(fmt.Sprintf(" | Error: %s", p.getShortErrorMessage(errorMsg)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) 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, "unreachable") {
|
||||||
|
return "Host unreachable"
|
||||||
|
} else if strings.Contains(errorLower, "no route") {
|
||||||
|
return "No route to host"
|
||||||
|
} else if strings.Contains(errorLower, "name resolution") || strings.Contains(errorLower, "unknown host") {
|
||||||
|
return "DNS resolution failed"
|
||||||
|
} else if strings.Contains(errorLower, "permission denied") {
|
||||||
|
return "Permission denied"
|
||||||
|
} else if strings.Contains(errorLower, "network is down") {
|
||||||
|
return "Network is down"
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, take first 50 characters and clean it up
|
||||||
|
shortMsg := errorMessage
|
||||||
|
if len(shortMsg) > 50 {
|
||||||
|
shortMsg = shortMsg[:50] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) parseSystemPingOutput(result *types.OperationResult, output string, expectedCount int) {
|
||||||
|
// Extract packet loss
|
||||||
|
lossRegex := regexp.MustCompile(`(\d+(?:\.\d+)?)%.*packet.*loss`)
|
||||||
|
if matches := lossRegex.FindStringSubmatch(output); len(matches) > 1 {
|
||||||
|
if loss, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||||
|
result.PacketLoss = loss
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract timing information (Linux/macOS format)
|
||||||
|
timingRegex := regexp.MustCompile(`rtt min/avg/max/(?:mdev|stddev) = ([\d.]+)/([\d.]+)/([\d.]+)/([\d.]+) ms`)
|
||||||
|
if matches := timingRegex.FindStringSubmatch(output); len(matches) > 4 {
|
||||||
|
if min, err := strconv.ParseFloat(matches[1], 64); err == nil {
|
||||||
|
result.MinRTT = time.Duration(min * float64(time.Millisecond))
|
||||||
|
}
|
||||||
|
if avg, err := strconv.ParseFloat(matches[2], 64); err == nil {
|
||||||
|
result.AvgRTT = time.Duration(avg * float64(time.Millisecond))
|
||||||
|
result.ResponseTime = result.AvgRTT
|
||||||
|
}
|
||||||
|
if max, err := strconv.ParseFloat(matches[3], 64); err == nil {
|
||||||
|
result.MaxRTT = time.Duration(max * float64(time.Millisecond))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract individual ping times and count successful pings
|
||||||
|
timeRegex := regexp.MustCompile(`time[<=]([\d.]+) ?ms`)
|
||||||
|
timeMatches := timeRegex.FindAllStringSubmatch(output, -1)
|
||||||
|
result.PacketsRecv = len(timeMatches)
|
||||||
|
|
||||||
|
for _, match := range timeMatches {
|
||||||
|
if len(match) > 1 {
|
||||||
|
if t, err := strconv.ParseFloat(match[1], 64); err == nil {
|
||||||
|
rtt := time.Duration(t * float64(time.Millisecond))
|
||||||
|
result.RTTs = append(result.RTTs, rtt)
|
||||||
|
|
||||||
|
// Update min/max if not set by summary stats
|
||||||
|
if result.MinRTT == 0 || rtt < result.MinRTT {
|
||||||
|
result.MinRTT = rtt
|
||||||
|
}
|
||||||
|
if result.MaxRTT == 0 || rtt > result.MaxRTT {
|
||||||
|
result.MaxRTT = rtt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate average RTT if not set by summary stats
|
||||||
|
if result.AvgRTT == 0 && len(result.RTTs) > 0 {
|
||||||
|
var total time.Duration
|
||||||
|
for _, rtt := range result.RTTs {
|
||||||
|
total += rtt
|
||||||
|
}
|
||||||
|
result.AvgRTT = total / time.Duration(len(result.RTTs))
|
||||||
|
result.ResponseTime = result.AvgRTT
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate packet loss if we have individual pings
|
||||||
|
if expectedCount > 0 {
|
||||||
|
result.PacketLoss = float64(expectedCount-result.PacketsRecv) / float64(expectedCount) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as successful if we got any responses
|
||||||
|
if result.PacketsRecv > 0 {
|
||||||
|
result.Success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Windows output format if Linux/macOS parsing didn't work
|
||||||
|
if result.PacketsRecv == 0 && strings.Contains(strings.ToLower(output), "reply from") {
|
||||||
|
p.parseWindowsPingOutput(result, output, expectedCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) parseWindowsPingOutput(result *types.OperationResult, output string, expectedCount int) {
|
||||||
|
// Windows: "Reply from 1.1.1.1: bytes=32 time=14ms TTL=56"
|
||||||
|
timeRegex := regexp.MustCompile(`time[<=]([\d.]+)ms`)
|
||||||
|
timeMatches := timeRegex.FindAllStringSubmatch(output, -1)
|
||||||
|
result.PacketsRecv = len(timeMatches)
|
||||||
|
|
||||||
|
for _, match := range timeMatches {
|
||||||
|
if len(match) > 1 {
|
||||||
|
if t, err := strconv.ParseFloat(match[1], 64); err == nil {
|
||||||
|
rtt := time.Duration(t * float64(time.Millisecond))
|
||||||
|
result.RTTs = append(result.RTTs, rtt)
|
||||||
|
|
||||||
|
if result.MinRTT == 0 || rtt < result.MinRTT {
|
||||||
|
result.MinRTT = rtt
|
||||||
|
}
|
||||||
|
if result.MaxRTT == 0 || rtt > result.MaxRTT {
|
||||||
|
result.MaxRTT = rtt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result.RTTs) > 0 {
|
||||||
|
var total time.Duration
|
||||||
|
for _, rtt := range result.RTTs {
|
||||||
|
total += rtt
|
||||||
|
}
|
||||||
|
result.AvgRTT = total / time.Duration(len(result.RTTs))
|
||||||
|
result.ResponseTime = result.AvgRTT
|
||||||
|
result.Success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if expectedCount > 0 {
|
||||||
|
result.PacketLoss = float64(expectedCount-result.PacketsRecv) / float64(expectedCount) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PingOperation) executeRawICMP(host string, count int) (*types.OperationResult, error) {
|
||||||
|
dst, err := net.ResolveIPAddr("ip4", host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve host %s: %v", host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to create ICMP connection with better error handling
|
||||||
|
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create ICMP connection (requires root/admin privileges): %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
result := &types.OperationResult{
|
||||||
|
Type: types.OperationPing,
|
||||||
|
Host: host,
|
||||||
|
PacketsSent: count,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRTT time.Duration
|
||||||
|
var minRTT, maxRTT time.Duration
|
||||||
|
pid := os.Getpid() & 0xffff
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
message := &icmp.Message{
|
||||||
|
Type: ipv4.ICMPTypeEcho,
|
||||||
|
Code: 0,
|
||||||
|
Body: &icmp.Echo{
|
||||||
|
ID: pid,
|
||||||
|
Seq: i + 1,
|
||||||
|
Data: []byte(fmt.Sprintf("Hello, World! %d", i)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := message.Marshal(nil)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
_, err = conn.WriteTo(data, dst)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set read deadline
|
||||||
|
err = conn.SetReadDeadline(time.Now().Add(p.timeout))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := make([]byte, 1500)
|
||||||
|
_, peer, err := conn.ReadFrom(reply)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rtt := time.Since(start)
|
||||||
|
|
||||||
|
// Parse the reply with better error handling - fix the type conversion
|
||||||
|
rm, err := icmp.ParseMessage(int(ipv4.ICMPTypeEchoReply), reply)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rm.Type {
|
||||||
|
case ipv4.ICMPTypeEchoReply:
|
||||||
|
if peer.String() == dst.String() {
|
||||||
|
echoReply, ok := rm.Body.(*icmp.Echo)
|
||||||
|
if ok && echoReply.ID == pid {
|
||||||
|
result.PacketsRecv++
|
||||||
|
totalRTT += rtt
|
||||||
|
|
||||||
|
if result.PacketsRecv == 1 || rtt < minRTT {
|
||||||
|
minRTT = rtt
|
||||||
|
}
|
||||||
|
if result.PacketsRecv == 1 || rtt > maxRTT {
|
||||||
|
maxRTT = rtt
|
||||||
|
}
|
||||||
|
|
||||||
|
result.RTTs = append(result.RTTs, rtt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep between pings except for the last one
|
||||||
|
if i < count-1 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
result.PacketLoss = float64(count-result.PacketsRecv) / float64(count) * 100
|
||||||
|
|
||||||
|
if result.PacketsRecv > 0 {
|
||||||
|
result.MinRTT = minRTT
|
||||||
|
result.MaxRTT = maxRTT
|
||||||
|
result.AvgRTT = totalRTT / time.Duration(result.PacketsRecv)
|
||||||
|
result.ResponseTime = result.AvgRTT
|
||||||
|
result.Success = true
|
||||||
|
// Create detailed success message for raw ICMP
|
||||||
|
result.Details = p.createDetailedSuccessMessage(result, host, dst.String())
|
||||||
|
} else {
|
||||||
|
result.Error = "No ICMP echo replies received"
|
||||||
|
result.Details = p.createDetailedErrorMessage("no replies", host, dst.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
package operations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"service-operation/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TCPOperation struct {
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTCPOperation(timeout time.Duration) *TCPOperation {
|
||||||
|
return &TCPOperation{timeout: timeout}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TCPOperation) Execute(host string, port int) (*types.OperationResult, error) {
|
||||||
|
result := &types.OperationResult{
|
||||||
|
Type: types.OperationTCP,
|
||||||
|
Host: host,
|
||||||
|
Port: port,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
address := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
conn, err := net.DialTimeout("tcp", address, t.timeout)
|
||||||
|
|
||||||
|
result.ResponseTime = time.Since(start)
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
result.TCPConnected = false
|
||||||
|
result.Success = false
|
||||||
|
result.Details = fmt.Sprintf("Failed to connect to %s:%d - %s", host, port, err.Error())
|
||||||
|
} else {
|
||||||
|
conn.Close()
|
||||||
|
result.TCPConnected = true
|
||||||
|
result.Success = true
|
||||||
|
result.Details = fmt.Sprintf("Successfully connected to %s:%d", host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
|
||||||
|
package ping
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/net/icmp"
|
||||||
|
"golang.org/x/net/ipv4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ICMPPinger struct {
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewICMPPinger(timeout time.Duration) *ICMPPinger {
|
||||||
|
return &ICMPPinger{timeout: timeout}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ICMPPinger) Ping(host string, count int) (*PingResult, error) {
|
||||||
|
dst, err := net.ResolveIPAddr("ip4", host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve host %s: %v", host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create ICMP connection: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
result := &PingResult{
|
||||||
|
Host: host,
|
||||||
|
PacketsSent: count,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRTT time.Duration
|
||||||
|
var minRTT, maxRTT time.Duration
|
||||||
|
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
message := &icmp.Message{
|
||||||
|
Type: ipv4.ICMPTypeEcho,
|
||||||
|
Code: 0,
|
||||||
|
Body: &icmp.Echo{
|
||||||
|
ID: os.Getpid() & 0xffff,
|
||||||
|
Seq: i + 1,
|
||||||
|
Data: []byte("Hello, World!"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := message.Marshal(nil)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
_, err = conn.WriteTo(data, dst)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.SetReadDeadline(time.Now().Add(p.timeout))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
reply := make([]byte, 1500)
|
||||||
|
_, peer, err := conn.ReadFrom(reply)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
rtt := time.Since(start)
|
||||||
|
|
||||||
|
// Parse the reply
|
||||||
|
rm, err := icmp.ParseMessage(ipv4.ICMPTypeEchoReply, reply)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rm.Type {
|
||||||
|
case ipv4.ICMPTypeEchoReply:
|
||||||
|
if peer.String() == dst.String() {
|
||||||
|
result.PacketsRecv++
|
||||||
|
totalRTT += rtt
|
||||||
|
|
||||||
|
if result.PacketsRecv == 1 || rtt < minRTT {
|
||||||
|
minRTT = rtt
|
||||||
|
}
|
||||||
|
if result.PacketsRecv == 1 || rtt > maxRTT {
|
||||||
|
maxRTT = rtt
|
||||||
|
}
|
||||||
|
|
||||||
|
result.RTTs = append(result.RTTs, rtt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < count-1 {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.EndTime = time.Now()
|
||||||
|
result.PacketLoss = float64(count-result.PacketsRecv) / float64(count) * 100
|
||||||
|
|
||||||
|
if result.PacketsRecv > 0 {
|
||||||
|
result.MinRTT = minRTT
|
||||||
|
result.MaxRTT = maxRTT
|
||||||
|
result.AvgRTT = totalRTT / time.Duration(result.PacketsRecv)
|
||||||
|
result.Success = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
package ping
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type PingResult struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
PacketsSent int `json:"packets_sent"`
|
||||||
|
PacketsRecv int `json:"packets_recv"`
|
||||||
|
PacketLoss float64 `json:"packet_loss"`
|
||||||
|
MinRTT time.Duration `json:"min_rtt"`
|
||||||
|
MaxRTT time.Duration `json:"max_rtt"`
|
||||||
|
AvgRTT time.Duration `json:"avg_rtt"`
|
||||||
|
RTTs []time.Duration `json:"rtts"`
|
||||||
|
StartTime time.Time `json:"start_time"`
|
||||||
|
EndTime time.Time `json:"end_time"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingRequest struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Count int `json:"count,omitempty"`
|
||||||
|
Timeout int `json:"timeout,omitempty"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
package pocketbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PocketBaseClient struct {
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPocketBaseClient(baseURL string) (*PocketBaseClient, error) {
|
||||||
|
// Use provided baseURL or default to localhost
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "http://localhost:8090"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &PocketBaseClient{
|
||||||
|
baseURL: baseURL,
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) GetBaseURL() string {
|
||||||
|
return c.baseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) TestConnection() error {
|
||||||
|
resp, err := c.httpClient.Get(fmt.Sprintf("%s/api/health", c.baseURL))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("PocketBase health check failed with status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) IsAuthenticated() bool {
|
||||||
|
// Since we're using public access mode, always return true
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
package pocketbase
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) SaveMetrics(metrics MetricsRecord) error {
|
||||||
|
return c.createRecord("services_metrics", metrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) SavePingData(pingData PingDataRecord) error {
|
||||||
|
return c.createRecord("ping_data", pingData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) SaveUptimeData(uptimeData UptimeDataRecord) error {
|
||||||
|
return c.createRecord("uptime_data", uptimeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) SaveDNSData(dnsData DNSDataRecord) error {
|
||||||
|
return c.createRecord("dns_data", dnsData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) SaveTCPData(tcpData TCPDataRecord) error {
|
||||||
|
return c.createRecord("tcp_data", tcpData)
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
|
||||||
|
package pocketbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) GetServices() ([]Service, error) {
|
||||||
|
req, err := http.NewRequest("GET",
|
||||||
|
fmt.Sprintf("%s/api/collections/services/records", c.baseURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No authentication header needed for public access
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to fetch services, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var servicesResponse ServicesResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&servicesResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return servicesResponse.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) GetService(serviceID string) (*Service, error) {
|
||||||
|
req, err := http.NewRequest("GET",
|
||||||
|
fmt.Sprintf("%s/api/collections/services/records/%s", c.baseURL, serviceID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No authentication header needed for public access
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to fetch service %s, status: %d", serviceID, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var service Service
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&service); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &service, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) GetActiveServices() ([]Service, error) {
|
||||||
|
// Only fetch services that are not paused
|
||||||
|
req, err := http.NewRequest("GET",
|
||||||
|
fmt.Sprintf("%s/api/collections/services/records?filter=(status!='paused')", c.baseURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No authentication header needed for public access
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("failed to fetch active services, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var servicesResponse ServicesResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&servicesResponse); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return servicesResponse.Items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) UpdateServiceStatus(serviceID string, status string, responseTime int64, errorMessage string) error {
|
||||||
|
// First check if the service is paused before updating
|
||||||
|
service, err := c.GetService(serviceID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check service status before update: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.Status == "paused" {
|
||||||
|
return fmt.Errorf("service %s is paused, skipping status update", serviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData := map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
"response_time": responseTime,
|
||||||
|
"last_checked": time.Now().Format(time.RFC3339),
|
||||||
|
}
|
||||||
|
|
||||||
|
if errorMessage != "" {
|
||||||
|
updateData["error_message"] = errorMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(updateData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("PATCH",
|
||||||
|
fmt.Sprintf("%s/api/collections/services/records/%s", c.baseURL, serviceID),
|
||||||
|
bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No authentication header needed
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
||||||
|
return fmt.Errorf("failed to update service status, status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
|
||||||
|
package pocketbase
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type AuthResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Record interface{} `json:"record"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricsRecord struct {
|
||||||
|
ServiceName string `json:"service_name"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Uptime float64 `json:"uptime"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
LastChecked string `json:"last_checked"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Domain string `json:"domain,omitempty"`
|
||||||
|
HeartbeatInterval int `json:"heartbeat_interval,omitempty"`
|
||||||
|
MaxRetries int `json:"max_retries,omitempty"`
|
||||||
|
NotificationID string `json:"notification_id,omitempty"`
|
||||||
|
TemplateID string `json:"template_id,omitempty"`
|
||||||
|
ServiceType string `json:"service_type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
Alerts string `json:"alerts,omitempty"`
|
||||||
|
StatusCodes string `json:"status_codes,omitempty"`
|
||||||
|
Keyword string `json:"keyword,omitempty"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
CheckedAt string `json:"checked_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingDataRecord struct {
|
||||||
|
ServiceID string `json:"service_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
PacketLoss string `json:"packet_loss"`
|
||||||
|
Latency string `json:"latency"`
|
||||||
|
MaxRTT string `json:"max_rtt"`
|
||||||
|
MinRTT string `json:"min_rtt"`
|
||||||
|
PacketsSent string `json:"packets_sent"`
|
||||||
|
PacketsRecv string `json:"packets_recv"`
|
||||||
|
AvgRTT string `json:"avg_rtt"`
|
||||||
|
RTTs string `json:"rtts"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UptimeDataRecord struct {
|
||||||
|
ServiceID string `json:"service_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Packets string `json:"packets"`
|
||||||
|
Latency string `json:"latency"`
|
||||||
|
StatusCodes string `json:"status_codes"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
ErrorMessage string `json:"error_message"`
|
||||||
|
Details string `json:"details"`
|
||||||
|
Region string `json:"region,omitempty"`
|
||||||
|
RegionID string `json:"region_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSDataRecord struct {
|
||||||
|
ServiceID string `json:"service_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
QueryType string `json:"query_type"`
|
||||||
|
ResolveIP string `json:"resolve_ip"`
|
||||||
|
MsgSize string `json:"msg_size"`
|
||||||
|
Question string `json:"question"`
|
||||||
|
Answer string `json:"answer"`
|
||||||
|
Authority string `json:"authority"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
RegionName string `json:"region_name,omitempty"`
|
||||||
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPDataRecord struct {
|
||||||
|
ServiceID string `json:"service_id"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Connection string `json:"connection"`
|
||||||
|
Latency string `json:"latency"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
ErrorMessage string `json:"error_message,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
RegionName string `json:"region_name,omitempty"`
|
||||||
|
AgentID string `json:"agent_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Uptime float64 `json:"uptime"`
|
||||||
|
ResponseTime int64 `json:"response_time"`
|
||||||
|
LastChecked string `json:"last_checked"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
HeartbeatInterval int `json:"heartbeat_interval"`
|
||||||
|
MaxRetries int `json:"max_retries"`
|
||||||
|
NotificationID string `json:"notification_id"`
|
||||||
|
TemplateID string `json:"template_id"`
|
||||||
|
ServiceType string `json:"service_type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Alerts string `json:"alerts"`
|
||||||
|
StatusCodes string `json:"status_codes"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
Created string `json:"created"`
|
||||||
|
Updated string `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServicesResponse struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PerPage int `json:"perPage"`
|
||||||
|
TotalItems int `json:"totalItems"`
|
||||||
|
TotalPages int `json:"totalPages"`
|
||||||
|
Items []Service `json:"items"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
package pocketbase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *PocketBaseClient) createRecord(collection string, data interface{}) error {
|
||||||
|
jsonData, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST",
|
||||||
|
fmt.Sprintf("%s/api/collections/%s/records", c.baseURL, collection),
|
||||||
|
bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// No authentication header needed
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||||
|
// Read response body for better error details
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
bodyString := string(bodyBytes)
|
||||||
|
|
||||||
|
fmt.Printf("Failed to create record in %s collection. Status: %d, Response: %s\n",
|
||||||
|
collection, resp.StatusCode, bodyString)
|
||||||
|
|
||||||
|
return fmt.Errorf("failed to create record in %s, status: %d, response: %s",
|
||||||
|
collection, resp.StatusCode, bodyString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type OperationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OperationPing OperationType = "ping"
|
||||||
|
OperationDNS OperationType = "dns"
|
||||||
|
OperationTCP OperationType = "tcp"
|
||||||
|
OperationHTTP OperationType = "http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OperationRequest struct {
|
||||||
|
Type OperationType `json:"type"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port,omitempty"` // For TCP
|
||||||
|
Count int `json:"count,omitempty"` // For ping
|
||||||
|
Timeout int `json:"timeout,omitempty"` // In seconds
|
||||||
|
Query string `json:"query,omitempty"` // For DNS
|
||||||
|
URL string `json:"url,omitempty"` // For HTTP
|
||||||
|
Method string `json:"method,omitempty"` // For HTTP (GET, POST, etc.)
|
||||||
|
ServiceID string `json:"service_id,omitempty"` // For linking to specific service
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationResult struct {
|
||||||
|
Type OperationType `json:"type"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port,omitempty"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ResponseTime time.Duration `json:"response_time"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Details string `json:"details,omitempty"`
|
||||||
|
|
||||||
|
// Ping specific fields
|
||||||
|
PacketsSent int `json:"packets_sent,omitempty"`
|
||||||
|
PacketsRecv int `json:"packets_recv,omitempty"`
|
||||||
|
PacketLoss float64 `json:"packet_loss,omitempty"`
|
||||||
|
MinRTT time.Duration `json:"min_rtt,omitempty"`
|
||||||
|
MaxRTT time.Duration `json:"max_rtt,omitempty"`
|
||||||
|
AvgRTT time.Duration `json:"avg_rtt,omitempty"`
|
||||||
|
RTTs []time.Duration `json:"rtts,omitempty"`
|
||||||
|
|
||||||
|
// DNS specific fields
|
||||||
|
DNSRecords []string `json:"dns_records,omitempty"`
|
||||||
|
DNSType string `json:"dns_type,omitempty"`
|
||||||
|
|
||||||
|
// TCP specific fields
|
||||||
|
TCPConnected bool `json:"tcp_connected,omitempty"`
|
||||||
|
|
||||||
|
// HTTP specific fields
|
||||||
|
HTTPStatusCode int `json:"http_status_code,omitempty"`
|
||||||
|
HTTPMethod string `json:"http_method,omitempty"`
|
||||||
|
HTTPHeaders map[string]string `json:"http_headers,omitempty"`
|
||||||
|
ContentLength int64 `json:"content_length,omitempty"`
|
||||||
|
ResponseBody string `json:"response_body,omitempty"`
|
||||||
|
|
||||||
|
StartTime time.Time `json:"start_time"`
|
||||||
|
EndTime time.Time `json:"end_time"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user