- Add package-level documentation with usage examples and architecture details - Document all public types, functions, and methods following godoc conventions - Remove unused logger.Error type and NewError function - Apply consistent documentation style across all packages Packages updated: - apitls: TLS certificate management with automatic renewal - config: Environment-based configuration system - config/depenv: Deployment environment handling - ekko: Enhanced Echo web framework wrapper - kafka: Kafka client wrapper with TLS support - logger: Structured logging with OpenTelemetry integration - tracing: OpenTelemetry distributed tracing setup - types: Shared data structures for NTP Pool project - xff/fastlyxff: Fastly CDN IP range management All tests pass after documentation changes.
231 lines
6.9 KiB
Go
231 lines
6.9 KiB
Go
// Package ekko provides an enhanced Echo web framework wrapper with pre-configured middleware.
|
|
//
|
|
// This package wraps the Echo web framework with a comprehensive middleware stack including:
|
|
// - OpenTelemetry distributed tracing with request context propagation
|
|
// - Prometheus metrics collection with per-service subsystems
|
|
// - Structured logging with trace ID correlation
|
|
// - Security headers (HSTS, content security policy)
|
|
// - Gzip compression for response optimization
|
|
// - Recovery middleware with detailed error logging
|
|
// - HTTP/2 support with H2C (HTTP/2 Cleartext) capability
|
|
//
|
|
// The package uses functional options pattern for flexible configuration
|
|
// and supports graceful shutdown with configurable timeouts. It's designed
|
|
// as the standard web service foundation for NTP Pool project services.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// ekko, err := ekko.New("myservice",
|
|
// ekko.WithPort(8080),
|
|
// ekko.WithPrometheus(prometheus.DefaultRegisterer),
|
|
// ekko.WithEchoSetup(func(e *echo.Echo) error {
|
|
// e.GET("/health", healthHandler)
|
|
// return nil
|
|
// }),
|
|
// )
|
|
// if err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
// err = ekko.Start(ctx)
|
|
package ekko
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/labstack/echo-contrib/echoprometheus"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
slogecho "github.com/samber/slog-echo"
|
|
"go.ntppool.org/common/logger"
|
|
"go.ntppool.org/common/version"
|
|
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/trace"
|
|
"golang.org/x/net/http2"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// New creates a new Ekko instance with the specified service name and functional options.
|
|
// The name parameter is used for OpenTelemetry service identification, Prometheus metrics
|
|
// subsystem naming, and server identification headers.
|
|
//
|
|
// Default configuration includes:
|
|
// - 60 second write timeout
|
|
// - 30 second read header timeout
|
|
// - HTTP/2 support with H2C
|
|
// - Standard middleware stack (tracing, metrics, logging, security)
|
|
//
|
|
// Use functional options to customize behavior:
|
|
// - WithPort(): Set server port (required for Start())
|
|
// - WithPrometheus(): Enable Prometheus metrics
|
|
// - WithEchoSetup(): Configure routes and handlers
|
|
// - WithLogFilters(): Filter access logs
|
|
// - WithOtelMiddleware(): Custom OpenTelemetry middleware
|
|
// - WithWriteTimeout(): Custom write timeout
|
|
// - WithReadHeaderTimeout(): Custom read header timeout
|
|
// - WithGzipConfig(): Custom gzip compression settings
|
|
func New(name string, options ...func(*Ekko)) (*Ekko, error) {
|
|
ek := &Ekko{
|
|
writeTimeout: 60 * time.Second,
|
|
readHeaderTimeout: 30 * time.Second,
|
|
}
|
|
|
|
for _, o := range options {
|
|
o(ek)
|
|
}
|
|
return ek, nil
|
|
}
|
|
|
|
// SetupEcho creates and configures an Echo instance without starting the server.
|
|
// This method is primarily intended for testing scenarios where you need access
|
|
// to the configured Echo instance without starting the HTTP server.
|
|
//
|
|
// The returned Echo instance includes all configured middleware and routes
|
|
// but requires manual server lifecycle management.
|
|
func (ek *Ekko) SetupEcho(ctx context.Context) (*echo.Echo, error) {
|
|
return ek.setup(ctx)
|
|
}
|
|
|
|
// Start creates the Echo instance and starts the HTTP server with graceful shutdown support.
|
|
// The server runs until either an error occurs or the provided context is cancelled.
|
|
//
|
|
// The server supports HTTP/2 with H2C (HTTP/2 Cleartext) and includes a 5-second
|
|
// graceful shutdown timeout when the context is cancelled. Server configuration
|
|
// (port, timeouts, middleware) must be set via functional options during New().
|
|
//
|
|
// Returns an error if server startup fails or if shutdown doesn't complete within
|
|
// the timeout period. Returns nil for clean shutdown via context cancellation.
|
|
func (ek *Ekko) Start(ctx context.Context) error {
|
|
log := logger.Setup()
|
|
|
|
e, err := ek.setup(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
g, ctx := errgroup.WithContext(ctx)
|
|
g.Go(func() error {
|
|
e.Server.Addr = fmt.Sprintf(":%d", ek.port)
|
|
log.Info("server starting", "port", ek.port)
|
|
// err := e.Server.ListenAndServe()
|
|
err := e.StartH2CServer(e.Server.Addr, &http2.Server{})
|
|
if err == http.ErrServerClosed {
|
|
return nil
|
|
}
|
|
return err
|
|
})
|
|
|
|
g.Go(func() error {
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
return e.Shutdown(shutdownCtx)
|
|
})
|
|
|
|
return g.Wait()
|
|
}
|
|
|
|
func (ek *Ekko) setup(ctx context.Context) (*echo.Echo, error) {
|
|
log := logger.Setup()
|
|
|
|
e := echo.New()
|
|
|
|
e.Server.ReadHeaderTimeout = ek.readHeaderTimeout
|
|
e.Server.WriteTimeout = ek.writeTimeout
|
|
|
|
e.Server.BaseContext = func(_ net.Listener) context.Context {
|
|
return ctx
|
|
}
|
|
|
|
trustOptions := []echo.TrustOption{
|
|
echo.TrustLoopback(true),
|
|
echo.TrustLinkLocal(false),
|
|
echo.TrustPrivateNet(true),
|
|
}
|
|
e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...)
|
|
|
|
if ek.otelmiddleware == nil {
|
|
e.Use(otelecho.Middleware(ek.name))
|
|
} else {
|
|
e.Use(ek.otelmiddleware)
|
|
}
|
|
|
|
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
|
|
LogErrorFunc: func(c echo.Context, err error, stack []byte) error {
|
|
log.ErrorContext(c.Request().Context(), err.Error(), "stack", string(stack))
|
|
fmt.Println(string(stack))
|
|
return err
|
|
},
|
|
}))
|
|
|
|
e.Use(slogecho.NewWithConfig(log,
|
|
slogecho.Config{
|
|
WithTraceID: false, // done by logger already
|
|
Filters: ek.logFilters,
|
|
},
|
|
))
|
|
|
|
if ek.prom != nil {
|
|
e.Use(echoprometheus.NewMiddlewareWithConfig(echoprometheus.MiddlewareConfig{
|
|
Subsystem: ek.name,
|
|
Registerer: ek.prom,
|
|
}))
|
|
}
|
|
|
|
if ek.gzipConfig != nil {
|
|
e.Use(middleware.GzipWithConfig(*ek.gzipConfig))
|
|
} else {
|
|
e.Use(middleware.Gzip())
|
|
}
|
|
|
|
secureConfig := middleware.DefaultSecureConfig
|
|
// secureConfig.ContentSecurityPolicy = "default-src *"
|
|
secureConfig.ContentSecurityPolicy = ""
|
|
secureConfig.HSTSMaxAge = int(time.Hour * 168 * 30 / time.Second)
|
|
secureConfig.HSTSPreloadEnabled = true
|
|
|
|
e.Use(middleware.SecureWithConfig(secureConfig))
|
|
|
|
e.Use(
|
|
func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
request := c.Request()
|
|
|
|
span := trace.SpanFromContext(request.Context())
|
|
if span.IsRecording() {
|
|
|
|
span.SetAttributes(attribute.String("http.real_ip", c.RealIP()))
|
|
span.SetAttributes(attribute.String("url.path", c.Request().RequestURI))
|
|
if q := c.QueryString(); len(q) > 0 {
|
|
span.SetAttributes(attribute.String("url.query", q))
|
|
}
|
|
c.Response().Header().Set("Traceparent", span.SpanContext().TraceID().String())
|
|
}
|
|
return next(c)
|
|
}
|
|
},
|
|
)
|
|
|
|
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
vinfo := version.VersionInfo()
|
|
v := ek.name + "/" + vinfo.Version + "+" + vinfo.GitRevShort
|
|
return func(c echo.Context) error {
|
|
c.Response().Header().Set(echo.HeaderServer, v)
|
|
return next(c)
|
|
}
|
|
})
|
|
|
|
if ek.routeFn != nil {
|
|
err := ek.routeFn(e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return e, nil
|
|
}
|