diff --git a/charts/nginx-ingress/values.yaml b/charts/nginx-ingress/values.yaml index abacb17cc1..07de77a5a5 100644 --- a/charts/nginx-ingress/values.yaml +++ b/charts/nginx-ingress/values.yaml @@ -161,7 +161,7 @@ controller: ## The log level of the Ingress Controller. Options include: trace, debug, info, warning, error, fatal logLevel: info - ## Sets the log format of Ingress Controller. Options include: glog, json, text + ## Sets the log format of Ingress Controller. Options include: glog, json, json-unix, json-unix-ms, text, text-unix, text-unix-ms logFormat: glog ## Enables auto adjusting some of the NGINX directives to help with safe configuration and prevent NGINX misconfigurations. diff --git a/cmd/nginx-ingress/flags.go b/cmd/nginx-ingress/flags.go index d5c1076a8a..869ea5e1a8 100644 --- a/cmd/nginx-ingress/flags.go +++ b/cmd/nginx-ingress/flags.go @@ -481,7 +481,7 @@ func validateLogLevel(logLevel string) error { // validateLogFormat makes sure a given logFormat is one of the allowed values func validateLogFormat(logFormat string) error { switch strings.ToLower(logFormat) { - case "glog", "json", "text": + case "glog", "json", "text", "json-unix", "json-unix-ms", "text-unix", "text-unix-ms": return nil } return fmt.Errorf("invalid log format: %v", logFormat) diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index 9b039002d2..a0f29d0628 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -1204,6 +1204,28 @@ func initLogger(logFormat string, level slog.Level, out io.Writer) context.Conte a.Value = slog.AnyValue(src) } } + // Handle custom timestamp formatting + if a.Key == slog.TimeKey { + if t, ok := a.Value.Any().(time.Time); ok { + switch logFormat { + case "json-unix", "text-unix": + // Unix timestamp in seconds + return slog.Attr{ + Key: slog.TimeKey, + Value: slog.Int64Value(t.Unix()), + } + case "json-unix-ms", "text-unix-ms": + // Unix timestamp with milliseconds + return slog.Attr{ + Key: slog.TimeKey, + Value: slog.Int64Value(t.UnixMilli()), + } + default: + // Default timestamp format (keep original time key and format eg. RFC3339) + return a + } + } + } return a }, } @@ -1211,9 +1233,9 @@ func initLogger(logFormat string, level slog.Level, out io.Writer) context.Conte switch { case logFormat == "glog": h = nic_glog.New(out, &nic_glog.Options{Level: programLevel}) - case logFormat == "json": + case strings.HasPrefix(logFormat, "json"): h = slog.NewJSONHandler(out, opts) - case logFormat == "text": + case strings.HasPrefix(logFormat, "text"): h = slog.NewTextHandler(out, opts) default: h = nic_glog.New(out, &nic_glog.Options{Level: programLevel}) diff --git a/cmd/nginx-ingress/main_test.go b/cmd/nginx-ingress/main_test.go index c2572d65ac..e54076411b 100644 --- a/cmd/nginx-ingress/main_test.go +++ b/cmd/nginx-ingress/main_test.go @@ -62,6 +62,61 @@ func TestLogFormats(t *testing.T) { } } +func TestLogTimeFormats(t *testing.T) { + testCases := []struct { + name string + logFormat string + wantre string + }{ + // JSON format tests + { + name: "json default time format", + logFormat: "json", + wantre: `^{"time":"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+.*","level":"INFO","source":\{"file":"[^"]+\.go","line":\d+\},"msg":".*}`, + }, + { + name: "json unix time format", + logFormat: "json-unix", + wantre: `^{"time":\d{10},"level":"INFO","source":\{"file":"[^"]+\.go","line":\d+\},"msg":".*}`, + }, + { + name: "json unix-ms time format", + logFormat: "json-unix-ms", + wantre: `^{"time":\d{13},"level":"INFO","source":\{"file":"[^"]+\.go","line":\d+\},"msg":".*}`, + }, + // TEXT format tests + { + name: "text default time format", + logFormat: "text", + wantre: `^time=\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+.*level=\w+\ssource=[^:]+\.go:\d+\smsg=\w+`, + }, + { + name: "text unix time format", + logFormat: "text-unix", + wantre: `^time=\d{10}\slevel=\w+\ssource=[^:]+\.go:\d+\smsg=\w+`, + }, + { + name: "text unix-ms time format", + logFormat: "text-unix-ms", + wantre: `^time=\d{13}\slevel=\w+\ssource=[^:]+\.go:\d+\smsg=\w+`, + }, + } + t.Parallel() + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var buf bytes.Buffer + ctx := initLogger(tc.logFormat, levels.LevelInfo, &buf) + l := nl.LoggerFromContext(ctx) + l.Log(ctx, levels.LevelInfo, "test") + got := buf.String() + re := regexp.MustCompile(tc.wantre) + if !re.MatchString(got) { + t.Errorf("\ngot:\n%q\nwant regex:\n%q", got, tc.wantre) + } + }) + } +} + func TestK8sVersionValidation(t *testing.T) { testCases := []struct { name string