mirror of
https://github.com/tiennm99/keepalive.git
synced 2026-10-11 03:13:31 +00:00
A failed connect or tick now closes the connection and reconnects after one minute, re-running initialization so a dropped table or row is recreated. Before, a failing tick logged every interval forever and connect retries logged every 10 seconds. Connection URLs that fail to parse no longer appear in logs, since their error repeats the password. An unknown adapter or a missing required config key now stops keepalive at startup instead of silently idling one service.
156 lines
3.9 KiB
Go
156 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/tiennm99/keepalive/adapter"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
defaultInterval = time.Minute
|
|
defaultCounterKey = "counter"
|
|
)
|
|
|
|
var defaultConfigFiles = []string{
|
|
"config.yml",
|
|
"config.yaml",
|
|
"/config.yml",
|
|
"/config.yaml",
|
|
}
|
|
|
|
type appConfig struct {
|
|
Interval string `json:"interval" yaml:"interval"`
|
|
CounterKey string `json:"counter_key" yaml:"counter_key"`
|
|
Services []serviceFileConfig `json:"services" yaml:"services"`
|
|
}
|
|
|
|
type serviceFileConfig struct {
|
|
Name string `json:"name" yaml:"name"`
|
|
Adapter string `json:"adapter" yaml:"adapter"`
|
|
Interval string `json:"interval" yaml:"interval"`
|
|
CounterKey string `json:"counter_key" yaml:"counter_key"`
|
|
Config map[string]string `json:"config" yaml:"config"`
|
|
}
|
|
|
|
type serviceConfig struct {
|
|
Name string
|
|
AdapterType string
|
|
Interval time.Duration
|
|
Config adapter.Config
|
|
}
|
|
|
|
func loadConfigFile(path string) ([]serviceConfig, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var raw appConfig
|
|
if err := yaml.Unmarshal(data, &raw); err != nil {
|
|
return nil, fmt.Errorf("parse config: %w", err)
|
|
}
|
|
return normalizeConfig(raw)
|
|
}
|
|
|
|
func defaultConfigFile() (string, error) {
|
|
return firstExistingConfigFile(defaultConfigFiles)
|
|
}
|
|
|
|
func firstExistingConfigFile(paths []string) (string, error) {
|
|
for _, path := range paths {
|
|
if _, err := os.Stat(path); err == nil {
|
|
return path, nil
|
|
} else if !errors.Is(err, os.ErrNotExist) {
|
|
return "", err
|
|
}
|
|
}
|
|
return "", fmt.Errorf("config file not found (looked for: %s)", strings.Join(paths, ", "))
|
|
}
|
|
|
|
func normalizeConfig(raw appConfig) ([]serviceConfig, error) {
|
|
if len(raw.Services) == 0 {
|
|
return nil, fmt.Errorf("services must contain at least one service")
|
|
}
|
|
|
|
globalInterval, err := parseConfigInterval("interval", raw.Interval, defaultInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
globalCounterKey := valueOrDefault(raw.CounterKey, defaultCounterKey)
|
|
|
|
services := make([]serviceConfig, 0, len(raw.Services))
|
|
usedNames := map[string]int{}
|
|
|
|
for i, rawService := range raw.Services {
|
|
servicePath := fmt.Sprintf("services[%d]", i)
|
|
adapterType := strings.TrimSpace(rawService.Adapter)
|
|
if adapterType == "" {
|
|
return nil, fmt.Errorf("%s.adapter is required", servicePath)
|
|
}
|
|
|
|
interval, err := parseConfigInterval(servicePath+".interval", rawService.Interval, globalInterval)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg := adapter.Config{}
|
|
for key, value := range rawService.Config {
|
|
cfg[key] = value
|
|
}
|
|
cfg["counter_key"] = valueOrDefault(rawService.CounterKey, globalCounterKey)
|
|
|
|
// Factories do no I/O, so building one here catches an unknown adapter
|
|
// or a missing config key at startup instead of silently idling.
|
|
if _, err := adapter.New(adapterType, cfg); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", servicePath, err)
|
|
}
|
|
|
|
name, err := normalizeServiceName(rawService.Name, adapterType, cfg, usedNames, servicePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
services = append(services, serviceConfig{
|
|
Name: name,
|
|
AdapterType: adapterType,
|
|
Interval: interval,
|
|
Config: cfg,
|
|
})
|
|
}
|
|
|
|
return services, nil
|
|
}
|
|
|
|
func parseConfigInterval(field, value string, def time.Duration) (time.Duration, error) {
|
|
if strings.TrimSpace(value) == "" {
|
|
return def, nil
|
|
}
|
|
if d, err := time.ParseDuration(value); err == nil {
|
|
if d <= 0 {
|
|
return 0, fmt.Errorf("%s must be greater than zero", field)
|
|
}
|
|
return d, nil
|
|
}
|
|
if n, err := strconv.Atoi(value); err == nil {
|
|
d := time.Duration(n) * time.Second
|
|
if d <= 0 {
|
|
return 0, fmt.Errorf("%s must be greater than zero", field)
|
|
}
|
|
return d, nil
|
|
}
|
|
return 0, fmt.Errorf("%s must be a duration like 30s, 5m, or 1h30m, or an integer number of seconds", field)
|
|
}
|
|
|
|
func valueOrDefault(value, def string) string {
|
|
if strings.TrimSpace(value) == "" {
|
|
return def
|
|
}
|
|
return value
|
|
}
|