From 5a3fc6d908e7b53f42d62b422a4a30c5647e9e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Tue, 9 Sep 2025 16:08:50 +0200 Subject: [PATCH 1/7] feat: advanced logging --- cmd/server.go | 4 +++ server/handler/error.go | 3 ++ server/handler/handler.go | 57 ++++++++++++++++++++++++++++----- server/handler/handler_test.go | 31 +++++++++--------- server/handler/metrics.go | 11 ++++++- server/middleware/middleware.go | 37 +++++++++++++++++++++ server/server.go | 56 ++++++++++++++++++++++++++++---- vcgencmd/vcgencmd.go | 17 +++++----- 8 files changed, 178 insertions(+), 38 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index bcd8728..e414e83 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -23,6 +23,8 @@ func init() { serverCmd.Flags().StringP("token", "t", "", "Bearer Token for authentication") serverCmd.Flags().BoolP("metrics", "m", false, "Enable Prometheus metrics") serverCmd.Flags().BoolP("redoc", "r", false, "Enable ReDoc API documentation") + serverCmd.Flags().StringP("log-format", "l", "text", "Log format (text, structured, json)") + serverCmd.Flags().StringP("log-level", "L", "info", "Log level (debug, info, warn, error)") rootCmd.AddCommand(serverCmd) } @@ -36,6 +38,8 @@ func RunServerCmd(cmd *cobra.Command, args []string) error { config.Token, _ = cmd.Flags().GetString("token") config.Metrics, _ = cmd.Flags().GetBool("metrics") config.Redoc, _ = cmd.Flags().GetBool("redoc") + config.LogFormat, _ = cmd.Flags().GetString("log-format") + config.LogLevel, _ = cmd.Flags().GetString("log-level") server.Run(config) diff --git a/server/handler/error.go b/server/handler/error.go index 580bbb9..4e5a8a5 100644 --- a/server/handler/error.go +++ b/server/handler/error.go @@ -6,6 +6,7 @@ package handler import ( "encoding/json" + "log/slog" "net/http" "github.com/tschaefer/rpinfo/version" @@ -20,9 +21,11 @@ func JSONError(w http.ResponseWriter, status int, message string) { } func NotFoundHandler(w http.ResponseWriter, r *http.Request) { + go makeLog(r, http.StatusInternalServerError, slog.LevelError, "not found") JSONError(w, http.StatusNotFound, "not found") } func MethodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { + go makeLog(r, http.StatusMethodNotAllowed, slog.LevelWarn, "method not allowed") JSONError(w, http.StatusMethodNotAllowed, "method not allowed") } diff --git a/server/handler/handler.go b/server/handler/handler.go index 1d0b66d..5472024 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -7,6 +7,7 @@ package handler import ( "encoding/json" "iter" + "log/slog" "maps" "net/http" "strings" @@ -18,11 +19,13 @@ type Handle struct { Cmd vcgencmd.Exec } -func runCmd(h Handle, w http.ResponseWriter, args ...string) map[string]string { - out := h.Cmd.Run(args...) - if out == nil { +func runCmd(h Handle, w http.ResponseWriter, r *http.Request, args ...string) map[string]string { + out, err := h.Cmd.Run(args...) + if err != nil { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) + + go makeLog(r, http.StatusInternalServerError, slog.LevelError, err.Error()) json.NewEncoder(w).Encode(map[string]string{"detail": "internal server error"}) return nil } @@ -31,11 +34,12 @@ func runCmd(h Handle, w http.ResponseWriter, args ...string) map[string]string { } func (h Handle) Temperature(w http.ResponseWriter, r *http.Request) { - temp := runCmd(h, w, "measure_temp") + temp := runCmd(h, w, r, "measure_temp") if temp == nil { return } + go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched temperature") json.NewEncoder(w).Encode(temp) } @@ -43,7 +47,7 @@ func (h Handle) Configuration(w http.ResponseWriter, r *http.Request) { options := []string{"int", "str"} config := make(map[string]string) for _, opt := range options { - out := runCmd(h, w, "get_config", opt) + out := runCmd(h, w, r, "get_config", opt) if out == nil { return } @@ -51,6 +55,7 @@ func (h Handle) Configuration(w http.ResponseWriter, r *http.Request) { maps.Copy(config, out) } + go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched configuration") json.NewEncoder(w).Encode(config) } @@ -58,7 +63,7 @@ func (h Handle) Voltages(w http.ResponseWriter, r *http.Request) { options := []string{"core", "sdram_c", "sdram_i", "sdram_p"} voltages := make(map[string]string) for _, opt := range options { - out := runCmd(h, w, "measure_volts", opt) + out := runCmd(h, w, r, "measure_volts", opt) if out == nil { return } @@ -66,11 +71,12 @@ func (h Handle) Voltages(w http.ResponseWriter, r *http.Request) { voltages[opt] = out["volt"] } + go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched voltages") json.NewEncoder(w).Encode(voltages) } func (h Handle) Throttled(w http.ResponseWriter, r *http.Request) { - throttled := runCmd(h, w, "get_throttled") + throttled := runCmd(h, w, r, "get_throttled") if throttled == nil { return } @@ -84,6 +90,7 @@ func (h Handle) Throttled(w http.ResponseWriter, r *http.Request) { throttled["throttled"] = message } + go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched throttled status") json.NewEncoder(w).Encode(throttled) } @@ -95,7 +102,7 @@ func (h Handle) Clock(w http.ResponseWriter, r *http.Request) { } clock := make(map[string]string) for _, opt := range options { - out := runCmd(h, w, "measure_clock", opt) + out := runCmd(h, w, r, "measure_clock", opt) if out == nil { return } @@ -111,5 +118,39 @@ func (h Handle) Clock(w http.ResponseWriter, r *http.Request) { clock[opt] = value() } + go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched clock rates") json.NewEncoder(w).Encode(clock) } + +func makeLog(r *http.Request, status int, level slog.Level, msg string) { + forwardedHeaders := []string{ + "X-Forwarded-For", + "X-Real-IP", + } + remoteAddr := r.RemoteAddr + for _, header := range forwardedHeaders { + if ip := r.Header.Get(header); ip != "" { + remoteAddr = ip + break + } + } + + args := []any{ + slog.String("RemoteAddr", remoteAddr), + slog.String("UserAgent", r.UserAgent()), + slog.Int("Status", status), + slog.String("RequestMethod", r.Method), + slog.String("RequestPath", r.RequestURI), + } + + switch level { + case slog.LevelInfo: + slog.Info(msg, args...) + case slog.LevelWarn: + slog.Warn(msg, args...) + case slog.LevelError: + slog.Error(msg, args...) + default: + slog.Info(msg, args...) + } +} diff --git a/server/handler/handler_test.go b/server/handler/handler_test.go index b2225a8..ccd9dd3 100644 --- a/server/handler/handler_test.go +++ b/server/handler/handler_test.go @@ -5,6 +5,7 @@ Licensed under the MIT license, see LICENSE in the project root for details. package handler import ( + "fmt" "net/http" "net/http/httptest" "strings" @@ -15,45 +16,45 @@ import ( type mockRunnerSuccess struct{} -func (m mockRunnerSuccess) Run(args ...string) map[string]string { +func (m mockRunnerSuccess) Run(args ...string) (map[string]string, error) { switch args[0] { case "measure_temp": - return map[string]string{"temp": "45.0'C"} + return map[string]string{"temp": "45.0'C"}, nil case "measure_volts": switch args[1] { case "core": - return map[string]string{"volt": "1.3500V"} + return map[string]string{"volt": "1.3500V"}, nil case "sdram_c": - return map[string]string{"volt": "1.2000V"} + return map[string]string{"volt": "1.2000V"}, nil case "sdram_i": - return map[string]string{"volt": "1.2000V"} + return map[string]string{"volt": "1.2000V"}, nil case "sdram_p": - return map[string]string{"volt": "1.2250V"} + return map[string]string{"volt": "1.2250V"}, nil default: - return nil + return nil, nil } case "get_config": - return map[string]string{"init_uart_clock": "0x2dc6c00", "overlay_prefix": "overlays/", "total_mem": "512"} + return map[string]string{"init_uart_clock": "0x2dc6c00", "overlay_prefix": "overlays/", "total_mem": "512"}, nil case "get_throttled": - return map[string]string{"throttled": "0x50000"} + return map[string]string{"throttled": "0x50000"}, nil case "measure_clock": switch args[1] { case "arm": - return map[string]string{"freq": "600000000"} + return map[string]string{"freq": "600000000"}, nil case "core": - return map[string]string{"freq": "250000000"} + return map[string]string{"freq": "250000000"}, nil default: - return map[string]string{"freq": "0"} + return map[string]string{"freq": "0"}, nil } default: - return nil + return nil, nil } } type mockRunnerError struct{} -func (m mockRunnerError) Run(args ...string) map[string]string { - return nil +func (m mockRunnerError) Run(args ...string) (map[string]string, error) { + return nil, fmt.Errorf("command failed") } func Test_TemperatureReturnsJSON(t *testing.T) { diff --git a/server/handler/metrics.go b/server/handler/metrics.go index 4eceea6..a72c643 100644 --- a/server/handler/metrics.go +++ b/server/handler/metrics.go @@ -9,6 +9,7 @@ import ( "bytes" "fmt" "iter" + "log/slog" "maps" "net/http" "strconv" @@ -48,9 +49,12 @@ func Metrics(w http.ResponseWriter, r *http.Request) { var buffer bytes.Buffer rpi.WritePrometheus(&buffer) if _, err := w.Write(buffer.Bytes()); err != nil { + + go makeLog(r, http.StatusInternalServerError, slog.LevelError, fmt.Sprintf("Failed to write metrics: %v", err)) http.Error(w, "Failed to write metrics", http.StatusInternalServerError) return } + go makeLog(r, http.StatusOK, slog.LevelInfo, "Served metrics") } func clock(kind string) float64 { @@ -102,5 +106,10 @@ func voltage(kind string) float64 { func exec(args ...string) map[string]string { h := Handle{Cmd: vcgencmd.Cmd{}} - return h.Cmd.Run(args...) + out, err := h.Cmd.Run(args...) + if err != nil { + return nil + } + + return out } diff --git a/server/middleware/middleware.go b/server/middleware/middleware.go index 535d9c7..af68818 100644 --- a/server/middleware/middleware.go +++ b/server/middleware/middleware.go @@ -6,6 +6,7 @@ package middleware import ( "encoding/json" + "log/slog" "net/http" "strings" @@ -31,6 +32,7 @@ func RequestHeaders(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { accept := r.Header.Get("Accept") if accept == "" || (accept != "application/json" && accept != "*/*") { + go makeLog(r, http.StatusNotAcceptable, slog.LevelWarn, "not acceptable") JSONError(w, http.StatusNotAcceptable, "not acceptable") return } @@ -47,12 +49,14 @@ func Authorization(auth bool, token string, next http.HandlerFunc) http.HandlerF bearer := r.Header.Get("Authorization") if bearer == "" { + go makeLog(r, http.StatusUnauthorized, slog.LevelWarn, "unauthorized") JSONError(w, http.StatusUnauthorized, "unauthorized") return } parts := strings.SplitN(bearer, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" || parts[1] != token { + go makeLog(r, http.StatusForbidden, slog.LevelWarn, "forbidden") JSONError(w, http.StatusForbidden, "forbidden") return } @@ -69,3 +73,36 @@ func ApplyAll(auth bool, token string, next http.HandlerFunc) http.HandlerFunc { return next } + +func makeLog(r *http.Request, status int, level slog.Level, msg string) { + forwardedHeaders := []string{ + "X-Forwarded-For", + "X-Real-IP", + } + remoteAddr := r.RemoteAddr + for _, header := range forwardedHeaders { + if ip := r.Header.Get(header); ip != "" { + remoteAddr = ip + break + } + } + + args := []any{ + slog.String("RemoteAddr", remoteAddr), + slog.String("UserAgent", r.UserAgent()), + slog.Int("Status", status), + slog.String("RequestMethod", r.Method), + slog.String("RequestPath", r.RequestURI), + } + + switch level { + case slog.LevelInfo: + slog.Info(msg, args...) + case slog.LevelWarn: + slog.Warn(msg, args...) + case slog.LevelError: + slog.Error(msg, args...) + default: + slog.Info(msg, args...) + } +} diff --git a/server/server.go b/server/server.go index 83c5578..c624269 100644 --- a/server/server.go +++ b/server/server.go @@ -7,7 +7,9 @@ package server import ( "fmt" "log" + "log/slog" "net/http" + "os" "time" "github.com/gorilla/mux" @@ -19,12 +21,14 @@ import ( ) type Config struct { - Port string - Host string - Auth bool - Token string - Metrics bool - Redoc bool + Port string + Host string + Auth bool + Token string + Metrics bool + Redoc bool + LogFormat string + LogLevel string } func Run(config Config) { @@ -48,6 +52,10 @@ func Run(config Config) { router.NotFoundHandler = http.HandlerFunc(handler.NotFoundHandler) router.MethodNotAllowedHandler = http.HandlerFunc(handler.MethodNotAllowedHandler) + if err := setLogger(config); err != nil { + log.Fatalf("Failed to set logger: %v", err) + } + server := &http.Server{ Addr: fmt.Sprintf("%s:%s", config.Host, config.Port), ReadTimeout: 5 * time.Second, @@ -60,3 +68,39 @@ func Run(config Config) { log.Printf("Listening on %s:%s, auth: %t, metrics: %t, redoc: %t", config.Host, config.Port, config.Auth, config.Metrics, config.Redoc) log.Fatal(server.ListenAndServe()) } + +func setLogger(config Config) error { + var leveler slog.Leveler + switch config.LogLevel { + case "debug": + leveler = slog.LevelDebug + case "info": + leveler = slog.LevelInfo + case "warn": + leveler = slog.LevelWarn + case "error": + leveler = slog.LevelError + default: + return fmt.Errorf("unknown log level: %s", config.LogLevel) + } + + opts := &slog.HandlerOptions{ + Level: leveler, + } + + var logger *slog.Logger + switch config.LogFormat { + case "structured": + logger = slog.New(slog.NewTextHandler(os.Stdout, opts)) + case "json": + logger = slog.New(slog.NewJSONHandler(os.Stdout, opts)) + case "text": + // Use default logger, print info level only. + return nil + default: + return fmt.Errorf("unknown log format: %s", config.LogFormat) + } + slog.SetDefault(logger) + + return nil +} diff --git a/vcgencmd/vcgencmd.go b/vcgencmd/vcgencmd.go index d81b444..c51dc42 100644 --- a/vcgencmd/vcgencmd.go +++ b/vcgencmd/vcgencmd.go @@ -5,27 +5,28 @@ Licensed under the MIT license, see LICENSE in the project root for details. package vcgencmd import ( - "log" + "fmt" + "log/slog" "os/exec" "strings" ) type Exec interface { - Run(args ...string) map[string]string + Run(args ...string) (map[string]string, error) } type Cmd struct{} -func (r Cmd) Run(args ...string) map[string]string { +func (r Cmd) Run(args ...string) (map[string]string, error) { execCommand := exec.Command("vcgencmd", args...) out, err := execCommand.Output() if err != nil { if out != nil { - log.Printf("vcgencmd error: %s", strings.TrimSpace(string(out))) + err = fmt.Errorf("vcgencmd error: %s - %v", strings.TrimSpace(string(out)), err) } else { - log.Printf("vcgencmd error: %s", err) + err = fmt.Errorf("vcgencmd error: %v", err) } - return nil + return nil, err } output := strings.TrimSpace(string(out)) @@ -34,7 +35,7 @@ func (r Cmd) Run(args ...string) map[string]string { for line := range strings.SplitSeq(output, "\n") { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { - log.Printf("vcgencmd warn: skipping data: %s", line) + slog.Warn(fmt.Sprintf("vcgencmd warn: skipping data: %s", line)) continue } @@ -43,5 +44,5 @@ func (r Cmd) Run(args ...string) map[string]string { outputMap[key] = value } - return outputMap + return outputMap, nil } From 0e45c853058f1233d62bf7e438f30cfca5d3c9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Wed, 10 Sep 2025 08:17:29 +0200 Subject: [PATCH 2/7] feat(log): centralize log --- server/handler/error.go | 5 +- server/handler/handler.go | 47 +++-------------- server/handler/metrics.go | 6 +-- server/log/log.go | 93 +++++++++++++++++++++++++++++++++ server/middleware/middleware.go | 41 ++------------- server/server.go | 50 ++++-------------- 6 files changed, 119 insertions(+), 123 deletions(-) create mode 100644 server/log/log.go diff --git a/server/handler/error.go b/server/handler/error.go index 4e5a8a5..73a1ecc 100644 --- a/server/handler/error.go +++ b/server/handler/error.go @@ -9,6 +9,7 @@ import ( "log/slog" "net/http" + "github.com/tschaefer/rpinfo/server/log" "github.com/tschaefer/rpinfo/version" ) @@ -21,11 +22,11 @@ func JSONError(w http.ResponseWriter, status int, message string) { } func NotFoundHandler(w http.ResponseWriter, r *http.Request) { - go makeLog(r, http.StatusInternalServerError, slog.LevelError, "not found") + go log.Request(r, http.StatusInternalServerError, slog.LevelError, "not found") JSONError(w, http.StatusNotFound, "not found") } func MethodNotAllowedHandler(w http.ResponseWriter, r *http.Request) { - go makeLog(r, http.StatusMethodNotAllowed, slog.LevelWarn, "method not allowed") + go log.Request(r, http.StatusMethodNotAllowed, slog.LevelWarn, "method not allowed") JSONError(w, http.StatusMethodNotAllowed, "method not allowed") } diff --git a/server/handler/handler.go b/server/handler/handler.go index 5472024..a70d503 100644 --- a/server/handler/handler.go +++ b/server/handler/handler.go @@ -7,11 +7,11 @@ package handler import ( "encoding/json" "iter" - "log/slog" "maps" "net/http" "strings" + "github.com/tschaefer/rpinfo/server/log" "github.com/tschaefer/rpinfo/vcgencmd" ) @@ -25,7 +25,7 @@ func runCmd(h Handle, w http.ResponseWriter, r *http.Request, args ...string) ma w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - go makeLog(r, http.StatusInternalServerError, slog.LevelError, err.Error()) + go log.RequestError(r, http.StatusInternalServerError, err.Error()) json.NewEncoder(w).Encode(map[string]string{"detail": "internal server error"}) return nil } @@ -39,7 +39,7 @@ func (h Handle) Temperature(w http.ResponseWriter, r *http.Request) { return } - go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched temperature") + go log.RequestInfo(r, http.StatusOK, "Fetched temperature") json.NewEncoder(w).Encode(temp) } @@ -55,7 +55,7 @@ func (h Handle) Configuration(w http.ResponseWriter, r *http.Request) { maps.Copy(config, out) } - go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched configuration") + go log.RequestInfo(r, http.StatusOK, "Fetched configuration") json.NewEncoder(w).Encode(config) } @@ -71,7 +71,7 @@ func (h Handle) Voltages(w http.ResponseWriter, r *http.Request) { voltages[opt] = out["volt"] } - go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched voltages") + go log.RequestInfo(r, http.StatusOK, "Fetched voltages") json.NewEncoder(w).Encode(voltages) } @@ -90,7 +90,7 @@ func (h Handle) Throttled(w http.ResponseWriter, r *http.Request) { throttled["throttled"] = message } - go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched throttled status") + go log.RequestInfo(r, http.StatusOK, "Fetched throttled status") json.NewEncoder(w).Encode(throttled) } @@ -118,39 +118,6 @@ func (h Handle) Clock(w http.ResponseWriter, r *http.Request) { clock[opt] = value() } - go makeLog(r, http.StatusOK, slog.LevelInfo, "Fetched clock rates") + go log.RequestInfo(r, http.StatusOK, "Fetched clock rates") json.NewEncoder(w).Encode(clock) } - -func makeLog(r *http.Request, status int, level slog.Level, msg string) { - forwardedHeaders := []string{ - "X-Forwarded-For", - "X-Real-IP", - } - remoteAddr := r.RemoteAddr - for _, header := range forwardedHeaders { - if ip := r.Header.Get(header); ip != "" { - remoteAddr = ip - break - } - } - - args := []any{ - slog.String("RemoteAddr", remoteAddr), - slog.String("UserAgent", r.UserAgent()), - slog.Int("Status", status), - slog.String("RequestMethod", r.Method), - slog.String("RequestPath", r.RequestURI), - } - - switch level { - case slog.LevelInfo: - slog.Info(msg, args...) - case slog.LevelWarn: - slog.Warn(msg, args...) - case slog.LevelError: - slog.Error(msg, args...) - default: - slog.Info(msg, args...) - } -} diff --git a/server/handler/metrics.go b/server/handler/metrics.go index a72c643..d86ee66 100644 --- a/server/handler/metrics.go +++ b/server/handler/metrics.go @@ -9,12 +9,12 @@ import ( "bytes" "fmt" "iter" - "log/slog" "maps" "net/http" "strconv" "github.com/VictoriaMetrics/metrics" + "github.com/tschaefer/rpinfo/server/log" "github.com/tschaefer/rpinfo/vcgencmd" "github.com/tschaefer/rpinfo/version" ) @@ -50,11 +50,11 @@ func Metrics(w http.ResponseWriter, r *http.Request) { rpi.WritePrometheus(&buffer) if _, err := w.Write(buffer.Bytes()); err != nil { - go makeLog(r, http.StatusInternalServerError, slog.LevelError, fmt.Sprintf("Failed to write metrics: %v", err)) + go log.RequestError(r, http.StatusInternalServerError, fmt.Sprintf("Failed to write metrics: %v", err)) http.Error(w, "Failed to write metrics", http.StatusInternalServerError) return } - go makeLog(r, http.StatusOK, slog.LevelInfo, "Served metrics") + go log.RequestInfo(r, http.StatusOK, "Served metrics") } func clock(kind string) float64 { diff --git a/server/log/log.go b/server/log/log.go new file mode 100644 index 0000000..058cc0d --- /dev/null +++ b/server/log/log.go @@ -0,0 +1,93 @@ +/* +Copyright (c) 2025 Tobias Schäfer. All rights reserved. +Licensed under the MIT license, see LICENSE in the project root for details. +*/ +package log + +import ( + "fmt" + "log/slog" + "net/http" + "os" +) + +func Logger(level, format string) error { + var leveler slog.Leveler + switch level { + case "debug": + leveler = slog.LevelDebug + case "info": + leveler = slog.LevelInfo + case "warn": + leveler = slog.LevelWarn + case "error": + leveler = slog.LevelError + default: + return fmt.Errorf("unknown log level: %s", level) + } + + opts := &slog.HandlerOptions{ + Level: leveler, + } + + var logger *slog.Logger + switch format { + case "structured": + logger = slog.New(slog.NewTextHandler(os.Stdout, opts)) + case "json": + logger = slog.New(slog.NewJSONHandler(os.Stdout, opts)) + case "text": + // Use default logger, print info level only. + return nil + default: + return fmt.Errorf("unknown log format: %s", format) + } + slog.SetDefault(logger) + + return nil +} + +func Request(r *http.Request, status int, level slog.Level, msg string) { + forwardedHeaders := []string{ + "X-Forwarded-For", + "X-Real-IP", + } + remoteAddr := r.RemoteAddr + for _, header := range forwardedHeaders { + if ip := r.Header.Get(header); ip != "" { + remoteAddr = ip + break + } + } + + args := []any{ + slog.String("RemoteAddr", remoteAddr), + slog.String("UserAgent", r.UserAgent()), + slog.Int("Status", status), + slog.String("RequestMethod", r.Method), + slog.String("RequestPath", r.RequestURI), + } + + switch level { + case slog.LevelInfo: + slog.Info(msg, args...) + case slog.LevelWarn: + slog.Warn(msg, args...) + case slog.LevelError: + slog.Error(msg, args...) + default: + slog.Info(msg, args...) + } +} + +func RequestInfo(r *http.Request, status int, msg string) { + Request(r, status, slog.LevelInfo, msg) +} + +func RequestWarn(r *http.Request, status int, msg string) { + Request(r, status, slog.LevelWarn, msg) +} + +func RequestError(r *http.Request, status int, msg string) { + Request(r, status, slog.LevelError, msg) +} diff --git a/server/middleware/middleware.go b/server/middleware/middleware.go index af68818..99c1c28 100644 --- a/server/middleware/middleware.go +++ b/server/middleware/middleware.go @@ -6,10 +6,10 @@ package middleware import ( "encoding/json" - "log/slog" "net/http" "strings" + "github.com/tschaefer/rpinfo/server/log" "github.com/tschaefer/rpinfo/version" ) @@ -32,7 +32,7 @@ func RequestHeaders(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { accept := r.Header.Get("Accept") if accept == "" || (accept != "application/json" && accept != "*/*") { - go makeLog(r, http.StatusNotAcceptable, slog.LevelWarn, "not acceptable") + go log.RequestWarn(r, http.StatusNotAcceptable, "not acceptable") JSONError(w, http.StatusNotAcceptable, "not acceptable") return } @@ -49,14 +49,14 @@ func Authorization(auth bool, token string, next http.HandlerFunc) http.HandlerF bearer := r.Header.Get("Authorization") if bearer == "" { - go makeLog(r, http.StatusUnauthorized, slog.LevelWarn, "unauthorized") + go log.RequestWarn(r, http.StatusUnauthorized, "unauthorized") JSONError(w, http.StatusUnauthorized, "unauthorized") return } parts := strings.SplitN(bearer, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" || parts[1] != token { - go makeLog(r, http.StatusForbidden, slog.LevelWarn, "forbidden") + go log.RequestWarn(r, http.StatusForbidden, "forbidden") JSONError(w, http.StatusForbidden, "forbidden") return } @@ -73,36 +73,3 @@ func ApplyAll(auth bool, token string, next http.HandlerFunc) http.HandlerFunc { return next } - -func makeLog(r *http.Request, status int, level slog.Level, msg string) { - forwardedHeaders := []string{ - "X-Forwarded-For", - "X-Real-IP", - } - remoteAddr := r.RemoteAddr - for _, header := range forwardedHeaders { - if ip := r.Header.Get(header); ip != "" { - remoteAddr = ip - break - } - } - - args := []any{ - slog.String("RemoteAddr", remoteAddr), - slog.String("UserAgent", r.UserAgent()), - slog.Int("Status", status), - slog.String("RequestMethod", r.Method), - slog.String("RequestPath", r.RequestURI), - } - - switch level { - case slog.LevelInfo: - slog.Info(msg, args...) - case slog.LevelWarn: - slog.Warn(msg, args...) - case slog.LevelError: - slog.Error(msg, args...) - default: - slog.Info(msg, args...) - } -} diff --git a/server/server.go b/server/server.go index c624269..2d47eb5 100644 --- a/server/server.go +++ b/server/server.go @@ -6,7 +6,6 @@ package server import ( "fmt" - "log" "log/slog" "net/http" "os" @@ -15,6 +14,7 @@ import ( "github.com/gorilla/mux" "github.com/tschaefer/rpinfo/server/assets" "github.com/tschaefer/rpinfo/server/handler" + "github.com/tschaefer/rpinfo/server/log" "github.com/tschaefer/rpinfo/server/middleware" "github.com/tschaefer/rpinfo/vcgencmd" "github.com/tschaefer/rpinfo/version" @@ -52,8 +52,9 @@ func Run(config Config) { router.NotFoundHandler = http.HandlerFunc(handler.NotFoundHandler) router.MethodNotAllowedHandler = http.HandlerFunc(handler.MethodNotAllowedHandler) - if err := setLogger(config); err != nil { - log.Fatalf("Failed to set logger: %v", err) + if err := log.Logger(config.LogLevel, config.LogFormat); err != nil { + slog.Error(fmt.Sprintf("Failed to set logger: %v", err)) + os.Exit(1) } server := &http.Server{ @@ -64,43 +65,10 @@ func Run(config Config) { Handler: router, } - log.Printf("Starting rpinfo server. Version: %s - %s", version.Release(), version.Commit()) - log.Printf("Listening on %s:%s, auth: %t, metrics: %t, redoc: %t", config.Host, config.Port, config.Auth, config.Metrics, config.Redoc) - log.Fatal(server.ListenAndServe()) -} - -func setLogger(config Config) error { - var leveler slog.Leveler - switch config.LogLevel { - case "debug": - leveler = slog.LevelDebug - case "info": - leveler = slog.LevelInfo - case "warn": - leveler = slog.LevelWarn - case "error": - leveler = slog.LevelError - default: - return fmt.Errorf("unknown log level: %s", config.LogLevel) - } - - opts := &slog.HandlerOptions{ - Level: leveler, + slog.Info(fmt.Sprintf("Starting rpinfo server. Version: %s - %s", version.Release(), version.Commit())) + slog.Info(fmt.Sprintf("Listening on %s:%s, auth: %t, metrics: %t, redoc: %t", config.Host, config.Port, config.Auth, config.Metrics, config.Redoc)) + if err := server.ListenAndServe(); err != nil { + slog.Error(fmt.Sprintf("Failed to start server: %v", err)) + os.Exit(1) } - - var logger *slog.Logger - switch config.LogFormat { - case "structured": - logger = slog.New(slog.NewTextHandler(os.Stdout, opts)) - case "json": - logger = slog.New(slog.NewJSONHandler(os.Stdout, opts)) - case "text": - // Use default logger, print info level only. - return nil - default: - return fmt.Errorf("unknown log format: %s", config.LogFormat) - } - slog.SetDefault(logger) - - return nil } From 5d6fb4ddaa92d43a6489d6d1d6e8702d9235e49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Wed, 10 Sep 2025 13:33:23 +0200 Subject: [PATCH 3/7] feat(log): tidy up --- cmd/server.go | 16 ++++++++-------- server/log/log.go | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index e414e83..8e49b90 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -17,14 +17,14 @@ var serverCmd = &cobra.Command{ } func init() { - serverCmd.Flags().StringP("port", "p", "8080", "Port to run the server on") - serverCmd.Flags().StringP("host", "H", "localhost", "Host to run the server on") - serverCmd.Flags().BoolP("auth", "a", false, "Enable authentication") - serverCmd.Flags().StringP("token", "t", "", "Bearer Token for authentication") - serverCmd.Flags().BoolP("metrics", "m", false, "Enable Prometheus metrics") - serverCmd.Flags().BoolP("redoc", "r", false, "Enable ReDoc API documentation") - serverCmd.Flags().StringP("log-format", "l", "text", "Log format (text, structured, json)") - serverCmd.Flags().StringP("log-level", "L", "info", "Log level (debug, info, warn, error)") + serverCmd.Flags().String("port", "8080", "Port to run the server on") + serverCmd.Flags().String("host", "localhost", "Host to run the server on") + serverCmd.Flags().Bool("auth", false, "Enable authentication") + serverCmd.Flags().String("token", "", "Bearer Token for authentication") + serverCmd.Flags().Bool("metrics", false, "Enable Prometheus metrics") + serverCmd.Flags().Bool("redoc", false, "Enable ReDoc API documentation") + serverCmd.Flags().String("log-format", "text", "Log format (text, structured, json)") + serverCmd.Flags().String("log-level", "info", "Log level (debug, info, warn, error)") rootCmd.AddCommand(serverCmd) } diff --git a/server/log/log.go b/server/log/log.go index 058cc0d..afdd825 100644 --- a/server/log/log.go +++ b/server/log/log.go @@ -75,11 +75,17 @@ func Request(r *http.Request, status int, level slog.Level, msg string) { slog.Warn(msg, args...) case slog.LevelError: slog.Error(msg, args...) + case slog.LevelDebug: + slog.Debug(msg, args...) default: slog.Info(msg, args...) } } +func RequestDebug(r *http.Request, status int, msg string) { + Request(r, status, slog.LevelDebug, msg) +} + func RequestInfo(r *http.Request, status int, msg string) { Request(r, status, slog.LevelInfo, msg) } From 5fe08dd365fea5ff5d7ac648fa894c11abc1a5be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Thu, 11 Sep 2025 07:44:55 +0200 Subject: [PATCH 4/7] feat(log): add tests --- go.mod | 6 ++- go.sum | 8 +++ server/log/log_test.go | 117 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 server/log/log_test.go diff --git a/go.mod b/go.mod index 67f7bd8..2bc12ad 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,19 @@ module github.com/tschaefer/rpinfo go 1.24.2 require ( + github.com/VictoriaMetrics/metrics v1.38.0 github.com/gorilla/mux v1.8.1 github.com/spf13/cobra v1.9.1 - github.com/VictoriaMetrics/metrics v1.38.0 + github.com/stretchr/testify v1.11.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect golang.org/x/sys v0.15.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 9dcc3d5..180da11 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,28 @@ github.com/VictoriaMetrics/metrics v1.38.0 h1:1d0dRgVH8Nnu8dKMfisKefPC3q7gqf3/odyO0quAvyA= github.com/VictoriaMetrics/metrics v1.38.0/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/log/log_test.go b/server/log/log_test.go new file mode 100644 index 0000000..5d306fb --- /dev/null +++ b/server/log/log_test.go @@ -0,0 +1,117 @@ +/* +Copyright (c) 2025 Tobias Schäfer. All rights reserved. +Licensed under the MIT license, see LICENSE in the project root for details. +*/ +package log + +import ( + "fmt" + "io" + "log/slog" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_LoggerReturnsErrorIfLevelIsUnknown(t *testing.T) { + err := Logger("fatal", "structured") + assert.NotNil(t, err) + assert.Equal(t, fmt.Errorf("unknown log level: fatal"), err) +} + +func Test_LoggerReturnsErrorIfFormatIsUnknown(t *testing.T) { + err := Logger("info", "xml") + assert.NotNil(t, err) + assert.Equal(t, fmt.Errorf("unknown log format: xml"), err) +} + +func Test_LoggerReturnsNoErrorIfLevelAndFormatAreKnown(t *testing.T) { + levels := []string{"debug", "info", "warn", "error"} + formats := []string{"structured", "json", "text"} + + for _, level := range levels { + for _, format := range formats { + err := Logger(level, format) + assert.Nil(t, err) + } + } +} + +func Test_RequestWritesLogMessage(t *testing.T) { + var b strings.Builder + w := io.Writer(&b) + h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: slog.LevelInfo}) + l := slog.New(h) + slog.SetDefault(l) + + r := httptest.NewRequest("GET", "/temperature", nil) + r.Header.Set("User-Agent", "rpinfo/1.0") + Request(r, http.StatusOK, slog.LevelInfo, "This is a message") + + assert.Contains(t, b.String(), `"level":"INFO"`) + assert.Contains(t, b.String(), `"msg":"This is a message"`) + assert.Contains(t, b.String(), `"RemoteAddr":"`+r.RemoteAddr+`"`) + assert.Contains(t, b.String(), `"UserAgent":"rpinfo/1.0"`) + assert.Contains(t, b.String(), `"Status":200`) + assert.Contains(t, b.String(), `"RequestMethod":"GET"`) + assert.Contains(t, b.String(), `"RequestPath":"/temperature"`) +} + +func Test_RequestDebugWritesDebugMessage(t *testing.T) { + var b strings.Builder + w := io.Writer(&b) + h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: slog.LevelDebug}) + l := slog.New(h) + slog.SetDefault(l) + + r := httptest.NewRequest("GET", "/temperature", nil) + RequestDebug(r, http.StatusOK, "This is a debug message") + + assert.Contains(t, b.String(), `"level":"DEBUG"`) + assert.Contains(t, b.String(), `"msg":"This is a debug message"`) +} + +func Test_RequestErrorWritesErrorMessage(t *testing.T) { + var b strings.Builder + w := io.Writer(&b) + h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: slog.LevelError}) + l := slog.New(h) + slog.SetDefault(l) + + r := httptest.NewRequest("GET", "/temperature", nil) + RequestError(r, http.StatusInternalServerError, "This is a error message") + + assert.Contains(t, b.String(), `"level":"ERROR"`) + assert.Contains(t, b.String(), `"msg":"This is a error message"`) +} + +func Test_RequestWarnWritesWarnMessage(t *testing.T) { + var b strings.Builder + w := io.Writer(&b) + h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: slog.LevelWarn}) + l := slog.New(h) + slog.SetDefault(l) + + r := httptest.NewRequest("GET", "/temperature", nil) + RequestWarn(r, http.StatusInternalServerError, "This is a warning message") + + assert.Contains(t, b.String(), `"level":"WARN"`) + assert.Contains(t, b.String(), `"msg":"This is a warning message"`) +} + +func Test_RequestInfoWritesInfoMessage(t *testing.T) { + var b strings.Builder + w := io.Writer(&b) + h := slog.NewJSONHandler(w, &slog.HandlerOptions{Level: slog.LevelInfo}) + l := slog.New(h) + slog.SetDefault(l) + + r := httptest.NewRequest("GET", "/temperature", nil) + RequestInfo(r, http.StatusOK, "This is a info message") + + assert.Contains(t, b.String(), `"level":"INFO"`) + assert.Contains(t, b.String(), `"msg":"This is a info message"`) +} From d2abafcc730c505d003f155df05ddae76ba8d2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Thu, 11 Sep 2025 07:53:01 +0200 Subject: [PATCH 5/7] chore(make): add test target --- Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 31aa05d..d3f6830 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ GitCommit := $(shell git rev-parse HEAD) LDFLAGS := "-s -w -X github.com/tschaefer/rpinfo/version.Version=$(Version) -X github.com/tschaefer/rpinfo/version.GitCommit=$(GitCommit)" .PHONY: all -all: fmt lint dist +all: fmt lint test dist .PHONY: fmt fmt: @@ -29,6 +29,10 @@ checksum: done && \ cd .. +.PHONY: test +test: + test -z $(shell go test ./... 2>&1 >/dev/null || echo 1) || (echo "[WARN] Fix test issues" && exit 1) + .PHONY: clean clean: rm -rf bin From eb8413f85dc1a5ed239a386c43fa64dfd3ee3cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Thu, 11 Sep 2025 08:22:04 +0200 Subject: [PATCH 6/7] docs: add log info --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 03e7e22..c6b892b 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,17 @@ Start the server on `localhost:8080` by default. ``` For further configuration, see the command-line options below. -| Flag | Description | Default | -|-------------------|--------------------------------------|-------------| -| `-H`, `--host` | Host to bind the server to | `localhost` | -| `-p`, `--port` | Port to run the server on | `8080` | -| `-a`, `--auth` | Enable bearer token authentication | `false` | -| `-t`, `--token` | Bearer token used for authentication | | -| `-m`, `--metrics` | Enable Prometheus metrics endpoint | `false` | -| `-r`, `--redoc` | Enable ReDoc API documentation | `false` | -| `-h`, `--help` | Show help for the server command | | +| Flag | Description | Default | +|----------------|-------------------------------------------------|-------------| +| `--host` | Host to bind the server to | `localhost` | +| `--port` | Port to run the server on | `8080` | +| `--auth` | Enable bearer token authentication | `false` | +| `--token` | Bearer token used for authentication | | +| `--metrics` | Enable Prometheus metrics endpoint | `false` | +| `--redoc` | Enable ReDoc API documentation | `false` | +| `--log-format` | Set log format: `text`, `structured`, `json` | `text` | +| `--log-level` | Set log level: `debug`, `info`, `warn`, `error` | `info` | +| `--help` | Show help for the server command | | Additional a systemd service file and environment file are provided in the [contrib directory](https://github.com/tschaefer/rpinfo/tree/main/contrib) for automatic startup on boot and management of the From 40105329ed3f2cb337bd1da722b3d686993ce975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Sch=C3=A4fer?= Date: Thu, 11 Sep 2025 09:53:11 +0200 Subject: [PATCH 7/7] feat(log): re-add short flags --- README.md | 22 +++++++++++----------- cmd/server.go | 16 ++++++++-------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c6b892b..7b54bbe 100644 --- a/README.md +++ b/README.md @@ -31,17 +31,17 @@ Start the server on `localhost:8080` by default. ``` For further configuration, see the command-line options below. -| Flag | Description | Default | -|----------------|-------------------------------------------------|-------------| -| `--host` | Host to bind the server to | `localhost` | -| `--port` | Port to run the server on | `8080` | -| `--auth` | Enable bearer token authentication | `false` | -| `--token` | Bearer token used for authentication | | -| `--metrics` | Enable Prometheus metrics endpoint | `false` | -| `--redoc` | Enable ReDoc API documentation | `false` | -| `--log-format` | Set log format: `text`, `structured`, `json` | `text` | -| `--log-level` | Set log level: `debug`, `info`, `warn`, `error` | `info` | -| `--help` | Show help for the server command | | +| Flag | Description | Default | +|----------------------|-------------------------------------------------|-------------| +| `-H`, `--host` | Host to bind the server to | `localhost` | +| `-p`, `--port` | Port to run the server on | `8080` | +| `-a`, `--auth` | Enable bearer token authentication | `false` | +| `-t`, `--token` | Bearer token used for authentication | | +| `-m`, `--metrics` | Enable Prometheus metrics endpoint | `false` | +| `-r`, `--redoc` | Enable ReDoc API documentation | `false` | +| `-f`, `--log-format` | Set log format: `text`, `structured`, `json` | `text` | +| `-l`, `--log-level` | Set log level: `debug`, `info`, `warn`, `error` | `info` | +| `-h`, `--help` | Show help for the server command | | Additional a systemd service file and environment file are provided in the [contrib directory](https://github.com/tschaefer/rpinfo/tree/main/contrib) for automatic startup on boot and management of the diff --git a/cmd/server.go b/cmd/server.go index 8e49b90..de29794 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -17,14 +17,14 @@ var serverCmd = &cobra.Command{ } func init() { - serverCmd.Flags().String("port", "8080", "Port to run the server on") - serverCmd.Flags().String("host", "localhost", "Host to run the server on") - serverCmd.Flags().Bool("auth", false, "Enable authentication") - serverCmd.Flags().String("token", "", "Bearer Token for authentication") - serverCmd.Flags().Bool("metrics", false, "Enable Prometheus metrics") - serverCmd.Flags().Bool("redoc", false, "Enable ReDoc API documentation") - serverCmd.Flags().String("log-format", "text", "Log format (text, structured, json)") - serverCmd.Flags().String("log-level", "info", "Log level (debug, info, warn, error)") + serverCmd.Flags().StringP("port", "p", "8080", "Port to run the server on") + serverCmd.Flags().StringP("host", "H", "localhost", "Host to run the server on") + serverCmd.Flags().BoolP("auth", "a", false, "Enable authentication") + serverCmd.Flags().StringP("token", "t", "", "Bearer Token for authentication") + serverCmd.Flags().BoolP("metrics", "m", false, "Enable Prometheus metrics") + serverCmd.Flags().BoolP("redoc", "r", false, "Enable ReDoc API documentation") + serverCmd.Flags().StringP("log-format", "f", "text", "Log format (text, structured, json)") + serverCmd.Flags().StringP("log-level", "l", "info", "Log level (debug, info, warn, error)") rootCmd.AddCommand(serverCmd) }