From 5d2a3ead05e9041d916af586e836c2902128df9a Mon Sep 17 00:00:00 2001 From: Adam Magued Date: Sat, 17 Oct 2026 17:51:48 +0300 Subject: [PATCH] fix: resolve issue #118 - Avoid logging for some "paths" Signed-off-by: Adam Magued --- README.md | 1 + config.go | 34 +++++++ doc.go | 1 + http_middlewares.go | 6 +- shell2http.1 | 1 + shell2http.go | 9 +- shell2http_test.go | 218 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 267 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e763f25..ae111a8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Usage -basic-auth="" : setup HTTP Basic Authentication ("user_name:password"), can be used several times -timeout=N : set timeout for execute shell command (in seconds) -no-log-timestamp : log output without timestamps + -avoid-log-for=url: don't log requests for specified paths ("path1,path2,...") -version -help diff --git a/config.go b/config.go index 4530240..ec36ade 100644 --- a/config.go +++ b/config.go @@ -68,6 +68,8 @@ type Config struct { showErrors bool // returns the standard output even if the command exits with a non-zero exit code includeStderr bool // also returns output written to stderr (default is stdout only) intServerErr bool // return 500 error if shell status code != 0 + avoidLogFor string // don't log requests for specified paths (comma separated: "path1,path2,...") + avoidLogPaths map[string]bool // parsed set of paths to avoid logging formCheckRe *regexp.Regexp // regexp for check form fields } @@ -104,6 +106,7 @@ func getConfig() (*Config, error) { flag.BoolVar(&cfg.showErrors, "show-errors", false, "show the standard output even if the command exits with a non-zero exit code") flag.BoolVar(&cfg.includeStderr, "include-stderr", false, "include stderr to output (default is stdout only)") flag.BoolVar(&cfg.intServerErr, "500", false, "return 500 error if shell exit code != 0") + flag.StringVar(&cfg.avoidLogFor, "avoid-log-for", "", "don't log requests for specified paths (\"path1,path2,...\")") flag.StringVar(&cfg.cert, "cert", "", "SSL certificate `path` (if specified -cert/-key options - run https server)") flag.StringVar(&cfg.key, "key", "", "SSL private key `/path/...`") flag.Var(&cfg.auth, "basic-auth", "setup HTTP Basic Authentication (\"user_name:password\"), can be used several times") @@ -163,9 +166,40 @@ func getConfig() (*Config, error) { cfg.formCheckRe = re } + if len(cfg.avoidLogFor) > 0 { + cfg.avoidLogPaths = make(map[string]bool) + for _, rawPath := range strings.Split(cfg.avoidLogFor, ",") { + p := strings.TrimSpace(rawPath) + if p == "" { + continue + } + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + cfg.avoidLogPaths[p] = true + if len(p) > 1 && strings.HasSuffix(p, "/") { + cfg.avoidLogPaths[strings.TrimSuffix(p, "/")] = true + } + } + } + return &cfg, nil } +// isAvoidLog - check if path is in avoid-log list +func (cfg Config) isAvoidLog(path string) bool { + if len(cfg.avoidLogPaths) == 0 { + return false + } + if cfg.avoidLogPaths[path] { + return true + } + if len(path) > 1 && strings.HasSuffix(path, "/") { + return cfg.avoidLogPaths[strings.TrimSuffix(path, "/")] + } + return cfg.avoidLogPaths[path+"/"] +} + // readableURL - get readable URL for logging func (cfg Config) readableURL(addr fmt.Stringer) string { prefix := "http" diff --git a/doc.go b/doc.go index bdbc067..f3932d2 100644 --- a/doc.go +++ b/doc.go @@ -43,6 +43,7 @@ Usage: -basic-auth="" : setup HTTP Basic Authentication ("user_name:password"), can be used several times -timeout=N : set timeout for execute shell command (in seconds) -no-log-timestamp : log output without timestamps + -avoid-log-for=url: don't log requests for specified paths ("path1,path2,...") -version -help diff --git a/http_middlewares.go b/http_middlewares.go index c63f455..288fb43 100644 --- a/http_middlewares.go +++ b/http_middlewares.go @@ -68,8 +68,12 @@ func mwBasicAuth(handler http.HandlerFunc, users authUsers) http.HandlerFunc { } // mwLogging - add logging for handler -func mwLogging(handler http.HandlerFunc) http.HandlerFunc { +func mwLogging(handler http.HandlerFunc, isAvoidLog ...func(path string) bool) http.HandlerFunc { return func(rw http.ResponseWriter, req *http.Request) { + if len(isAvoidLog) > 0 && isAvoidLog[0] != nil && isAvoidLog[0](req.URL.Path) { + handler.ServeHTTP(rw, req) + return + } remoteAddr := req.RemoteAddr if realIP, ok := req.Header["X-Real-Ip"]; ok && len(realIP) > 0 { remoteAddr = realIP[0] + ", " + remoteAddr diff --git a/shell2http.1 b/shell2http.1 index 4c6c2b2..2805952 100644 --- a/shell2http.1 +++ b/shell2http.1 @@ -35,6 +35,7 @@ options: \-basic\-auth="" : setup HTTP Basic Authentication ("user_name:password"), can be used several times \-timeout=N : set timeout for execute shell command (in seconds) \-no\-log\-timestamp : log output without timestamps + \-avoid\-log\-for=url: don\'t log requests for specified paths ("path1,path2,\.\.\.") \-version \-help . diff --git a/shell2http.go b/shell2http.go index c6a94d6..886a9a5 100644 --- a/shell2http.go +++ b/shell2http.go @@ -337,7 +337,9 @@ func setupHandlers(cmdHandlers []command, appConfig Config, cacheTTL raphanus.DB cmd: "index page", handler: func(rw http.ResponseWriter, req *http.Request) { if req.URL.Path != "/" { - log.Printf("%s - 404", req.URL.Path) + if !appConfig.isAvoidLog(req.URL.Path) { + log.Printf("%s - 404", req.URL.Path) + } http.NotFound(rw, req) return } @@ -612,7 +614,10 @@ func main() { if appConfig.oneThread { handlerFunc = mwOneThread(handlerFunc) } - handlerFunc = mwLogging(mwCommonHeaders(handlerFunc)) + handlerFunc = mwCommonHeaders(handlerFunc) + if !appConfig.isAvoidLog(handler.path) { + handlerFunc = mwLogging(handlerFunc, appConfig.isAvoidLog) + } http.HandleFunc(handler.path, handlerFunc) log.Printf("register: %s (%s)\n", handler.path, handler.cmd) diff --git a/shell2http_test.go b/shell2http_test.go index 149c213..2f26487 100644 --- a/shell2http_test.go +++ b/shell2http_test.go @@ -1,16 +1,21 @@ package main import ( + "bytes" "fmt" "io" "io/ioutil" + "log" "net" "net/http" + "net/http/httptest" "os" "reflect" "strings" "testing" "time" + + "github.com/msoap/raphanus" ) func Test_parseCGIHeaders(t *testing.T) { @@ -409,3 +414,216 @@ func Test_parsePathAndCommands(t *testing.T) { }) } } + +func Test_isAvoidLog(t *testing.T) { + tests := []struct { + name string + avoidFor string + checkPaths map[string]bool + }{ + { + name: "empty config", + avoidFor: "", + checkPaths: map[string]bool{ + "/health": false, + "/": false, + "/date": false, + }, + }, + { + name: "single path without slash", + avoidFor: "health", + checkPaths: map[string]bool{ + "/health": true, + "/health/": true, + "/date": false, + }, + }, + { + name: "single path with slash", + avoidFor: "/health", + checkPaths: map[string]bool{ + "/health": true, + "/health/": true, + "/date": false, + }, + }, + { + name: "single path with trailing slash", + avoidFor: "/health/", + checkPaths: map[string]bool{ + "/health": true, + "/health/": true, + "/date": false, + }, + }, + { + name: "multiple paths with spaces and slashes", + avoidFor: " health, /metrics , /status/ ", + checkPaths: map[string]bool{ + "/health": true, + "/health/": true, + "/metrics": true, + "/metrics/": true, + "/status": true, + "/status/": true, + "/date": false, + }, + }, + { + name: "root path", + avoidFor: "/", + checkPaths: map[string]bool{ + "/": true, + "/health": false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := Config{avoidLogFor: tt.avoidFor} + if len(cfg.avoidLogFor) > 0 { + cfg.avoidLogPaths = make(map[string]bool) + for _, rawPath := range strings.Split(cfg.avoidLogFor, ",") { + p := strings.TrimSpace(rawPath) + if p == "" { + continue + } + if !strings.HasPrefix(p, "/") { + p = "/" + p + } + cfg.avoidLogPaths[p] = true + if len(p) > 1 && strings.HasSuffix(p, "/") { + cfg.avoidLogPaths[strings.TrimSuffix(p, "/")] = true + } + } + } + + for path, expected := range tt.checkPaths { + if got := cfg.isAvoidLog(path); got != expected { + t.Errorf("isAvoidLog(%q) = %v, expected %v", path, got, expected) + } + } + }) + } +} + +func Test_mwLogging_avoidLog(t *testing.T) { + cfg := Config{ + avoidLogPaths: map[string]bool{ + "/health": true, + "/health/": true, + }, + } + + dummyHandler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + rw.WriteHeader(http.StatusOK) + _, _ = rw.Write([]byte("ok")) + }) + + mw := mwLogging(dummyHandler, cfg.isAvoidLog) + + var logBuf bytes.Buffer + origOutput := log.Writer() + log.SetOutput(&logBuf) + defer log.SetOutput(origOutput) + + // 1. Avoided path should not produce any log + logBuf.Reset() + reqHealth := httptest.NewRequest("GET", "/health", nil) + rwHealth := httptest.NewRecorder() + mw.ServeHTTP(rwHealth, reqHealth) + if rwHealth.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rwHealth.Code) + } + if logBuf.Len() != 0 { + t.Errorf("expected no log for /health, got: %s", logBuf.String()) + } + + // 2. Avoided path with query string should not produce any log + logBuf.Reset() + reqHealthQuery := httptest.NewRequest("GET", "/health?probe=1", nil) + rwHealthQuery := httptest.NewRecorder() + mw.ServeHTTP(rwHealthQuery, reqHealthQuery) + if rwHealthQuery.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rwHealthQuery.Code) + } + if logBuf.Len() != 0 { + t.Errorf("expected no log for /health?probe=1, got: %s", logBuf.String()) + } + + // 3. Non-avoided path should produce log + logBuf.Reset() + reqDate := httptest.NewRequest("GET", "/date", nil) + rwDate := httptest.NewRecorder() + mw.ServeHTTP(rwDate, reqDate) + if rwDate.Code != http.StatusOK { + t.Errorf("expected 200, got %d", rwDate.Code) + } + if !strings.Contains(logBuf.String(), `GET /date`) { + t.Errorf("expected log for /date, got: %s", logBuf.String()) + } + + // 4. Backward compatibility: mwLogging without isAvoidLog + mwDefault := mwLogging(dummyHandler) + logBuf.Reset() + mwDefault.ServeHTTP(rwHealth, reqHealth) + if !strings.Contains(logBuf.String(), `GET /health`) { + t.Errorf("expected log for default mwLogging, got: %s", logBuf.String()) + } +} + +func Test_index404_avoidLog(t *testing.T) { + cfg := Config{ + avoidLogPaths: map[string]bool{ + "/health": true, + "/health/": true, + }, + } + handlers, err := setupHandlers([]command{{path: "/date", cmd: "date"}}, cfg, raphanus.DB{}) + if err != nil { + t.Fatalf("setupHandlers failed: %s", err) + } + + var rootHandler http.HandlerFunc + for _, h := range handlers { + if h.path == "/" { + rootHandler = h.handler + break + } + } + if rootHandler == nil { + t.Fatal("root handler not found") + } + + var logBuf bytes.Buffer + origOutput := log.Writer() + log.SetOutput(&logBuf) + defer log.SetOutput(origOutput) + + // 404 for avoided path /health + logBuf.Reset() + reqHealth := httptest.NewRequest("GET", "http://localhost/health", nil) + rwHealth := httptest.NewRecorder() + rootHandler.ServeHTTP(rwHealth, reqHealth) + if rwHealth.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d", rwHealth.Code) + } + if logBuf.Len() != 0 { + t.Errorf("expected no 404 log for /health, got: %s", logBuf.String()) + } + + // 404 for non-avoided path /unknown + logBuf.Reset() + reqUnknown := httptest.NewRequest("GET", "http://localhost/unknown", nil) + rwUnknown := httptest.NewRecorder() + rootHandler.ServeHTTP(rwUnknown, reqUnknown) + if rwUnknown.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d", rwUnknown.Code) + } + if !strings.Contains(logBuf.String(), "/unknown - 404") { + t.Errorf("expected 404 log for /unknown, got: %s", logBuf.String()) + } +} +