package statusServer import ( "context" "fmt" "net/http" "runtime" "strings" "time" "gitea.tecamino.com/paadi/statusServer/utils" "gitea.tecamino.com/paadi/statusServer/models" logging "gitea.tecamino.com/paadi/tecamino-logger/logging" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) // StatusServer wraps an HTTP server with PubSub over websockets. // It manages HTTP routes, PubSub broadcasting, and runtime metrics publishing. type StatusServer struct { Ip string // listen address (defaults to all interfaces) Port int // server port Routes *gin.Engine // Gin router for HTTP endpoints pubSub *StatusWebsocket // websocket-enabled PubSub system log *logging.Logger // application logger } // NewStatusServer initializes and configures a new StatusServer instance. // // Setup steps: // - Apply default values to the config. // - Initialize logging. // - Configure CORS middleware (including localhost + detected local IP). // - Create PubSubWebsocket with worker/message limits. // - Register the /status websocket endpoint. // // Returns the server instance or an error if initialization fails. func NewStatusServer(config models.Config) (*StatusServer, error) { config.Default() // initialize logger logger, err := logging.NewLogger("statusServer.log", &config.Log) if err != nil { return nil, err } logger.Debug("NewStatusServer", "initialize new server with allowOrigins: "+ strings.Join(config.AllowOrigins, ", ")+" listening on port: "+fmt.Sprint(config.Port)) r := gin.Default() // always allow local dev origins (http://localhost:PORT) config.AllowOrigins = append(config.AllowOrigins, fmt.Sprintf("http://localhost:%d", config.Port)) // detect and allow local IP in CORS origins localIP, err := utils.GetLocalIP() if err != nil { logger.Error("NewStatusServer", "get local ip: "+err.Error()) } else { config.AllowOrigins = append(config.AllowOrigins, fmt.Sprintf("http://%s:%d", localIP, config.Port)) } // configure CORS middleware r.Use(cors.New(cors.Config{ AllowOrigins: config.AllowOrigins, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, MaxAge: 12 * time.Hour, })) // build the server s := &StatusServer{ Ip: "0.0.0.0", // listen on all interfaces Port: config.Port, Routes: r, log: logger, } // initialize PubSub websocket service logger.Debug("NewStatusServer", fmt.Sprintf( "initialize new PubSubWebsocket service with %d workers %d messages", config.PubSub.Workers, config.PubSub.MaxMessages, )) s.pubSub, err = NewStatusWebsocket(config.PubSub.Workers, config.PubSub.MaxMessages, logger) if err != nil { logger.Error("NewPubSubWebsocket", err.Error()) } // register websocket endpoint r.GET("/status", s.pubSub.NewConection) r.GET("/info", s.pubSub.NewConection) return s, nil } // ServeHttp starts the HTTP server and continuously publishes runtime metrics // (memory allocation + GC count) to the "info" topic. // // It blocks until the HTTP server stops. Resources are cleaned up on shutdown: // PubSub is closed and a shutdown message is logged. func (s *StatusServer) ServeHttp() error { s.log.Debug("ServeHttp", "start publishing runtime metrics (memory + GC count)") // create cancellable context for metrics goroutine ctx, cancel := context.WithCancel(context.Background()) defer cancel() // metrics publisher goroutine go func() { for { var init bool var m runtime.MemStats var oldM uint64 var oldGc uint32 var info struct { Memory uint64 `json:"memory"` GC uint32 `json:"gc"` } ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): // stop publishing if context is canceled return case <-ticker.C: // read runtime memory statistics runtime.ReadMemStats(&m) info.GC = m.NumGC // publish only when values changed or first run if oldGc != m.NumGC || oldM != m.Alloc/1024 || !init { info.Memory = m.Alloc / 1024 s.pubSub.Publish("info", info) oldGc = m.NumGC oldM = m.Alloc / 1024 init = true } } } } }() // log startup s.log.Info("ServeHttp", fmt.Sprintf("start listening on %s:%d", s.Ip, s.Port)) // configure HTTP server srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", s.Ip, s.Port), Handler: s.Routes, } // log shutdown + close PubSub on exit defer s.log.Info("ServeHttp", fmt.Sprintf("close server listening on %s:%d", s.Ip, s.Port)) defer s.pubSub.Close() // blocking call return srv.ListenAndServe() }