Nik Afiq 39b7d6d4b4
All checks were successful
CI / changes (push) Successful in 19s
CI / test (push) Successful in 31s
CI / build-ai-gateway (push) Successful in 1m8s
CI / build-ha-gateway (push) Successful in 1m19s
CI / build-discord-bot (push) Successful in 59s
CI / build-alexa-bridge (push) Successful in 1m8s
CI / build-alert-bridge (push) Successful in 1m17s
CI / build-tts-gateway (push) Successful in 1m10s
CI / build-tts-sidecar (push) Has been skipped
CI / build-tts-model (push) Has been skipped
feat: add alert-bridge service for external cronjob -> Discord alerts
New standalone service that receives authenticated HTTP POSTs (e.g. from a
cronjob elsewhere on the home network) and posts them to a Discord channel
via an Incoming Webhook. Fire-and-forget, no dependency on discord-bot's bot
session at all - mirrors alexa-bridge's precedent of a small bridge service
with its own HTTP listener and auth rather than adding an inbound edge to an
existing internal service.

Wires alert-bridge into go.work, CI (changes filter, test job, new
build-alert-bridge job), and the other five Dockerfiles' COPY lines.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 22:45:18 +09:00

94 lines
2.5 KiB
Go

package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/joho/godotenv"
httpadapter "gitea.nik4nao.com/nik/home-services/alert-bridge/internal/adapters/primary/http"
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/adapters/secondary/discordwebhook"
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/app"
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/config"
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/logger"
"gitea.nik4nao.com/nik/home-services/alert-bridge/internal/telemetry"
)
var version = "dev"
func main() {
_ = godotenv.Load()
cfg, err := config.Load()
if err != nil {
os.Stderr.WriteString("config error: " + err.Error() + "\n")
os.Exit(1)
}
log := logger.New(cfg.LogFormat, cfg.LogLevel)
slog.SetDefault(log)
log.Info("starting alert-bridge",
"version", version,
"http_port", cfg.HTTPPort,
"log_level", cfg.LogLevel,
"log_format", cfg.LogFormat,
)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
ctx = logger.WithLogger(ctx, log)
shutdown, err := telemetry.Setup(ctx, "alert-bridge", version, cfg)
if err != nil {
log.Error("telemetry setup failed", "err", err)
os.Exit(1)
}
if cfg.OTELEndpoint != "" {
log.Info("telemetry enabled", "endpoint", cfg.OTELEndpoint)
} else {
log.Debug("telemetry disabled")
}
notifier := discordwebhook.New(cfg.DiscordWebhookURL, cfg.MentionUserID)
alertApp := app.NewAlertApp(notifier)
alertHandler := httpadapter.NewHandler(alertApp, cfg.APIKey)
mux := http.NewServeMux()
mux.Handle("POST /alerts", alertHandler)
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
srv := &http.Server{
Addr: ":" + cfg.HTTPPort,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
log.Info("alert-bridge listening", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Error("serve failed", "err", err)
}
}()
<-ctx.Done()
log.Info("shutdown signal received, draining")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Error("http server shutdown failed", "err", err)
}
log.Info("shutdown complete")
if err := shutdown(context.Background()); err != nil {
log.Error("telemetry shutdown error", "err", err)
}
}