Skip to content

Commit b25b418

Browse files
Breaking: replace key value pairs with slog.Attrs (#27)
* Breaking: replace key value pairs with slog.Attrs * Format README.md * Use slices.Concat
1 parent 62fd4dd commit b25b418

File tree

10 files changed

+109
-73
lines changed

10 files changed

+109
-73
lines changed

README.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ package main
3434
import (
3535
"fmt"
3636
"os"
37+
"log/slog"
3738

3839
"go.followtheprocess.codes/log"
3940
)
@@ -42,7 +43,11 @@ func main() {
4243
logger := log.New(os.Stderr)
4344

4445
logger.Debug("Debug me") // By default this one won't show up, default log level is INFO
45-
logger.Info("Some information here", "really", true)
46+
logger.Info(
47+
"Some information here",
48+
// Yep! You use slog.Attrs for key value pairs, why reinvent the wheel?
49+
slog.Bool("really", true),
50+
)
4651
logger.Warn("Uh oh!")
4752
logger.Error("Goodbye")
4853
}
@@ -84,18 +89,24 @@ logger := log.New(os.Stderr, log.WithLevel(log.LevelDebug))
8489

8590
### Key Value Pairs
8691

87-
`log` provides "semi structured" logs in that the message is free form text but you can attach arbitrary key value pairs to any of the log methods
92+
`log` uses [slog.Attr] to provide "semi structured" logs. The message is free form text but you can attach arbitrary key, value pairs with any of the log methods
8893

8994
```go
90-
logger.Info("Doing something", "cache", true, "duration", 30 * time.Second, "number", 42)
95+
logger.Info(
96+
"Doing something",
97+
slog.Bool("cache", true),
98+
slog.Duration("duration", 30 * time.Second),
99+
slog.Int("number", 42),
100+
)
91101
```
92102

93103
You can also create a "sub logger" with persistent key value pairs applied to every message
94104

95105
```go
96-
sub := logger.With("sub", true)
106+
sub := logger.With(slog.Bool("sub", true))
97107

98-
sub.Info("Hello from the sub logger", "subkey", "yes") // They can have their own per-method keys too!
108+
// They can have their own per-method keys too!
109+
sub.Info("Hello from the sub logger", slog.String("subkey", "yes"))
99110
```
100111

101112
<p align="center">
@@ -120,3 +131,5 @@ prefixed := logger.Prefixed("http")
120131
<p align="center">
121132
<img src="https://github.com/FollowTheProcess/log/raw/main/docs/img/prefix.gif" alt="demo">
122133
</p>
134+
135+
[slog.Attr]: https://pkg.go.dev/log/slog#Attr

docs/img/demo.gif

-465 Bytes
Loading

docs/img/keys.gif

598 Bytes
Loading

docs/img/prefix.gif

-475 Bytes
Loading

examples/demo/main.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"log/slog"
45
"math/rand/v2"
56
"os"
67
"time"
@@ -11,15 +12,23 @@ import (
1112
func main() {
1213
logger := log.New(os.Stderr, log.WithLevel(log.LevelDebug))
1314

14-
logger.Debug("Searing steak", "cook", "rare", "temp", "42°C", "time", 2*time.Minute)
15+
logger.Debug(
16+
"Searing steak",
17+
slog.String("cook", "rare"),
18+
slog.Int("temp", 42),
19+
slog.Duration("time", 2*time.Minute),
20+
)
1521
sleep()
16-
logger.Info("Choosing wine pairing", "choices", []string{"merlot", "malbec", "rioja"})
22+
logger.Info(
23+
"Choosing wine pairing",
24+
slog.Any("choices", []string{"merlot", "malbec", "rioja"}),
25+
)
1726
sleep()
1827
logger.Error("No malbec left!")
1928
sleep()
20-
logger.Warn("Falling back to second choice", "fallback", "rioja")
29+
logger.Warn("Falling back to second choice", slog.String("fallback", "rioja"))
2130

22-
logger.Info("Eating steak", "cut", "sirloin", "enjoying", true)
31+
logger.Info("Eating steak", slog.String("cut", "sirloin"), slog.Bool("enjoying", true))
2332
}
2433

2534
func sleep() {

examples/keys/main.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"log/slog"
45
"math/rand/v2"
56
"os"
67
"time"
@@ -11,11 +12,16 @@ import (
1112
func main() {
1213
logger := log.New(os.Stderr, log.WithLevel(log.LevelDebug))
1314

14-
logger.Info("Doing something", "cache", true, "duration", 30*time.Second, "number", 42)
15+
logger.Info(
16+
"Doing something",
17+
slog.Bool("cache", true),
18+
slog.Duration("duration", 30*time.Second),
19+
slog.Int("number", 42),
20+
)
1521
sleep()
1622

17-
sub := logger.With("sub", true)
18-
sub.Info("Hello from the sub logger", "subkey", "yes")
23+
sub := logger.With(slog.Bool("sub", true))
24+
sub.Info("Hello from the sub logger", slog.String("subkey", "yes"))
1925
}
2026

2127
func sleep() {

examples/prefix/main.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"log/slog"
45
"math/rand/v2"
56
"net/http"
67
"os"
@@ -13,14 +14,26 @@ func main() {
1314
logger := log.New(os.Stderr)
1415
prefixed := logger.Prefixed("http")
1516

16-
logger.Info("Calling GitHub API", "url", "https://api.github.com/")
17+
logger.Info("Calling GitHub API", slog.String("url", "https://api.github.com/"))
1718
sleep()
1819

19-
prefixed.Warn("Slow endpoint", "endpoint", "users/slow", "duration", 10*time.Second)
20+
prefixed.Warn(
21+
"Slow endpoint",
22+
slog.String("endpoint", "users/slow"),
23+
slog.Duration("duration", 10*time.Second),
24+
)
2025
sleep()
21-
prefixed.Info("Response from get repos", "status", http.StatusOK, "duration", 500*time.Millisecond)
26+
prefixed.Info(
27+
"Response from get repos",
28+
slog.Int("status", http.StatusOK),
29+
slog.Duration("duration", 500*time.Millisecond),
30+
)
2231
sleep()
23-
prefixed.Error("Response from something else", "status", http.StatusBadRequest, "duration", 33*time.Millisecond)
32+
prefixed.Error(
33+
"Response from something else",
34+
slog.Int("status", http.StatusBadRequest),
35+
slog.Duration("duration", 33*time.Millisecond),
36+
)
2437
}
2538

2639
func sleep() {

level.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ const (
2929
errorString = "ERROR"
3030
)
3131

32-
func (l Level) styled() string {
32+
// String returns the stylised representation of the log level.
33+
func (l Level) String() string {
3334
switch l {
3435
case LevelDebug:
3536
return debugStyle.Text(debugString)

log.go

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ package log // import "go.followtheprocess.codes/log"
1111
import (
1212
"bytes"
1313
"context"
14-
"fmt"
1514
"io"
15+
"log/slog"
1616
"os"
1717
"slices"
1818
"strconv"
@@ -37,9 +37,6 @@ const (
3737
errorStyle = hue.Red | hue.Bold
3838
)
3939

40-
// missingValue is the placeholder text for a missing value in a log line's key value pair.
41-
const missingValue = "<MISSING>"
42-
4340
// ctxKey is the unexported type used for context key so this key never collides with another.
4441
type ctxKey struct{}
4542

@@ -54,7 +51,7 @@ type Logger struct {
5451
mu *sync.Mutex // Protects w
5552
timeFormat string // The time format layout string, defaults to [time.RFC3339]
5653
prefix string // Optional prefix to prepend to all log messages
57-
kv []any // Persistent key value pairs
54+
attrs []slog.Attr // Persistent key value pairs
5855
level Level // The configured level of this logger, logs below this level are not shown
5956
isDiscard atomic.Bool // w == [io.Discard], cached
6057
}
@@ -103,10 +100,10 @@ func FromContext(ctx context.Context) *Logger {
103100
// With returns a new [Logger] with the given persistent key value pairs.
104101
//
105102
// The returned logger is otherwise an exact clone of the caller.
106-
func (l *Logger) With(kv ...any) *Logger {
103+
func (l *Logger) With(attrs ...slog.Attr) *Logger {
107104
sub := l.clone()
108105

109-
sub.kv = slices.Concat(sub.kv, kv)
106+
sub.attrs = slices.Concat(sub.attrs, attrs)
110107

111108
return sub
112109
}
@@ -123,27 +120,27 @@ func (l *Logger) Prefixed(prefix string) *Logger {
123120
}
124121

125122
// Debug writes a debug level log line.
126-
func (l *Logger) Debug(msg string, kv ...any) {
127-
l.log(LevelDebug, msg, kv...)
123+
func (l *Logger) Debug(msg string, attrs ...slog.Attr) {
124+
l.log(LevelDebug, msg, attrs...)
128125
}
129126

130127
// Info writes an info level log line.
131-
func (l *Logger) Info(msg string, kv ...any) {
132-
l.log(LevelInfo, msg, kv...)
128+
func (l *Logger) Info(msg string, attrs ...slog.Attr) {
129+
l.log(LevelInfo, msg, attrs...)
133130
}
134131

135132
// Warn writes a warning level log line.
136-
func (l *Logger) Warn(msg string, kv ...any) {
137-
l.log(LevelWarn, msg, kv...)
133+
func (l *Logger) Warn(msg string, attrs ...slog.Attr) {
134+
l.log(LevelWarn, msg, attrs...)
138135
}
139136

140137
// Error writes an error level log line.
141-
func (l *Logger) Error(msg string, kv ...any) {
142-
l.log(LevelError, msg, kv...)
138+
func (l *Logger) Error(msg string, attrs ...slog.Attr) {
139+
l.log(LevelError, msg, attrs...)
143140
}
144141

145142
// log logs the given levelled message.
146-
func (l *Logger) log(level Level, msg string, kv ...any) {
143+
func (l *Logger) log(level Level, msg string, attrs ...slog.Attr) {
147144
if l.isDiscard.Load() || l.level > level {
148145
// Do as little work as possible
149146
return
@@ -157,7 +154,7 @@ func (l *Logger) log(level Level, msg string, kv ...any) {
157154

158155
buf.WriteString(timestampStyle.Text(l.timeFunc().Format(l.timeFormat)))
159156
buf.WriteByte(' ')
160-
buf.WriteString(level.styled())
157+
buf.WriteString(level.String())
161158

162159
if l.prefix != "" {
163160
buf.WriteString(" " + prefixStyle.Text(l.prefix))
@@ -173,24 +170,14 @@ func (l *Logger) log(level Level, msg string, kv ...any) {
173170
buf.WriteString(strings.Repeat(" ", padding))
174171
buf.WriteString(msg)
175172

176-
if numKVs := len(l.kv) + len(kv); numKVs != 0 {
177-
kvs := make([]any, 0, numKVs)
178-
179-
kvs = append(kvs, l.kv...)
180-
if len(kvs)%2 != 0 {
181-
kvs = append(kvs, missingValue)
182-
}
183-
184-
kvs = append(kvs, kv...)
185-
if len(kvs)%2 != 0 {
186-
kvs = append(kvs, missingValue)
187-
}
173+
if totalAttrs := len(l.attrs) + len(attrs); totalAttrs != 0 {
174+
all := slices.Concat(l.attrs, attrs)
188175

189-
for i := 0; i < len(kvs); i += 2 {
176+
for _, attr := range all {
190177
buf.WriteByte(' ')
191178

192-
key := keyStyle.Sprint(kvs[i])
193-
val := fmt.Sprintf("%+v", kvs[i+1])
179+
key := keyStyle.Text(attr.Key)
180+
val := attr.Value.String()
194181

195182
if needsQuotes(val) || val == "" {
196183
val = strconv.Quote(val)

0 commit comments

Comments
 (0)