Basic HTTP servers (#2) and various improvements over docs.
Some checks failed
Linting and tests / Linting (push) Has been cancelled
Linting and tests / Tests (push) Has been cancelled

This commit is contained in:
2024-10-12 23:07:49 +05:00
parent 0b5d3a2c37
commit 2ecfe7f8ac
17 changed files with 417 additions and 5 deletions

View File

@@ -5,6 +5,7 @@ import (
"go.dev.pztrn.name/featurer/server/internal/application"
"go.dev.pztrn.name/featurer/server/internal/services/core/datastore"
"go.dev.pztrn.name/featurer/server/internal/services/core/http"
)
func main() {
@@ -23,6 +24,7 @@ func main() {
// Initializing core services first.
checkError(datastore.Initialize(app))
checkError(http.Initialize(app))
// Then - features services.

View File

@@ -0,0 +1,37 @@
package core
import (
"errors"
"github.com/gin-gonic/gin"
)
const (
// ServerNameAPI is a HTTP server name for API.
ServerNameAPI = "api"
// ServerNameCMS is a HTTP server name for CMS.
ServerNameCMS = "cms"
// ServiceNameHTTP is a name for service responsible for HTTP servers controlloing.
ServiceNameHTTP = "http servers"
)
var (
// ErrHTTP указывает на ошибку в сервисе управления HTTP серверами.
ErrHTTP = errors.New("HTTP server")
// ErrHTTPAssetsBundleAlreadyRegistered говорит о попытке зарегистрировать бандл с ассетами с уже использованным
// именем.
ErrHTTPAssetsBundleAlreadyRegistered = errors.New("assets bundle already registered")
// ErrHTTPCMSAddressInvalid говорит о неправильно заданном адресе для прослушки, который был получен из
// переменных окружения.
ErrHTTPCMSAddressInvalid = errors.New("CMS server address invalid")
// ErrHTTPServerNotFound возникает при попытке получить неизвестный HTTP сервер.
ErrHTTPServerNotFound = errors.New("HTTP server not found")
// ErrHTTPServiceIsInvalid говорит о неправильной имплементации сервиса управления HTTP серверами.
ErrHTTPServiceIsInvalid = errors.New("service implementation is invalid")
)
// HTTP это интерфейс для сервиса управления HTTP серверами.
type HTTP interface {
RegisterHandler(serverName, method, path string, handler func(*gin.Context)) error
ReplyJSON(ctx *gin.Context, statusCode int, data interface{})
}

View File

@@ -0,0 +1,29 @@
package http
import (
"fmt"
"github.com/gin-gonic/gin"
"go.dev.pztrn.name/featurer/server/internal/services/core"
)
func (h *http) RegisterHandler(serverName, method, path string, handler func(*gin.Context)) error {
h.serversMutex.RLock()
defer h.serversMutex.RUnlock()
srv, found := h.servers[serverName]
if !found {
return fmt.Errorf(
"%w: registering handler '%s %s': %w",
core.ErrHTTP,
method,
path,
core.ErrHTTPServerNotFound,
)
}
_ = srv.Handle(method, path, handler)
return nil
}

View File

@@ -0,0 +1,90 @@
package http
import (
"fmt"
"log/slog"
stdhttp "net/http"
"os"
"strings"
"sync"
"go.dev.pztrn.name/featurer/server/internal/application"
"go.dev.pztrn.name/featurer/server/internal/services/core"
"github.com/gin-gonic/gin"
)
const (
subsystem = "HTTP servers"
)
var _ = core.HTTP(&http{})
type http struct {
app *application.Application
servers map[string]*gin.Engine
httpServers map[string]*stdhttp.Server
serversMutex sync.RWMutex
}
// Initialize initializes service.
func Initialize(app *application.Application) error {
httpSrv := &http{
app: app,
}
if err := app.RegisterService(httpSrv); err != nil {
return fmt.Errorf("%w: register service: %w", core.ErrHTTP, err)
}
return nil
}
func (h *http) ConnectDependencies() error {
return nil
}
func (h *http) GetName() string {
return core.ServiceNameHTTP
}
func (h *http) Initialize() error {
slog.Info("Initializing service...", "service", subsystem)
h.servers = make(map[string]*gin.Engine)
h.httpServers = make(map[string]*stdhttp.Server)
serversNames := []string{core.ServerNameAPI, core.ServerNameCMS}
for _, name := range serversNames {
addr, found := os.LookupEnv("FEATURER_" + strings.ToUpper(name) + "_SERVER_ADDRESS")
if !found {
return fmt.Errorf("%w: getting address for server '%s' from env: not found", core.ErrHTTP, name)
}
h.createServer(name, addr)
}
return nil
}
func (h *http) LaunchStartupTasks() error {
h.startServers()
return nil
}
func (h *http) Shutdown() error {
h.serversMutex.Lock()
defer h.serversMutex.Unlock()
for name, server := range h.httpServers {
slog.Info("Stopping HTTP server...", "service", subsystem, "server", name)
if err := server.Shutdown(h.app.GetContext()); err != nil {
slog.Error("Failed to stop HTTP server!", "service", subsystem, "server", name, "error", err.Error())
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
package http
import (
"log/slog"
"time"
"github.com/gin-gonic/gin"
)
func (h *http) requestLogger(serverName string) gin.HandlerFunc {
return func(ctx *gin.Context) {
startTime := time.Now()
ctx.Next()
slog.Info(
"HTTP request processed",
"service", subsystem,
"server", serverName,
"client-ip", ctx.ClientIP(),
"user-agent", ctx.Request.UserAgent(),
"path", ctx.Request.Method+" "+ctx.Request.URL.String(),
"request-size", ctx.Request.ContentLength,
"response-code", ctx.Writer.Status(),
"response-length", ctx.Writer.Size(),
"response-time", time.Since(startTime).String(),
)
}
}

View File

@@ -0,0 +1,23 @@
package http
import (
"fmt"
"go.dev.pztrn.name/featurer/server/internal/services/core"
"github.com/gin-gonic/gin"
)
func (h *http) RegisterMiddleware(serverName string, middleware gin.HandlerFunc) error {
h.serversMutex.RLock()
defer h.serversMutex.RUnlock()
router, found := h.servers[serverName]
if !found {
return fmt.Errorf("%w: registering middleware: %w", core.ErrHTTP, core.ErrHTTPServerNotFound)
}
_ = router.Use(middleware)
return nil
}

View File

@@ -0,0 +1,10 @@
package http
import (
"github.com/gin-gonic/gin"
)
func (h *http) ReplyJSON(ctx *gin.Context, statusCode int, data interface{}) {
ctx.Writer.Header().Add("Content-Type", "application/json")
ctx.JSON(statusCode, data)
}

View File

@@ -0,0 +1,56 @@
package http
import (
"log/slog"
stdhttp "net/http"
"time"
"github.com/gin-gonic/gin"
)
func (h *http) createServer(name, address string) {
engine := gin.New()
//nolint:gomnd,mnd
httpServer := &stdhttp.Server{
Addr: address,
Handler: engine.Handler(),
// ToDo: move into configuration.
ReadTimeout: time.Second * 5,
WriteTimeout: time.Second * 10,
}
h.serversMutex.Lock()
h.servers[name] = engine
h.httpServers[name] = httpServer
h.serversMutex.Unlock()
// ToDo: trusted proxies.
engine.Use(h.requestLogger(name))
}
func (h *http) startServers() {
h.serversMutex.RLock()
defer h.serversMutex.RUnlock()
for srvName, httpServer := range h.httpServers {
go func(name string, srv *stdhttp.Server) {
slog.Info(
"Starting HTTP server...",
"service", subsystem,
"server-name", name,
"server-address", srv.Addr,
)
if err := srv.ListenAndServe(); err != nil {
slog.Error(
"Failed to start HTTP server!",
"service", subsystem,
"server-name", name,
"server-address", srv.Addr,
"error", err.Error(),
)
}
}(srvName, httpServer)
}
}

View File

@@ -5,9 +5,15 @@ services:
build:
context: ../../../
dockerfile: server/Dockerfile.featurer
ports:
- "15000:5000"
- "15001:5001"
networks:
featurer:
ipv4_address: 248.248.0.2
environment:
FEATURER_API_SERVER_ADDRESS: "0.0.0.0:5000"
FEATURER_CMS_SERVER_ADDRESS: "0.0.0.0:5001"
cap_add:
- SYS_PTRACE