feat: add notify scripts for state transitions

- Add notify_master/notify_backup/notify_fault support
- Support both file path and inline shell script
- Async execution with 60s timeout
- Inject GO_ALIVED_INSTANCE and GO_ALIVED_EVENT env vars
- Bump version to 1.3.0

🤖 Generated with [Qoder][https://qoder.com]
This commit is contained in:
loveuer
2026-03-05 06:02:01 -08:00
parent 22e13c9c0d
commit bd2988855c
3 changed files with 112 additions and 1 deletions

View File

@@ -7,7 +7,7 @@ import (
)
// Version can be set at build time via ldflags
var Version = "1.2.1"
var Version = "1.3.0"
var rootCmd = &cobra.Command{
Use: "go-alived",

View File

@@ -58,6 +58,7 @@ func runService(cmd *cobra.Command, args []string) {
os.Exit(1)
}
setupNotifyScripts(vrrpMgr, cfg, log)
setupHealthTracking(vrrpMgr, healthMgr, log)
healthMgr.StartAll()
@@ -100,6 +101,27 @@ func cleanup(log *logger.Logger, vrrpMgr *vrrp.Manager, healthMgr *health.Manage
vrrpMgr.StopAll()
}
func setupNotifyScripts(vrrpMgr *vrrp.Manager, cfg *config.Config, log *logger.Logger) {
for _, vrrpCfg := range cfg.VRRP {
if vrrpCfg.NotifyMaster == "" && vrrpCfg.NotifyBackup == "" && vrrpCfg.NotifyFault == "" {
continue
}
inst, ok := vrrpMgr.GetInstance(vrrpCfg.Name)
if !ok {
continue
}
vrrp.SetupNotify(inst, &vrrp.NotifyConfig{
Name: vrrpCfg.Name,
NotifyMaster: vrrpCfg.NotifyMaster,
NotifyBackup: vrrpCfg.NotifyBackup,
NotifyFault: vrrpCfg.NotifyFault,
Log: log,
})
}
}
func setupHealthTracking(vrrpMgr *vrrp.Manager, healthMgr *health.Manager, log *logger.Logger) {
instances := vrrpMgr.GetAllInstances()

89
internal/vrrp/notify.go Normal file
View File

@@ -0,0 +1,89 @@
package vrrp
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"time"
"github.com/loveuer/go-alived/pkg/logger"
)
const notifyTimeout = 60 * time.Second
// NotifyConfig holds the notify script configuration for a VRRP instance.
type NotifyConfig struct {
Name string
NotifyMaster string
NotifyBackup string
NotifyFault string
Log *logger.Logger
}
// SetupNotify registers notify scripts as state change callbacks on the instance.
func SetupNotify(inst *Instance, cfg *NotifyConfig) {
if cfg.NotifyMaster != "" {
script := cfg.NotifyMaster
inst.OnMaster(func() {
cfg.Log.Info("[%s] executing notify_master script", cfg.Name)
go runNotifyScript(cfg.Log, cfg.Name, "notify_master", script)
})
cfg.Log.Info("[%s] registered notify_master script", cfg.Name)
}
if cfg.NotifyBackup != "" {
script := cfg.NotifyBackup
inst.OnBackup(func() {
cfg.Log.Info("[%s] executing notify_backup script", cfg.Name)
go runNotifyScript(cfg.Log, cfg.Name, "notify_backup", script)
})
cfg.Log.Info("[%s] registered notify_backup script", cfg.Name)
}
if cfg.NotifyFault != "" {
script := cfg.NotifyFault
inst.OnFault(func() {
cfg.Log.Info("[%s] executing notify_fault script", cfg.Name)
go runNotifyScript(cfg.Log, cfg.Name, "notify_fault", script)
})
cfg.Log.Info("[%s] registered notify_fault script", cfg.Name)
}
}
func runNotifyScript(log *logger.Logger, instName, event, script string) {
ctx, cancel := context.WithTimeout(context.Background(), notifyTimeout)
defer cancel()
cmd := buildCommand(ctx, script)
cmd.Env = append(os.Environ(),
fmt.Sprintf("GO_ALIVED_INSTANCE=%s", instName),
fmt.Sprintf("GO_ALIVED_EVENT=%s", event),
)
output, err := cmd.CombinedOutput()
if err != nil {
log.Error("[%s] %s script failed: %v (output: %s)",
instName, event, err, strings.TrimSpace(string(output)))
return
}
if len(output) > 0 {
log.Info("[%s] %s script output: %s",
instName, event, strings.TrimSpace(string(output)))
}
log.Info("[%s] %s script completed successfully", instName, event)
}
func buildCommand(ctx context.Context, script string) *exec.Cmd {
script = strings.TrimSpace(script)
// If the script is a path to an existing executable file, run it directly
if info, err := os.Stat(script); err == nil && !info.IsDir() {
return exec.CommandContext(ctx, script)
}
// Otherwise treat as inline shell script
return exec.CommandContext(ctx, "sh", "-c", script)
}