mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-10-29 11:26:31 +08:00
Log through `log/slog`, with a console `slog.Handler` that writes the text the runner has always written (sorted `key=value` fields on a plain stream, a colored prefix on a terminal), so nothing changes for users. I compared its output byte for byte with logrus's `TextFormatter` over plain, colored, timestamped and caller cases. Why not `slog.TextHandler`: its output differs from what users and their log parsers see today, and `ReplaceAttr` cannot close the gap. - level is `WARN` where logrus wrote `warning`, and time has milliseconds and no quotes - fields keep call order instead of being sorted - the caller is one `source=` key, where the runner writes `func=` and `file=` (reshaped by `callPrettyfier`), and `ReplaceAttr` returns a single attribute - no colored prefix on a terminal - no hook to rewrite a whole formatted line, which the process-wide secret masking relies on Libraries that log through logrus's global logger (the workflow model, the Docker client) are forwarded to the process-wide logger, so they keep following its level, format and secret masking. This is why logrus stays in `go.mod`. Stacked on https://gitea.com/gitea/runner/pulls/1303. Assisted-by: Codet Co-authored-by: bircni <bircni@icloud.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1304 Reviewed-by: bircni <bircni@icloud.com>
79 lines
2.9 KiB
Go
79 lines
2.9 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package logging
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"log/slog"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func consoleLine(t *testing.T, opts TextOptions, terminal, reportCaller bool, build func(*Logger)) string {
|
|
t.Helper()
|
|
var out bytes.Buffer
|
|
c := newConsole(&out)
|
|
c.opts, c.terminal, c.reportCaller = opts, terminal, reportCaller
|
|
c.level.Set(slog.LevelDebug)
|
|
build(New(NewSlogHandler(&consoleHandler{c: c})))
|
|
return out.String()
|
|
}
|
|
|
|
func TestConsolePlain(t *testing.T) {
|
|
line := consoleLine(t, TextOptions{DisableTimestamp: true}, false, false, func(l *Logger) {
|
|
l.With("job", "build x", "n", 3).WithError(errors.New("boom")).Warnf("hello world")
|
|
l.With("msg", "clash", "empty", "").Info("")
|
|
})
|
|
assert.Equal(t, "level=warning msg=\"hello world\" error=boom job=\"build x\" n=3\n"+
|
|
"level=info empty= fields.msg=clash\n", line)
|
|
}
|
|
|
|
func TestConsolePlainTimeFilterAndCaller(t *testing.T) {
|
|
opts := TextOptions{
|
|
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
|
return "[fn]", "[" + strings.TrimPrefix(f.Function, "x") + "]"
|
|
},
|
|
Filter: func(b []byte) []byte { return bytes.ReplaceAll(b, []byte("secret"), []byte("***")) },
|
|
}
|
|
line := consoleLine(t, opts, false, true, func(l *Logger) { l.With("k", "secret").Debugf("a secret") })
|
|
assert.Regexp(t, `^time="[^"]+" level=debug msg="a \*\*\*" func="\[fn\]" file="\[.*TestConsolePlainTimeFilterAndCaller[^"]*\]" k=\*\*\*\n$`, line)
|
|
}
|
|
|
|
func TestConsoleColored(t *testing.T) {
|
|
line := consoleLine(t, TextOptions{DisableTimestamp: true}, true, false, func(l *Logger) {
|
|
l.With("job", "j").Warn("careful")
|
|
})
|
|
assert.Equal(t, "\x1b[33mWARN\x1b[0m "+"careful"+strings.Repeat(" ", 44-len("careful"))+" \x1b[33mjob\x1b[0m=j\n", line)
|
|
|
|
full := consoleLine(t, TextOptions{FullTimestamp: true}, true, false, func(l *Logger) { l.Error("bad") })
|
|
assert.Regexp(t, `^\x1b\[31mERRO\x1b\[0m\[\d{4}-\d\d-\d\dT[^\]]+\] bad`, full)
|
|
|
|
elapsed := consoleLine(t, TextOptions{}, true, false, func(l *Logger) { l.Info("x") })
|
|
assert.Regexp(t, `^\x1b\[36mINFO\x1b\[0m\[\d{4}\] x`, elapsed)
|
|
|
|
plain := consoleLine(t, TextOptions{DisableColors: true, DisableTimestamp: true}, true, false, func(l *Logger) { l.Info("x") })
|
|
assert.Equal(t, "level=info msg=x\n", plain)
|
|
}
|
|
|
|
func TestConsoleLevelAndOutput(t *testing.T) {
|
|
var out bytes.Buffer
|
|
c := newConsole(&out)
|
|
h := &consoleHandler{c: c}
|
|
assert.False(t, h.Enabled(t.Context(), slog.LevelDebug)) // info unless raised
|
|
c.level.Set(slog.LevelError)
|
|
assert.False(t, h.Enabled(t.Context(), slog.LevelWarn))
|
|
assert.True(t, h.Enabled(t.Context(), slog.LevelError))
|
|
|
|
rec := slog.NewRecord(time.Unix(0, 0), slog.LevelError, "m", 0)
|
|
rec.AddAttrs(slog.String("c", "3"))
|
|
require.NoError(t, h.WithAttrs([]slog.Attr{slog.Int("a", 1)}).Handle(t.Context(), rec))
|
|
assert.Contains(t, out.String(), "a=1 c=3")
|
|
}
|