Skip to content
This repository has been archived by the owner on Nov 2, 2024. It is now read-only.

Commit

Permalink
switch off slog for now
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewmueller committed Oct 3, 2023
1 parent 8904117 commit 4100f3e
Show file tree
Hide file tree
Showing 18 changed files with 597 additions and 354 deletions.
12 changes: 5 additions & 7 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

[![Go Reference](https://pkg.go.dev/badge/github.com/livebud/log.svg)](https://pkg.go.dev/github.com/livebud/log)

Log utilities for the upcoming `log/slog` package in the Go v1.21.
Simple logger for Go.

![log](https://github.com/livebud/log/assets/170299/f520d535-99ab-4db6-915c-ef06af4fa831)

## Features

- Built on top of the new `log/slog` package
- Pretty `console` handler for terminals
- Adds a level filter handler
- json and logfmt handlers
- Adds a multi-logger

## Install
Expand All @@ -19,16 +19,14 @@ Log utilities for the upcoming `log/slog` package in the Go v1.21.
go get github.com/livebud/log
```

**Note:** This package depends on `log/slog`, which is only available for Go v1.21+.

## Example

```go
log := log.Multi(
log.Filter(log.LevelInfo, &log.Console{Writer: os.Stderr}),
slog.NewJSONHandler(os.Stderr, nil),
log.Filter(log.LevelInfo, log.Console(color.Ignore(), os.Stderr)),
log.Json(os.Stderr),
)
log.WithGroup("hello").Debug("world", "args", 10)
log.Debug("world", "args", 10)
log.Info("hello", "planet", "world", "args", 10)
log.Warn("hello", "planet", "world", "args", 10)
log.Error("hello world", "planet", "world", "args", 10)
Expand Down
16 changes: 16 additions & 0 deletions buffer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package log

func Buffer() *bufferHandler {
return &bufferHandler{}
}

type bufferHandler struct {
Entries []*Entry
}

var _ Handler = (*bufferHandler)(nil)

func (h *bufferHandler) Log(entry *Entry) error {
h.Entries = append(h.Entries, entry)
return nil
}
51 changes: 51 additions & 0 deletions buffer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package log_test

import (
"errors"
"testing"

"github.com/livebud/log"
"github.com/matryer/is"
)

func TestLog(t *testing.T) {
is := is.New(t)
handler := log.Buffer()
log := log.New(handler)
log.Info("hello")
log.Info("world")
log.Warnf("hello %s!", "mars")
log.Fields(map[string]interface{}{
"file": "memory_test.go",
"detail": "file not exist",
}).Field("one", "two").Errorf("%d. oh noz", 1)
is.Equal(len(handler.Entries), 4)
is.Equal(handler.Entries[0].Level.String(), "info")
is.Equal(handler.Entries[0].Message, "hello")
is.Equal(handler.Entries[1].Level.String(), "info")
is.Equal(handler.Entries[1].Message, "world")
is.Equal(handler.Entries[2].Level.String(), "warn")
is.Equal(handler.Entries[2].Message, "hello mars!")
is.Equal(handler.Entries[3].Level.String(), "error")
is.Equal(handler.Entries[3].Message, "1. oh noz")
fields := handler.Entries[3].Fields
is.Equal(len(fields), 3)
is.Equal(fields.Keys()[0], "detail")
is.Equal(fields.Get(fields.Keys()[0]), "file not exist")
is.Equal(fields.Keys()[1], "file")
is.Equal(fields.Get(fields.Keys()[1]), "memory_test.go")
is.Equal(fields.Keys()[2], "one")
is.Equal(fields.Get(fields.Keys()[2]), "two")
}

func TestErr(t *testing.T) {
is := is.New(t)
handler := log.Buffer()
log := log.New(handler)
log.Error(errors.New("one"), "two", "three")
is.Equal(len(handler.Entries), 1)
is.Equal(handler.Entries[0].Level.String(), "error")
is.Equal(handler.Entries[0].Message, "one two three")
fields := handler.Entries[0].Fields
is.Equal(len(fields), 0)
}
174 changes: 84 additions & 90 deletions console.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
package log

import (
"context"
"fmt"
"io"
"log/slog"
"os"
"path"
"runtime"
"strings"
"sync"

"github.com/go-logfmt/logfmt"
"github.com/livebud/color"
)

// Debugger log
func Debugger() Log {
return New(Console(color.Default(), os.Stderr))
}

// Default log
func Default() Log {
return New(Filter(LevelInfo, Console(color.Default(), os.Stderr)))
}

// Parse the logger with a given filter
func Parse(filter string) (Log, error) {
level, err := ParseLevel(filter)
if err != nil {
return nil, err
}
return New(Filter(level, Console(color.Default(), os.Stderr))), nil
}

// Console handler
func Console(color color.Writer, writer io.Writer) Handler {
return &console{color, sync.Mutex{}, writer, prefixes(color)}
}

// console logger
type console struct {
color color.Writer
mu sync.Mutex
writer io.Writer
prefixes map[Level]string
}

// Prefixes
func prefixes(color color.Writer) map[Level]string {
if color.Enabled() {
Expand All @@ -33,106 +61,72 @@ func prefixes(color color.Writer) map[Level]string {
}
}

// Console handler for printing logs to the terminal
type Console struct {
Color color.Writer
Writer io.Writer
AddSource bool
prefixes map[Level]string
mu sync.Mutex // mu protects the writer
once sync.Once // Called once to setup defaults
attrs []slog.Attr
groups []string
}

var _ Handler = (*Console)(nil)

// Setup defaults
func (c *Console) setup() {
if c.Writer == nil {
c.Writer = os.Stderr
}
if c.Color == nil {
c.Color = color.Default()
func (c *console) format(level Level, msg string) string {
switch level {
case LevelDebug:
return c.color.Dim(msg)
case LevelInfo:
return c.color.Blue(msg)
case LevelNotice:
return c.color.Pink(msg)
case LevelWarn:
return c.color.Yellow(msg)
case LevelError:
return c.color.Red(msg)
default:
return ""
}
c.prefixes = prefixes(c.Color)
}

// Enabled is always set to true. Use log.Filter to filter out log levels
func (c *Console) Enabled(context.Context, Level) bool {
return true
}

func (c *Console) Handle(ctx context.Context, record slog.Record) error {
c.once.Do(c.setup)
// Log implements Logger
func (c *console) Log(entry *Entry) error {
// Format the message
msg := new(strings.Builder)
msg.WriteString(c.format(record.Level, c.prefixes[record.Level]) + " " + record.Message)
msg.WriteString(c.format(entry.Level, c.prefixes[entry.Level]) + " " + entry.Message)

// Format and log the fields
fields := new(strings.Builder)
enc := logfmt.NewEncoder(fields)
if record.NumAttrs() > 0 {
prefix := strings.Join(c.groups, ".")
record.Attrs(func(attr slog.Attr) bool {
if prefix != "" {
attr.Key = prefix + "." + attr.Key
}
enc.EncodeKeyval(attr.Key, attr.Value.String())
return true
})
}
if c.AddSource {
enc.EncodeKeyval("source", c.source(record.PC))
if len(entry.Fields) > 0 {
keys := entry.Fields.Keys()
fields := new(strings.Builder)
enc := logfmt.NewEncoder(fields)
for _, key := range keys {
enc.EncodeKeyval(key, entry.Fields.Get(key))
}
enc.Reset()
msg.WriteString(" " + c.color.Dim(fields.String()))
}
enc.Reset()
msg.WriteString(" " + c.Color.Dim(fields.String()))
msg.WriteString("\n")

// Write out
c.mu.Lock()
fmt.Fprint(c.Writer, msg.String())
fmt.Fprint(c.writer, msg.String())
c.mu.Unlock()

return nil
}

func (c *Console) source(pc uintptr) string {
fs := runtime.CallersFrames([]uintptr{pc})
f, _ := fs.Next()
return fmt.Sprintf("%s:%d", path.Base(f.File), f.Line)
}

func (c *Console) WithAttrs(attrs []slog.Attr) slog.Handler {
return &Console{
Color: c.Color,
Writer: c.Writer,
AddSource: c.AddSource,
groups: c.groups,
attrs: append(append([]slog.Attr{}, c.attrs...), attrs...),
}
}

func (c *Console) WithGroup(group string) slog.Handler {
return &Console{
Color: c.Color,
Writer: c.Writer,
AddSource: c.AddSource,
attrs: c.attrs,
groups: append(append([]string{}, c.groups...), group),
}
}
// Stderr is a console log singleton that writes to stderr
var stderr = Default()

func (c *Console) format(level Level, msg string) string {
switch level {
case LevelDebug:
return c.Color.Dim(msg)
case LevelInfo:
return c.Color.Blue(msg)
case LevelWarn:
return c.Color.Yellow(msg)
case LevelError:
return c.Color.Red(msg)
default:
return ""
}
}
var (
// Debug message is written to the console
Debug = stderr.Debug
// Debugf message is written to the console
Debugf = stderr.Debugf
// Info message is written to the console
Info = stderr.Info
// Infof message is written to the console
Infof = stderr.Infof
// Notice message is written to the console
Notice = stderr.Notice
// Noticef message is written to the console
Noticef = stderr.Noticef
// Warn message is written to the console
Warn = stderr.Warn
// Warnf message is written to the console
Warnf = stderr.Warnf
// Error message is written to the console
Error = stderr.Error
// Errorf message is written to the console
Errorf = stderr.Errorf
)
51 changes: 5 additions & 46 deletions console_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,16 @@ import (
"strings"
"testing"

"log/slog"

"testing/slogtest"

"github.com/livebud/color"
"github.com/livebud/log"
"github.com/matryer/is"
)

func TestConsoleHandler(t *testing.T) {
t.Skip("skip until: https://github.com/golang/go/issues/61706")
is := is.New(t)
color := color.Ignore()
buf := new(bytes.Buffer)
console := &log.Console{
Color: color,
Writer: buf,
}
err := slogtest.TestHandler(console, func() []map[string]any {
lines := strings.Split(strings.TrimRight(buf.String(), "\n"), "\n")
results := make([]map[string]any, len(lines))
for i, line := range lines {
result := map[string]any{}
result[slog.LevelKey] = nil
result[slog.TimeKey] = nil
line = strings.TrimLeft(line, "| ")
pairs := strings.Split(line, " ")
result[slog.MessageKey] = pairs[0]
for _, pair := range pairs[1:] {
kv := strings.SplitN(pair, "=", 2)
result[kv[0]] = kv[1]
}
results[i] = result
}
return results
})
is.NoErr(err)
}

func TestConsole(t *testing.T) {
is := is.New(t)
buf := new(bytes.Buffer)
log := slog.New(&log.Console{
Color: color.New(),
Writer: buf,
})
log.WithGroup("hello").Debug("world", "args", 10)
log := log.New(log.Console(color.New(), buf))
log.Debug("world", "args", 10)
log.Info("hello", "planet", "world", "args", 10)
log.Warn("hello", "planet", "world", "args", 10)
log.Error("hello world", "planet", "world", "args", 10)
Expand All @@ -61,15 +24,11 @@ func TestConsole(t *testing.T) {
}

func ExampleConsole() {
log := slog.New(&log.Console{
Color: color.Ignore(),
Writer: os.Stdout,
AddSource: false,
})
log.WithGroup("hello").Debug("world", "args", 10)
log := log.New(log.Console(color.Ignore(), os.Stdout))
log.Debug("world", "args", 10)
log.Info("hello", "planet", "world", "args", 10)
log.Warn("hello", "planet", "world", "args", 10)
log.Error("hello world", slog.String("planet", "world"), "args", 10)
// log.Error("hello world", slog.String("planet", "world"), "args", 10)
// Output:
// debug: world hello.args=10
// info: hello planet=world args=10
Expand Down
Loading

0 comments on commit 4100f3e

Please sign in to comment.