Skip to content

Commit

Permalink
log/slog: add LogLoggerLevel to enable setting level on the default l…
Browse files Browse the repository at this point in the history
…ogger

Fixes golang#62418

Change-Id: I889a53d00c8a463b4d7ddb41893c000d7cd0e7b8
Reviewed-on: https://go-review.googlesource.com/c/go/+/525096
LUCI-TryBot-Result: Go LUCI <[email protected]>
Reviewed-by: Heschi Kreinick <[email protected]>
Reviewed-by: Jonathan Amsterdam <[email protected]>
  • Loading branch information
panjf2000 authored and jba committed Nov 10, 2023
1 parent ea14b63 commit 3188758
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 6 deletions.
1 change: 1 addition & 0 deletions api/next/62418.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pkg log/slog, func SetLogLoggerLevel(Level) Level #62418
58 changes: 58 additions & 0 deletions src/log/slog/example_log_level_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package slog_test

import (
"log"
"log/slog"
"log/slog/internal/slogtest"
"os"
)

// This example shows how to use slog.SetLogLoggerLevel to change the minimal level
// of the internal default handler for slog package before calling slog.SetDefault.
func ExampleSetLogLoggerLevel_log() {
defer log.SetFlags(log.Flags()) // revert changes after the example
log.SetFlags(0)
defer log.SetOutput(log.Writer()) // revert changes after the example
log.SetOutput(os.Stdout)

// Default logging level is slog.LevelInfo.
log.Print("log debug") // log debug
slog.Debug("debug") // no output
slog.Info("info") // INFO info

// Set the default logging level to slog.LevelDebug.
currentLogLevel := slog.SetLogLoggerLevel(slog.LevelDebug)
defer slog.SetLogLoggerLevel(currentLogLevel) // revert changes after the example

log.Print("log debug") // log debug
slog.Debug("debug") // DEBUG debug
slog.Info("info") // INFO info

// Output:
// log debug
// INFO info
// log debug
// DEBUG debug
// INFO info
}

// This example shows how to use slog.SetLogLoggerLevel to change the minimal level
// of the internal writer that uses the custom handler for log package after
// calling slog.SetDefault.
func ExampleSetLogLoggerLevel_slog() {
// Set the default logging level to slog.LevelError.
currentLogLevel := slog.SetLogLoggerLevel(slog.LevelError)
defer slog.SetLogLoggerLevel(currentLogLevel) // revert changes after the example

defer slog.SetDefault(slog.Default()) // revert changes after the example
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: slogtest.RemoveTime})))

log.Print("error") // level=ERROR msg=error

// Output:
// level=ERROR msg=error
}
2 changes: 1 addition & 1 deletion src/log/slog/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func newDefaultHandler(output func(uintptr, []byte) error) *defaultHandler {
}

func (*defaultHandler) Enabled(_ context.Context, l Level) bool {
return l >= LevelInfo
return l >= logLoggerLevel.Level()
}

// Collect the level, attributes and message in a string and
Expand Down
42 changes: 37 additions & 5 deletions src/log/slog/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,36 @@ import (

var defaultLogger atomic.Pointer[Logger]

var logLoggerLevel LevelVar

// SetLogLoggerLevel controls the level for the bridge to the [log] package.
//
// Before [SetDefault] is called, slog top-level logging functions call the default [log.Logger].
// In that mode, SetLogLoggerLevel sets the minimum level for those calls.
// By default, the minimum level is Info, so calls to [Debug]
// (as well as top-level logging calls at lower levels)
// will not be passed to the log.Logger. After calling
//
// slog.SetLogLoggerLevel(slog.LevelDebug)
//
// calls to [Debug] will be passed to the log.Logger.
//
// After [SetDefault] is called, calls to the default [log.Logger] are passed to the
// slog default handler. In that mode,
// SetLogLoggerLevel sets the level at which those calls are logged.
// That is, after calling
//
// slog.SetLogLoggerLevel(slog.LevelDebug)
//
// A call to [log.Printf] will result in output at level [LevelDebug].
//
// SetLogLoggerLevel returns the previous value.
func SetLogLoggerLevel(level Level) (oldLevel Level) {
oldLevel = logLoggerLevel.Level()
logLoggerLevel.Set(level)
return
}

func init() {
defaultLogger.Store(New(newDefaultHandler(loginternal.DefaultOutput)))
}
Expand All @@ -25,7 +55,8 @@ func Default() *Logger { return defaultLogger.Load() }

// SetDefault makes l the default [Logger].
// After this call, output from the log package's default Logger
// (as with [log.Print], etc.) will be logged at [LevelInfo] using l's Handler.
// (as with [log.Print], etc.) will be logged using l's Handler,
// at a level controlled by [SetLogLoggerLevel].
func SetDefault(l *Logger) {
defaultLogger.Store(l)
// If the default's handler is a defaultHandler, then don't use a handleWriter,
Expand All @@ -36,7 +67,7 @@ func SetDefault(l *Logger) {
// See TestSetDefault.
if _, ok := l.Handler().(*defaultHandler); !ok {
capturePC := log.Flags()&(log.Lshortfile|log.Llongfile) != 0
log.SetOutput(&handlerWriter{l.Handler(), LevelInfo, capturePC})
log.SetOutput(&handlerWriter{l.Handler(), &logLoggerLevel, capturePC})
log.SetFlags(0) // we want just the log message, no time or location
}
}
Expand All @@ -45,12 +76,13 @@ func SetDefault(l *Logger) {
// It is used to link the default log.Logger to the default slog.Logger.
type handlerWriter struct {
h Handler
level Level
level Leveler
capturePC bool
}

func (w *handlerWriter) Write(buf []byte) (int, error) {
if !w.h.Enabled(context.Background(), w.level) {
level := w.level.Level()
if !w.h.Enabled(context.Background(), level) {
return 0, nil
}
var pc uintptr
Expand All @@ -66,7 +98,7 @@ func (w *handlerWriter) Write(buf []byte) (int, error) {
if len(buf) > 0 && buf[len(buf)-1] == '\n' {
buf = buf[:len(buf)-1]
}
r := NewRecord(time.Now(), w.level, string(buf), pc)
r := NewRecord(time.Now(), level, string(buf), pc)
return origLen, w.h.Handle(context.Background(), r)
}

Expand Down
66 changes: 66 additions & 0 deletions src/log/slog/logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ func TestCallDepth(t *testing.T) {
}
}

defer SetDefault(Default()) // restore
logger := New(h)
SetDefault(logger)

Expand Down Expand Up @@ -363,6 +364,71 @@ func TestSetDefault(t *testing.T) {
}
}

// Test defaultHandler minimum level without calling slog.SetDefault.
func TestLogLoggerLevelForDefaultHandler(t *testing.T) {
// Revert any changes to the default logger, flags, and level of log and slog.
currentLogLoggerLevel := logLoggerLevel.Level()
currentLogWriter := log.Writer()
currentLogFlags := log.Flags()
t.Cleanup(func() {
logLoggerLevel.Set(currentLogLoggerLevel)
log.SetOutput(currentLogWriter)
log.SetFlags(currentLogFlags)
})

var logBuf bytes.Buffer
log.SetOutput(&logBuf)
log.SetFlags(0)

for _, test := range []struct {
logLevel Level
logFn func(string, ...any)
want string
}{
{LevelDebug, Debug, "DEBUG a"},
{LevelDebug, Info, "INFO a"},
{LevelInfo, Debug, ""},
{LevelInfo, Info, "INFO a"},
} {
SetLogLoggerLevel(test.logLevel)
test.logFn("a")
checkLogOutput(t, logBuf.String(), test.want)
logBuf.Reset()
}
}

// Test handlerWriter minimum level by calling slog.SetDefault.
func TestLogLoggerLevelForHandlerWriter(t *testing.T) {
removeTime := func(_ []string, a Attr) Attr {
if a.Key == TimeKey {
return Attr{}
}
return a
}

// Revert any changes to the default logger. This is important because other
// tests might change the default logger using SetDefault. Also ensure we
// restore the default logger at the end of the test.
currentLogger := Default()
currentLogLoggerLevel := logLoggerLevel.Level()
currentLogWriter := log.Writer()
currentFlags := log.Flags()
t.Cleanup(func() {
SetDefault(currentLogger)
logLoggerLevel.Set(currentLogLoggerLevel)
log.SetOutput(currentLogWriter)
log.SetFlags(currentFlags)
})

var logBuf bytes.Buffer
log.SetOutput(&logBuf)
log.SetFlags(0)
SetLogLoggerLevel(LevelError)
SetDefault(New(NewTextHandler(&logBuf, &HandlerOptions{ReplaceAttr: removeTime})))
log.Print("error")
checkLogOutput(t, logBuf.String(), `level=ERROR msg=error`)
}

func TestLoggerError(t *testing.T) {
var buf bytes.Buffer

Expand Down

0 comments on commit 3188758

Please sign in to comment.