Fx module for httpserver.
go get github.com/ankorstore/yokai/fxhttpserver
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
This module is intended to be used alongside:
- the fxconfig module
- the fxlog module
- the fxtrace module
- the fxmetrics module
- the fxgenerate module
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 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 varAPP_DEBUG=true
), error responses will not be obfuscated and stack trace will be added
This module offers the possibility to easily register handlers, groups, middlewares and error handlers.
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.Options(
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()
}
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.Options(
// 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 examplefxhttpserver.AsHandler("*", ...)
- valid HTTP methods are
CONNECT
,DELETE
,GET
,HEAD
,OPTIONS
,PATCH
,POST
,PUT
,TRACE
,PROPFIND
andREPORT
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
- under a common route
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.Options(
// 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 examplefxhttpserver.NewHandlerRegistration("*", ...)
- valid HTTP methods are
CONNECT
,DELETE
,GET
,HEAD
,OPTIONS
,PATCH
,POST
,PUT
,TRACE
,PROPFIND
andREPORT
You can use the AsErrorHandler()
function to register a custom error handler on your http server.
You can provide any ErrorHandler interface implementation (will be autowired from Fx container)
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/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"go.uber.org/fx"
)
type SomeErrorHandler struct {
config *config.Config
}
func NewSomeErrorHandler(config *config.Config) *SomeErrorHandler {
return &SomeErrorHandler{
config: config,
}
}
func (h *SomeErrorHandler) Handle() echo.HTTPErrorHandler {
return func(err error, c echo.Context) {
if c.Response().Committed {
return
}
c.String(http.StatusInternalServerError, fmt.Sprintf("error handled in %s: %s", h.config.AppName(), err))
}
}
func main() {
fx.New(
fxconfig.FxConfigModule, // load the module dependencies
fxlog.FxLogModule,
fxtrace.FxTraceModule,
fxmetrics.FxMetricsModule,
fxgenerate.FxGenerateModule,
fxhttpserver.FxHttpServerModule, // load the module
fx.Options(
fxhttpserver.AsErrorHandler(NewSomeErrorHandler), // register SomeErrorHandler as error handler
),
).Run()
}
This module supports the WebSocket
protocol, see the Echo documentation for more information.
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.Options(
fxhttpserver.AsHandler("GET", "/app", NewTemplateHandler),
),
).Run()
}
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()
}
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.