Files
statusServer/statusServer.go
2025-08-24 08:12:25 +02:00

166 lines
4.7 KiB
Go

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()
}