moved to codeberg

This commit is contained in:
2025-12-06 20:58:07 +01:00
parent 6fed3af065
commit ee076a4df8
10 changed files with 3 additions and 804 deletions

View File

@@ -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 }})

View File

@@ -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]

View File

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

View File

@@ -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/

View File

@@ -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.

View File

@@ -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")
}

14
go.mod
View File

@@ -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
)

21
go.sum
View File

@@ -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=

View File

@@ -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,
}
}

View File

@@ -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)
}
}
}