Skip to content

Commit

Permalink
Update to work with released log/slog (#1320)
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmuller authored Aug 19, 2023
1 parent d2aeb27 commit b454e18
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 7 deletions.
2 changes: 1 addition & 1 deletion exp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.19
require (
github.com/stretchr/testify v1.8.1
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb
)

require (
Expand Down
4 changes: 2 additions & 2 deletions exp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI=
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb h1:mIKbk8weKhSeLH2GmUTrvx8CjkyJmnU1wFmg59CUjFA=
golang.org/x/exp v0.0.0-20230811145659-89c5cff77bcb/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
5 changes: 2 additions & 3 deletions exp/zapslog/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
// Package zapslog provides an implementation of slog.Handler which writes to
// the supplied zapcore.Core.
//
// Since the slog proposal has not been officially accepted by the creation of this package,
// we do not want Zap's standard packages to take a dependency on `golang.org/x/exp/slog`.
// Instead, we provide this separate module as a way for users to integrate Zap with slog.
// For versions of Go before 1.21, this package uses golang.org/x/exp/slog.
// For Go 1.21 or newer, this package uses the standard log/slog package.
package zapslog // import "go.uber.org/zap/exp/zapslog"
79 changes: 79 additions & 0 deletions exp/zapslog/example_go121_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) 2023 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.

//go:build go1.21

package zapslog_test

import (
"context"
"log/slog"
"net"
"time"

"go.uber.org/zap"
"go.uber.org/zap/exp/zapslog"
)

type Password string

func (p Password) LogValue() slog.Value {
return slog.StringValue("REDACTED")
}

func Example_slog() {
logger := zap.NewExample(zap.IncreaseLevel(zap.InfoLevel))
defer logger.Sync()

sl := slog.New(zapslog.NewHandler(logger.Core(), nil /* options */))
ctx := context.Background()

sl.Info("user", "name", "Al", "secret", Password("secret"))
sl.Error("oops", "err", net.ErrClosed, "status", 500)
sl.LogAttrs(
ctx,
slog.LevelError,
"oops",
slog.Any("err", net.ErrClosed),
slog.Int("status", 500),
)
sl.Info("message",
slog.Group("group",
slog.Float64("pi", 3.14),
slog.Duration("1min", time.Minute),
),
)
sl.WithGroup("s").LogAttrs(
ctx,
slog.LevelWarn,
"warn msg", // message
slog.Uint64("u", 1),
slog.Any("m", map[string]any{
"foo": "bar",
}))
sl.LogAttrs(ctx, slog.LevelDebug, "not show up")

// Output:
// {"level":"info","msg":"user","name":"Al","secret":"REDACTED"}
// {"level":"error","msg":"oops","err":"use of closed network connection","status":500}
// {"level":"error","msg":"oops","err":"use of closed network connection","status":500}
// {"level":"info","msg":"message","group":{"pi":3.14,"1min":"1m0s"}}
// {"level":"warn","msg":"warn msg","s":{"u":1,"m":{"foo":"bar"}}}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

//go:build !go1.21

package zapslog_test

import (
Expand Down
190 changes: 190 additions & 0 deletions exp/zapslog/slog_go121.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// Copyright (c) 2023 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.

//go:build go1.21

package zapslog

import (
"context"
"log/slog"
"runtime"

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

// Handler implements the slog.Handler by writing to a zap Core.
type Handler struct {
core zapcore.Core
name string // logger name
addSource bool
}

// HandlerOptions are options for a Zap-based [slog.Handler].
type HandlerOptions struct {
// LoggerName is used for log entries received from slog.
//
// Defaults to empty.
LoggerName string

// AddSource configures the handler to annotate each message with the filename,
// line number, and function name.
// AddSource is false by default to skip the cost of computing
// this information.
AddSource bool
}

// NewHandler builds a [Handler] that writes to the supplied [zapcore.Core]
// with the default options.
func NewHandler(core zapcore.Core, opts *HandlerOptions) *Handler {
if opts == nil {
opts = &HandlerOptions{}
}
return &Handler{
core: core,
name: opts.LoggerName,
addSource: opts.AddSource,
}
}

var _ slog.Handler = (*Handler)(nil)

// groupObject holds all the Attrs saved in a slog.GroupValue.
type groupObject []slog.Attr

func (gs groupObject) MarshalLogObject(enc zapcore.ObjectEncoder) error {
for _, attr := range gs {
convertAttrToField(attr).AddTo(enc)
}
return nil
}

func convertAttrToField(attr slog.Attr) zapcore.Field {
switch attr.Value.Kind() {
case slog.KindBool:
return zap.Bool(attr.Key, attr.Value.Bool())
case slog.KindDuration:
return zap.Duration(attr.Key, attr.Value.Duration())
case slog.KindFloat64:
return zap.Float64(attr.Key, attr.Value.Float64())
case slog.KindInt64:
return zap.Int64(attr.Key, attr.Value.Int64())
case slog.KindString:
return zap.String(attr.Key, attr.Value.String())
case slog.KindTime:
return zap.Time(attr.Key, attr.Value.Time())
case slog.KindUint64:
return zap.Uint64(attr.Key, attr.Value.Uint64())
case slog.KindGroup:
return zap.Object(attr.Key, groupObject(attr.Value.Group()))
case slog.KindLogValuer:
return convertAttrToField(slog.Attr{
Key: attr.Key,
// TODO: resolve the value in a lazy way.
// This probably needs a new Zap field type
// that can be resolved lazily.
Value: attr.Value.Resolve(),
})
default:
return zap.Any(attr.Key, attr.Value.Any())
}
}

// convertSlogLevel maps slog Levels to zap Levels.
// Note that there is some room between slog levels while zap levels are continuous, so we can't 1:1 map them.
// See also https://go.googlesource.com/proposal/+/master/design/56345-structured-logging.md?pli=1#levels
func convertSlogLevel(l slog.Level) zapcore.Level {
switch {
case l >= slog.LevelError:
return zapcore.ErrorLevel
case l >= slog.LevelWarn:
return zapcore.WarnLevel
case l >= slog.LevelInfo:
return zapcore.InfoLevel
default:
return zapcore.DebugLevel
}
}

// Enabled reports whether the handler handles records at the given level.
func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
return h.core.Enabled(convertSlogLevel(level))
}

// Handle handles the Record.
func (h *Handler) Handle(ctx context.Context, record slog.Record) error {
ent := zapcore.Entry{
Level: convertSlogLevel(record.Level),
Time: record.Time,
Message: record.Message,
LoggerName: h.name,
// TODO: do we need to set the following fields?
// Stack:
}
ce := h.core.Check(ent, nil)
if ce == nil {
return nil
}

if h.addSource && record.PC != 0 {
frame, _ := runtime.CallersFrames([]uintptr{record.PC}).Next()
if frame.PC != 0 {
ce.Caller = zapcore.EntryCaller{
Defined: true,
PC: frame.PC,
File: frame.File,
Line: frame.Line,
Function: frame.Function,
}
}
}

fields := make([]zapcore.Field, 0, record.NumAttrs())
record.Attrs(func(attr slog.Attr) bool {
fields = append(fields, convertAttrToField(attr))
return true
})
ce.Write(fields...)
return nil
}

// WithAttrs returns a new Handler whose attributes consist of
// both the receiver's attributes and the arguments.
func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
fields := make([]zapcore.Field, len(attrs))
for i, attr := range attrs {
fields[i] = convertAttrToField(attr)
}
return h.withFields(fields...)
}

// WithGroup returns a new Handler with the given group appended to
// the receiver's existing groups.
func (h *Handler) WithGroup(group string) slog.Handler {
return h.withFields(zap.Namespace(group))
}

// withFields returns a cloned Handler with the given fields.
func (h *Handler) withFields(fields ...zapcore.Field) *Handler {
cloned := *h
cloned.core = h.core.With(fields)
return &cloned
}
50 changes: 50 additions & 0 deletions exp/zapslog/slog_go121_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright (c) 2023 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.

//go:build go1.21

package zapslog

import (
"log/slog"
"testing"

"github.com/stretchr/testify/require"
"go.uber.org/zap/zapcore"
"go.uber.org/zap/zaptest/observer"
)

func TestAddSource(t *testing.T) {
r := require.New(t)
fac, logs := observer.New(zapcore.DebugLevel)
sl := slog.New(NewHandler(fac, &HandlerOptions{
AddSource: true,
}))
sl.Info("msg")

r.Len(logs.AllUntimed(), 1, "Expected exactly one entry to be logged")
entry := logs.AllUntimed()[0]
r.Equal("msg", entry.Message, "Unexpected message")
r.Regexp(
`/slog_go121_test.go:\d+$`,
entry.Caller.String(),
"Unexpected caller annotation.",
)
}
2 changes: 2 additions & 0 deletions exp/zapslog/slog.go → exp/zapslog/slog_pre_go121.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

//go:build !go1.21

package zapslog

import (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

//go:build !go1.21

package zapslog

import (
Expand All @@ -41,7 +43,7 @@ func TestAddSource(t *testing.T) {
entry := logs.AllUntimed()[0]
r.Equal("msg", entry.Message, "Unexpected message")
r.Regexp(
`/slog_test.go:\d+$`,
`/slog_pre_go121_test.go:\d+$`,
entry.Caller.String(),
"Unexpected caller annotation.",
)
Expand Down

0 comments on commit b454e18

Please sign in to comment.