feat: implement ntfy-discord bridge in Go
- SSE subscription to ntfy with auto-reconnect - Discord webhook integration with embed formatting - Priority to color mapping, tag to emoji conversion - Native HashiCorp Vault support (Kubernetes + token auth) - Hot reload secrets via fsnotify or Vault polling - Prometheus metrics (/metrics endpoint) - Health/ready endpoints for Kubernetes probes - Comprehensive unit tests and fuzz tests - Multi-stage Docker build (~10MB scratch image) - CI/CD pipeline for Gitea Actions
This commit is contained in:
93
internal/server/server.go
Normal file
93
internal/server/server.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.daviestechlabs.io/daviestechlabs/ntfy-discord/internal/bridge"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
// Server provides health and metrics endpoints
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
bridge *bridge.Bridge
|
||||
}
|
||||
|
||||
// New creates a new HTTP server
|
||||
func New(port string, b *bridge.Bridge) *Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
s := &Server{
|
||||
bridge: b,
|
||||
httpServer: &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: mux,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
mux.HandleFunc("/health", s.handleHealth)
|
||||
mux.HandleFunc("/ready", s.handleReady)
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Start begins serving HTTP requests
|
||||
func (s *Server) Start() {
|
||||
slog.Info("starting HTTP server", "addr", s.httpServer.Addr)
|
||||
if err := s.httpServer.ListenAndServe(); err != http.ErrServerClosed {
|
||||
slog.Error("HTTP server error", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown gracefully stops the server
|
||||
func (s *Server) Shutdown(ctx context.Context) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||
slog.Error("HTTP server shutdown error", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
status := struct {
|
||||
Status string `json:"status"`
|
||||
Healthy bool `json:"healthy"`
|
||||
}{
|
||||
Status: "ok",
|
||||
Healthy: s.bridge.IsHealthy(),
|
||||
}
|
||||
|
||||
if !status.Healthy {
|
||||
status.Status = "unhealthy"
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
|
||||
status := struct {
|
||||
Status string `json:"status"`
|
||||
Ready bool `json:"ready"`
|
||||
}{
|
||||
Status: "ok",
|
||||
Ready: s.bridge.IsReady(),
|
||||
}
|
||||
|
||||
if !status.Ready {
|
||||
status.Status = "not ready"
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
Reference in New Issue
Block a user