Categorygithub.com/ankorstore/yokai/fxhttpserver
modulepackage
1.6.1
Repository: https://github.com/ankorstore/yokai.git
Documentation: pkg.go.dev

# README

Fx Http Server Module

ci go report codecov Deps PkgGoDev

Fx module for httpserver.

Installation

go get github.com/ankorstore/yokai/fxhttpserver

Features

This module provides a http server to your Fx application with:

  • automatic panic recovery
  • automatic requests logging and tracing (method, path, duration, ...)
  • automatic requests metrics (count and duration)
  • possibility to register handlers, groups and middlewares
  • possibility to render HTML templates

Documentation

Dependencies

This module is intended to be used alongside:

Loading

To load the module in your Fx application:

package main

import (
	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"go.uber.org/fx"
)

func main() {
	fx.New(
		fxconfig.FxConfigModule,         // load the module dependencies
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule, // load the module
	).Run()
}

Configuration

Configuration reference:

# ./configs/config.yaml
app:
  name: app
  env: dev
  version: 0.1.0
  debug: true
modules:
  log:
    level: info
    output: stdout
  trace:
    processor:
      type: stdout
  http:
    server:
      address: ":8080"                # http server listener address (default :8080)
      errors:
        obfuscate: false              # to obfuscate error messages on the http server responses
        stack: false                  # to add error stack trace to error response of the http server
      log:
        headers:                      # to log incoming request headers on the http server
          x-foo: foo                  # to log for example the header x-foo in the log field foo
          x-bar: bar
        exclude:                      # to exclude specific routes from logging
          - /foo
          - /bar
        level_from_response: true     # to use response status code for log level (ex: 500=error)
      trace:
        enabled: true                 # to trace incoming request headers on the http server
        exclude:                      # to exclude specific routes from tracing
          - /foo
          - /bar
      metrics:
        collect:
          enabled: true               # to collect http server metrics
          namespace: foo              # http server metrics namespace (empty by default)
          subsystem: bar              # http server metrics subsystem (empty by default)
        buckets: 0.1, 1, 10           # to override default request duration buckets
        normalize:               
          request_path: true          # to normalize http request path, disabled by default
          response_status: true       # to normalize http response status code (2xx, 3xx, ...), disabled by default
      templates:
        enabled: true                 # disabled by default
        path: templates/*.html        # templates path lookup pattern

Notes:

  • the http server requests logging will be based on the fxlog module configuration
  • the http server requests tracing will be based on the fxtrace module configuration
  • if app.debug=true (or env var APP_DEBUG=true), error responses will not be obfuscated and stack trace will be added

Registration

This module offers the possibility to easily register handlers, groups and middlewares.

Middlewares

You can use the AsMiddleware() function to register global middlewares on your http server:

  • you can provide any Middleware interface implementation (will be autowired from Fx container)
  • or any echo.MiddlewareFunc, for example any built-in Echo middleware
package main

import (
	"github.com/ankorstore/yokai/config"
	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"github.com/ankorstore/yokai/httpserver"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"go.uber.org/fx"
)

type SomeMiddleware struct {
	config *config.Config
}

func NewSomeMiddleware(config *config.Config) *SomeMiddleware {
	return &SomeMiddleware{
		config: config,
	}
}

func (m *SomeMiddleware) Handle() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// request correlated log
			httpserver.CtxLogger(c).Info().Msg("in some middleware")

			// use injected dependency
			c.Response().Header().Add("app-name", m.config.AppName())

			return next(c)
		}
	}
}

func main() {
	fx.New(
		fxconfig.FxConfigModule,         // load the module dependencies
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule, // load the module
		fx.Provide(
			fxhttpserver.AsMiddleware(middleware.CORS(), fxhttpserver.GlobalUse), // register echo CORS middleware via echo.Use()
			fxhttpserver.AsMiddleware(NewSomeMiddleware, fxhttpserver.GlobalPre), // register and autowire the SomeMiddleware via echo.Pre()
		),
	).Run()
}

Handlers

You can use the AsHandler() function to register handlers and their middlewares on your http server:

  • you can provide any Handler interface implementation (will be autowired from Fx container)
  • or any echo.HandlerFunc
package main

import (
	"fmt"
	"net/http"

	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"github.com/ankorstore/yokai/config"
	"github.com/ankorstore/yokai/httpserver"
	"github.com/ankorstore/yokai/log"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"go.uber.org/fx"
)

type SomeMiddleware struct {
	config *config.Config
}

func NewSomeMiddleware(config *config.Config) *SomeMiddleware {
	return &SomeMiddleware{
		config: config,
	}
}

func (m *SomeMiddleware) Handle() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// request correlated log
			httpserver.CtxLogger(c).Info().Msg("in some middleware")

			// use injected dependency
			c.Response().Header().Add("app-name", m.config.AppName())

			return next(c)
		}
	}
}

type SomeHandler struct {
	config *config.Config
}

func NewSomeHandler(config *config.Config) *SomeHandler {
	return &SomeHandler{
		config: config,
	}
}

func (h *SomeHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		// request correlated trace span
		ctx, span := httpserver.CtxTracer(c).Start(c.Request().Context(), "some span")
		defer span.End()

		// request correlated log
		log.CtxLogger(ctx).Info().Msg("in some handler")

		// use injected dependency
		return c.String(http.StatusOK, fmt.Sprintf("app name: %s", h.config.AppName()))
	}
}

func main() {
	fx.New(
		fxconfig.FxConfigModule,         // load the module dependencies
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule, // load the module
		fx.Provide(
			// register and autowire the SomeHandler handler for [GET] /some-path, with the SomeMiddleware and echo CORS() middlewares
			fxhttpserver.AsHandler("GET", "/some-path", NewSomeHandler, NewSomeMiddleware, middleware.CORS()),
		),
	).Run()
}

Notes:

  • you can specify several valid HTTP methods (comma separated) while registering a handler, for example fxhttpserver.AsHandler("GET,POST", ...)
  • you can use the shortcut * to register a handler for all valid HTTP methods, for example fxhttpserver.AsHandler("*", ...)
  • valid HTTP methods are CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, PROPFIND and REPORT

Handlers groups

You can use the AsHandlersGroup() function to register handlers groups and their middlewares on your http server:

  • you can provide any Handler interface implementation (will be autowired from Fx container) or any echo.HandlerFunc, with their middlewares
  • and group them
    • under a common route prefix
    • with common Middleware interface implementation (will be autowired from Fx container) or any echo.MiddlewareFunc
package main

import (
	"fmt"
	"net/http"

	"github.com/ankorstore/yokai/config"
	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"github.com/ankorstore/yokai/httpserver"
	"github.com/ankorstore/yokai/log"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"go.uber.org/fx"
)

type SomeMiddleware struct {
	config *config.Config
}

func NewSomeMiddleware(config *config.Config) *SomeMiddleware {
	return &SomeMiddleware{
		config: config,
	}
}

func (m *SomeMiddleware) Handle() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			// request correlated log
			httpserver.CtxLogger(c).Info().Msg("in some middleware")

			// use injected dependency
			c.Response().Header().Add("app-name", m.config.AppName())

			return next(c)
		}
	}
}

type SomeHandler struct {
	config *config.Config
}

func NewSomeHandler(config *config.Config) *SomeHandler {
	return &SomeHandler{
		config: config,
	}
}

func (h *SomeHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		// request correlated trace span
		ctx, span := httpserver.CtxTracer(c).Start(c.Request().Context(), "some span")
		defer span.End()

		// request correlated log
		log.CtxLogger(ctx).Info().Msg("in some handler")

		// use injected dependency
		return c.String(http.StatusOK, fmt.Sprintf("app name: %s", h.config.AppName()))
	}
}

type OtherHandler struct {
	config *config.Config
}

func NewOtherHandler(config *config.Config) *OtherHandler {
	return &OtherHandler{
		config: config,
	}
}

func (h *OtherHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		// use injected dependency
		return c.String(http.StatusOK, fmt.Sprintf("app version: %s", h.config.AppVersion()))
	}
}

func main() {
	fx.New(
		fxconfig.FxConfigModule,         // load the module dependencies
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule, // load the module
		fx.Provide(
			// register and autowire the SomeHandler handler with NewSomeMiddleware middleware for [GET] /group/some-path
			// register and autowire the OtherHandler handler with echo CORS middleware for [POST] /group/other-path
			// register the echo CSRF middleware for all handlers of this group
			fxhttpserver.AsHandlersGroup(
				"/group",
				[]*fxhttpserver.HandlerRegistration{
					fxhttpserver.NewHandlerRegistration("GET", "/some-path", NewSomeHandler, NewSomeMiddleware),
					fxhttpserver.NewHandlerRegistration("POST", "/other-path", NewOtherHandler, middleware.CORS()),
				},
				middleware.CSRF(),
			),
		),
	).Run()
}

Notes:

  • you can specify several valid HTTP methods (comma separated) while registering a handler in a group, for example fxhttpserver.NewHandlerRegistration("GET,POST", ...)
  • you can use the shortcut * to register a handler for all valid HTTP methods, for example fxhttpserver.NewHandlerRegistration("*", ...)
  • valid HTTP methods are CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE, PROPFIND and REPORT

WebSocket

This module supports the WebSocket protocol, see the Echo documentation for more information.

Templates

The module will look up HTML templates to render if modules.http.server.templates.enabled=true.

The HTML templates will be loaded from a path matching the pattern specified in modules.http.server.templates.path.

Considering the configuration:

# ./configs/config.yaml
app:
  name: app
modules:
  http:
    server:
      templates:
        enabled: true
        path: templates/*.html

And the template:

<!-- templates/app.html -->
<html>
    <body>
        <h1>App name is {{index . "name"}}</h1>
    </body>
</html>

To render it:

package main

import (
	"net/http"

	"github.com/ankorstore/yokai/config"
	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"github.com/ankorstore/yokai/httpserver"
	"github.com/labstack/echo/v4"
	"go.uber.org/fx"
)

type TemplateHandler struct {
	config *config.Config
}

func NewTemplateHandler(cfg *config.Config) *TemplateHandler {
	return &TemplateHandler{
		config: cfg,
	}
}

func (h *TemplateHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		// output: "App name is app"
		return c.Render(http.StatusOK, "app.html", map[string]interface{}{
			"name": h.config.AppName(),
		})
	}
}

func main() {
	fx.New(
		fxconfig.FxConfigModule,         // load the module dependencies
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule, // load the module
		fx.Provide(
			fxhttpserver.AsHandler("GET", "/app", NewTemplateHandler),
		),
	).Run()
}

Override

By default, the echo.Echo is created by the DefaultHttpServerFactory.

If needed, you can provide your own factory and override the module:

package main

import (
	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"github.com/ankorstore/yokai/httpserver"
	"github.com/labstack/echo/v4"
	"go.uber.org/fx"
)

type CustomHttpServerFactory struct{}

func NewCustomHttpServerFactory() httpserver.HttpServerFactory {
	return &CustomHttpServerFactory{}
}

func (f *CustomHttpServerFactory) Create(options ...httpserver.HttpServerOption) (*echo.Echo, error) {
	return echo.New(), nil
}

func main() {
	fx.New(
		fxconfig.FxConfigModule,                 // load the module dependencies
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule,         // load the module
		fx.Decorate(NewCustomHttpServerFactory), // override the module with a custom factory
		fx.Invoke(func(httpServer *echo.Echo) {  // invoke the custom http server
			// ...
		}),
	).Run()
}

Testing

This module allows you to easily provide functional tests for your handlers.

For example, considering this handler:

package handler

import (
	"net/http"

	"github.com/labstack/echo/v4"
)

type SomeHandler struct{}

func NewSomeHandler() *SomeHandler {
	return &SomeHandler{}
}

func (h *SomeHandler) Handle() echo.HandlerFunc {
	return func(c echo.Context) error {
		return c.String(http.StatusOK, "ok")
	}
}

You can then test it, considering logs, traces and metrics are enabled:

package handler_test

import (
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/ankorstore/yokai/fxconfig"
	"github.com/ankorstore/yokai/fxgenerate"
	"github.com/ankorstore/yokai/fxhttpserver"
	"github.com/ankorstore/yokai/fxlog"
	"github.com/ankorstore/yokai/fxmetrics"
	"github.com/ankorstore/yokai/fxtrace"
	"github.com/ankorstore/yokai/log/logtest"
	"github.com/ankorstore/yokai/trace/tracetest"
	"github.com/labstack/echo/v4"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/testutil"
	"github.com/stretchr/testify/assert"
	semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
	"go.uber.org/fx"
	"go.uber.org/fx/fxtest"
	"handler"
)

func TestSomeHandler(t *testing.T) {
	var httpServer *echo.Echo
	var logBuffer logtest.TestLogBuffer
	var traceExporter tracetest.TestTraceExporter
	var metricsRegistry *prometheus.Registry

	fxtest.New(
		t,
		fx.NopLogger,
		fxconfig.FxConfigModule,
		fxlog.FxLogModule,
		fxtrace.FxTraceModule,
		fxmetrics.FxMetricsModule,
		fxgenerate.FxGenerateModule,
		fxhttpserver.FxHttpServerModule,
		fx.Options(
			fxhttpserver.AsHandler("GET", "/test", handler.NewSomeHandler),
		),
		fx.Populate(&httpServer, &logBuffer, &traceExporter, &metricsRegistry), // extract components
	).RequireStart().RequireStop()

	// http call [GET] /test on the server
	req := httptest.NewRequest(http.MethodGet, "/test", nil)
	rec := httptest.NewRecorder()
	httpServer.ServeHTTP(rec, req)

	// assertions on http response
	assert.Equal(t, http.StatusOK, rec.Code)
	assert.Equal(t, rec.Body.String(), "ok")

	// assertion on the logs buffer
	logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{
		"level":   "info",
		"service": "test",
		"module":  "httpserver",
		"method":  "GET",
		"uri":     "/test",
		"status":  200,
		"message": "request logger",
	})

	// assertion on the traces exporter
	tracetest.AssertHasTraceSpan(
		t,
		traceExporter,
		"GET /test",
		semconv.HTTPRoute("/test"),
		semconv.HTTPMethod(http.MethodGet),
		semconv.HTTPStatusCode(http.StatusOK),
	)

	// assertion on the metrics registry
	expectedMetric := `
		# HELP app_httpserver_requests_total Number of processed HTTP requests
		# TYPE app_httpserver_requests_total counter
		app_httpserver_requests_total{path="/test",method="GET",status="2xx"} 1
	`

	err := testutil.GatherAndCompare(
		metricsRegistry,
		strings.NewReader(expectedMetric),
		"app_httpserver_requests_total",
	)
	assert.NoError(t, err)
}

You can find more tests examples in this module own tests.

# Functions

AsHandler registers a handler into Fx.
AsHandlersGroup registers a handlers group into Fx.
AsMiddleware registers a middleware into Fx.
Contains returns true if a given string can be found in a given slice of strings.
No description provided by the author
GetReturnType returns the return type of a target.
GetType returns the type of a target.
IsConcreteHandler returns true if the handler is a concrete [echo.HandlerFunc] implementation.
IsConcreteMiddleware returns true if the middleware is a concrete [echo.MiddlewareFunc] implementation.
NewFxHttpServer returns a new [echo.Echo].
NewFxHttpServerModuleInfo returns a new [FxHttpServerModuleInfo].
NewFxHttpServerRegistry returns as new [HttpServerRegistry].
NewHandlerDefinition returns a new [HandlerDefinition].
NewHandlerRegistration returns a new [HandlerRegistration].
NewHandlersGroupDefinition returns a new [HandlersGroupDefinition].
NewHandlersGroupRegistration returns a new [HandlersGroupRegistration].
NewMiddlewareDefinition returns a new [MiddlewareDefinition].
NewMiddlewareRegistration returns a new [MiddlewareRegistration].
NewResolvedHandler returns a new [ResolvedHandler].
NewResolvedHandlersGroup returns a new [ResolvedHandlersGroup].
NewResolvedMiddleware returns a new [ResolvedMiddleware].
RegisterHandler registers a handler registration into Fx.
RegisterHandlersGroup registers a handlers group registration into Fx.
RegisterMiddleware registers a middleware registration into Fx.
Sanitize transforms a given string to not contain spaces or dashes, and to be in lower case.
Split trims and splits a provided string by comma.

# Constants

AllMethods is a shortcut to specify all valid methods.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
MethodPropfind can be used on collection and property resources.
MethodReport Method can be used to get information about a resource, see rfc 3253.
No description provided by the author

# Variables

FxHttpServerModule is the [Fx] httpserver module.

# Structs

FxHttpServerModuleInfo is a module info collector for fxcore.
FxHttpServerParam allows injection of the required dependencies in [NewFxHttpServer].
FxHttpServerRegistryParam allows injection of the required dependencies in [NewFxHttpServerRegistry].
HandlerRegistration is a handler registration.
HandlersGroupRegistration is a handlers group registration.
HttpServerRegistry is the registry collecting middlewares, handlers, handlers groups and their definitions.
MiddlewareRegistration is a middleware registration.

# Interfaces

Handler is the interface for handlers.
HandlerDefinition is the interface for handlers definitions.
HandlersGroupDefinition is the interface for handlers groups definitions.
Middleware is the interface for middlewares.
MiddlewareDefinition is the interface for middlewares definitions.
ResolvedHandler is an interface for the resolved handlers.
ResolvedHandlersGroup is an interface for the resolved handlers groups.
ResolvedMiddleware is an interface for the resolved middlewares.

# Type aliases

MiddlewareKind is an enum for the middleware kinds (global, pre, post).