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:
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 })
}