From 37c7e808edd60d44c3a657291cfab6f092a22750 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Tue, 23 Dec 2025 13:33:05 +0100 Subject: [PATCH] replace the fiber framework with net/http.ServeMux --- anydb.1 | 4 +- cfg/config.go | 41 ++++++++++++++++- go.mod | 8 +--- go.sum | 18 +------- rest/handlers.go | 110 ++++++++++++++++++++++------------------------ rest/log.go | 100 ++++++++++++++++++++++++++++++++++++++++++ rest/serve.go | 112 ++++++++++++++++++----------------------------- 7 files changed, 241 insertions(+), 152 deletions(-) create mode 100644 rest/log.go diff --git a/anydb.1 b/anydb.1 index 1fd4492..5963217 100644 --- a/anydb.1 +++ b/anydb.1 @@ -1,4 +1,4 @@ -.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.40) +.\" Automatically generated by Pod::Man 4.14 (Pod::Simple 3.42) .\" .\" Standard preamble: .\" ======================================================================== @@ -133,7 +133,7 @@ .\" ======================================================================== .\" .IX Title "ANYDB 1" -.TH ANYDB 1 "2025-02-11" "1" "User Commands" +.TH ANYDB 1 "2025-11-03" "1" "User Commands" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l diff --git a/cfg/config.go b/cfg/config.go index c7b4402..0cfe96d 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -19,11 +19,15 @@ package cfg import ( "fmt" "io" + "log/slog" "os" + "runtime/debug" - "github.com/pelletier/go-toml" "codeberg.org/scip/anydb/app" "codeberg.org/scip/anydb/common" + "github.com/lmittmann/tint" + "github.com/pelletier/go-toml" + "github.com/tlinden/yadu" ) var Version string = "v0.2.6" @@ -58,6 +62,8 @@ func (conf *Config) GetConfig(files []string) error { } } + conf.SetLogger() + return nil } @@ -114,3 +120,36 @@ func (conf *Config) ParseConfigFile(file string) error { return nil } + +func (conf *Config) SetLogger() { + if conf.Debug { + buildInfo, _ := debug.ReadBuildInfo() + opts := &yadu.Options{ + Level: slog.LevelDebug, + AddSource: true, + } + + slog.SetLogLoggerLevel(slog.LevelDebug) + + handler := yadu.NewHandler(os.Stdout, opts) + debuglogger := slog.New(handler).With( + slog.Group("program_info", + slog.Int("pid", os.Getpid()), + slog.String("go_version", buildInfo.GoVersion), + ), + ) + slog.SetDefault(debuglogger) + + slog.Debug("parsed config", "conf", conf) + } else { + opts := &tint.Options{ + Level: slog.LevelInfo, + AddSource: false, + } + + handler := tint.NewHandler(os.Stderr, opts) + logger := slog.New(handler) + + slog.SetDefault(logger) + } +} diff --git a/go.mod b/go.mod index dd0ddfe..4b513e7 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ toolchain go1.24.1 require ( github.com/dustin/go-humanize v1.0.1 - github.com/gofiber/fiber/v2 v2.52.9 github.com/inconshreveable/mousetrap v1.1.0 + github.com/lmittmann/tint v1.1.2 github.com/olekukonko/tablewriter v1.1.0 github.com/pelletier/go-toml v1.9.5 github.com/rogpeppe/go-internal v1.14.1 @@ -20,10 +20,7 @@ require ( ) require ( - github.com/andybalholm/brotli v1.1.1 // indirect github.com/fatih/color v1.16.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -31,9 +28,6 @@ require ( github.com/olekukonko/ll v0.0.9 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/spf13/pflag v1.0.9 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.55.0 // indirect - github.com/valyala/tcplisten v1.0.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/tools v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6e1ea9f..dd979eb 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= -github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,16 +5,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw= -github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= -github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= +github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -46,14 +40,6 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY= github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8= -github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= -github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= -github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= diff --git a/rest/handlers.go b/rest/handlers.go index ecc9d02..15be3e6 100644 --- a/rest/handlers.go +++ b/rest/handlers.go @@ -19,7 +19,9 @@ package rest import ( //"github.com/alecthomas/repr" - "github.com/gofiber/fiber/v2" + "encoding/json" + "net/http" + "codeberg.org/scip/anydb/app" "codeberg.org/scip/anydb/cfg" ) @@ -40,101 +42,95 @@ type SingleResponse struct { Entry *app.DbEntry } -func RestList(c *fiber.Ctx, conf *cfg.Config) error { +func RestList(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) { attr := new(app.DbAttr) - if len(c.Body()) > 0 { - if err := c.BodyParser(attr); err != nil { - return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ - "errors": err.Error(), - }) - + err := json.NewDecoder(req.Body).Decode(&attr) + if err != nil { + if err.Error() != `EOF` { + http.Error(resp, err.Error(), http.StatusBadRequest) + return } } // get list entries, err := conf.DB.List(attr, attr.Fulltext) if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Unable to list keys: "+err.Error()) + JsonStatus(resp, http.StatusForbidden, "Unable to list keys: "+err.Error()) + return } - return c.Status(fiber.StatusOK).JSON( + resp.Header().Set("Content-Type", "application/json") + + json.NewEncoder(resp).Encode( ListResponse{ + Code: http.StatusOK, Success: true, - Code: fiber.StatusOK, Entries: entries, - }, - ) + }) } -func RestGet(c *fiber.Ctx, conf *cfg.Config) error { - if c.Params("key") == "" { - return JsonStatus(c, fiber.StatusForbidden, - "key not provided") +func RestGet(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) { + if key == "" { + JsonStatus(resp, http.StatusForbidden, "key not provided") + return } // get list - entry, err := conf.DB.Get(&app.DbAttr{Key: c.Params("key")}) + entry, err := conf.DB.Get(&app.DbAttr{Key: key}) if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Unable to get key: "+err.Error()) + JsonStatus(resp, http.StatusForbidden, "Unable to get key: "+err.Error()) + return } if entry.Key == "" { - return JsonStatus(c, fiber.StatusForbidden, - "Key does not exist") + JsonStatus(resp, http.StatusForbidden, "Key does not exist") + return } - return c.Status(fiber.StatusOK).JSON( + resp.Header().Set("Content-Type", "application/json") + + json.NewEncoder(resp).Encode( SingleResponse{ + Code: http.StatusOK, Success: true, - Code: fiber.StatusOK, Entry: entry, - }, - ) + }) } -func RestDelete(c *fiber.Ctx, conf *cfg.Config) error { - if c.Params("key") == "" { - return JsonStatus(c, fiber.StatusForbidden, - "key not provided") +func RestDelete(resp http.ResponseWriter, req *http.Request, key string, conf *cfg.Config) { + if key == "" { + JsonStatus(resp, http.StatusForbidden, "key not provided") + return } // get list - err := conf.DB.Del(&app.DbAttr{Key: c.Params("key")}) + err := conf.DB.Del(&app.DbAttr{Key: key}) if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Unable to delete key: "+err.Error()) + JsonStatus(resp, http.StatusForbidden, "Unable to delete key: "+err.Error()) + return } - return c.Status(fiber.StatusOK).JSON( - Result{ - Success: true, - Code: fiber.StatusOK, - Message: "key deleted", - }, - ) + JsonStatus(resp, http.StatusOK, "key deleted") } -func RestSet(c *fiber.Ctx, conf *cfg.Config) error { +func RestSet(resp http.ResponseWriter, req *http.Request, conf *cfg.Config) { attr := new(app.DbAttr) - if err := c.BodyParser(attr); err != nil { - return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{ - "errors": err.Error(), - }) - } - - err := conf.DB.Set(attr) + err := json.NewDecoder(req.Body).Decode(&attr) if err != nil { - return JsonStatus(c, fiber.StatusForbidden, - "Unable to set key: "+err.Error()) + http.Error(resp, err.Error(), http.StatusBadRequest) + return } - return c.Status(fiber.StatusOK).JSON( - Result{ - Success: true, - Code: fiber.StatusOK, - }, - ) + err = conf.DB.Set(attr) + if err != nil { + JsonStatus(resp, http.StatusForbidden, "Unable to set key: "+err.Error()) + return + } + + JsonStatus(resp, http.StatusOK, "key added/updated") +} + +func Home(resp http.ResponseWriter) { + resp.Write([]byte("Use the REST API on " + apiprefix + "\r\n")) } diff --git a/rest/log.go b/rest/log.go new file mode 100644 index 0000000..b0b37dc --- /dev/null +++ b/rest/log.go @@ -0,0 +1,100 @@ +/* + This logging middleware is based on + +https://github.com/elithrar/admission-control/blob/v0.6.3/request_logger.go + + by Matt Silverlock licensed under the Apache-2.0 license. + + I am using slog and added a couple of small modifications. +*/ +package rest + +import ( + "log/slog" + "net/http" + "runtime/debug" + "time" +) + +// responseWriter is a minimal wrapper for http.ResponseWriter that allows the +// written HTTP status code to be captured for logging. +type responseWriter struct { + http.ResponseWriter + status int + size int + wroteHeader bool +} + +func wrapResponseWriter(w http.ResponseWriter) *responseWriter { + return &responseWriter{ResponseWriter: w} +} + +func (rw *responseWriter) Status() int { + return rw.status +} + +func (rw *responseWriter) Size() int { + return rw.size +} + +func (rw *responseWriter) WriteHeader(code int) { + if rw.wroteHeader { + return + } + + rw.status = code + rw.ResponseWriter.WriteHeader(code) + rw.wroteHeader = true + + return +} + +func (rw *responseWriter) Write(data []byte) (int, error) { + + written, err := rw.ResponseWriter.Write(data) + rw.size += written + + return written, err +} + +// LoggingMiddleware logs the incoming HTTP request & its duration. +func LogHandler() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + fn := func(resp http.ResponseWriter, req *http.Request) { + defer func() { + if err := recover(); err != nil { + resp.WriteHeader(http.StatusInternalServerError) + slog.Info( + "internal server error", + "err", err, + "trace", string(debug.Stack()), + ) + } + }() + + start := time.Now() + wrapped := wrapResponseWriter(resp) + next.ServeHTTP(wrapped, req) + + header := wrapped.Header()["Content-Type"] + contenttype := "" + if header == nil { + contenttype = "text/plain" + } else { + contenttype = header[0] + } + + slog.Info("request", + "ip", req.RemoteAddr, + "status", wrapped.status, + "method", req.Method, + "path", req.URL.EscapedPath(), + "size", wrapped.Size(), + "content-type", contenttype, + "duration", time.Since(start), + ) + } + + return http.HandlerFunc(fn) + } +} diff --git a/rest/serve.go b/rest/serve.go index fed2881..3fc2d93 100644 --- a/rest/serve.go +++ b/rest/serve.go @@ -17,10 +17,9 @@ along with this program. If not, see . package rest import ( - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/compress" - "github.com/gofiber/fiber/v2/middleware/cors" - "github.com/gofiber/fiber/v2/middleware/logger" + "encoding/json" + "net/http" + "codeberg.org/scip/anydb/cfg" ) @@ -31,84 +30,59 @@ type Result struct { Code int `json:"code"` } +const apiprefix = `/anydb/v1/` + func Runserver(conf *cfg.Config, args []string) error { // setup api server - router := SetupServer(conf) + mux := http.NewServeMux() - // public rest api routes - api := router.Group("/anydb/v1") - { - api.Get("/", func(c *fiber.Ctx) error { - return RestList(c, conf) - }) - - api.Post("/", func(c *fiber.Ctx) error { - // same thing as above but allows to supply parameters, see app.Dbattr{} - return RestList(c, conf) - }) - - api.Get("/:key", func(c *fiber.Ctx) error { - return RestGet(c, conf) - }) - - api.Delete("/:key", func(c *fiber.Ctx) error { - return RestDelete(c, conf) - }) - - api.Put("/", func(c *fiber.Ctx) error { - return RestSet(c, conf) - }) - } - - // public routes - { - router.Get("/", func(c *fiber.Ctx) error { - return c.Send([]byte("Use the REST API")) - }) - } - - return router.Listen(conf.Listen) -} - -func SetupServer(conf *cfg.Config) *fiber.App { - // disable colors - fiber.DefaultColors = fiber.Colors{} - - router := fiber.New(fiber.Config{ - CaseSensitive: true, - StrictRouting: true, - Immutable: true, - ServerHeader: "anydb serve", - AppName: "anydb", + mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { + Home(w) }) - router.Use(logger.New(logger.Config{ - Format: "${pid} ${ip}:${port} ${status} - ${method} ${path}\n", - DisableColors: true, - })) + mux.HandleFunc("GET "+apiprefix, func(w http.ResponseWriter, r *http.Request) { + RestList(w, r, conf) + }) - router.Use(cors.New(cors.Config{ - AllowMethods: "GET,PUT,POST,DELETE", - ExposeHeaders: "Content-Type,Accept", - })) + mux.HandleFunc("POST "+apiprefix, func(w http.ResponseWriter, r *http.Request) { + RestList(w, r, conf) + }) - router.Use(compress.New(compress.Config{ - Level: compress.LevelBestSpeed, - })) + mux.HandleFunc("GET "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) { + key := r.PathValue("key") + RestGet(w, r, key, conf) + }) - return router + mux.HandleFunc("DELETE "+apiprefix+"{key}", func(w http.ResponseWriter, r *http.Request) { + key := r.PathValue("key") + RestDelete(w, r, key, conf) + }) + + mux.HandleFunc("PUT "+apiprefix, func(w http.ResponseWriter, r *http.Request) { + RestList(w, r, conf) + }) + + logger := LogHandler() + + return http.ListenAndServe(conf.Listen, logger(mux)) } /* Wrapper to respond with proper json status, message and code, shall be prepared and called by the handlers directly. */ -func JsonStatus(c *fiber.Ctx, code int, msg string) error { - success := code == fiber.StatusOK +func JsonStatus(resp http.ResponseWriter, code int, msg string) error { + success := code == http.StatusOK - return c.Status(code).JSON(Result{ - Code: code, - Message: msg, - Success: success, - }) + resp.Header().Set("Content-Type", "application/json") + resp.WriteHeader(code) + + json.NewEncoder(resp).Encode( + Result{ + Code: code, + Message: msg, + Success: success, + }) + + return nil }