update slog/logger #351

Merged
vtolstov merged 11 commits from devstigneev/micro:fix_attrs_logger into v3 2024-10-12 12:37:44 +03:00
6 changed files with 101 additions and 167 deletions
Showing only changes of commit 1acaa898a2 - Show all commits

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5
go.unistack.org/micro-proto/v3 v3.4.1
golang.org/x/sync v0.3.0 golang.org/x/sync v0.3.0
google.golang.org/grpc v1.57.0 google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.31.0 google.golang.org/protobuf v1.31.0

2
go.sum
View File

@ -13,6 +13,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E= github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5 h1:G/FZtUu7a6NTWl3KUHMV9jkLAh/Rvtf03NWMHaEDl+E=
github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I= github.com/silas/dag v0.0.0-20220518035006-a7e85ada93c5/go.mod h1:7RTUFBdIRC9nZ7/3RyRNH1bdqIShrDejd1YbLwgPS+I=
go.unistack.org/micro-proto/v3 v3.4.1 h1:UTjLSRz2YZuaHk9iSlVqqsA50JQNAEK2ZFboGqtEa9Q=
go.unistack.org/micro-proto/v3 v3.4.1/go.mod h1:okx/cnOhzuCX0ggl/vToatbCupi0O44diiiLLsZ93Zo=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=

View File

@ -45,20 +45,27 @@ type Logger interface {
// Fatal level message // Fatal level message
Fatal(ctx context.Context, args ...interface{}) Fatal(ctx context.Context, args ...interface{})
// Infof level message // Infof level message
// DEPRECATED
Infof(ctx context.Context, msg string, args ...interface{}) Infof(ctx context.Context, msg string, args ...interface{})
// Tracef level message // Tracef level message
// DEPRECATED
Tracef(ctx context.Context, msg string, args ...interface{}) Tracef(ctx context.Context, msg string, args ...interface{})
// Debug level message // Debugf level message
// DEPRECATED
Debugf(ctx context.Context, msg string, args ...interface{}) Debugf(ctx context.Context, msg string, args ...interface{})
// Warn level message // Warnf level message
// DEPRECATED
Warnf(ctx context.Context, msg string, args ...interface{}) Warnf(ctx context.Context, msg string, args ...interface{})
// Error level message // Errorf level message
// DEPRECATED
Errorf(ctx context.Context, msg string, args ...interface{}) Errorf(ctx context.Context, msg string, args ...interface{})
// Fatal level message // Fatalf level message
// DEPRECATED
Fatalf(ctx context.Context, msg string, args ...interface{}) Fatalf(ctx context.Context, msg string, args ...interface{})
// Log logs message with needed level // Log logs message with needed level
Log(ctx context.Context, level Level, args ...interface{}) Log(ctx context.Context, level Level, args ...interface{})
// Logf logs message with needed level // Logf logs message with needed level
// DEPRECATED
Logf(ctx context.Context, level Level, msg string, args ...interface{}) Logf(ctx context.Context, level Level, msg string, args ...interface{})
// Name returns broker instance name // Name returns broker instance name
Name() string Name() string

View File

@ -102,14 +102,14 @@ func WithOutput(out io.Writer) Option {
} }
} }
// WitAddStacktrace controls writing stacktrace on error // WithAddStacktrace controls writing stacktrace on error
func WithAddStacktrace(v bool) Option { func WithAddStacktrace(v bool) Option {
return func(o *Options) { return func(o *Options) {
o.AddStacktrace = v o.AddStacktrace = v
} }
} }
// WitAddSource controls writing source file and pos in log // WithAddSource controls writing source file and pos in log
func WithAddSource(v bool) Option { func WithAddSource(v bool) Option {
return func(o *Options) { return func(o *Options) {
o.AddSource = v o.AddSource = v

View File

@ -15,6 +15,9 @@ import (
"go.unistack.org/micro/v3/tracer" "go.unistack.org/micro/v3/tracer"
) )
const badKey = "!BADKEY"
const emptyMSg = "!EMPTYMSG"
var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`) var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`)
var ( var (
@ -152,22 +155,39 @@ func (s *slogLogger) Init(opts ...logger.Option) error {
func (s *slogLogger) Log(ctx context.Context, lvl logger.Level, attrs ...interface{}) { func (s *slogLogger) Log(ctx context.Context, lvl logger.Level, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", lvl.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", lvl.String()).Inc()
if !s.V(lvl) { if !s.V(lvl) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] msg := attrs[0]
r := slog.NewRecord(s.opts.TimeFunc(), loggerToSlogLevel(lvl), fmt.Sprintf("%s", attrs[0]), pcs[0]) attrs = prepareAttributes(attrs[1:])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...) ve, hasErr := msg.(error)
if hasErr && ve != nil {
attrs = append(attrs, slog.String(s.opts.ErrorKey, ve.Error()))
} }
for _, fn := range s.opts.ContextAttrFuncs {
a := prepareAttributes(fn(ctx))
attrs = append(attrs, a...)
}
if !hasErr {
for idx, attr := range attrs { for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil { if ve, hasErr = attr.(error); hasErr && ve != nil {
if idx%2 == 1 {
attrs = append(attrs, slog.String(s.opts.ErrorKey, ve.Error()))
} else {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error()) attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
}
if span, ok := tracer.SpanFromContext(ctx); ok {
span.SetStatus(tracer.SpanStatusError, ve.Error())
}
break break
} }
} }
}
if s.opts.AddStacktrace && lvl == logger.ErrorLevel { if s.opts.AddStacktrace && lvl == logger.ErrorLevel {
stackInfo := make([]byte, 1024*1024) stackInfo := make([]byte, 1024*1024)
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 { if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
@ -177,22 +197,18 @@ func (s *slogLogger) Log(ctx context.Context, lvl logger.Level, attrs ...interfa
} }
} }
} }
r.Add(attrs[1:]...)
r.Attrs(func(a slog.Attr) bool { var pcs [1]uintptr
if a.Key == s.opts.ErrorKey { runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
if span, ok := tracer.SpanFromContext(ctx); ok { r := slog.NewRecord(s.opts.TimeFunc(), loggerToSlogLevel(lvl), fmt.Sprintf("%s", msg), pcs[0])
span.SetStatus(tracer.SpanStatusError, a.Value.String()) r.Add(attrs...)
return false
}
}
return true
})
_ = s.handler.Handle(ctx, r) _ = s.handler.Handle(ctx, r)
} }
// Logf DEPRECATED
func (s *slogLogger) Logf(ctx context.Context, lvl logger.Level, msg string, attrs ...interface{}) { func (s *slogLogger) Logf(ctx context.Context, lvl logger.Level, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", lvl.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", lvl.String()).Inc()
if !s.V(lvl) { if !s.V(lvl) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -231,30 +247,13 @@ func (s *slogLogger) Logf(ctx context.Context, lvl logger.Level, msg string, att
} }
func (s *slogLogger) Info(ctx context.Context, attrs ...interface{}) { func (s *slogLogger) Info(ctx context.Context, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.InfoLevel.String()).Inc() s.Log(ctx, logger.InfoLevel, attrs...)
if !s.V(logger.InfoLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(s.opts.TimeFunc(), slog.LevelInfo, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.handler.Handle(ctx, r)
} }
// Infof DEPRECATED
func (s *slogLogger) Infof(ctx context.Context, msg string, attrs ...interface{}) { func (s *slogLogger) Infof(ctx context.Context, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.InfoLevel.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.InfoLevel.String()).Inc()
if !s.V(logger.InfoLevel) { if !s.V(logger.InfoLevel) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -275,30 +274,13 @@ func (s *slogLogger) Infof(ctx context.Context, msg string, attrs ...interface{}
} }
func (s *slogLogger) Debug(ctx context.Context, attrs ...interface{}) { func (s *slogLogger) Debug(ctx context.Context, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.DebugLevel.String()).Inc() s.Log(ctx, logger.DebugLevel, attrs...)
if !s.V(logger.DebugLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(s.opts.TimeFunc(), slog.LevelDebug, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.handler.Handle(ctx, r)
} }
// Debugf DEPRECATED
func (s *slogLogger) Debugf(ctx context.Context, msg string, attrs ...interface{}) { func (s *slogLogger) Debugf(ctx context.Context, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.DebugLevel.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.DebugLevel.String()).Inc()
if !s.V(logger.DebugLevel) { if !s.V(logger.DebugLevel) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -319,30 +301,13 @@ func (s *slogLogger) Debugf(ctx context.Context, msg string, attrs ...interface{
} }
func (s *slogLogger) Trace(ctx context.Context, attrs ...interface{}) { func (s *slogLogger) Trace(ctx context.Context, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.TraceLevel.String()).Inc() s.Log(ctx, logger.TraceLevel, attrs...)
if !s.V(logger.TraceLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(s.opts.TimeFunc(), slog.LevelDebug-1, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.handler.Handle(ctx, r)
} }
// Tracef DEPRECATED
func (s *slogLogger) Tracef(ctx context.Context, msg string, attrs ...interface{}) { func (s *slogLogger) Tracef(ctx context.Context, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.TraceLevel.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.TraceLevel.String()).Inc()
if !s.V(logger.TraceLevel) { if !s.V(logger.TraceLevel) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -363,48 +328,13 @@ func (s *slogLogger) Tracef(ctx context.Context, msg string, attrs ...interface{
} }
func (s *slogLogger) Error(ctx context.Context, attrs ...interface{}) { func (s *slogLogger) Error(ctx context.Context, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.ErrorLevel.String()).Inc() s.Log(ctx, logger.ErrorLevel, attrs...)
if !s.V(logger.ErrorLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(s.opts.TimeFunc(), slog.LevelError, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
if s.opts.AddStacktrace {
stackInfo := make([]byte, 1024*1024)
if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 {
traceLines := reTrace.Split(string(stackInfo[:stackSize]), -1)
if len(traceLines) != 0 {
attrs = append(attrs, slog.String("stacktrace", traceLines[len(traceLines)-1]))
}
}
}
r.Add(attrs[1:]...)
r.Attrs(func(a slog.Attr) bool {
if a.Key == s.opts.ErrorKey {
if span, ok := tracer.SpanFromContext(ctx); ok {
span.SetStatus(tracer.SpanStatusError, a.Value.String())
return false
}
}
return true
})
_ = s.handler.Handle(ctx, r)
} }
// Errorf DEPRECATED
func (s *slogLogger) Errorf(ctx context.Context, msg string, attrs ...interface{}) { func (s *slogLogger) Errorf(ctx context.Context, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.ErrorLevel.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.ErrorLevel.String()).Inc()
if !s.V(logger.ErrorLevel) { if !s.V(logger.ErrorLevel) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -443,31 +373,14 @@ func (s *slogLogger) Errorf(ctx context.Context, msg string, attrs ...interface{
} }
func (s *slogLogger) Fatal(ctx context.Context, attrs ...interface{}) { func (s *slogLogger) Fatal(ctx context.Context, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.FatalLevel.String()).Inc() s.Log(ctx, logger.FatalLevel, attrs...)
if !s.V(logger.FatalLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(s.opts.TimeFunc(), slog.LevelError+1, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.handler.Handle(ctx, r)
os.Exit(1) os.Exit(1)
} }
// Fatalf DEPRECATED
func (s *slogLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{}) { func (s *slogLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.FatalLevel.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.FatalLevel.String()).Inc()
if !s.V(logger.FatalLevel) { if !s.V(logger.FatalLevel) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -489,30 +402,13 @@ func (s *slogLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{
} }
func (s *slogLogger) Warn(ctx context.Context, attrs ...interface{}) { func (s *slogLogger) Warn(ctx context.Context, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.WarnLevel.String()).Inc() s.Log(ctx, logger.WarnLevel, attrs...)
if !s.V(logger.WarnLevel) {
return
}
var pcs [1]uintptr
runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof]
r := slog.NewRecord(s.opts.TimeFunc(), slog.LevelWarn, fmt.Sprintf("%s", attrs[0]), pcs[0])
for _, fn := range s.opts.ContextAttrFuncs {
attrs = append(attrs, fn(ctx)...)
}
for idx, attr := range attrs {
if ve, ok := attr.(error); ok && ve != nil {
attrs[idx] = slog.String(s.opts.ErrorKey, ve.Error())
break
}
}
r.Add(attrs[1:]...)
_ = s.handler.Handle(ctx, r)
} }
// Warnf DEPRECATED
func (s *slogLogger) Warnf(ctx context.Context, msg string, attrs ...interface{}) { func (s *slogLogger) Warnf(ctx context.Context, msg string, attrs ...interface{}) {
s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.WarnLevel.String()).Inc() s.opts.Meter.Counter(semconv.LoggerMessageTotal, "level", logger.WarnLevel.String()).Inc()
if !s.V(logger.WarnLevel) { if !s.V(logger.WarnLevel) || len(attrs) == 0 {
return return
} }
var pcs [1]uintptr var pcs [1]uintptr
@ -581,3 +477,13 @@ func slogToLoggerLevel(level slog.Level) logger.Level {
return logger.InfoLevel return logger.InfoLevel
} }
} }
func prepareAttributes(attrs []interface{}) []interface{} {
if len(attrs)%2 == 1 {
attrs = append(attrs, badKey)
attrs[len(attrs)-1], attrs[len(attrs)-2] = attrs[len(attrs)-2], attrs[len(attrs)-1]
}
return attrs
}

View File

@ -3,6 +3,7 @@ package slog
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"log" "log"
"testing" "testing"
@ -29,13 +30,25 @@ func TestError(t *testing.T) {
func TestErrorf(t *testing.T) { func TestErrorf(t *testing.T) {
ctx := context.TODO() ctx := context.TODO()
buf := bytes.NewBuffer(nil) buf := bytes.NewBuffer(nil)
l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf), logger.WithAddStacktrace(true)) l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf), logger.WithAddStacktrace(true))
if err := l.Init(); err != nil { if err := l.Init(logger.WithContextAttrFuncs(func(ctx context.Context) []interface{} {
return nil
})); err != nil {
t.Fatal(err) t.Fatal(err)
} }
l.Errorf(ctx, "message", fmt.Errorf("error message")) l.Log(ctx, logger.ErrorLevel, "message", errors.New("error msg"))
if !bytes.Contains(buf.Bytes(), []byte(`"!BADKEY":"`)) {
t.Fatalf("logger BADKEY not works, buf contains: %s", buf.Bytes())
}
l.Log(ctx, logger.ErrorLevel, errors.New("error msg"))
if !bytes.Contains(buf.Bytes(), []byte(`"error":"error msg"`)) {
t.Fatalf("logger error not works, buf contains: %s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`"stacktrace":"`)) { if !bytes.Contains(buf.Bytes(), []byte(`"stacktrace":"`)) {
t.Fatalf("logger stacktrace not works, buf contains: %s", buf.Bytes()) t.Fatalf("logger stacktrace not works, buf contains: %s", buf.Bytes())
} }
@ -99,6 +112,11 @@ func TestFromContextWithFields(t *testing.T) {
if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) { if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) {
t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes()) t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes())
} }
l.Info(ctx, "test", "uncorrected number attributes")
if !bytes.Contains(buf.Bytes(), []byte(`"!BADKEY":"uncorrected number attributes"`)) {
t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes())
}
} }
func TestClone(t *testing.T) { func TestClone(t *testing.T) {