package main import ( "fmt" "log" "log/slog" "net" "net/http" "os" "time" "github.com/urfave/cli/v2" ) type ( // struct for holding response details responseData struct { status int size int } // our http.ResponseWriter implementation loggingResponseWriter struct { http.ResponseWriter // compose original http.ResponseWriter responseData *responseData } ) func (r *loggingResponseWriter) Write(b []byte) (int, error) { size, err := r.ResponseWriter.Write(b) // write response using original http.ResponseWriter r.responseData.size += size // capture size return size, err } func (r *loggingResponseWriter) WriteHeader(statusCode int) { r.ResponseWriter.WriteHeader(statusCode) // write status code using original http.ResponseWriter r.responseData.status = statusCode // capture status code } func WithLogging(h http.Handler) http.Handler { loggingFn := func(w http.ResponseWriter, r *http.Request) { start := time.Now() responseData := &responseData{ status: 0, size: 0, } lrw := loggingResponseWriter{ ResponseWriter: w, // compose original http.ResponseWriter responseData: responseData, } h.ServeHTTP(&lrw, r) // inject our implementation of http.ResponseWriter duration := time.Since(start) slog.Info("Request:", slog.String("method", r.Method), slog.String("path", r.RequestURI), slog.String("url", r.URL.Path), slog.String("host", r.Host), slog.Int("status", responseData.status), slog.Int64("duration", duration.Microseconds()), slog.Int("size", responseData.size), ) } return http.HandlerFunc(loggingFn) } func LoggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) logger.Info("Request:", slog.String("method", r.Method), slog.String("path", r.RequestURI), slog.String("url", r.URL.Path), slog.String("host", r.Host), ) }) } func pingHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) } func main() { logLevel := &slog.LevelVar{} // INFO opts := slog.HandlerOptions{ Level: logLevel, } logLevel.Set(slog.LevelDebug) handler := slog.NewTextHandler(os.Stdout, &opts) logger := slog.New(handler) slog.SetDefault(logger) var host string var port string var directory string app := &cli.App{ Name: "snice", Usage: "Serve Static Files", Version: "v0.1.0", DefaultCommand: "", Commands: []*cli.Command{ { Name: "serve", Aliases: []string{"s"}, Usage: "Serve directory", Action: func(cCtx *cli.Context) error { var addr string = host + ":" + port srv := &http.Server{ Addr: addr, } mux := http.NewServeMux() mux.Handle("/ping", http.HandlerFunc(pingHandler)) fileHandler := http.FileServer(http.Dir(directory)) mux.Handle("/", fileHandler) srv.Handler = WithLogging(mux) listener, err := net.Listen("tcp", addr) if err != nil { logger.Error("Listen error", err) } logger.Info(fmt.Sprintf("Serving directory %q on http://%v", directory, listener.Addr())) err = srv.Serve(listener) if err != nil { logger.Error("Serve error", err) } return nil }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "directory", Aliases: []string{"dir", "d"}, EnvVars: []string{"DIRECTORY"}, Value: ".", Usage: "Directory to serve", Destination: &directory, }, &cli.StringFlag{ Name: "host", EnvVars: []string{"HOST"}, Value: "0.0.0.0", Usage: "Host to listen on", Destination: &host, }}, }, { Name: "healthcheck", Aliases: []string{"hc"}, Usage: "Call healthcheck endpoint", Action: func(cCtx *cli.Context) error { _, err := http.Get(fmt.Sprintf("http://127.0.0.1:%s/ping", port)) if err != nil { os.Exit(1) } os.Exit(0) return nil }}}, Flags: []cli.Flag{ &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}}, &cli.StringFlag{ Name: "port", Aliases: []string{"p"}, EnvVars: []string{"PORT"}, Value: "3000", Usage: "Port to serve on", Destination: &port, }, }, EnableBashCompletion: true, Compiled: time.Time{}, Authors: []*cli.Author{}, Reader: nil, Writer: nil, ErrWriter: nil, SliceFlagSeparator: "", DisableSliceFlagSeparator: false, UseShortOptionHandling: true, Suggest: true, AllowExtFlags: false, SkipFlagParsing: false, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } }