Skip to content

Commit

Permalink
Implement attributes to the loggo package
Browse files Browse the repository at this point in the history
In order to correctly interface with the log/slog package and to
take advantage of structured logging, we now introduce attributes.
Currently Info and Infof are different, in the fact that Info is
the structured variation. It expects every argument to be a attribute
after the initial message string.

This then allows us to have a slog backend without migrating away
from loggo directly. We have all the benefits of loggo plus we can
take advantage of the new logging infra.
  • Loading branch information
SimonRichardson committed Sep 26, 2023
1 parent 54a1038 commit 022df51
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 21 deletions.
79 changes: 79 additions & 0 deletions attrs/attrs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package attrs

import (
"fmt"
"time"
)

type AttrValue[T any] interface {
Key() string
Value() T
}

type attr[T any] struct {
key string
value T
}

func (s attr[T]) Key() string {
return s.key
}

func (s attr[T]) Value() T {
return s.value
}

func String(k, v string) AttrValue[string] {
return attr[string]{key: k, value: v}
}

func Int(k string, v int) AttrValue[int] {
return attr[int]{key: k, value: v}
}

func Int64(k string, v int64) AttrValue[int64] {
return attr[int64]{key: k, value: v}
}

func Uint64(k string, v uint64) AttrValue[uint64] {
return attr[uint64]{key: k, value: v}
}

func Float64(k string, v float64) AttrValue[float64] {
return attr[float64]{key: k, value: v}
}

func Bool(k string, v bool) AttrValue[bool] {
return attr[bool]{key: k, value: v}
}

func Time(k string, v time.Time) AttrValue[time.Time] {
return attr[time.Time]{key: k, value: v}
}

func Duration(k string, v time.Duration) AttrValue[time.Duration] {
return attr[time.Duration]{key: k, value: v}
}

func Any(k string, v any) AttrValue[any] {
return attr[any]{key: k, value: v}
}

func Valid(attrs []any) error {
for _, attr := range attrs {
switch a := attr.(type) {
case AttrValue[string]:
case AttrValue[int]:
case AttrValue[int64]:
case AttrValue[uint64]:
case AttrValue[float64]:
case AttrValue[bool]:
case AttrValue[time.Time]:
case AttrValue[time.Duration]:
case AttrValue[any]:
default:
return fmt.Errorf("invalid attribute type %T", a)
}
}
return nil
}
4 changes: 4 additions & 0 deletions entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ type Entry struct {
Message string
// Labels is the label associated with the log message.
Labels []string
// PC is the program counter of the log call.
PC uintptr
// Attrs is the list of attributes associated with the log message.
Attrs []any
}
3 changes: 2 additions & 1 deletion example/first.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package main

import (
"github.com/juju/loggo"
"github.com/juju/loggo/attrs"
)

var first = loggo.GetLogger("first")

func FirstCritical(message string) {
first.Criticalf(message)
first.Critical(message, attrs.String("baz", "boo"))
}

func FirstError(message string) {
Expand Down
31 changes: 25 additions & 6 deletions example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,48 @@ package main
import (
"fmt"
"log"
"log/slog"
"os"

"github.com/juju/loggo"
"github.com/juju/loggo/attrs"
loggoslog "github.com/juju/loggo/slog"
)

var rootLogger = loggo.GetLogger("")

func main() {
args := os.Args
if len(args) > 1 {
if len(args) == 0 {
fmt.Println("Add a parameter to configure the logging:")
fmt.Println(`E.g. "<root>=INFO;first=TRACE" or "<root>=INFO;first=TRACE" "slog"`)
}
num := len(args)
if num > 1 {
if err := loggo.ConfigureLoggers(args[1]); err != nil {
log.Fatal(err)
}
} else {
fmt.Println("Add a parameter to configure the logging:")
fmt.Println("E.g. \"<root>=INFO;first=TRACE\"")
}

fmt.Println("\nCurrent logging levels:")
fmt.Println(loggo.LoggerInfo())

if num > 2 {
if args[2] == "slog" {
handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: loggoslog.DefaultLevel(loggo.DefaultContext().Config()),
})
loggo.ReplaceDefaultWriter(loggoslog.NewSlogWriter(handler))

fmt.Println("Using log/slog writer:")
} else {
log.Fatalf("unknown logging type %q", args[2])
}
}

fmt.Println("")

rootLogger.Infof("Start of test.")
rootLogger.Info("Start of test.", attrs.String("foo", "bar"))

FirstCritical("first critical")
FirstError("first error")
Expand All @@ -39,5 +59,4 @@ func main() {
SecondInfo("second info")
SecondDebug("second debug")
SecondTrace("second trace")

}
47 changes: 45 additions & 2 deletions formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,60 @@ import (
"os"
"path/filepath"
"time"

"github.com/juju/loggo/attrs"
)

// DefaultFormatter returns the parameters separated by spaces except for
// filename and line which are separated by a colon. The timestamp is shown
// to second resolution in UTC. For example:
// 2016-07-02 15:04:05
//
// 2016-07-02 15:04:05
func DefaultFormatter(entry Entry) string {
ts := entry.Timestamp.In(time.UTC).Format("2006-01-02 15:04:05")
// Just get the basename from the filename
filename := filepath.Base(entry.Filename)
return fmt.Sprintf("%s %s %s %s:%d %s", ts, entry.Level, entry.Module, filename, entry.Line, entry.Message)

var (
format string
values []any
)
for _, attr := range entry.Attrs {
switch a := attr.(type) {
case attrs.AttrValue[string]:
format += " %s=%s"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[int]:
format += " %s=%d"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[int64]:
format += " %s=%d"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[uint64]:
format += " %s=%d"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[float64]:
format += " %s=%f"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[bool]:
format += " %s=%t"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[time.Time]:
format += " %s=%v"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[time.Duration]:
format += " %s=%v"
values = append(values, a.Key(), a.Value())
case attrs.AttrValue[any]:
format += " %s=%v"
values = append(values, a.Key(), a.Value())
}
}

args := []any{ts, entry.Level, entry.Module, filename, entry.Line, entry.Message}
args = append(args, values...)

return fmt.Sprintf("%s %s %s %s:%d %s"+format, args...)
}

// TimeFormat is the time format used for the default writer.
Expand Down
72 changes: 71 additions & 1 deletion logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"fmt"
"runtime"
"time"

"github.com/juju/loggo/attrs"
)

// A Logger represents a logging module. It has an associated logging
Expand Down Expand Up @@ -127,7 +129,7 @@ func (logger Logger) LogCallf(calldepth int, level Level, message string, args .
now := time.Now() // get this early.
// Param to Caller is the call depth. Since this method is called from
// the Logger methods, we want the place that those were called from.
_, file, line, ok := runtime.Caller(calldepth + 1)
pc, file, line, ok := runtime.Caller(calldepth + 1)
if !ok {
file = "???"
line = 0
Expand All @@ -154,6 +156,7 @@ func (logger Logger) LogCallf(calldepth int, level Level, message string, args .
Timestamp: now,
Message: formattedMessage,
Labels: module.labels,
PC: pc,
})
}

Expand Down Expand Up @@ -222,3 +225,70 @@ func (logger Logger) IsDebugEnabled() bool {
func (logger Logger) IsTraceEnabled() bool {
return logger.IsLevelEnabled(TRACE)
}

// Trace logs the message at trace level.
func (logger Logger) Trace(message string, attrs ...any) error {
return logger.LogCall(1, TRACE, message, attrs...)
}

// Debug logs the message at debug level.
func (logger Logger) Debug(message string, attrs ...any) error {
return logger.LogCall(1, DEBUG, message, attrs...)
}

// Info logs the message at info level.
func (logger Logger) Info(message string, attrs ...any) error {
return logger.LogCall(1, INFO, message, attrs...)
}

// Error logs the message at error level.
func (logger Logger) Error(message string, attrs ...any) error {
return logger.LogCall(1, ERROR, message, attrs...)
}

// Warning logs the message at warning level.
func (logger Logger) Warning(message string, attrs ...any) error {
return logger.LogCall(1, WARNING, message, attrs...)
}

// Critical logs the message at critical level.
func (logger Logger) Critical(message string, attrs ...any) error {
return logger.LogCall(1, CRITICAL, message, attrs...)
}

func (logger Logger) LogCall(calldepth int, level Level, message string, attributes ...any) error {
if err := attrs.Valid(attributes); err != nil {
return err
}

module := logger.getModule()
if !module.willWrite(level) {
return nil
}
// Gather time, and filename, line number.
now := time.Now() // get this early.
// Param to Caller is the call depth. Since this method is called from
// the Logger methods, we want the place that those were called from.
pc, file, line, ok := runtime.Caller(calldepth + 1)
if !ok {
file = "???"
line = 0
}
// Trim newline off format string, following usual
// Go logging conventions.
if len(message) > 0 && message[len(message)-1] == '\n' {
message = message[0 : len(message)-1]
}

module.write(Entry{
Level: level,
Filename: file,
Line: line,
Timestamp: now,
Message: message,
Labels: module.labels,
PC: pc,
Attrs: attributes,
})
return nil
}
25 changes: 25 additions & 0 deletions loggocolor/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"fmt"
"io"
"path/filepath"
"time"

"github.com/juju/ansiterm"
"github.com/juju/loggo"
"github.com/juju/loggo/attrs"
)

var (
Expand Down Expand Up @@ -55,4 +57,27 @@ func (w *colorWriter) Write(entry loggo.Entry) {
fmt.Fprintf(w.writer, " %s ", entry.Module)
LocationColor.Fprintf(w.writer, "%s:%d ", filename, entry.Line)
fmt.Fprintln(w.writer, entry.Message)

for _, attr := range entry.Attrs {
switch a := attr.(type) {
case attrs.AttrValue[string]:
fmt.Fprintf(w.writer, " %s=%s\n", a.Key(), a.Value())
case attrs.AttrValue[int]:
fmt.Fprintf(w.writer, " %s=%d\n", a.Key(), a.Value())
case attrs.AttrValue[int64]:
fmt.Fprintf(w.writer, " %s=%d\n", a.Key(), a.Value())
case attrs.AttrValue[uint64]:
fmt.Fprintf(w.writer, " %s=%d\n", a.Key(), a.Value())
case attrs.AttrValue[float64]:
fmt.Fprintf(w.writer, " %s=%f\n", a.Key(), a.Value())
case attrs.AttrValue[bool]:
fmt.Fprintf(w.writer, " %s=%t\n", a.Key(), a.Value())
case attrs.AttrValue[time.Time]:
fmt.Fprintf(w.writer, " %s=%v\n", a.Key(), a.Value())
case attrs.AttrValue[time.Duration]:
fmt.Fprintf(w.writer, " %s=%v\n", a.Key(), a.Value())
case attrs.AttrValue[any]:
fmt.Fprintf(w.writer, " %s=%v\n", a.Key(), a.Value())
}
}
}
Loading

0 comments on commit 022df51

Please sign in to comment.