api.spaceplanner.app

Spaceplanner API
git clone git://jacobedwards.org/api.spaceplanner.app
Log | Files | Refs

commit 9a907d021c37d7706c43455a870ace0a239be998
parent be11385bf74de34de66ceab44729e8840a2f5259
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Tue, 15 Oct 2024 12:45:32 -0700

Add request and response logging

This is practically a necessity for when there are users that aren't
developers.

Diffstat:
Mcmd/api/Makefile | 2+-
Mcmd/api/main.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/api/respond.go | 4+++-
3 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/cmd/api/Makefile b/cmd/api/Makefile @@ -32,7 +32,7 @@ run: kfcgi -dv -n 1 -N 1 \ -s ${srvsock} -u ${srvsockuser} \ -U ${srvuser} \ - -p ${srvroot} ${srvdir}/${prog} < api.config + -p ${srvroot} ${srvdir}/${prog} < api.config 2>/var/www/logs/spaceplanner.api.log test-run: ${name} ./${prog} -t -s -b localhost:8888 < api.config diff --git a/cmd/api/main.go b/cmd/api/main.go @@ -1,10 +1,13 @@ package main import ( + "bytes" "encoding/json" "flag" "io" + "io/ioutil" "log" + "log/slog" "net/http" "net/http/fcgi" "os" @@ -28,12 +31,26 @@ func main() { var useStdin bool var testing bool var bindTo string + var logTo string flag.BoolVar(&testing, "t", false, "Test mode") flag.BoolVar(&useStdin, "s", false, "Read config from stdin") flag.StringVar(&bindTo, "b", "", "Bind address and port") + flag.StringVar(&logTo, "l", "-", "Log file") flag.Parse() + var logFile io.Writer + if logTo == "-" { + logFile = os.Stderr + } else { + var err error + logFile, err = os.Open(logTo) + if err != nil { + log.Fatalf("%s: Could not open log file: %s", logTo, err.Error()) + } + } + slog.SetDefault(slog.New(slog.NewJSONHandler(logFile, nil))) + var f io.Reader if useStdin { f = os.Stdin @@ -74,6 +91,7 @@ func main() { } engine := gin.Default() engine.NoRoute(noRoute) + engine.Use(RequestLoggerMiddleware()) engine.Use(cors.New(corsConfig)) engine.Use(AuthMiddleware(e.Auth)) setRoutes(e, engine.Group("/v0")) @@ -157,3 +175,39 @@ func (e *Env) VerifyPayment(c *gin.Context) { func noRoute(c *gin.Context) { RespondError(c, http.StatusNotFound, "Endpoint does not exist") } + +func RequestLoggerMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + var buf bytes.Buffer + + tee := io.TeeReader(c.Request.Body, &buf) + req, _ := ioutil.ReadAll(tee) + c.Request.Body = ioutil.NopCloser(&buf) + + c.Next() + + s := c.Writer.Status() + if s >= 200 && s < 300 { + slog.Info("Request", "endpoint", c.Request.URL.Path, slog.Group("response", slog.Int("code", s))) + return + } + + log := slog.Warn + if s >= 500 && s < 600 { + log = slog.Error + } + + resp, _ := c.Get("error") + if resp == nil { + resp = "" + } + log("Request", + slog.String("endpoint", c.Request.URL.Path), + slog.Group("response", + slog.Int("code", s), + slog.String("message", resp.(string)), + ), + slog.String("body", string(req)), + ) + } +} diff --git a/cmd/api/respond.go b/cmd/api/respond.go @@ -27,6 +27,8 @@ func Respond(c *gin.Context, status int, body any) { } func RespondError(c *gin.Context, status int, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + c.Set("error", msg) c.AbortWithStatusJSON(status, - Response{ Status: "error", Error: fmt.Sprintf(format, args...) }) + Response{ Status: "error", Error: msg }) }