diff --git a/.goreleaser.yaml b/.goreleaser.yaml deleted file mode 100644 index 3dcb5ac..0000000 --- a/.goreleaser.yaml +++ /dev/null @@ -1,65 +0,0 @@ -# vim: set ts=2 sw=2 tw=0 fo=cnqoj - -version: 2 - -before: - hooks: - - go mod tidy - -gitea_urls: - api: https://codeberg.org/api/v1 - download: https://codeberg.org - -builds: - - env: - - CGO_ENABLED=0 - goos: - - linux - - freebsd - -archives: - - formats: [tar.gz] - # this name template makes the OS and Arch compatible with the results of `uname`. - name_template: >- - {{ .ProjectName }}_ - {{- title .Os }}_ - {{- if eq .Arch "amd64" }}x86_64 - {{- else if eq .Arch "386" }}i386 - {{- else }}{{ .Arch }}{{ end }} - {{- if .Arm }}v{{ .Arm }}{{ end }}_{{ .Tag }} - # use zip for windows archives - format_overrides: - - goos: windows - formats: [zip] - - goos: linux - formats: [tar.gz,binary] - files: - - src: "*.md" - strip_parent: true - - src: Makefile.dist - dst: Makefile - wrap_in_directory: true - -changelog: - sort: asc - filters: - exclude: - - "^docs:" - - "^test:" - groups: - - title: Improved - regexp: '^.*?(feat|add|new)(\([[:word:]]+\))??!?:.+$' - order: 0 - - title: Fixed - regexp: '^.*?(bug|fix)(\([[:word:]]+\))??!?:.+$' - order: 1 - - title: Changed - order: 999 - -release: - header: "# Release Notes" - footer: >- - - --- - - Full Changelog: [{{ .PreviousTag }}...{{ .Tag }}](https://codeberg.org/scip/yadu/compare/{{ .PreviousTag }}...{{ .Tag }}) diff --git a/.woodpecker/build.yaml b/.woodpecker/build.yaml deleted file mode 100644 index e20cc6f..0000000 --- a/.woodpecker/build.yaml +++ /dev/null @@ -1,36 +0,0 @@ -matrix: - platform: - - linux/amd64 - goversion: - - 1.24 - -labels: - platform: ${platform} - -steps: - build: - when: - event: [push] - image: golang:${goversion} - commands: - - go get - - go build - - linter: - when: - event: [push] - image: golang:${goversion} - commands: - - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0 - - golangci-lint --version - - golangci-lint run ./... - depends_on: [build] - - test: - when: - event: [push] - image: golang:${goversion} - commands: - - go get - - go test -v -cover - depends_on: [build,linter] diff --git a/Makefile b/Makefile deleted file mode 100644 index ba0d06a..0000000 --- a/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright © 2023 Thomas von Dein - -# This module is published under the terms of the BSD 3-Clause -# License. Please read the file LICENSE for details. - -# -# no need to modify anything below - -VERSION = $(shell grep VERSION handler.go | head -1 | cut -d '"' -f2) - -all: buildlocal - -buildlocal: - go build -o example/example example/example.go - -clean: - rm -rf $(tool) coverage.out testdata t/out example/example - -test: clean - go test $(ARGS) - -singletest: - @echo "Call like this: make singletest TEST=TestPrepareColumns ARGS=-v" - go test -run $(TEST) $(ARGS) - -cover-report: - go test -cover -coverprofile=coverage.out - go tool cover -html=coverage.out - -goupdate: - go get -t -u=patch ./... - -lint: - golangci-lint run -p bugs -p unused - -release: buildlocal test - gh release create v$(VERSION) --generate-notes - - diff --git a/Makefile.dist b/Makefile.dist deleted file mode 100644 index 55d2f38..0000000 --- a/Makefile.dist +++ /dev/null @@ -1,18 +0,0 @@ -# -*-make-*- - -.PHONY: install all - -tool = rpn -PREFIX = /usr/local -UID = root -GID = 0 - -all: - @echo "Type 'sudo make install' to install the tool." - @echo "To change prefix, type 'sudo make install PREFIX=/opt'" - -install: - install -d -o $(UID) -g $(GID) $(PREFIX)/bin - install -d -o $(UID) -g $(GID) $(PREFIX)/share/doc - install -o $(UID) -g $(GID) -m 555 $(tool) $(PREFIX)/sbin/ - install -o $(UID) -g $(GID) -m 444 *.md $(PREFIX)/share/doc/ diff --git a/README.md b/README.md index e4c971f..1ef9fe2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,9 @@ # yadu - a human readable yaml based slog.Handler +> [!CAUTION] +> This software is now being maintained on [Codeberg](https://codeberg.org/scip/yadu/). + ## Introduction Package yadu provides a handler for the log/slog logging framework. diff --git a/example/example.go b/example/example.go deleted file mode 100644 index 75474dd..0000000 --- a/example/example.go +++ /dev/null @@ -1,66 +0,0 @@ -package main - -import ( - "log/slog" - "os" - - "github.com/tlinden/yadu/v2" -) - -type body string - -type Ammo struct { - Forweapon string - Impact int - Cost int - Range int -} - -func (a *Ammo) LogValue() slog.Value { - return slog.GroupValue( - slog.String("Forweapon", a.Forweapon), - ) -} - -type Enemy struct { - Alive bool - Health int - Name string - Body body `yaml:"-"` - Ammo []Ammo -} - -func (e *Enemy) LogValue() slog.Value { - return slog.GroupValue( - slog.String("name", e.Name), - ) -} - -func removeTime(_ []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey { - return slog.Attr{} - } - return a -} - -func main() { - opts := &yadu.Options{ - Level: slog.LevelDebug, - ReplaceAttr: removeTime, - AddSource: true, - } - - logger := slog.New(yadu.NewHandler(os.Stdout, opts)) - - slog.SetDefault(logger) - - e := &Enemy{Alive: true, Health: 10, Name: "Bodo", Body: "body\nbody\n", - Ammo: []Ammo{{Forweapon: "Railgun", Range: 400, Impact: 100, Cost: 100000}}, - } - - 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.Debug("debug text") - slog.Error("error") -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 108fad3..0000000 --- a/go.mod +++ /dev/null @@ -1,14 +0,0 @@ -module github.com/tlinden/yadu/v2 - -go 1.21 - -require ( - github.com/fatih/color v1.16.0 - gopkg.in/yaml.v3 v3.0.1 -) - -require ( - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.29.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index f9e5959..0000000 --- a/go.sum +++ /dev/null @@ -1,21 +0,0 @@ -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/tlinden/yadu v0.1.3 h1:5cRCUmj+l5yvlM2irtpFBIJwVV2DPEgYSaWvF19FtcY= -github.com/tlinden/yadu v0.1.3/go.mod h1:l3bRmHKL9zGAR6pnBHY2HRPxBecf7L74BoBgOOpTcUA= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler.go b/handler.go deleted file mode 100644 index 9533613..0000000 --- a/handler.go +++ /dev/null @@ -1,306 +0,0 @@ -package yadu - -import ( - "bytes" - "context" - "fmt" - "io" - "log/slog" - "regexp" - "runtime" - "slices" - "strings" - "sync" - - "gopkg.in/yaml.v3" - - "github.com/fatih/color" -) - -const VERSION = "0.1.3" - -// We use RFC datestring by default -const DefaultTimeFormat = "2006-01-02T03:04.05 MST" - -// Default log level is INFO: -const defaultLevel = slog.LevelInfo - -// holds attributes added with logger.With() -type attributes map[string]interface{} - -type Handler struct { - writer io.Writer - mu *sync.Mutex - level slog.Leveler - groups []string - attrs attributes - timeFormat string - replaceAttr func(groups []string, a slog.Attr) slog.Attr - addSource bool - 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]. -// -// Level sets the minimum log level. -// -// ReplaceAttr is a function you can define to customize how supplied -// attrs are being handled. It is empty by default, so nothing will be -// altered. -// -// Loglevel and message cannot be altered using ReplaceAttr. Timestamp -// can only be removed, see example. Keep in mind that everything will -// be passed to yaml.Marshal() in the end. -type Options struct { - Level slog.Leveler - ReplaceAttr func(groups []string, a slog.Attr) slog.Attr - TimeFormat string - AddSource bool - NoColor bool -} - -func (h *Handler) Handle(ctx context.Context, r slog.Record) error { - level := r.Level.String() + ":" - - switch r.Level { - case slog.LevelDebug: - level = color.MagentaString(level) - case slog.LevelInfo: - level = color.BlueString(level) - case slog.LevelWarn: - level = color.YellowString(level) - case slog.LevelError: - level = color.RedString(level) - } - - fields := make(map[string]interface{}, r.NumAttrs()) - r.Attrs(func(a slog.Attr) bool { - //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 - }) - - tree := "" - source := "" - - if h.addSource && r.PC != 0 { - source = h.getSource(r.PC) - } - - if len(h.attrs) > 0 { - bytetree, err := yaml.Marshal(h.attrs) - if err != nil { - return err - } - tree = h.Postprocess(bytetree) - } - - if len(fields) > 0 { - bytetree, err := yaml.Marshal(&fields) - if err != nil { - return err - } - - tree += h.Postprocess(bytetree) - } - - timeStr := "" - timeAttr := slog.Time(slog.TimeKey, r.Time) - - if h.replaceAttr != nil { - timeAttr = h.replaceAttr(nil, timeAttr) - } - - if !r.Time.IsZero() && !timeAttr.Equal(slog.Attr{}) { - timeStr = r.Time.Format(h.timeFormat) - } - - msg := color.CyanString(r.Message) - - buf := bytes.Buffer{} - - if len(timeStr) > 0 { - buf.WriteString(timeStr) - buf.WriteString(" ") - } - buf.WriteString(level) - buf.WriteString(" ") - buf.WriteString(msg) - buf.WriteString(" ") - buf.WriteString(source) - buf.WriteString(" ") - buf.WriteString(color.WhiteString(tree)) - buf.WriteString("\n") - - h.mu.Lock() - defer h.mu.Unlock() - _, err := h.writer.Write(buf.Bytes()) - - 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 { - 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. -// Default options are used if opts is nil. -func NewHandler(out io.Writer, opts *Options) *Handler { - if opts == nil { - opts = &Options{} - } - - h := &Handler{ - writer: out, - mu: &sync.Mutex{}, - level: opts.Level, - timeFormat: opts.TimeFormat, - replaceAttr: opts.ReplaceAttr, - addSource: opts.AddSource, - indenter: regexp.MustCompile(`(?m)^`), - yamlcleaner: regexp.MustCompile("(?m)^( *)\"([^\"]*)\":"), - } - - if opts.Level == nil { - h.level = defaultLevel - } - - if h.timeFormat == "" { - h.timeFormat = DefaultTimeFormat - } - - if opts.NoColor { - color.NoColor = true - } - - return h -} - -// Enabled indicates whether the receiver logs at the given level. -func (h *Handler) Enabled(_ context.Context, l slog.Level) bool { - return l >= h.level.Level() -} - -// attributes plus attrs. -func (h *Handler) appendAttr(wa map[string]interface{}, a slog.Attr) { - a.Value = a.Value.Resolve() - - if a.Value.Kind() == slog.KindGroup { - attrs := a.Value.Group() - name := "" - if len(attrs) == 0 { - return - } - - if a.Key != "" { - name = a.Key - h.groups = append(h.groups, a.Key) - } - - innerwa := make(map[string]interface{}) - for _, a := range attrs { - h.appendAttr(innerwa, a) - } - wa[name] = innerwa - - if a.Key != "" && len(h.groups) > 0 { - h.groups = h.groups[:len(h.groups)-1] - } - - return - } - - if h.replaceAttr != nil { - a = h.replaceAttr(h.groups, a) - } - - if !a.Equal(slog.Attr{}) { - wa[a.Key] = a.Value.Any() - } -} - -// sub logger is to be created, possibly with attrs, add them to h.attrs -func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { - if len(attrs) == 0 { - return h - } - - h2 := h.clone() - - wa := make(map[string]interface{}) - - for _, a := range attrs { - h2.appendAttr(wa, a) - } - - h2.attrs = wa - - return h2 -} - -// WithGroup returns a new [log/slog.Handler] with name appended to the -// receiver's groups. -func (h *Handler) WithGroup(name string) slog.Handler { - if name == "" { - return h - } - h2 := h.clone() - h2.groups = append(h2.groups, name) - return h2 -} - -func (h *Handler) clone() *Handler { - return &Handler{ - writer: h.writer, - mu: h.mu, - level: h.level, - groups: slices.Clip(h.groups), - attrs: h.attrs, - timeFormat: h.timeFormat, - replaceAttr: h.replaceAttr, - addSource: h.addSource, - indenter: h.indenter, - yamlcleaner: h.yamlcleaner, - } -} diff --git a/handler_test.go b/handler_test.go deleted file mode 100644 index 1775c5b..0000000 --- a/handler_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package yadu_test - -import ( - "bytes" - "log/slog" - "strings" - "testing" - "time" - - "github.com/fatih/color" - "github.com/tlinden/yadu/v2" -) - -type body string - -type Ammo struct { - Forweapon string - Impact int - Cost int - Range float32 -} - -func (a *Ammo) LogValue() slog.Value { - return slog.GroupValue( - slog.String("Forweapon", "Use weapon: "+a.Forweapon), - ) -} - -type Enemy struct { - Alive bool - Health int - Name string - Body body `yaml:"-"` - Ammo []Ammo -} - -type Point struct { - Y, N, True, False int -} - -type Tests struct { - name string - want string - negate bool - opts yadu.Options - with slog.Attr - haswith bool -} - -const testTimeFormat = "03:04.05" - -var tests = []Tests{ - { - name: "has-railgun", - want: "forweapon: Railgun", - negate: false, - }, - { - name: "has-ammo", - want: "ammo:", - 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", - want: "alive: true", - negate: false, - }, - { - name: "has-no-body", - want: "body:", - negate: true, - }, - { - name: "has-time", - want: time.Now().Format(yadu.DefaultTimeFormat), - negate: false, - }, - { - name: "has-no-time", - want: time.Now().Format(yadu.DefaultTimeFormat), - opts: yadu.Options{ - ReplaceAttr: removeTime, - }, - negate: true, - }, - { - name: "has-custom-time", - want: time.Now().Format(testTimeFormat), - opts: yadu.Options{ - TimeFormat: testTimeFormat, - }, - negate: false, - }, - { - name: "with-group", - want: "pid:", - negate: false, - with: slog.Group("program_info", - slog.Int("pid", 1923), - slog.Bool("alive", true), - ), - haswith: true, - }, - { - name: "has-debug", - want: "DEBUG", - negate: false, - opts: yadu.Options{ - Level: slog.LevelDebug, - }, - }, - { - name: "has-warn", - want: "WARN", - negate: false, - opts: yadu.Options{ - Level: slog.LevelWarn, - }, - }, - { - name: "has-error", - want: "ERROR", - negate: false, - opts: yadu.Options{ - 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 - name: "disable-color", - want: "\x1b[0m", - negate: true, - opts: yadu.Options{ - NoColor: true, - }, - }, - { - // check if output is colored - name: "enable-color", - want: "\x1b[0m", - negate: false, - }, -} - -func GetEnemy() *Enemy { - return &Enemy{Alive: true, Health: 10, Name: "Bodo", Body: "body\nbody\n", - 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 { - if a.Key == slog.TimeKey { - return slog.Attr{} - } - return a -} - -func TestLogger(t *testing.T) { - t.Parallel() - - for _, tt := range tests { - var buf bytes.Buffer - ttopts := tt.opts - logger := slog.New(yadu.NewHandler(&buf, &ttopts)) - - if !tt.with.Equal(slog.Attr{}) { - logger = logger.With(tt.with) - } - - if !tt.opts.NoColor { - color.NoColor = false - } - - slog.SetDefault(logger) - - switch tt.opts.Level { - case slog.LevelDebug: - logger.Debug("attack", "enemy", GetEnemy(), "ammo", GetAmmo()) - case slog.LevelWarn: - logger.Warn("attack", "enemy", GetEnemy(), "ammo", GetAmmo()) - case slog.LevelError: - logger.Error("attack", "enemy", GetEnemy(), "ammo", GetAmmo()) - default: - logger.Info("attack", "enemy", GetEnemy(), "ammo", GetAmmo()) - } - - got := buf.String() - - if strings.Contains(got, tt.want) == tt.negate { - t.Errorf("test %s failed.\n want:\n%s\n got: %s\n", tt.name, tt.want, got) - } - - 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) - } - } -}