Skip to content

Commit d3a738a

Browse files
author
Dean Karn
authored
slog support + Groups (#52)
1 parent d2677a2 commit d3a738a

File tree

18 files changed

+515
-96
lines changed

18 files changed

+515
-96
lines changed

.github/workflows/go.yml

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
test:
99
strategy:
1010
matrix:
11-
go-version: [1.18.x]
11+
go-version: [1.18.x, 1.21.x]
1212
os: [ubuntu-latest, macos-latest, windows-latest]
1313
runs-on: ${{ matrix.os }}
1414
steps:
@@ -32,7 +32,7 @@ jobs:
3232
run: go test -race -covermode=atomic -coverprofile="profile.cov" ./...
3333

3434
- name: Send Coverage
35-
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.18.x'
35+
if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.21.x'
3636
uses: shogo82148/actions-goveralls@v1
3737
with:
3838
path-to-profile: profile.cov
@@ -43,9 +43,9 @@ jobs:
4343
steps:
4444
- uses: actions/setup-go@v3
4545
with:
46-
go-version: 1.18.x
46+
go-version: 1.21.x
4747
- uses: actions/checkout@v3
4848
- name: golangci-lint
4949
uses: golangci/golangci-lint-action@v3
5050
with:
51-
version: v1.46.2
51+
version: latest

CHANGELOG.md

+15-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
## [8.1.0] - 2023-08-16
10+
### Added
11+
- log.G as shorthand for adding a set of Grouped fields. This ability has always been present but is now fully supported in the default logger and with helper function for ease of use.
12+
- slog support added in Go 1.21+ both to use as an slog.Handler or redirect.
13+
14+
### Fixed
15+
- errors.Chain handling from default withErrorFn handler after dep upgrade.
16+
17+
## [8.0.2] - 2023-06-22
18+
### Fixed
19+
- Corrected removal of default logger upon registering a custom one.
20+
921
## [8.0.1] - 2022-06-23
1022
### Fixed
1123
- Handling un-hashable tag values during dedupe process.
@@ -30,6 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3042
- Removed ability to remove individual log levels externally; RemoveHandler+AddHandler can do the same.
3143

3244

33-
[Unreleased]: https://github.com/go-playground/log/compare/v8.0.1...HEAD
45+
[Unreleased]: https://github.com/go-playground/log/compare/v8.1.0...HEAD
46+
[8.1.0]: https://github.com/go-playground/log/compare/v8.0.2...v8.1.0
47+
[8.0.2]: https://github.com/go-playground/log/compare/v8.0.1...v8.0.2
3448
[8.0.1]: https://github.com/go-playground/log/compare/v8.0.0...v8.0.1
3549
[8.0.0]: https://github.com/go-playground/log/compare/v7.0.2...v8.0.0

README.md

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
## log
2-
<img align="center" src="https://raw.githubusercontent.com/go-playground/log/master/logo.png">![Project status](https://img.shields.io/badge/version-8.0.1-green.svg)
2+
<img align="center" src="https://raw.githubusercontent.com/go-playground/log/master/logo.png">![Project status](https://img.shields.io/badge/version-8.1.0-green.svg)
33
[![Test](https://github.com/go-playground/log/actions/workflows/go.yml/badge.svg)](https://github.com/go-playground/log/actions/workflows/go.yml)
44
[![Coverage Status](https://coveralls.io/repos/github/go-playground/log/badge.svg?branch=master)](https://coveralls.io/github/go-playground/log?branch=master)
55
[![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/log)](https://goreportcard.com/report/github.com/go-playground/log)
@@ -152,9 +152,19 @@ func main() {
152152
// logging with fields can be used with any of the above
153153
log.WithField("key", "value").Info("test info")
154154
}
155-
156155
```
157156

157+
#### Go 1.21+ slog compatibility
158+
159+
There is a compatibility layer for slog, which allows redirecting slog to this logger and ability to output to an slog.Handler+.
160+
161+
| type | Definition |
162+
|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
163+
| [Handler](_examples/slog/hanlder/main.go) | This example demonstrates how to redirect the std log and slog to this logger by using it as an slog.Handler. |
164+
| [Redirect](_examples/slog/redirect/main.go) | This example demonstrates how to redirect the std log and slog to this logger and output back out to any slog.Handler, as well as any other handler(s) registered with this logger. |
165+
166+
```go
167+
158168
Log Level Definitions
159169
---------------------
160170

_examples/slog/handler/main.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
package main
5+
6+
import (
7+
"github.com/go-playground/log/v8"
8+
stdlog "log"
9+
"log/slog"
10+
)
11+
12+
func main() {
13+
14+
// This example demonstrates how to redirect the std log and slog to this logger by using it as
15+
// an slog.Handler.
16+
log.RedirectGoStdLog(true)
17+
log.WithFields(log.G("grouped", log.F("key", "value"))).Debug("test")
18+
stdlog.Println("test stdlog")
19+
slog.Info("test slog", slog.Group("group", "key", "value"))
20+
}

_examples/slog/redirect/main.go

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
package main
5+
6+
import (
7+
"github.com/go-playground/log/v8"
8+
slogredirect "github.com/go-playground/log/v8/handlers/slog"
9+
stdlog "log"
10+
"log/slog"
11+
"os"
12+
)
13+
14+
func main() {
15+
16+
// This example demonstrates how to redirect the std log and slog to this logger and output back out to any
17+
// slog.Handler, as well as any other handler(s) registered with this logger.
18+
log.AddHandler(slogredirect.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
19+
ReplaceAttr: slogredirect.ReplaceAttrFn, // for custom log level output
20+
})), log.AllLevels...)
21+
log.WithFields(log.G("grouped", log.F("key", "value"))).Debug("test")
22+
stdlog.Println("test stdlog")
23+
slog.Info("test slog", slog.Group("group", "key", "value"))
24+
}

benchmarks/benchmark_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"testing"
88
"time"
99

10-
"github.com/go-playground/log/v8"
10+
log "github.com/go-playground/log/v8"
1111
"github.com/go-playground/log/v8/handlers/json"
1212
)
1313

default_logger.go

+34-10
Original file line numberDiff line numberDiff line change
@@ -72,49 +72,73 @@ func (c *Logger) Log(e Entry) {
7272
buff.B = append(buff.B, space)
7373
buff.B = append(buff.B, e.Message...)
7474

75-
for _, f := range e.Fields {
76-
buff.B = append(buff.B, space)
77-
buff.B = append(buff.B, f.Key...)
78-
buff.B = append(buff.B, equals)
75+
c.addFields("", buff, e.Fields)
76+
buff.B = append(buff.B, newLine)
77+
78+
c.m.Lock()
79+
_, _ = c.writer.Write(buff.B)
80+
c.m.Unlock()
81+
82+
BytePool().Put(buff)
83+
}
84+
85+
func (c *Logger) addFields(prefix string, buff *Buffer, fields []Field) {
86+
for _, f := range fields {
7987

8088
switch t := f.Value.(type) {
8189
case string:
90+
printKey(buff, prefix+f.Key)
8291
buff.B = append(buff.B, t...)
8392
case int:
93+
printKey(buff, prefix+f.Key)
8494
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
8595
case int8:
96+
printKey(buff, prefix+f.Key)
8697
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
8798
case int16:
99+
printKey(buff, prefix+f.Key)
88100
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
89101
case int32:
102+
printKey(buff, prefix+f.Key)
90103
buff.B = strconv.AppendInt(buff.B, int64(t), base10)
91104
case int64:
105+
printKey(buff, prefix+f.Key)
92106
buff.B = strconv.AppendInt(buff.B, t, base10)
93107
case uint:
108+
printKey(buff, prefix+f.Key)
94109
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
95110
case uint8:
111+
printKey(buff, prefix+f.Key)
96112
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
97113
case uint16:
114+
printKey(buff, prefix+f.Key)
98115
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
99116
case uint32:
117+
printKey(buff, prefix+f.Key)
100118
buff.B = strconv.AppendUint(buff.B, uint64(t), base10)
101119
case uint64:
120+
printKey(buff, prefix+f.Key)
102121
buff.B = strconv.AppendUint(buff.B, t, base10)
103122
case float32:
123+
printKey(buff, prefix+f.Key)
104124
buff.B = strconv.AppendFloat(buff.B, float64(t), 'f', -1, 32)
105125
case float64:
126+
printKey(buff, prefix+f.Key)
106127
buff.B = strconv.AppendFloat(buff.B, t, 'f', -1, 64)
107128
case bool:
129+
printKey(buff, prefix+f.Key)
108130
buff.B = strconv.AppendBool(buff.B, t)
131+
case []Field:
132+
c.addFields(prefix+f.Key+".", buff, t)
109133
default:
134+
printKey(buff, prefix+f.Key)
110135
buff.B = append(buff.B, fmt.Sprintf(v, f.Value)...)
111136
}
112137
}
113-
buff.B = append(buff.B, newLine)
114-
115-
c.m.Lock()
116-
_, _ = c.writer.Write(buff.B)
117-
c.m.Unlock()
138+
}
118139

119-
BytePool().Put(buff)
140+
func printKey(buff *Buffer, key string) {
141+
buff.B = append(buff.B, space)
142+
buff.B = append(buff.B, key...)
143+
buff.B = append(buff.B, equals)
120144
}

errors.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -102,13 +102,11 @@ func formatLink(l *errors.Link, b []byte) []byte {
102102
b = extractSource(b, l.Source)
103103
if l.Prefix != "" {
104104
b = append(b, l.Prefix...)
105-
}
106105

107-
if _, ok := l.Err.(errors.Chain); !ok {
108-
if l.Prefix != "" {
106+
if l.Err != nil {
109107
b = append(b, ": "...)
108+
b = append(b, l.Err.Error()...)
110109
}
111-
b = append(b, l.Err.Error()...)
112110
}
113111
return b
114112
}

go.mod

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ module github.com/go-playground/log/v8
33
go 1.18
44

55
require (
6-
github.com/go-playground/errors/v5 v5.2.3
7-
github.com/go-playground/pkg/v5 v5.6.0
8-
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467
6+
github.com/go-playground/errors/v5 v5.3.1
7+
github.com/go-playground/pkg/v5 v5.21.2
8+
golang.org/x/term v0.11.0
99
)
1010

11-
require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
11+
require golang.org/x/sys v0.11.0 // indirect

go.sum

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
github.com/go-playground/errors/v5 v5.2.3 h1:RPxaFHgJZjgk/OFkUcfytJgRQKRINLtueVxgOdnfPpg=
2-
github.com/go-playground/errors/v5 v5.2.3/go.mod h1:DincxRGwraWmq39TZDqtnOtHGOJ+AbNbO0OmBzX6MLw=
3-
github.com/go-playground/pkg/v5 v5.6.0 h1:97YpRFzIcS5NFP2Uzxj8cPYz4zTuwweyXADYcA1KfUQ=
4-
github.com/go-playground/pkg/v5 v5.6.0/go.mod h1:TvZ2nNtNh6VfoNteY9ApA2BXt1ZwJliFZ4hzPAwLS9Y=
5-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
6-
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7-
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 h1:CBpWXWQpIRjzmkkA+M7q9Fqnwd2mZr3AFqexg8YTfoM=
8-
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
1+
github.com/go-playground/errors/v5 v5.3.1 h1:J2qU+9Whg863g3SATXKSJyFA91Zz85pYD3+obr5Oodk=
2+
github.com/go-playground/errors/v5 v5.3.1/go.mod h1:LcLhmzQ/RuEntAs9r38NSV+xtbHffhMx/1yuuEroc7M=
3+
github.com/go-playground/pkg/v5 v5.21.2 h1:DgVr88oMI3pfMFkEN9E6hp9YGG8NHc+019LRJfnUOfU=
4+
github.com/go-playground/pkg/v5 v5.21.2/go.mod h1:UgHNntEQnMJSygw2O2RQ3LAB0tprx81K90c/pOKh7cU=
5+
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
6+
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7+
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
8+
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=

handlers/json/json.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"io"
77
"sync"
88

9-
"github.com/go-playground/log/v8"
9+
log "github.com/go-playground/log/v8"
1010
)
1111

1212
// Handler implementation.

handlers/json/json_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55
"strings"
66
"testing"
77

8-
"github.com/go-playground/log/v8"
8+
log "github.com/go-playground/log/v8"
99
)
1010

1111
func TestJSONLogger(t *testing.T) {

handlers/slog/slog_redirect.go

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
package slog
5+
6+
import (
7+
"context"
8+
log "github.com/go-playground/log/v8"
9+
"log/slog"
10+
)
11+
12+
// Handler implementation.
13+
type Handler struct {
14+
handler slog.Handler
15+
}
16+
17+
// New handler wraps an slog.Handler for log output.
18+
//
19+
// Calling this function automatically calls the slog.RedirectGoStdLog function in order to intercept and forward
20+
// the Go standard library log output to this handler.
21+
func New(handler slog.Handler) *Handler {
22+
log.RedirectGoStdLog(true)
23+
return &Handler{handler: handler}
24+
}
25+
26+
// Log handles the log entry
27+
func (h *Handler) Log(e log.Entry) {
28+
r := slog.NewRecord(e.Timestamp, slog.Level(e.Level), e.Message, 0)
29+
r.AddAttrs(h.convertFields(e.Fields)...)
30+
_ = h.handler.Handle(context.Background(), r)
31+
}
32+
33+
func (h *Handler) convertFields(fields []log.Field) []slog.Attr {
34+
attrs := make([]slog.Attr, 0, len(fields))
35+
for _, f := range fields {
36+
switch t := f.Value.(type) {
37+
case []log.Field:
38+
a := h.convertFields(t)
39+
arr := make([]any, 0, len(a))
40+
for _, v := range a {
41+
arr = append(arr, v)
42+
}
43+
attrs = append(attrs, slog.Group(f.Key, arr...))
44+
default:
45+
attrs = append(attrs, slog.Any(f.Key, f.Value))
46+
}
47+
}
48+
return attrs
49+
}
50+
51+
// ReplaceAttrFn can be used with slog.HandlerOptions to replace attributes.
52+
// This function replaces the "level" attribute to get the custom log levels of this package.
53+
var ReplaceAttrFn = func(groups []string, a slog.Attr) slog.Attr {
54+
if a.Key == slog.LevelKey {
55+
level := log.Level(a.Value.Any().(slog.Level))
56+
a.Value = slog.StringValue(level.String())
57+
}
58+
return a
59+
}

handlers/slog/slog_redirect_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//go:build go1.21
2+
// +build go1.21
3+
4+
package slog
5+
6+
import (
7+
"bytes"
8+
"encoding/json"
9+
"github.com/go-playground/log/v8"
10+
"log/slog"
11+
"strings"
12+
"testing"
13+
"testing/slogtest"
14+
)
15+
16+
func TestSlogRedirect(t *testing.T) {
17+
var buff bytes.Buffer
18+
log.AddHandler(New(slog.NewJSONHandler(&buff, &slog.HandlerOptions{
19+
ReplaceAttr: ReplaceAttrFn, // for custom log level output
20+
})), log.AllLevels...)
21+
h := slog.Default().Handler()
22+
23+
results := func() []map[string]any {
24+
var ms []map[string]any
25+
for _, line := range bytes.Split(buff.Bytes(), []byte{'\n'}) {
26+
if len(line) == 0 {
27+
continue
28+
}
29+
var m map[string]any
30+
if err := json.Unmarshal(line, &m); err != nil {
31+
panic(err) // In a real test, use t.Fatal.
32+
}
33+
ms = append(ms, m)
34+
}
35+
return ms
36+
}
37+
err := slogtest.TestHandler(h, results)
38+
if err != nil {
39+
// if a single error and is time key errors, is ok this logger always sets that.
40+
// sad this its the only way to hook into these errors because none of concrete and
41+
// Joined errors has no way to reach into them when not.
42+
if strings.Count(err.Error(), "\n") != 0 || !strings.Contains(err.Error(), "unexpected key \"time\": a Handler should ignore a zero Record.Time") {
43+
t.Fatal(err)
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)