diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index d74c40c..0829abc 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -9,6 +9,11 @@ import ( "time" cacheproxy "git.jeffthecoder.xyz/guochao/cache-proxy" + "git.jeffthecoder.xyz/guochao/cache-proxy/pkgs/middleware" + "git.jeffthecoder.xyz/guochao/cache-proxy/pkgs/middleware/handlerlog" + "git.jeffthecoder.xyz/guochao/cache-proxy/pkgs/middleware/httplog" + "git.jeffthecoder.xyz/guochao/cache-proxy/pkgs/middleware/recover" + "github.com/getsentry/sentry-go" "gopkg.in/yaml.v3" ) @@ -115,9 +120,16 @@ func main() { server := cacheproxy.NewServer(*config) - http.HandleFunc("GET /{path...}", server.HandleRequestWithCache) + mux := http.NewServeMux() + + mux.HandleFunc("GET /", server.HandleRequestWithCache) + slog.With("addr", ":8881").Info("serving app") - if err := http.ListenAndServe(":8881", nil); err != nil { + if err := http.ListenAndServe(":8881", middleware.Use(mux, + recover.Recover(), + handlerlog.FindHandler(mux), + httplog.Log(httplog.Config{LogStart: true, LogFinish: true}), + )); err != nil { slog.With("error", err).Error("failed to start server") os.Exit(-1) } diff --git a/pkgs/middleware/handlerlog/log.go b/pkgs/middleware/handlerlog/log.go new file mode 100644 index 0000000..90eedd2 --- /dev/null +++ b/pkgs/middleware/handlerlog/log.go @@ -0,0 +1,30 @@ +package handlerlog + +import ( + "context" + "net/http" +) + +type ctxKey int + +var ( + key ctxKey = 0 +) + +func FindHandler(mux *http.ServeMux) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if _, pattern := mux.Handler(r); pattern != "" { + ctx = context.WithValue(ctx, key, pattern) + } + + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +func Pattern(r *http.Request) (string, bool) { + pattern, ok := r.Context().Value(key).(string) + return pattern, ok +} diff --git a/pkgs/middleware/httplog/log.go b/pkgs/middleware/httplog/log.go new file mode 100644 index 0000000..b6186e2 --- /dev/null +++ b/pkgs/middleware/httplog/log.go @@ -0,0 +1,75 @@ +package httplog + +import ( + "context" + "log/slog" + "net/http" + "time" + + "git.jeffthecoder.xyz/guochao/cache-proxy/pkgs/middleware/handlerlog" +) + +type ctxKey int + +const ( + loggerKey ctxKey = iota +) + +type Config struct { + LogStart bool + LogStartLevel slog.Level + + LogFinish bool + LogFinishLevel slog.Level +} + +type responseRecorder struct { + http.ResponseWriter + StatusCode int +} + +func (recorder *responseRecorder) WriteHeader(statusCode int) { + recorder.StatusCode = statusCode + + recorder.ResponseWriter.WriteHeader(statusCode) +} + +func Log(config Config) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + args := []any{ + "remote_addr", r.RemoteAddr, + "host", r.Host, + "path", r.URL.Path, + } + + if pattern, handlerFound := handlerlog.Pattern(r); handlerFound { + args = append(args, "handler", pattern) + } + + startTime := time.Now() + if config.LogStart { + slog.With(append(args, "time", startTime)...).Log(r.Context(), config.LogStartLevel, "request start") + } + recorder := &responseRecorder{ResponseWriter: w, StatusCode: 200} + next.ServeHTTP(recorder, r.WithContext(context.WithValue(r.Context(), loggerKey, slog.With(args...)))) + if config.LogFinish { + finishTime := time.Now() + slog.With(append(args, "time", finishTime, "duration", finishTime.Sub(startTime), "status", recorder.StatusCode)...).Log(r.Context(), config.LogFinishLevel, "request finished") + } + }) + } +} + +func Logger(r *http.Request) *slog.Logger { + if logger, ok := r.Context().Value(loggerKey).(*slog.Logger); ok { + return logger.With("time", time.Now()) + } + + return slog.With( + "remote_addr", r.RemoteAddr, + "host", r.Host, + "path", r.URL.Path, + "time", time.Now(), + ) +} diff --git a/pkgs/middleware/middleware.go b/pkgs/middleware/middleware.go new file mode 100644 index 0000000..a91380b --- /dev/null +++ b/pkgs/middleware/middleware.go @@ -0,0 +1,14 @@ +package middleware + +import ( + "net/http" + "slices" +) + +func Use(handler http.Handler, wrappers ...func(http.Handler) http.Handler) http.Handler { + slices.Reverse(wrappers) + for _, wrapper := range wrappers { + handler = wrapper(handler) + } + return handler +} diff --git a/pkgs/middleware/recover/recover.go b/pkgs/middleware/recover/recover.go new file mode 100644 index 0000000..356569e --- /dev/null +++ b/pkgs/middleware/recover/recover.go @@ -0,0 +1,25 @@ +package recover + +import ( + "fmt" + "net/http" + + "git.jeffthecoder.xyz/guochao/cache-proxy/pkgs/middleware/httplog" +) + +func recoverer(w http.ResponseWriter, r *http.Request) { + if err := recover(); err != nil { + httplog.Logger(r).With("error", err).Error("request panicked") + http.Error(w, fmt.Sprint(err), http.StatusInternalServerError) + } +} + +func Recover() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer recoverer(w, r) + + next.ServeHTTP(w, r) + }) + } +}