Files
snice/main.go
Hadley Rich d703edb282
All checks were successful
Go / build (push) Successful in 1m20s
Tweak logging
2023-10-28 09:33:18 +13:00

213 lines
5.4 KiB
Go

package main
import (
"fmt"
"log/slog"
"net"
"net/http"
"os"
"strings"
"time"
"github.com/lmittmann/tint"
"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()
remoteAddr := r.RemoteAddr
fwdAddress := r.Header.Get("X-Forwarded-For")
if fwdAddress != "" {
remoteAddr = fwdAddress
ips := strings.Split(fwdAddress, ", ")
if len(ips) > 1 {
remoteAddr = ips[0]
}
}
remoteAddr, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
slog.Error("Failed to format remoteAddr", slog.Any("err", err))
}
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)
// t=2023-10-27T18:08:47.231895532+13:00
slog.Info("Request Completed:",
slog.String("method", r.Method),
slog.String("path", r.RequestURI),
slog.String("url", r.URL.Path),
slog.String("host", r.Host),
slog.String("referer", r.Referer()),
slog.String("remote_addr", remoteAddr),
slog.Int("status", responseData.status),
slog.String("duration", duration.String()),
slog.Int("size", responseData.size),
)
}
return http.HandlerFunc(loggingFn)
}
func pingHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
func main() {
logLevel := &slog.LevelVar{} // INFO
opts := tint.Options{
Level: logLevel,
}
logLevel.Set(slog.LevelDebug)
handler := tint.NewHandler(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.2.0",
DefaultCommand: "",
Commands: []*cli.Command{
{
Name: "serve",
Aliases: []string{"s"},
Usage: "Serve directory",
Action: func(cCtx *cli.Context) error {
var addr string = host + ":" + port
mux := http.NewServeMux()
mux.Handle("/ping", http.HandlerFunc(pingHandler))
fileHandler := http.FileServer(http.Dir(directory))
mux.Handle("/", fileHandler)
srv := &http.Server{
Addr: addr,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
srv.Handler = WithLogging(mux)
listener, err := net.Listen("tcp", addr)
if err != nil {
slog.Error("Listen error", err)
}
slog.Info("Starting server",
slog.String("dir", directory),
slog.String("addr", fmt.Sprintf(":%s", listener.Addr())),
)
err = srv.Serve(listener)
if err != nil {
slog.Error("Serve error", err)
}
return nil
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "directory",
Aliases: []string{"dir", "d"},
EnvVars: []string{"DIRECTORY"},
Value: "/srv",
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 {
url := fmt.Sprintf("http://127.0.0.1:%s/ping", port)
slog.Debug("Healthcheck: ", slog.String("url", url))
client := http.Client{
Timeout: 1 * time.Second,
}
res, err := client.Get(url)
if err != nil {
return cli.Exit("FAIL", 1)
}
if res.StatusCode == 200 {
return cli.Exit("OK", 0)
}
slog.Debug(fmt.Sprintf("Status: %d\n", res.StatusCode))
return cli.Exit("FAIL", 1)
}}},
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 {
os.Exit(1)
}
}