| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- package main
- import (
- "fmt"
- "log"
- "net/http"
- "os"
- "strings"
- "time"
- "github.com/gin-gonic/gin"
- "gopkg.in/yaml.v3"
- )
- // serviceMonitor defines one configured service endpoint to check.
- type serviceMonitor struct {
- Name string `yaml:"name"`
- URL string `yaml:"url"`
- Category string `yaml:"category"`
- }
- // appConfig contains the monitor definitions loaded from the YAML config file.
- type appConfig struct {
- Monitors []serviceMonitor `yaml:"monitors"`
- }
- // serviceStatus represents the latest observed health information for a monitor.
- type serviceStatus struct {
- Name string `json:"name"`
- URL string `json:"url"`
- Category string `json:"category"`
- Protocol string `json:"protocol"`
- Healthy bool `json:"healthy"`
- StatusCode int `json:"status_code"`
- ResponseTime string `json:"response_time"`
- LastChecked time.Time `json:"last_checked"`
- Message string `json:"message"`
- Details string `json:"details"`
- }
- // main starts the status server, schedules monitor refreshes, and wires routes.
- func main() {
- store := &statusStore{}
- hub := newWSHub()
- client := &http.Client{Timeout: 5 * time.Second}
- addr := serverAddr()
- tlsCertFile, tlsKeyFile, err := serverTLSFiles()
- if err != nil {
- log.Fatal(err)
- }
- theme := serverTheme()
- monitors, err := loadMonitors(configPath())
- if err != nil {
- log.Fatal(err)
- }
- refreshStatuses(store, client, monitors, hub)
- go func() {
- ticker := time.NewTicker(30 * time.Second)
- defer ticker.Stop()
- for range ticker.C {
- refreshStatuses(store, client, monitors, hub)
- }
- }()
- router := gin.Default()
- router.LoadHTMLGlob("templates/*.html")
- router.Static("/static", "./static")
- router.GET("/", func(c *gin.Context) {
- c.HTML(http.StatusOK, "status.html", gin.H{
- "title": "Status Board",
- "theme": theme,
- })
- })
- router.GET("/api/status", func(c *gin.Context) {
- c.JSON(http.StatusOK, gin.H{
- "generated_at": time.Now(),
- "services": store.list(),
- })
- })
- router.GET("/ws", func(c *gin.Context) {
- serveWebSocket(c, hub)
- })
- scheme := "http"
- if tlsCertFile != "" {
- scheme = "https"
- }
- log.Printf("status page listening on %s://%s", scheme, addr)
- if err := runServer(router, addr, tlsCertFile, tlsKeyFile); err != nil {
- log.Fatal(err)
- }
- }
- // configPath returns the monitor configuration file path.
- func configPath() string {
- if path := os.Getenv("CONFIG_PATH"); path != "" {
- return path
- }
- return "config.yaml"
- }
- // serverAddr returns the HTTP listen address for the status server.
- func serverAddr() string {
- if addr := os.Getenv("ADDR"); addr != "" {
- return addr
- }
- if port := os.Getenv("PORT"); port != "" {
- return ":" + port
- }
- return "127.0.0.1:8080"
- }
- // serverTLSFiles returns the configured TLS certificate and key file paths.
- func serverTLSFiles() (string, string, error) {
- certFile := os.Getenv("TLS_CERT_FILE")
- keyFile := os.Getenv("TLS_KEY_FILE")
- if certFile == "" && keyFile == "" {
- return "", "", nil
- }
- if certFile == "" || keyFile == "" {
- return "", "", fmt.Errorf("both TLS_CERT_FILE and TLS_KEY_FILE must be set to enable HTTPS")
- }
- return certFile, keyFile, nil
- }
- // serverTheme returns the normalized UI theme name.
- func serverTheme() string {
- switch strings.ToLower(os.Getenv("THEME")) {
- case "light":
- return "light"
- case "dark", "":
- return "dark"
- default:
- return "dark"
- }
- }
- // runServer starts the Gin server with or without TLS based on configuration.
- func runServer(router *gin.Engine, addr, certFile, keyFile string) error {
- if certFile != "" {
- return router.RunTLS(addr, certFile, keyFile)
- }
- return router.Run(addr)
- }
- // loadMonitors reads, validates, and returns monitor definitions from disk.
- func loadMonitors(path string) ([]serviceMonitor, error) {
- data, err := os.ReadFile(path)
- if err != nil {
- return nil, fmt.Errorf("read config %q: %w", path, err)
- }
- var cfg appConfig
- if err := yaml.Unmarshal(data, &cfg); err != nil {
- return nil, fmt.Errorf("parse config %q: %w", path, err)
- }
- if len(cfg.Monitors) == 0 {
- return nil, fmt.Errorf("config %q does not define any monitors", path)
- }
- for i, monitor := range cfg.Monitors {
- if monitor.Name == "" || monitor.URL == "" || monitor.Category == "" {
- return nil, fmt.Errorf("config %q has an incomplete monitor at index %d", path, i)
- }
- if _, err := parseMonitorURL(monitor.URL); err != nil {
- return nil, fmt.Errorf("config %q has an invalid URL for monitor %q: %w", path, monitor.Name, err)
- }
- }
- return cfg.Monitors, nil
- }
|