diff --git a/cmd/cli/log_writer.go b/cmd/cli/log_writer.go index d7f6839..adb29f3 100644 --- a/cmd/cli/log_writer.go +++ b/cmd/cli/log_writer.go @@ -25,6 +25,30 @@ const ( logWriterLogEndMarker = "\n\n=== LOG_END ===\n\n" ) +// Custom level encoders that handle NOTICE level +// Since NOTICE and WARN share the same numeric value (1), we handle them specially +// in the encoder to display NOTICE messages with the "NOTICE" prefix. +// Note: WARN messages will also display as "NOTICE" because they share the same level value. +// This is the intended behavior for visual distinction. + +func noticeLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch l { + case ctrld.NoticeLevel: + enc.AppendString("NOTICE") + default: + zapcore.CapitalLevelEncoder(l, enc) + } +} + +func noticeColorLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + switch l { + case ctrld.NoticeLevel: + enc.AppendString("\x1b[36mNOTICE\x1b[0m") // Cyan color for NOTICE + default: + zapcore.CapitalColorLevelEncoder(l, enc) + } +} + type logViewResponse struct { Data string `json:"data"` } @@ -136,6 +160,7 @@ func (p *prog) initInternalLogging(externalCores []zapcore.Core) { // Create a multi-core logger multiCore := zapcore.NewTee(cores...) logger := zap.New(multiCore) + mainLog.Store(&ctrld.Logger{Logger: logger}) } @@ -219,7 +244,7 @@ func newHumanReadableZapCore(w io.Writer, level zapcore.Level) zapcore.Core { encoderConfig := zap.NewDevelopmentEncoderConfig() encoderConfig.TimeKey = "time" encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli) - encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + encoderConfig.EncodeLevel = noticeColorLevelEncoder encoder := zapcore.NewConsoleEncoder(encoderConfig) return zapcore.NewCore(encoder, zapcore.AddSync(w), level) } @@ -242,7 +267,7 @@ func newMachineFriendlyZapCore(w io.Writer, level zapcore.Level) zapcore.Core { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.TimeKey = "time" encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.StampMilli) - encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + encoderConfig.EncodeLevel = noticeLevelEncoder encoder := zapcore.NewConsoleEncoder(encoderConfig) return zapcore.NewCore(encoder, zapcore.AddSync(w), level) } diff --git a/cmd/cli/log_writer_test.go b/cmd/cli/log_writer_test.go index 5336d4e..1138fca 100644 --- a/cmd/cli/log_writer_test.go +++ b/cmd/cli/log_writer_test.go @@ -1,9 +1,15 @@ package cli import ( + "bytes" "strings" "sync" "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/Control-D-Inc/ctrld" ) func Test_logWriter_Write(t *testing.T) { @@ -83,3 +89,56 @@ func Test_logWriter_MarkerInitEnd(t *testing.T) { t.Fatalf("unexpected log content: %s", lw.buf.String()) } } + +// TestNoticeLevel tests that the custom NOTICE level works correctly +func TestNoticeLevel(t *testing.T) { + // Create a buffer to capture log output + var buf bytes.Buffer + + // Create encoder config with custom NOTICE level support + encoderConfig := zap.NewDevelopmentEncoderConfig() + encoderConfig.TimeKey = "time" + encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("15:04:05.000") + encoderConfig.EncodeLevel = noticeLevelEncoder + + // Test with NOTICE level + encoder := zapcore.NewConsoleEncoder(encoderConfig) + core := zapcore.NewCore(encoder, zapcore.AddSync(&buf), ctrld.NoticeLevel) + logger := zap.New(core) + ctrldLogger := &ctrld.Logger{Logger: logger} + + // Log messages at different levels + ctrldLogger.Debug().Msg("This is a DEBUG message") + ctrldLogger.Info().Msg("This is an INFO message") + ctrldLogger.Notice().Msg("This is a NOTICE message") + ctrldLogger.Warn().Msg("This is a WARN message") + ctrldLogger.Error().Msg("This is an ERROR message") + + output := buf.String() + + // Verify that DEBUG and INFO messages are NOT logged (filtered out) + if strings.Contains(output, "DEBUG") { + t.Error("DEBUG message should not be logged when level is NOTICE") + } + if strings.Contains(output, "INFO") { + t.Error("INFO message should not be logged when level is NOTICE") + } + + // Verify that NOTICE, WARN, and ERROR messages ARE logged + if !strings.Contains(output, "NOTICE") { + t.Error("NOTICE message should be logged when level is NOTICE") + } + if !strings.Contains(output, "WARN") { + t.Error("WARN message should be logged when level is NOTICE") + } + if !strings.Contains(output, "ERROR") { + t.Error("ERROR message should be logged when level is NOTICE") + } + + // Verify the NOTICE message content + if !strings.Contains(output, "This is a NOTICE message") { + t.Error("NOTICE message content should be present") + } + + t.Logf("Log output with NOTICE level:\n%s", output) +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index cb06504..b3bda67 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -83,19 +83,18 @@ func normalizeLogFilePath(logFilePath string) string { // initConsoleLogging initializes console logging, then storing to mainLog. func initConsoleLogging() { - consoleWriterLevel = zapcore.InfoLevel + consoleWriterLevel = ctrld.NoticeLevel switch { case silent: // For silent mode, use a no-op logger l := zap.NewNop() mainLog.Store(&ctrld.Logger{Logger: l}) case verbose == 1: - // Info level is default + // Info level + consoleWriterLevel = zapcore.InfoLevel case verbose > 1: // Debug level consoleWriterLevel = zapcore.DebugLevel - default: - // Notice level maps to Info in zap } consoleWriter = newHumanReadableZapCore(os.Stdout, consoleWriterLevel) l := zap.New(consoleWriter) @@ -172,6 +171,8 @@ func initLoggingWithBackup(doBackup bool) []zapcore.Core { level = zapcore.DebugLevel case "info": level = zapcore.InfoLevel + case "notice": + level = ctrld.NoticeLevel case "warn": level = zapcore.WarnLevel case "error": diff --git a/log.go b/log.go index c961268..a55157a 100644 --- a/log.go +++ b/log.go @@ -10,6 +10,14 @@ import ( "go.uber.org/zap/zapcore" ) +// Custom log level for NOTICE (between INFO and WARN) +// DEBUG = -1, INFO = 0, WARN = 1, ERROR = 2, FATAL = 3 +// Since there's no integer between INFO (0) and WARN (1), we'll use the same value as WARN +// but handle NOTICE specially in the encoder to display it differently. +// Note: NOTICE and WARN share the same numeric value (1), so they will both display as "NOTICE" +// when using the custom encoder. This is the intended behavior for visual distinction. +const NoticeLevel = zapcore.Level(zapcore.WarnLevel) // Same value as WARN, but handled specially + // LoggerCtxKey is the context.Context key for a logger. type LoggerCtxKey struct{} @@ -169,7 +177,7 @@ func (l *Logger) Fatal() *LogEvent { func (l *Logger) Notice() *LogEvent { return &LogEvent{ logger: l.Logger, - level: zapcore.InfoLevel, // zap doesn't have Notice level, use Info + level: NoticeLevel, // Custom NOTICE level between INFO and WARN fields: []zap.Field{}, } }