log,log/slog: get correct source line when slog calls log

Before slog.SetDefault is called the first time, calls to slog's
default Logger invoke log's default Logger.

Originally, this was done by calling log.Output. This caused source
line information to be wrong sometimes, because log.Output requires a
call depth and the code invoking it could not know how many calls were
between it and the original logging call (slog.Info, etc.). The line
information would be right if the default handler was called directly,
but wrong if it was wrapped by another handler. The handler has the pc
of the logging call, but it couldn't give that pc to the log package.

This CL fixes the problem by adding a function in the log package
that uses the pc instead of a call depth, and making that function
available to slog.

The simplest way to add pc functionality to the log package is to add
a pc argument to Logger.output, which uses it only if it's not zero.

To make that function visible to slog without exporting it, we store
the function in a variable that lives in the new log/internal package.

Change-Id: I0bb6daebb4abc518a7ccc4e6d2f3c1093b1d0fe4
Reviewed-on: https://go-review.googlesource.com/c/go/+/482239
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Joseph Tsai <joetsai@digital-static.net>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
This commit is contained in:
Jonathan Amsterdam 2023-04-04 07:08:57 -04:00
parent ebc13fb0b8
commit 134af2e3db
7 changed files with 61 additions and 35 deletions

View File

@ -45,6 +45,7 @@ var depsRules = `
internal/cpu, internal/goarch, internal/cpu, internal/goarch,
internal/goexperiment, internal/goos, internal/goexperiment, internal/goos,
internal/goversion, internal/nettrace, internal/platform, internal/goversion, internal/nettrace, internal/platform,
log/internal,
maps, slices, unicode/utf8, unicode/utf16, unicode, maps, slices, unicode/utf8, unicode/utf16, unicode,
unsafe; unsafe;
@ -369,7 +370,7 @@ var depsRules = `
< NET; < NET;
# logging - most packages should not import; http and up is allowed # logging - most packages should not import; http and up is allowed
FMT FMT, log/internal
< log; < log;
log, log/slog !< crypto/tls, database/sql, go/importer, testing; log, log/slog !< crypto/tls, database/sql, go/importer, testing;
@ -382,7 +383,7 @@ var depsRules = `
FMT, FMT,
encoding, encoding/json, encoding, encoding/json,
log, log, log/internal,
log/slog/internal, log/slog/internal/buffer, log/slog/internal, log/slog/internal/buffer,
slices slices
< log/slog < log/slog

View File

@ -0,0 +1,12 @@
// 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 internal contains definitions used by both log and log/slog.
package internal
// DefaultOutput holds a function which calls the default log.Logger's
// output function.
// It allows slog.defaultHandler to call into an unexported function of
// the log package.
var DefaultOutput func(pc uintptr, data []byte) error

View File

@ -17,6 +17,7 @@ package log
import ( import (
"fmt" "fmt"
"io" "io"
"log/internal"
"os" "os"
"runtime" "runtime"
"sync" "sync"
@ -191,12 +192,14 @@ func putBuffer(p *[]byte) {
// paths it will be 2. // paths it will be 2.
func (l *Logger) Output(calldepth int, s string) error { func (l *Logger) Output(calldepth int, s string) error {
calldepth++ // +1 for this frame. calldepth++ // +1 for this frame.
return l.output(calldepth, func(b []byte) []byte { return l.output(0, calldepth, func(b []byte) []byte {
return append(b, s...) return append(b, s...)
}) })
} }
func (l *Logger) output(calldepth int, appendOutput func([]byte) []byte) error { // output can take either a calldepth or a pc to get source line information.
// It uses the pc if it is non-zero.
func (l *Logger) output(pc uintptr, calldepth int, appendOutput func([]byte) []byte) error {
if l.isDiscard.Load() { if l.isDiscard.Load() {
return nil return nil
} }
@ -211,11 +214,21 @@ func (l *Logger) output(calldepth int, appendOutput func([]byte) []byte) error {
var file string var file string
var line int var line int
if flag&(Lshortfile|Llongfile) != 0 { if flag&(Lshortfile|Llongfile) != 0 {
var ok bool if pc == 0 {
_, file, line, ok = runtime.Caller(calldepth) var ok bool
if !ok { _, file, line, ok = runtime.Caller(calldepth)
file = "???" if !ok {
line = 0 file = "???"
line = 0
}
} else {
fs := runtime.CallersFrames([]uintptr{pc})
f, _ := fs.Next()
file = f.File
if file == "" {
file = "???"
}
line = f.Line
} }
} }
@ -233,10 +246,18 @@ func (l *Logger) output(calldepth int, appendOutput func([]byte) []byte) error {
return err return err
} }
func init() {
internal.DefaultOutput = func(pc uintptr, data []byte) error {
return std.output(pc, 0, func(buf []byte) []byte {
return append(buf, data...)
})
}
}
// Print calls l.Output to print to the logger. // Print calls l.Output to print to the logger.
// Arguments are handled in the manner of fmt.Print. // Arguments are handled in the manner of fmt.Print.
func (l *Logger) Print(v ...any) { func (l *Logger) Print(v ...any) {
l.output(2, func(b []byte) []byte { l.output(0, 2, func(b []byte) []byte {
return fmt.Append(b, v...) return fmt.Append(b, v...)
}) })
} }
@ -244,7 +265,7 @@ func (l *Logger) Print(v ...any) {
// Printf calls l.Output to print to the logger. // Printf calls l.Output to print to the logger.
// Arguments are handled in the manner of fmt.Printf. // Arguments are handled in the manner of fmt.Printf.
func (l *Logger) Printf(format string, v ...any) { func (l *Logger) Printf(format string, v ...any) {
l.output(2, func(b []byte) []byte { l.output(0, 2, func(b []byte) []byte {
return fmt.Appendf(b, format, v...) return fmt.Appendf(b, format, v...)
}) })
} }
@ -252,7 +273,7 @@ func (l *Logger) Printf(format string, v ...any) {
// Println calls l.Output to print to the logger. // Println calls l.Output to print to the logger.
// Arguments are handled in the manner of fmt.Println. // Arguments are handled in the manner of fmt.Println.
func (l *Logger) Println(v ...any) { func (l *Logger) Println(v ...any) {
l.output(2, func(b []byte) []byte { l.output(0, 2, func(b []byte) []byte {
return fmt.Appendln(b, v...) return fmt.Appendln(b, v...)
}) })
} }
@ -365,7 +386,7 @@ func Writer() io.Writer {
// Print calls Output to print to the standard logger. // Print calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Print. // Arguments are handled in the manner of fmt.Print.
func Print(v ...any) { func Print(v ...any) {
std.output(2, func(b []byte) []byte { std.output(0, 2, func(b []byte) []byte {
return fmt.Append(b, v...) return fmt.Append(b, v...)
}) })
} }
@ -373,7 +394,7 @@ func Print(v ...any) {
// Printf calls Output to print to the standard logger. // Printf calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Printf. // Arguments are handled in the manner of fmt.Printf.
func Printf(format string, v ...any) { func Printf(format string, v ...any) {
std.output(2, func(b []byte) []byte { std.output(0, 2, func(b []byte) []byte {
return fmt.Appendf(b, format, v...) return fmt.Appendf(b, format, v...)
}) })
} }
@ -381,7 +402,7 @@ func Printf(format string, v ...any) {
// Println calls Output to print to the standard logger. // Println calls Output to print to the standard logger.
// Arguments are handled in the manner of fmt.Println. // Arguments are handled in the manner of fmt.Println.
func Println(v ...any) { func Println(v ...any) {
std.output(2, func(b []byte) []byte { std.output(0, 2, func(b []byte) []byte {
return fmt.Appendln(b, v...) return fmt.Appendln(b, v...)
}) })
} }

View File

@ -87,11 +87,11 @@ type Handler interface {
type defaultHandler struct { type defaultHandler struct {
ch *commonHandler ch *commonHandler
// log.Output, except for testing // internal.DefaultOutput, except for testing
output func(calldepth int, message string) error output func(pc uintptr, data []byte) error
} }
func newDefaultHandler(output func(int, string) error) *defaultHandler { func newDefaultHandler(output func(uintptr, []byte) error) *defaultHandler {
return &defaultHandler{ return &defaultHandler{
ch: &commonHandler{json: false}, ch: &commonHandler{json: false},
output: output, output: output,
@ -113,9 +113,7 @@ func (h *defaultHandler) Handle(ctx context.Context, r Record) error {
state := h.ch.newHandleState(buf, true, " ", nil) state := h.ch.newHandleState(buf, true, " ", nil)
defer state.free() defer state.free()
state.appendNonBuiltIns(r) state.appendNonBuiltIns(r)
return h.output(r.PC, *buf)
// skip [h.output, defaultHandler.Handle, handlerWriter.Write, log.Output]
return h.output(4, buf.String())
} }
func (h *defaultHandler) WithAttrs(as []Attr) Handler { func (h *defaultHandler) WithAttrs(as []Attr) Handler {

View File

@ -85,8 +85,8 @@ func TestDefaultHandle(t *testing.T) {
} { } {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
var got string var got string
var h Handler = newDefaultHandler(func(_ int, s string) error { var h Handler = newDefaultHandler(func(_ uintptr, b []byte) error {
got = s got = string(b)
return nil return nil
}) })
if test.with != nil { if test.with != nil {

View File

@ -7,6 +7,7 @@ package slog
import ( import (
"context" "context"
"log" "log"
loginternal "log/internal"
"log/slog/internal" "log/slog/internal"
"runtime" "runtime"
"sync/atomic" "sync/atomic"
@ -16,7 +17,7 @@ import (
var defaultLogger atomic.Value var defaultLogger atomic.Value
func init() { func init() {
defaultLogger.Store(New(newDefaultHandler(log.Output))) defaultLogger.Store(New(newDefaultHandler(loginternal.DefaultOutput)))
} }
// Default returns the default Logger. // Default returns the default Logger.

View File

@ -11,6 +11,7 @@ import (
"internal/testenv" "internal/testenv"
"io" "io"
"log" "log"
loginternal "log/internal"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
@ -70,7 +71,7 @@ func TestConnections(t *testing.T) {
// tests might change the default logger using SetDefault. Also ensure we // tests might change the default logger using SetDefault. Also ensure we
// restore the default logger at the end of the test. // restore the default logger at the end of the test.
currentLogger := Default() currentLogger := Default()
SetDefault(New(newDefaultHandler(log.Output))) SetDefault(New(newDefaultHandler(loginternal.DefaultOutput)))
t.Cleanup(func() { t.Cleanup(func() {
SetDefault(currentLogger) SetDefault(currentLogger)
}) })
@ -94,16 +95,8 @@ func TestConnections(t *testing.T) {
t.Run("wrap default handler", func(t *testing.T) { t.Run("wrap default handler", func(t *testing.T) {
// It should be possible to wrap the default handler and get the right output. // It should be possible to wrap the default handler and get the right output.
// But because the call depth to log.Output is hard-coded, the source line is wrong. // This works because the default handler uses the pc in the Record
// We want to use the pc inside the Record, but there is no way to give that to // to get the source line, rather than a call depth.
// the log package.
//
// TODO(jba): when slog lives under log in the standard library, we can
// move the bulk of log.Logger.Output to a function in an internal
// package, so both log and slog can call it.
//
// While slog lives in exp, we punt.
t.Skip("skip until this package is in the standard library")
logger := New(wrappingHandler{Default().Handler()}) logger := New(wrappingHandler{Default().Handler()})
logger.Info("msg", "d", 4) logger.Info("msg", "d", 4)
checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`) checkLogOutput(t, logbuf.String(), `logger_test.go:\d+: INFO msg d=4`)