7 Commits

Author SHA1 Message Date
9d34e78475 fix #12: unquote eventually quoted keys generated by yaml.Marshall() 2024-02-14 13:06:30 +01:00
e9af16a8a2 bump version 2024-02-10 13:17:57 +01:00
9ede62037a fix #10: check group length before manipulation 2024-02-10 13:17:57 +01:00
5c0aadd54a fix #6: support LogValuer() attributes 2024-01-22 13:59:51 +01:00
33798bddb3 Fix #7: implement AddSource flag, add as message 2024-01-22 13:59:51 +01:00
T.v.Dein
d53c1db7d0 added ref badge (#5) 2024-01-19 18:48:42 +01:00
T.v.Dein
1c65084c37 Develop (#4)
* finalized tests, made .With() work to create sub-loggers
2024-01-19 13:45:25 +01:00
5 changed files with 134 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ VERSION = $(shell grep VERSION handler.go | head -1 | cut -d '"' -f2)
all: buildlocal all: buildlocal
buildlocal: buildlocal:
go build go build -o example/example example/example.go
clean: clean:
rm -rf $(tool) coverage.out testdata t/out example/example rm -rf $(tool) coverage.out testdata t/out example/example
@@ -34,5 +34,6 @@ lint:
golangci-lint run -p bugs -p unused golangci-lint run -p bugs -p unused
release: buildlocal test release: buildlocal test
gh release create v$(VERSION) --generate-notes releases/* gh release create v$(VERSION) --generate-notes

View File

@@ -2,6 +2,7 @@
[![Actions](https://github.com/tlinden/yadu/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/yadu/actions) [![Actions](https://github.com/tlinden/yadu/actions/workflows/ci.yaml/badge.svg)](https://github.com/tlinden/yadu/actions)
[![Go Coverage](https://github.com/tlinden/yadu/wiki/coverage.svg)](https://raw.githack.com/wiki/tlinden/yadu/coverage.html) [![Go Coverage](https://github.com/tlinden/yadu/wiki/coverage.svg)](https://raw.githack.com/wiki/tlinden/yadu/coverage.html)
![GitHub License](https://img.shields.io/github/license/tlinden/yadu) ![GitHub License](https://img.shields.io/github/license/tlinden/yadu)
[![GoDoc](https://godoc.org/github.com/tlinden/yadu?status.svg)](https://godoc.org/github.com/tlinden/yadu)
# yadu - a human readable yaml based slog.Handler # yadu - a human readable yaml based slog.Handler

View File

@@ -16,6 +16,12 @@ type Ammo struct {
Range int Range int
} }
func (a *Ammo) LogValue() slog.Value {
return slog.GroupValue(
slog.String("Forweapon", a.Forweapon),
)
}
type Enemy struct { type Enemy struct {
Alive bool Alive bool
Health int Health int
@@ -24,6 +30,12 @@ type Enemy struct {
Ammo []Ammo Ammo []Ammo
} }
func (e *Enemy) LogValue() slog.Value {
return slog.GroupValue(
slog.String("name", e.Name),
)
}
func removeTime(_ []string, a slog.Attr) slog.Attr { func removeTime(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey { if a.Key == slog.TimeKey {
return slog.Attr{} return slog.Attr{}
@@ -35,6 +47,7 @@ func main() {
opts := &yadu.Options{ opts := &yadu.Options{
Level: slog.LevelDebug, Level: slog.LevelDebug,
ReplaceAttr: removeTime, ReplaceAttr: removeTime,
AddSource: true,
} }
logger := slog.New(yadu.NewHandler(os.Stdout, opts)) logger := slog.New(yadu.NewHandler(os.Stdout, opts))
@@ -46,6 +59,7 @@ func main() {
} }
slog.Info("info", "enemy", e, "spawn", 199) slog.Info("info", "enemy", e, "spawn", 199)
slog.Info("info", "ammo", &Ammo{Forweapon: "axe", Impact: 1})
slog.Info("connecting", "enemies", 100, "players", 2, "world", "600x800") slog.Info("connecting", "enemies", 100, "players", 2, "world", "600x800")
slog.Debug("debug text") slog.Debug("debug text")
slog.Error("error") slog.Error("error")

View File

@@ -3,9 +3,11 @@ package yadu
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"log/slog" "log/slog"
"regexp" "regexp"
"runtime"
"slices" "slices"
"strings" "strings"
"sync" "sync"
@@ -15,7 +17,7 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
) )
const VERSION = "0.1.0" const VERSION = "0.1.3"
// We use RFC datestring by default // We use RFC datestring by default
const DefaultTimeFormat = "2006-01-02T03:04.05 MST" const DefaultTimeFormat = "2006-01-02T03:04.05 MST"
@@ -36,6 +38,35 @@ type Handler struct {
replaceAttr func(groups []string, a slog.Attr) slog.Attr replaceAttr func(groups []string, a slog.Attr) slog.Attr
addSource bool addSource bool
indenter *regexp.Regexp indenter *regexp.Regexp
/*
This is being used in Postprocess() to fix
https://github.com/go-yaml/yaml/issues/1020 and
https://github.com/TLINDEN/yadu/issues/12 respectively.
yaml.v3 follows the YAML standard and quotes all keys and values
matching this regex (see https://yaml.org/type/bool.html):
`y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF`
The problem is, that the YAML "standard" does not state wether
this applies to values or keys or values&keys and most
implementors, as gopkg.in/yaml.v3, do it just for keys and values.
Therefore if we dump a struct containing a key "Y" it ends up
being quoted, while any other keys remain unquoted, which looks
pretty ugly, makes evaluating the output harder, especially in
game development where you have to dump coordinates, points etc,
all containing X,Y with X unquoted and Y quoted.
To fix this utter nonsence, I just replace all quotes in all
keys. Period. This is just a logging module, nobody will and can
use its output to postprocess it with some yaml parser, because
we not only dump the structs as yaml, we also write a one liner
in front of it with the timestamp and the message. So, we don't
output valid YAML anyway and we don't give a shit about
compliance because of this. AND because this rule is bullshit.
*/
yamlcleaner *regexp.Regexp
} }
// Options are options for the Yadu [log/slog.Handler]. // Options are options for the Yadu [log/slog.Handler].
@@ -73,11 +104,20 @@ func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
fields := make(map[string]interface{}, r.NumAttrs()) fields := make(map[string]interface{}, r.NumAttrs())
r.Attrs(func(a slog.Attr) bool { r.Attrs(func(a slog.Attr) bool {
fields[a.Key] = a.Value.Any() //fields[a.Key] = a.Value.Any()
a.Value = a.Value.Resolve()
wa := make(map[string]interface{})
h.appendAttr(wa, a)
fields[a.Key] = wa[a.Key]
return true return true
}) })
tree := "" tree := ""
source := ""
if h.addSource && r.PC != 0 {
source = h.getSource(r.PC)
}
if len(h.attrs) > 0 { if len(h.attrs) > 0 {
bytetree, err := yaml.Marshal(h.attrs) bytetree, err := yaml.Marshal(h.attrs)
@@ -119,6 +159,8 @@ func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
buf.WriteString(" ") buf.WriteString(" ")
buf.WriteString(msg) buf.WriteString(msg)
buf.WriteString(" ") buf.WriteString(" ")
buf.WriteString(source)
buf.WriteString(" ")
buf.WriteString(color.WhiteString(tree)) buf.WriteString(color.WhiteString(tree))
buf.WriteString("\n") buf.WriteString("\n")
@@ -129,8 +171,17 @@ func (h *Handler) Handle(ctx context.Context, r slog.Record) error {
return err return err
} }
// report caller source+line as yaml string
func (h *Handler) getSource(pc uintptr) string {
fs := runtime.CallersFrames([]uintptr{pc})
source, _ := fs.Next()
return fmt.Sprintf("%s: %d", source.File, source.Line)
}
func (h *Handler) Postprocess(yamlstr []byte) string { func (h *Handler) Postprocess(yamlstr []byte) string {
return "\n " + strings.TrimSpace(h.indenter.ReplaceAllString(string(yamlstr), " ")) tree := string(yamlstr)
clean := h.yamlcleaner.ReplaceAllString(tree, "$1$2:")
return "\n " + strings.TrimSpace(h.indenter.ReplaceAllString(clean, " "))
} }
// NewHandler returns a [log/slog.Handler] using the receiver's options. // NewHandler returns a [log/slog.Handler] using the receiver's options.
@@ -148,6 +199,7 @@ func NewHandler(out io.Writer, opts *Options) *Handler {
replaceAttr: opts.ReplaceAttr, replaceAttr: opts.ReplaceAttr,
addSource: opts.AddSource, addSource: opts.AddSource,
indenter: regexp.MustCompile(`(?m)^`), indenter: regexp.MustCompile(`(?m)^`),
yamlcleaner: regexp.MustCompile("(?m)^( *)\"([^\"]*)\":"),
} }
if opts.Level == nil { if opts.Level == nil {
@@ -192,7 +244,7 @@ func (h *Handler) appendAttr(wa map[string]interface{}, a slog.Attr) {
} }
wa[name] = innerwa wa[name] = innerwa
if a.Key != "" { if a.Key != "" && len(h.groups) > 0 {
h.groups = h.groups[:len(h.groups)-1] h.groups = h.groups[:len(h.groups)-1]
} }
@@ -249,5 +301,6 @@ func (h *Handler) clone() *Handler {
replaceAttr: h.replaceAttr, replaceAttr: h.replaceAttr,
addSource: h.addSource, addSource: h.addSource,
indenter: h.indenter, indenter: h.indenter,
yamlcleaner: h.yamlcleaner,
} }
} }

View File

@@ -20,6 +20,12 @@ type Ammo struct {
Range float32 Range float32
} }
func (a *Ammo) LogValue() slog.Value {
return slog.GroupValue(
slog.String("Forweapon", "Use weapon: "+a.Forweapon),
)
}
type Enemy struct { type Enemy struct {
Alive bool Alive bool
Health int Health int
@@ -28,6 +34,10 @@ type Enemy struct {
Ammo []Ammo Ammo []Ammo
} }
type Point struct {
y, Y, yes, n, N, no, True, False, on, off int
}
type Tests struct { type Tests struct {
name string name string
want string want string
@@ -50,6 +60,16 @@ var tests = []Tests{
want: "ammo:", want: "ammo:",
negate: false, negate: false,
}, },
{
name: "has-ammo-logvaluer",
want: "Use weapon: Axe",
negate: false,
},
{
name: "has-ammo-logvaluer-does-resolve",
want: "impact: 50", // should NOT appear
negate: true,
},
{ {
name: "has-alive", name: "has-alive",
want: "alive: true", want: "alive: true",
@@ -115,6 +135,14 @@ var tests = []Tests{
Level: slog.LevelError, Level: slog.LevelError,
}, },
}, },
{
name: "has-source",
want: "handler_test.go",
negate: false,
opts: yadu.Options{
AddSource: true,
},
},
{ {
// check if output is NOT colored when disabling it // check if output is NOT colored when disabling it
name: "disable-color", name: "disable-color",
@@ -136,9 +164,15 @@ func GetEnemy() *Enemy {
return &Enemy{Alive: true, Health: 10, Name: "Bodo", Body: "body\nbody\n", return &Enemy{Alive: true, Health: 10, Name: "Bodo", Body: "body\nbody\n",
Ammo: []Ammo{{Forweapon: "Railgun", Range: 400, Impact: 100, Cost: 100000}}, Ammo: []Ammo{{Forweapon: "Railgun", Range: 400, Impact: 100, Cost: 100000}},
} }
} }
func GetAmmo() *Ammo {
return &Ammo{Forweapon: "Axe", Range: 50, Impact: 1, Cost: 50}
}
func GetPoint() *Point {
return &Point{}
}
func removeTime(_ []string, a slog.Attr) slog.Attr { func removeTime(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey { if a.Key == slog.TimeKey {
return slog.Attr{} return slog.Attr{}
@@ -146,7 +180,7 @@ func removeTime(_ []string, a slog.Attr) slog.Attr {
return a return a
} }
func Test(t *testing.T) { func TestLogger(t *testing.T) {
t.Parallel() t.Parallel()
for _, tt := range tests { for _, tt := range tests {
@@ -166,13 +200,13 @@ func Test(t *testing.T) {
switch tt.opts.Level { switch tt.opts.Level {
case slog.LevelDebug: case slog.LevelDebug:
logger.Debug("attack", "enemy", GetEnemy()) logger.Debug("attack", "enemy", GetEnemy(), "ammo", GetAmmo())
case slog.LevelWarn: case slog.LevelWarn:
logger.Warn("attack", "enemy", GetEnemy()) logger.Warn("attack", "enemy", GetEnemy(), "ammo", GetAmmo())
case slog.LevelError: case slog.LevelError:
logger.Error("attack", "enemy", GetEnemy()) logger.Error("attack", "enemy", GetEnemy(), "ammo", GetAmmo())
default: default:
logger.Info("attack", "enemy", GetEnemy()) logger.Info("attack", "enemy", GetEnemy(), "ammo", GetAmmo())
} }
got := buf.String() got := buf.String()
@@ -184,3 +218,22 @@ func Test(t *testing.T) {
buf.Reset() buf.Reset()
} }
} }
func TestYamlCleaner(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
logger := slog.New(yadu.NewHandler(&buf, &yadu.Options{}))
slog.SetDefault(logger)
logger.Info("got a point", "point", GetPoint())
got := buf.String()
bools := []string{"y:", "n:", "true:", "false:"}
for _, want := range bools {
if !strings.Contains(got, want) {
t.Errorf("test TestYamlCleaner failed.\n want: %s:\n got: %s\n", want, got)
}
}
}