Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions custom_levels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
package zap

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"
"go.uber.org/zap/internal/color"
"go.uber.org/zap/zapcore"
)

func TestCustomLevels(t *testing.T) {
// Clear any existing custom levels
ClearCustomLevels()

// Test registering a custom level
err := RegisterCustomLevel("NOTICE", 6, color.Cyan)
if err != nil {
t.Fatalf("Failed to register custom level: %v", err)
}

// Test retrieving the custom level
level, exists := GetCustomLevel("NOTICE")
if !exists {
t.Fatal("Custom level not found after registration")
}

if level != 6 {
t.Errorf("Expected level value 6, got %d", level)
}

// Test that the level string representation works
if level.String() != "NOTICE" {
t.Errorf("Expected level string 'NOTICE', got '%s'", level.String())
}

// Test that the level capital string representation works
if level.CapitalString() != "NOTICE" {
t.Errorf("Expected level capital string 'NOTICE', got '%s'", level.CapitalString())
}

// Test that the level is recognized as custom
if !level.IsCustom() {
t.Error("Level should be recognized as custom")
}

// Test that custom level metadata can be retrieved
metadata := level.Custom()
if metadata == nil {
t.Fatal("Custom level metadata should not be nil")
}

if metadata.Name != "NOTICE" {
t.Errorf("Expected metadata name 'NOTICE', got '%s'", metadata.Name)
}
}

func TestCustomLevelsWithLogger(t *testing.T) {
// Clear any existing custom levels
ClearCustomLevels()

// Create a buffer to capture output
var buf bytes.Buffer

// Create logger with custom levels
logger := NewWithCustomLevels(
zapcore.NewCore(
zapcore.NewConsoleEncoder(NewDevelopmentEncoderConfig()),
zapcore.AddSync(&buf),
zapcore.DebugLevel,
),
CustomLevelOption{Name: "NOTICE", Value: 6, Color: "cyan"},
CustomLevelOption{Name: "TRACE", Value: 7, Color: "black"},
)

// Test logging with custom levels
noticeLevel, exists := GetCustomLevel("NOTICE")
if !exists {
t.Fatal("NOTICE level not found")
}

traceLevel, exists := GetCustomLevel("TRACE")
if !exists {
t.Fatal("TRACE level not found")
}

// Log messages with custom levels
logger.Log(noticeLevel, "This is a notice message")
logger.Log(traceLevel, "This is a trace message")

// Debug: print the actual output
t.Logf("Actual output: %s", buf.String())

// Check that output contains the custom level names
if !bytes.Contains(buf.Bytes(), []byte("NOTICE")) {
t.Error("Output should contain 'NOTICE' level")
}
if !bytes.Contains(buf.Bytes(), []byte("TRACE")) {
t.Error("Output should contain 'TRACE' level")
}
}

func TestCustomLevelsParsing(t *testing.T) {
// Clear any existing custom levels
ClearCustomLevels()

// Register custom levels
err1 := RegisterCustomLevel("NOTICE", 6, color.Cyan)
if err1 != nil {
t.Fatalf("Failed to register NOTICE level: %v", err1)
}

err2 := RegisterCustomLevel("TRACE", 7, color.Black)
if err2 != nil {
t.Fatalf("Failed to register TRACE level: %v", err2)
}

// Debug: check if levels are registered
noticeLevel, exists := GetCustomLevel("NOTICE")
if !exists {
t.Fatal("NOTICE level not found after registration")
}
t.Logf("NOTICE level registered with value: %d", noticeLevel)

traceLevel2, exists := GetCustomLevel("TRACE")
if !exists {
t.Fatal("TRACE level not found after registration")
}
t.Logf("TRACE level registered with value: %d", traceLevel2)

// Test parsing custom level names
noticeLevel, err := zapcore.ParseLevel("NOTICE")
if err != nil {
t.Fatalf("Failed to parse 'NOTICE' level: %v", err)
}
if noticeLevel != 6 {
t.Errorf("Expected level value 6, got %d", noticeLevel)
}

traceLevel, err := zapcore.ParseLevel("TRACE")
if err != nil {
t.Fatalf("Failed to parse 'TRACE' level: %v", err)
}
if traceLevel != 7 {
t.Errorf("Expected level value 7, got %d", traceLevel)
}

// Test case-insensitive parsing
noticeLevelLower, err := zapcore.ParseLevel("notice")
if err != nil {
t.Fatalf("Failed to parse 'notice' level: %v", err)
}
if noticeLevelLower != 6 {
t.Errorf("Expected level value 6, got %d", noticeLevelLower)
}

// Debug: check what level was actually parsed
t.Logf("Parsed 'notice' as level: %d", noticeLevelLower)
}

func TestCustomLevelsConflicts(t *testing.T) {
// Clear any existing custom levels
ClearCustomLevels()

// Test that we can't register a level at a built-in level value
err := RegisterCustomLevel("CUSTOM_INFO", 0, color.Blue) // 0 is InfoLevel
if err == nil {
t.Error("Expected error when registering at built-in level value")
}

// Test that we can't register a level with duplicate name
RegisterCustomLevel("UNIQUE", 10, color.Red)
err = RegisterCustomLevel("UNIQUE", 11, color.Blue)
if err == nil {
t.Error("Expected error when registering duplicate name")
}

// Test that we can't register a level with duplicate value
RegisterCustomLevel("FIRST", 20, color.Green)
err = RegisterCustomLevel("SECOND", 20, color.Yellow)
if err == nil {
t.Error("Expected error when registering duplicate value")
}
}

func TestCustomLevelsBetweenExistingLevels(t *testing.T) {
ClearCustomLevels()

// Test inserting a NOTICE level between INFO and WARN
noticeLevel, err := RegisterCustomLevelBetween("NOTICE", zapcore.InfoLevel, zapcore.WarnLevel, color.Blue)
require.NoError(t, err)
require.True(t, noticeLevel.IsCustom())

// Debug: print the actual values
t.Logf("NOTICE level: %d (InfoLevel: %d, WarnLevel: %d)", int8(noticeLevel), int8(zapcore.InfoLevel), int8(zapcore.WarnLevel))

// Verify the level value is outside the built-in range but maintains proper ordering
levelValue := int8(noticeLevel)
require.True(t, levelValue > 1, "NOTICE level should be greater than WarnLevel (1), got %d", levelValue)
require.True(t, levelValue < 100, "NOTICE level should be less than 100, got %d", levelValue)

// Test inserting a TRACE level below DEBUG
traceLevel, err := RegisterCustomLevelBetween("TRACE", zapcore.DebugLevel, zapcore.InfoLevel, color.Cyan)
require.NoError(t, err)
require.True(t, traceLevel.IsCustom())

// Debug: print the actual values
t.Logf("TRACE level: %d (DebugLevel: %d, InfoLevel: %d)", int8(traceLevel), int8(zapcore.DebugLevel), int8(zapcore.InfoLevel))

// Verify the level value is outside the built-in range but maintains proper ordering
traceValue := int8(traceLevel)
require.True(t, traceValue < -1, "TRACE level should be less than DebugLevel (-1), got %d", traceValue)

// Test inserting a CRITICAL level between ERROR and DPANIC
criticalLevel, err := RegisterCustomLevelBetween("CRITICAL", zapcore.ErrorLevel, zapcore.DPanicLevel, color.Red)
require.NoError(t, err)
require.True(t, criticalLevel.IsCustom())

// Debug: print the actual values
t.Logf("CRITICAL level: %d (ErrorLevel: %d, DPanicLevel: %d)", int8(criticalLevel), int8(zapcore.ErrorLevel), int8(zapcore.DPanicLevel))

// Verify the level value is outside the built-in range but maintains proper ordering
criticalValue := int8(criticalLevel)
require.True(t, criticalValue > 2, "CRITICAL level should be greater than ErrorLevel (2), got %d", criticalValue)
require.True(t, criticalValue < 100, "CRITICAL level should be less than 100, got %d", criticalValue)

// Test that we can retrieve these levels by name
retrievedNotice, exists := GetCustomLevel("NOTICE")
require.True(t, exists)
require.Equal(t, noticeLevel, retrievedNotice)

retrievedTrace, exists := GetCustomLevel("TRACE")
require.True(t, exists)
require.Equal(t, traceLevel, retrievedTrace)

retrievedCritical, exists := GetCustomLevel("CRITICAL")
require.True(t, exists)
require.Equal(t, criticalLevel, retrievedCritical)

// Test that the levels maintain proper semantic ordering relative to built-in levels
// We use the IsBetween function to check semantic ordering rather than direct int8 comparisons
require.True(t, traceLevel.IsBetween(zapcore.DebugLevel, zapcore.InfoLevel), "TRACE should be semantically between DEBUG and INFO")
require.True(t, noticeLevel.IsBetween(zapcore.InfoLevel, zapcore.WarnLevel), "NOTICE should be semantically between INFO and WARN")
require.True(t, criticalLevel.IsBetween(zapcore.ErrorLevel, zapcore.DPanicLevel), "CRITICAL should be semantically between ERROR and DPANIC")
}

func TestCustomLevelsBetweenExistingLevelsErrors(t *testing.T) {
ClearCustomLevels()

// Test error when lower level >= higher level
_, err := RegisterCustomLevelBetween("INVALID", zapcore.WarnLevel, zapcore.InfoLevel, color.Red)
require.Error(t, err)
require.Contains(t, err.Error(), "lower level must be less than higher level")

// Test error when trying to register between same level
_, err = RegisterCustomLevelBetween("INVALID", zapcore.InfoLevel, zapcore.InfoLevel, color.Red)
require.Error(t, err)
require.Contains(t, err.Error(), "lower level must be less than higher level")
}
52 changes: 52 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"time"

"go.uber.org/zap"
"go.uber.org/zap/internal/color"
"go.uber.org/zap/zapcore"
)

Expand Down Expand Up @@ -456,3 +457,54 @@ func ExampleObjectValues() {
// Output:
// {"level":"debug","msg":"starting tunnels","addrs":[{"url":"/foo","ip":"127.0.0.1","port":8080,"remote":{"ip":"123.45.67.89","port":4040}},{"url":"/bar","ip":"127.0.0.1","port":8080,"remote":{"ip":"127.0.0.1","port":31200}}]}
}

func Example_customLogLevels() {
// Clear any existing custom levels to ensure a clean state
zap.ClearCustomLevels()

// Register a NOTICE level between INFO and WARN
noticeLevel, err := zap.RegisterCustomLevelBetween("NOTICE", zap.InfoLevel, zap.WarnLevel, color.Blue)
if err != nil {
panic(err)
}

// Create a logger with the NOTICE level set as the minimum level
// This means only NOTICE and higher levels (WARN, ERROR, etc.) will be logged

// Create a custom level enabler that allows NOTICE and higher levels
levelEnabler := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
// Allow NOTICE level (value 6) and all built-in levels >= WARN (value 1)
// Since WARN (1) < NOTICE (6), we need to handle this specially
if lvl == noticeLevel {
return true
}
// For built-in levels, use the standard comparison
return lvl >= zapcore.WarnLevel
})

// Use JSON encoder for cleaner, more predictable output
jsonConfig := zap.NewProductionEncoderConfig()
jsonConfig.TimeKey = "" // Remove timestamp
jsonConfig.StacktraceKey = "" // Remove stack trace
jsonEncoder := zapcore.NewJSONEncoder(jsonConfig)

logger := zap.New(zapcore.NewCore(
jsonEncoder,
zapcore.AddSync(os.Stdout),
levelEnabler,
))
defer logger.Sync()

// Add a NOTICE message - should be printed
logger.Log(noticeLevel, "This is a NOTICE message")

// Add a WARN message - should be printed (higher than NOTICE)
logger.Warn("This is a WARN message")

// Add an INFO message - should NOT be printed (lower than NOTICE)
logger.Info("This is an INFO message")

// Output:
// {"level":"NOTICE","msg":"This is a NOTICE message"}
// {"level":"warn","msg":"This is a WARN message"}
}
46 changes: 45 additions & 1 deletion logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"strings"

"go.uber.org/zap/internal/bufferpool"
"go.uber.org/zap/internal/color"
"go.uber.org/zap/internal/stacktrace"
"go.uber.org/zap/zapcore"
)
Expand Down Expand Up @@ -139,6 +140,39 @@ func NewExample(options ...Option) *Logger {
return New(core).WithOptions(options...)
}

// NewWithCustomLevels creates a new logger with custom log levels
func NewWithCustomLevels(core zapcore.Core, customLevels ...CustomLevelOption) *Logger {
// Register custom levels first
for _, lvl := range customLevels {
clr := parseColor(lvl.Color)
zapcore.RegisterCustomLevel(lvl.Name, lvl.Value, clr)
}

// Create logger with registered levels
return New(core)
}

// RegisterCustomLevel registers a custom level globally
func RegisterCustomLevel(name string, value int8, color color.Color) error {
return zapcore.RegisterCustomLevel(name, value, color)
}

// RegisterCustomLevelBetween registers a custom level between two existing levels
// For example, RegisterCustomLevelBetween("NOTICE", zapcore.InfoLevel, zapcore.WarnLevel, color.Blue)
func RegisterCustomLevelBetween(name string, lower, higher zapcore.Level, clr color.Color) (zapcore.Level, error) {
return zapcore.RegisterCustomLevelBetween(name, lower, higher, clr)
}

// GetCustomLevel retrieves a custom level by name
func GetCustomLevel(name string) (zapcore.Level, bool) {
return zapcore.GetCustomLevel(name)
}

// ClearCustomLevels removes all custom levels from the global registry
func ClearCustomLevels() {
zapcore.ClearCustomLevels()
}

// Sugar wraps the Logger to provide a more ergonomic, but slightly slower,
// API. Sugaring a Logger is quite inexpensive, so it's reasonable for a
// single application to use both Loggers and SugaredLoggers, converting
Expand Down Expand Up @@ -365,7 +399,17 @@ func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
// Thread the error output through to the CheckedEntry.
ce.ErrorOutput = log.errorOutput

addStack := log.addStack.Enabled(ce.Level)
// Check if we should add stack trace
// For custom levels, we need to handle them specially since the default addStack
// LevelEnabler might not work correctly with custom level values
addStack := false
if ce.Level.IsCustom() {
addStack = ce.Level.IsBetween(zapcore.ErrorLevel, zapcore.DPanicLevel)
} else {
// For built-in levels, use the configured addStack LevelEnabler
addStack = log.addStack.Enabled(ce.Level)
}

if !log.addCaller && !addStack {
return ce
}
Expand Down
Loading