Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ package main
import (
"fmt"
"os"
"log/slog"

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

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

### Key Value Pairs

`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
`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

```go
logger.Info("Doing something", "cache", true, "duration", 30 * time.Second, "number", 42)
logger.Info(
"Doing something",
slog.Bool("cache", true),
slog.Duration("duration", 30 * time.Second),
slog.Int("number", 42),
)
```

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

```go
sub := logger.With("sub", true)
sub := logger.With(slog.Bool("sub", true))

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

<p align="center">
Expand All @@ -120,3 +131,5 @@ prefixed := logger.Prefixed("http")
<p align="center">
<img src="https://github.com/FollowTheProcess/log/raw/main/docs/img/prefix.gif" alt="demo">
</p>

[slog.Attr]: https://pkg.go.dev/log/slog#Attr
Binary file modified docs/img/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/keys.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/prefix.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 13 additions & 4 deletions examples/demo/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"log/slog"
"math/rand/v2"
"os"
"time"
Expand All @@ -11,15 +12,23 @@ import (
func main() {
logger := log.New(os.Stderr, log.WithLevel(log.LevelDebug))

logger.Debug("Searing steak", "cook", "rare", "temp", "42°C", "time", 2*time.Minute)
logger.Debug(
"Searing steak",
slog.String("cook", "rare"),
slog.Int("temp", 42),
slog.Duration("time", 2*time.Minute),
)
sleep()
logger.Info("Choosing wine pairing", "choices", []string{"merlot", "malbec", "rioja"})
logger.Info(
"Choosing wine pairing",
slog.Any("choices", []string{"merlot", "malbec", "rioja"}),
)
sleep()
logger.Error("No malbec left!")
sleep()
logger.Warn("Falling back to second choice", "fallback", "rioja")
logger.Warn("Falling back to second choice", slog.String("fallback", "rioja"))

logger.Info("Eating steak", "cut", "sirloin", "enjoying", true)
logger.Info("Eating steak", slog.String("cut", "sirloin"), slog.Bool("enjoying", true))
}

func sleep() {
Expand Down
12 changes: 9 additions & 3 deletions examples/keys/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"log/slog"
"math/rand/v2"
"os"
"time"
Expand All @@ -11,11 +12,16 @@ import (
func main() {
logger := log.New(os.Stderr, log.WithLevel(log.LevelDebug))

logger.Info("Doing something", "cache", true, "duration", 30*time.Second, "number", 42)
logger.Info(
"Doing something",
slog.Bool("cache", true),
slog.Duration("duration", 30*time.Second),
slog.Int("number", 42),
)
sleep()

sub := logger.With("sub", true)
sub.Info("Hello from the sub logger", "subkey", "yes")
sub := logger.With(slog.Bool("sub", true))
sub.Info("Hello from the sub logger", slog.String("subkey", "yes"))
}

func sleep() {
Expand Down
21 changes: 17 additions & 4 deletions examples/prefix/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"log/slog"
"math/rand/v2"
"net/http"
"os"
Expand All @@ -13,14 +14,26 @@ func main() {
logger := log.New(os.Stderr)
prefixed := logger.Prefixed("http")

logger.Info("Calling GitHub API", "url", "https://api.github.com/")
logger.Info("Calling GitHub API", slog.String("url", "https://api.github.com/"))
sleep()

prefixed.Warn("Slow endpoint", "endpoint", "users/slow", "duration", 10*time.Second)
prefixed.Warn(
"Slow endpoint",
slog.String("endpoint", "users/slow"),
slog.Duration("duration", 10*time.Second),
)
sleep()
prefixed.Info("Response from get repos", "status", http.StatusOK, "duration", 500*time.Millisecond)
prefixed.Info(
"Response from get repos",
slog.Int("status", http.StatusOK),
slog.Duration("duration", 500*time.Millisecond),
)
sleep()
prefixed.Error("Response from something else", "status", http.StatusBadRequest, "duration", 33*time.Millisecond)
prefixed.Error(
"Response from something else",
slog.Int("status", http.StatusBadRequest),
slog.Duration("duration", 33*time.Millisecond),
)
}

func sleep() {
Expand Down
3 changes: 2 additions & 1 deletion level.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const (
errorString = "ERROR"
)

func (l Level) styled() string {
// String returns the stylised representation of the log level.
func (l Level) String() string {
switch l {
case LevelDebug:
return debugStyle.Text(debugString)
Expand Down
51 changes: 19 additions & 32 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ package log // import "go.followtheprocess.codes/log"
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"os"
"slices"
"strconv"
Expand All @@ -37,9 +37,6 @@ const (
errorStyle = hue.Red | hue.Bold
)

// missingValue is the placeholder text for a missing value in a log line's key value pair.
const missingValue = "<MISSING>"

// ctxKey is the unexported type used for context key so this key never collides with another.
type ctxKey struct{}

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

sub.kv = slices.Concat(sub.kv, kv)
sub.attrs = slices.Concat(sub.attrs, attrs)

return sub
}
Expand All @@ -123,27 +120,27 @@ func (l *Logger) Prefixed(prefix string) *Logger {
}

// Debug writes a debug level log line.
func (l *Logger) Debug(msg string, kv ...any) {
l.log(LevelDebug, msg, kv...)
func (l *Logger) Debug(msg string, attrs ...slog.Attr) {
l.log(LevelDebug, msg, attrs...)
}

// Info writes an info level log line.
func (l *Logger) Info(msg string, kv ...any) {
l.log(LevelInfo, msg, kv...)
func (l *Logger) Info(msg string, attrs ...slog.Attr) {
l.log(LevelInfo, msg, attrs...)
}

// Warn writes a warning level log line.
func (l *Logger) Warn(msg string, kv ...any) {
l.log(LevelWarn, msg, kv...)
func (l *Logger) Warn(msg string, attrs ...slog.Attr) {
l.log(LevelWarn, msg, attrs...)
}

// Error writes an error level log line.
func (l *Logger) Error(msg string, kv ...any) {
l.log(LevelError, msg, kv...)
func (l *Logger) Error(msg string, attrs ...slog.Attr) {
l.log(LevelError, msg, attrs...)
}

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

buf.WriteString(timestampStyle.Text(l.timeFunc().Format(l.timeFormat)))
buf.WriteByte(' ')
buf.WriteString(level.styled())
buf.WriteString(level.String())

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

if numKVs := len(l.kv) + len(kv); numKVs != 0 {
kvs := make([]any, 0, numKVs)

kvs = append(kvs, l.kv...)
if len(kvs)%2 != 0 {
kvs = append(kvs, missingValue)
}

kvs = append(kvs, kv...)
if len(kvs)%2 != 0 {
kvs = append(kvs, missingValue)
}
if totalAttrs := len(l.attrs) + len(attrs); totalAttrs != 0 {
all := slices.Concat(l.attrs, attrs)

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

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

if needsQuotes(val) || val == "" {
val = strconv.Quote(val)
Expand Down
Loading
Loading