Skip to content

Commit 28f24aa

Browse files
authored
feat: add -log-level option (#234)
1 parent c2e3600 commit 28f24aa

3 files changed

Lines changed: 86 additions & 9 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,11 @@ variables (or a combination of the two):
103103
| - | - | - | - |
104104
| `-allowed-redirect-domains` | `ALLOWED_REDIRECT_DOMAINS` | Comma-separated list of domains the /redirect-to endpoint will allow | |
105105
| `-exclude-headers` | `EXCLUDE_HEADERS` | Drop platform-specific headers. Comma-separated list of headers key to drop, supporting wildcard suffix matching. For example: `"foo,bar,x-fc-*"` | - |
106-
| `-host` | `HOST` | Host to listen on | "0.0.0.0" |
106+
| `-host` | `HOST` | Host to listen on | 0.0.0.0 |
107107
| `-https-cert-file` | `HTTPS_CERT_FILE` | HTTPS Server certificate file | |
108108
| `-https-key-file` | `HTTPS_KEY_FILE` | HTTPS Server private key file | |
109-
| `-log-format` | `LOG_FORMAT` | Log format (text or json) | "text" |
109+
| `-log-format` | `LOG_FORMAT` | Log format (text or json) | text |
110+
| `-log-level` | `LOG_LEVEL` | Logging level (DEBUG, INFO, WARN, ERROR, OFF) | INFO |
110111
| `-max-body-size` | `MAX_BODY_SIZE` | Maximum size of request or response, in bytes | 1048576 |
111112
| `-max-duration` | `MAX_DURATION` | Maximum duration a response may take | 10s |
112113
| `-port` | `PORT` | Port to listen on | 8080 |

httpbin/cmd/cmd.go

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"io"
1111
"log/slog"
12+
"math"
1213
"net"
1314
"net/http"
1415
"os"
@@ -25,8 +26,12 @@ const (
2526
defaultListenHost = "0.0.0.0"
2627
defaultListenPort = 8080
2728
defaultLogFormat = "text"
29+
defaultLogLevel = "INFO"
2830
defaultEnvPrefix = "HTTPBIN_ENV_"
2931

32+
// Disable all logging by setting the level above any possible value
33+
logLevelOff = slog.Level(math.MaxInt)
34+
3035
// Reasonable defaults for the underlying http.Server
3136
defaultSrvReadTimeout = 5 * time.Second
3237
defaultSrvReadHeaderTimeout = 1 * time.Second
@@ -67,13 +72,7 @@ func mainImpl(args []string, getEnvVal func(string) string, getEnviron func() []
6772
return 1
6873
}
6974

70-
logger := slog.New(slog.NewTextHandler(out, nil))
71-
72-
if cfg.LogFormat == "json" {
73-
// use structured logging if requested
74-
handler := slog.NewJSONHandler(out, nil)
75-
logger = slog.New(handler)
76-
}
75+
logger := setupLogger(out, cfg.LogFormat, cfg.LogLevel)
7776

7877
opts := []httpbin.OptionFunc{
7978
httpbin.WithEnv(cfg.Env),
@@ -127,6 +126,7 @@ type config struct {
127126
TLSCertFile string
128127
TLSKeyFile string
129128
LogFormat string
129+
LogLevel slog.Level
130130
SrvMaxHeaderBytes int
131131
SrvReadHeaderTimeout time.Duration
132132
SrvReadTimeout time.Duration
@@ -141,6 +141,7 @@ type config struct {
141141

142142
// temporary placeholders for arguments that need extra processing
143143
rawAllowedRedirectDomains string
144+
rawLogLevel string
144145
rawUseRealHostname bool
145146
}
146147

@@ -176,6 +177,7 @@ func loadConfig(args []string, getEnvVal func(string) string, getEnviron func()
176177
fs.StringVar(&cfg.TLSKeyFile, "https-key-file", "", "HTTPS Server private key file")
177178
fs.StringVar(&cfg.ExcludeHeaders, "exclude-headers", "", "Drop platform-specific headers. Comma-separated list of headers key to drop, supporting wildcard matching.")
178179
fs.StringVar(&cfg.LogFormat, "log-format", defaultLogFormat, "Log format (text or json)")
180+
fs.StringVar(&cfg.rawLogLevel, "log-level", defaultLogLevel, "Logging level (DEBUG, INFO, WARN, ERROR, OFF)")
179181
fs.IntVar(&cfg.SrvMaxHeaderBytes, "srv-max-header-bytes", defaultSrvMaxHeaderBytes, "Value to use for the http.Server's MaxHeaderBytes option")
180182
fs.DurationVar(&cfg.SrvReadHeaderTimeout, "srv-read-header-timeout", defaultSrvReadHeaderTimeout, "Value to use for the http.Server's ReadHeaderTimeout option")
181183
fs.DurationVar(&cfg.SrvReadTimeout, "srv-read-timeout", defaultSrvReadTimeout, "Value to use for the http.Server's ReadTimeout option")
@@ -272,6 +274,13 @@ func loadConfig(args []string, getEnvVal func(string) string, getEnviron func()
272274
if cfg.LogFormat != "text" && cfg.LogFormat != "json" {
273275
return nil, configErr(`invalid log format %q, must be "text" or "json"`, cfg.LogFormat)
274276
}
277+
if cfg.rawLogLevel == defaultLogLevel && getEnvVal("LOG_LEVEL") != "" {
278+
cfg.rawLogLevel = getEnvVal("LOG_LEVEL")
279+
}
280+
cfg.LogLevel, err = parseLogLevel(cfg.rawLogLevel)
281+
if err != nil {
282+
return nil, configErr(`invalid log level %q, must be one of "DEBUG", "INFO", "WARN", "ERROR", "OFF"`, cfg.rawLogLevel)
283+
}
275284

276285
if getEnvBool(getEnvVal("USE_REAL_HOSTNAME")) {
277286
cfg.rawUseRealHostname = true
@@ -319,6 +328,7 @@ func loadConfig(args []string, getEnvVal func(string) string, getEnviron func()
319328

320329
// reset temporary fields to their zero values
321330
cfg.rawAllowedRedirectDomains = ""
331+
cfg.rawLogLevel = ""
322332
cfg.rawUseRealHostname = false
323333

324334
for _, envVar := range getEnviron() {
@@ -339,6 +349,42 @@ func getEnvBool(val string) bool {
339349
return val == "1" || val == "true"
340350
}
341351

352+
func parseLogLevel(s string) (slog.Level, error) {
353+
switch strings.ToUpper(strings.TrimSpace(s)) {
354+
case "DEBUG":
355+
return slog.LevelDebug, nil
356+
case "INFO":
357+
return slog.LevelInfo, nil
358+
case "WARN":
359+
return slog.LevelWarn, nil
360+
case "ERROR":
361+
return slog.LevelError, nil
362+
case "OFF":
363+
return logLevelOff, nil
364+
default:
365+
return 0, fmt.Errorf("invalid log level %q", s)
366+
}
367+
}
368+
369+
func setupLogger(out io.Writer, logFormat string, level slog.Level) *slog.Logger {
370+
if level == logLevelOff {
371+
out = io.Discard
372+
}
373+
374+
opts := &slog.HandlerOptions{
375+
Level: level,
376+
}
377+
378+
var handler slog.Handler
379+
if logFormat == "json" {
380+
handler = slog.NewJSONHandler(out, opts)
381+
} else {
382+
handler = slog.NewTextHandler(out, opts)
383+
}
384+
385+
return slog.New(handler)
386+
}
387+
342388
func listenAndServeGracefully(srv *http.Server, cfg *config, logger *slog.Logger) error {
343389
doneCh := make(chan error, 1)
344390

httpbin/cmd/cmd_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"flag"
77
"fmt"
8+
"log/slog"
89
"os"
910
"reflect"
1011
"testing"
@@ -32,6 +33,8 @@ const usage = `Usage of go-httpbin:
3233
HTTPS Server private key file
3334
-log-format string
3435
Log format (text or json) (default "text")
36+
-log-level string
37+
Logging level (DEBUG, INFO, WARN, ERROR, OFF) (default "INFO")
3538
-max-body-size int
3639
Maximum size of request or response, in bytes (default 1048576)
3740
-max-duration duration
@@ -78,6 +81,7 @@ func TestLoadConfig(t *testing.T) {
7881
MaxBodySize: httpbin.DefaultMaxBodySize,
7982
MaxDuration: httpbin.DefaultMaxDuration,
8083
LogFormat: defaultLogFormat,
84+
LogLevel: slog.LevelInfo,
8185
SrvMaxHeaderBytes: defaultSrvMaxHeaderBytes,
8286
SrvReadHeaderTimeout: defaultSrvReadHeaderTimeout,
8387
SrvReadTimeout: defaultSrvReadTimeout,
@@ -391,6 +395,27 @@ func TestLoadConfig(t *testing.T) {
391395
}),
392396
},
393397

398+
// log-level
399+
"ok log level OFF": {
400+
args: []string{"-log-level", "OFF"},
401+
wantCfg: mergedConfig(defaultCfg, &config{
402+
LogLevel: logLevelOff,
403+
}),
404+
},
405+
"ok log level from env": {
406+
env: map[string]string{"LOG_LEVEL": "DEBUG"},
407+
wantCfg: mergedConfig(defaultCfg, &config{
408+
LogLevel: slog.LevelDebug,
409+
}),
410+
},
411+
"ok log level CLI takes precedence over env": {
412+
args: []string{"-log-level", "ERROR"},
413+
env: map[string]string{"LOG_LEVEL": "DEBUG"},
414+
wantCfg: mergedConfig(defaultCfg, &config{
415+
LogLevel: slog.LevelError,
416+
}),
417+
},
418+
394419
// srv-max-header-bytes
395420
"invalid -srv-max-header-bytes": {
396421
args: []string{"-srv-max-header-bytes", "foo"},
@@ -613,6 +638,11 @@ func TestMainImpl(t *testing.T) {
613638
wantCode: 2,
614639
wantOut: "error: invalid log format \"invalid\", must be \"text\" or \"json\"\n\n" + usage,
615640
},
641+
"log level error": {
642+
args: []string{"-log-level", "NOPE"},
643+
wantCode: 2,
644+
wantOut: "error: invalid log level \"NOPE\", must be one of \"DEBUG\", \"INFO\", \"WARN\", \"ERROR\", \"OFF\"\n\n" + usage,
645+
},
616646
}
617647

618648
for name, tc := range testCases {

0 commit comments

Comments
 (0)