diff --git a/cmd/gotenberg.go b/cmd/gotenberg.go index 6931521..676c74f 100644 --- a/cmd/gotenberg.go +++ b/cmd/gotenberg.go @@ -49,6 +49,7 @@ func Run() { fs.String("log-fields-prefix", "", "Prepend a specified prefix to each log field key") fs.String("log-std-format", gotenberg.AutoLoggingFormat, "Set the log format for standard output") fs.Bool("log-std-enable-gcp-fields", false, "Use GCP-compatible field names in log output") + fs.String("log-std-level-case", gotenberg.LowerLevelCase, "Set the case of the level field in the standard output, either lower or upper") // Deprecated logging flags. fs.String("log-format", gotenberg.AutoLoggingFormat, "Set the log format") @@ -123,6 +124,7 @@ func Run() { LogFieldsPrefix: parsedFlags.MustString("log-fields-prefix"), LogStdFormat: parsedFlags.MustDeprecatedString("log-format", "log-std-format"), LogStdEnableGcpFields: parsedFlags.MustDeprecatedBool("log-enable-gcp-fields", "log-std-enable-gcp-fields"), + LogStdLevelCase: parsedFlags.MustString("log-std-level-case"), } // LogLevel uses its own flag, not the format flag. telemetryCfg.LogLevel = parsedFlags.MustString("log-level") diff --git a/pkg/gotenberg/internal/log/stdhandler.go b/pkg/gotenberg/internal/log/stdhandler.go index 3ca2eb4..8744ae5 100644 --- a/pkg/gotenberg/internal/log/stdhandler.go +++ b/pkg/gotenberg/internal/log/stdhandler.go @@ -50,7 +50,12 @@ func (h traceContextHandler) WithGroup(name string) slog.Handler { } // NewStdHandler returns a [slog.Handler] instance for the standard output. -func NewStdHandler(level slog.Level, format string, fieldsPrefix string, enableGcpFields bool) (slog.Handler, error) { +// upperLevelCase is the value of the level-case setting that keeps the level +// field uppercase in the standard output. It mirrors gotenberg.UpperLevelCase, +// duplicated here because the internal log package cannot import gotenberg. +const upperLevelCase = "upper" + +func NewStdHandler(level slog.Level, format string, fieldsPrefix string, enableGcpFields bool, levelCase string) (slog.Handler, error) { // #nosec: G115 isTerminal := term.IsTerminal(int(os.Stdout.Fd())) @@ -82,7 +87,11 @@ func NewStdHandler(level slog.Level, format string, fieldsPrefix string, enableG a.Key = "severity" a.Value = slog.StringValue(gcpSeverity(l)) default: - a.Value = slog.StringValue(strings.ToLower(l.String())) + if levelCase == upperLevelCase { + a.Value = slog.StringValue(l.String()) + } else { + a.Value = slog.StringValue(strings.ToLower(l.String())) + } } } diff --git a/pkg/gotenberg/internal/log/stdhandler_test.go b/pkg/gotenberg/internal/log/stdhandler_test.go new file mode 100644 index 0000000..97159e1 --- /dev/null +++ b/pkg/gotenberg/internal/log/stdhandler_test.go @@ -0,0 +1,53 @@ +package log + +import ( + "encoding/json" + "io" + "log/slog" + "os" + "testing" +) + +func TestNewStdHandler_LevelCase(t *testing.T) { + for _, tc := range []struct { + name string + levelCase string + want string + }{ + {"lower is the default behavior", "lower", "info"}, + {"upper keeps slog casing", "upper", "INFO"}, + } { + t.Run(tc.name, func(t *testing.T) { + original := os.Stderr + reader, writer, err := os.Pipe() + if err != nil { + t.Fatalf("create pipe: %v", err) + } + os.Stderr = writer + defer func() { os.Stderr = original }() + + handler, err := NewStdHandler(slog.LevelInfo, "json", "", false, tc.levelCase) + if err != nil { + t.Fatalf("create handler: %v", err) + } + + slog.New(handler).Info("hello") + + if err := writer.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + out, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("read output: %v", err) + } + + var record map[string]any + if err := json.Unmarshal(out, &record); err != nil { + t.Fatalf("parse log line %q: %v", out, err) + } + if record["level"] != tc.want { + t.Errorf("level = %v, want %v", record["level"], tc.want) + } + }) + } +} diff --git a/pkg/gotenberg/telemetry.go b/pkg/gotenberg/telemetry.go index d009b66..37399f7 100644 --- a/pkg/gotenberg/telemetry.go +++ b/pkg/gotenberg/telemetry.go @@ -30,6 +30,11 @@ const ( DebugLoggingLevel = "debug" ) +const ( + LowerLevelCase = "lower" + UpperLevelCase = "upper" +) + // TelemetryConfig gathers the configuration data for Gotenberg's telemetry. type TelemetryConfig struct { ServiceName string @@ -39,6 +44,7 @@ type TelemetryConfig struct { LogFieldsPrefix string LogStdFormat string LogStdEnableGcpFields bool + LogStdLevelCase string } func (cfg TelemetryConfig) slogLevel() slog.Level { @@ -86,6 +92,16 @@ func (cfg TelemetryConfig) Validate() error { ) } + switch cfg.LogStdLevelCase { + case LowerLevelCase, UpperLevelCase: + break + default: + err = errors.Join( + err, + fmt.Errorf("standard log level case must be either %s or %s", LowerLevelCase, UpperLevelCase), + ) + } + return err } @@ -93,7 +109,7 @@ func (cfg TelemetryConfig) Validate() error { func StartTelemetry(cfg TelemetryConfig) (shutdown func(context.Context) error, err error) { var handlers []slog.Handler - stdHandler, err := log.NewStdHandler(cfg.slogLevel(), cfg.LogStdFormat, cfg.LogFieldsPrefix, cfg.LogStdEnableGcpFields) + stdHandler, err := log.NewStdHandler(cfg.slogLevel(), cfg.LogStdFormat, cfg.LogFieldsPrefix, cfg.LogStdEnableGcpFields, cfg.LogStdLevelCase) if err != nil { return nil, fmt.Errorf("get standard logger handler: %w", err) }