Skip to content

Commit

Permalink
zapcore: Add LevelOf(LevelEnabler), UnknownLevel
Browse files Browse the repository at this point in the history
Add a new function LevelOf that reports the minimum enabled log level
for a LevelEnabler.

This works by looping through all known Zap levels in-order,
returning the newly introduced UnknownLevel if none of the known levels
are enabled.

A LevelEnabler or Core implementation may implement the `Level() Level`
method to override and optimize this behavior.
AtomicLevel already implemented this method.
This change adds the method to all Core implementations shipped with
Zap.

Note:
UnknownLevel is set at FatalLevel+1 to account for the possibility that
users of Zap are using DebugLevel-1 as their own custom log level--even
though this isn't supported, it's preferable not to break these users.
Users are less likely to use FatalLevel+1 since Fatal means "exit the
application."

Resolves #1144
Supersedes #1143, which was not backwards compatible
  • Loading branch information
abhinav committed Aug 15, 2022
1 parent bdd673d commit 6087ffe
Show file tree
Hide file tree
Showing 16 changed files with 275 additions and 14 deletions.
35 changes: 35 additions & 0 deletions internal/level_enab.go
@@ -0,0 +1,35 @@
// Copyright (c) 2022 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package internal

import "go.uber.org/zap/zapcore"

// LeveledEnabler is an interface satisfied by LevelEnablers that are able to
// report their own level.
//
// This interface is defined to use more conveniently in tests and non-zapcore
// packages.
// This cannot be imported from zapcore because of the cyclic dependency.
type LeveledEnabler interface {
zapcore.LevelEnabler

Level() zapcore.Level
}
3 changes: 3 additions & 0 deletions level.go
Expand Up @@ -22,6 +22,7 @@ package zap

import (
"go.uber.org/atomic"
"go.uber.org/zap/internal"
"go.uber.org/zap/zapcore"
)

Expand Down Expand Up @@ -70,6 +71,8 @@ type AtomicLevel struct {
l *atomic.Int32
}

var _ internal.LeveledEnabler = AtomicLevel{}

// NewAtomicLevel creates an AtomicLevel with InfoLevel and above logging
// enabled.
func NewAtomicLevel() AtomicLevel {
Expand Down
9 changes: 9 additions & 0 deletions zapcore/core.go
Expand Up @@ -69,6 +69,15 @@ type ioCore struct {
out WriteSyncer
}

var (
_ Core = (*ioCore)(nil)
_ leveledEnabler = (*ioCore)(nil)
)

func (c *ioCore) Level() Level {
return LevelOf(c.LevelEnabler)
}

func (c *ioCore) With(fields []Field) Core {
clone := c.clone()
addFields(clone.enc, fields)
Expand Down
4 changes: 4 additions & 0 deletions zapcore/core_test.go
Expand Up @@ -82,6 +82,10 @@ func TestIOCore(t *testing.T) {
).With([]Field{makeInt64Field("k", 1)})
defer assert.NoError(t, core.Sync(), "Expected Syncing a temp file to succeed.")

t.Run("LevelOf", func(t *testing.T) {
assert.Equal(t, InfoLevel, LevelOf(core), "Incorrect Core Level")
})

if ce := core.Check(Entry{Level: DebugLevel, Message: "debug"}, nil); ce != nil {
ce.Write(makeInt64Field("k", 2))
}
Expand Down
9 changes: 9 additions & 0 deletions zapcore/hook.go
Expand Up @@ -27,6 +27,11 @@ type hooked struct {
funcs []func(Entry) error
}

var (
_ Core = (*hooked)(nil)
_ leveledEnabler = (*hooked)(nil)
)

// RegisterHooks wraps a Core and runs a collection of user-defined callback
// hooks each time a message is logged. Execution of the callbacks is blocking.
//
Expand All @@ -40,6 +45,10 @@ func RegisterHooks(core Core, hooks ...func(Entry) error) Core {
}
}

func (h *hooked) Level() Level {
return LevelOf(h.Core)
}

func (h *hooked) Check(ent Entry, ce *CheckedEntry) *CheckedEntry {
// Let the wrapped Core decide whether to log this message or not. This
// also gives the downstream a chance to register itself directly with the
Expand Down
11 changes: 11 additions & 0 deletions zapcore/hook_test.go
Expand Up @@ -27,6 +27,7 @@ import (
"go.uber.org/zap/zaptest/observer"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestHooks(t *testing.T) {
Expand All @@ -42,6 +43,11 @@ func TestHooks(t *testing.T) {

for _, tt := range tests {
fac, logs := observer.New(tt.coreLevel)

// sanity check
require.Equal(t, tt.coreLevel, LevelOf(fac),
"Original logger has the wrong level")

intField := makeInt64Field("foo", 42)
ent := Entry{Message: "bar", Level: tt.entryLevel}

Expand All @@ -57,6 +63,11 @@ func TestHooks(t *testing.T) {
ce.Write()
}

t.Run("LevelOf", func(t *testing.T) {
assert.Equal(t, tt.coreLevel, LevelOf(h),
"Wrapped logger has the wrong log level")
})

if tt.expectCall {
assert.Equal(t, 1, called, "Expected to call hook once.")
assert.Equal(
Expand Down
9 changes: 9 additions & 0 deletions zapcore/increase_level.go
Expand Up @@ -27,6 +27,11 @@ type levelFilterCore struct {
level LevelEnabler
}

var (
_ Core = (*levelFilterCore)(nil)
_ leveledEnabler = (*levelFilterCore)(nil)
)

// NewIncreaseLevelCore creates a core that can be used to increase the level of
// an existing Core. It cannot be used to decrease the logging level, as it acts
// as a filter before calling the underlying core. If level decreases the log level,
Expand All @@ -45,6 +50,10 @@ func (c *levelFilterCore) Enabled(lvl Level) bool {
return c.level.Enabled(lvl)
}

func (c *levelFilterCore) Level() Level {
return LevelOf(c.level)
}

func (c *levelFilterCore) With(fields []Field) Core {
return &levelFilterCore{c.core.With(fields), c.level}
}
Expand Down
9 changes: 9 additions & 0 deletions zapcore/increase_level_test.go
Expand Up @@ -82,6 +82,10 @@ func TestIncreaseLevel(t *testing.T) {
t.Run(msg, func(t *testing.T) {
logger, logs := observer.New(tt.coreLevel)

// sanity check
require.Equal(t, tt.coreLevel, LevelOf(logger),
"Original logger has the wrong level")

filteredLogger, err := NewIncreaseLevelCore(logger, tt.increaseLevel)
if tt.wantErr {
require.Error(t, err)
Expand All @@ -95,6 +99,11 @@ func TestIncreaseLevel(t *testing.T) {

require.NoError(t, err)

t.Run("LevelOf", func(t *testing.T) {
assert.Equal(t, tt.increaseLevel, LevelOf(filteredLogger),
"Filtered logger has the wrong level")
})

for l := DebugLevel; l <= FatalLevel; l++ {
enabled := filteredLogger.Enabled(l)
entry := Entry{Level: l}
Expand Down
42 changes: 42 additions & 0 deletions zapcore/level.go
Expand Up @@ -53,6 +53,11 @@ const (

_minLevel = DebugLevel
_maxLevel = FatalLevel

// UnknownLevel is an invalid value for Level.
//
// Core implementations may panic if they see messages of this level.
UnknownLevel = _maxLevel + 1
)

// ParseLevel parses a level based on the lower-case or all-caps ASCII
Expand All @@ -67,6 +72,43 @@ func ParseLevel(text string) (Level, error) {
return level, err
}

type leveledEnabler interface {
LevelEnabler

Level() Level
}

// LevelOf reports the minimum enabled log level for the given LevelEnabler
// from Zap's supported log levels, or [UnknownLevel] if none of them are
// enabled.
//
// A LevelEnabler may implement a 'Level() Level' method to override the
// behavior of this function.
//
// func (c *core) Level() Level {
// return c.currentLevel
// }
//
// It is recommended that [Core] implementations that wrap other cores use
// LevelOf to retrieve the level of the wrapped core. For example,
//
// func (c *coreWrapper) Level() Level {
// return zapcore.LevelOf(c.wrappedCore)
// }
func LevelOf(enab LevelEnabler) Level {
if lvler, ok := enab.(leveledEnabler); ok {
return lvler.Level()
}

for lvl := _minLevel; lvl <= _maxLevel; lvl++ {
if enab.Enabled(lvl) {
return lvl
}
}

return UnknownLevel
}

// String returns a lower-case ASCII representation of the log level.
func (l Level) String() string {
switch l {
Expand Down
63 changes: 55 additions & 8 deletions zapcore/level_test.go
Expand Up @@ -31,14 +31,15 @@ import (

func TestLevelString(t *testing.T) {
tests := map[Level]string{
DebugLevel: "debug",
InfoLevel: "info",
WarnLevel: "warn",
ErrorLevel: "error",
DPanicLevel: "dpanic",
PanicLevel: "panic",
FatalLevel: "fatal",
Level(-42): "Level(-42)",
DebugLevel: "debug",
InfoLevel: "info",
WarnLevel: "warn",
ErrorLevel: "error",
DPanicLevel: "dpanic",
PanicLevel: "panic",
FatalLevel: "fatal",
Level(-42): "Level(-42)",
UnknownLevel: "Level(6)", // UnknownLevel does not have a name
}

for lvl, stringLevel := range tests {
Expand Down Expand Up @@ -197,3 +198,49 @@ func TestLevelAsFlagValue(t *testing.T) {
"Unexpected error output from invalid flag input.",
)
}

// enablerWithCustomLevel is a LevelEnabler that implements a custom Level
// method.
type enablerWithCustomLevel struct{ lvl Level }

var _ leveledEnabler = (*enablerWithCustomLevel)(nil)

func (l *enablerWithCustomLevel) Enabled(lvl Level) bool {
return l.lvl.Enabled(lvl)
}

func (l *enablerWithCustomLevel) Level() Level {
return l.lvl
}

func TestLevelOf(t *testing.T) {
tests := []struct {
desc string
give LevelEnabler
want Level
}{
{desc: "debug", give: DebugLevel, want: DebugLevel},
{desc: "info", give: InfoLevel, want: InfoLevel},
{desc: "warn", give: WarnLevel, want: WarnLevel},
{desc: "error", give: ErrorLevel, want: ErrorLevel},
{desc: "dpanic", give: DPanicLevel, want: DPanicLevel},
{desc: "panic", give: PanicLevel, want: PanicLevel},
{desc: "fatal", give: FatalLevel, want: FatalLevel},
{
desc: "leveledEnabler",
give: &enablerWithCustomLevel{lvl: InfoLevel},
want: InfoLevel,
},
{
desc: "noop",
give: NewNopCore(), // always disabled
want: UnknownLevel,
},
}

for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
assert.Equal(t, tt.want, LevelOf(tt.give), "Reported level did not match.")
})
}
}
11 changes: 10 additions & 1 deletion zapcore/sampler.go
@@ -1,4 +1,4 @@
// Copyright (c) 2016 Uber Technologies, Inc.
// Copyright (c) 2016-2022 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -175,6 +175,11 @@ type sampler struct {
hook func(Entry, SamplingDecision)
}

var (
_ Core = (*sampler)(nil)
_ leveledEnabler = (*sampler)(nil)
)

// NewSampler creates a Core that samples incoming entries, which
// caps the CPU and I/O load of logging while attempting to preserve a
// representative subset of your logs.
Expand All @@ -192,6 +197,10 @@ func NewSampler(core Core, tick time.Duration, first, thereafter int) Core {
return NewSamplerWithOptions(core, tick, first, thereafter)
}

func (s *sampler) Level() Level {
return LevelOf(s.Core)
}

func (s *sampler) With(fields []Field) Core {
return &sampler{
Core: s.Core.With(fields),
Expand Down
13 changes: 11 additions & 2 deletions zapcore/sampler_test.go
@@ -1,4 +1,4 @@
// Copyright (c) 2016 Uber Technologies, Inc.
// Copyright (c) 2016-2022 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -88,6 +88,16 @@ func TestSampler(t *testing.T) {
}
}

func TestLevelOfSampler(t *testing.T) {
levels := []Level{DebugLevel, InfoLevel, WarnLevel, ErrorLevel, DPanicLevel, PanicLevel, FatalLevel}
for _, lvl := range levels {
t.Run(lvl.String(), func(t *testing.T) {
sampler, _ := fakeSampler(lvl, time.Minute, 2, 3)
assert.Equal(t, lvl, LevelOf(sampler), "Sampler level did not match.")
})
}
}

func TestSamplerDisabledLevels(t *testing.T) {
sampler, logs := fakeSampler(InfoLevel, time.Minute, 1, 100)

Expand Down Expand Up @@ -232,7 +242,6 @@ func TestSamplerConcurrent(t *testing.T) {
int(dropped.Load()),
"Unexpected number of logs dropped",
)

}

func TestSamplerRaces(t *testing.T) {
Expand Down

0 comments on commit 6087ffe

Please sign in to comment.