refactor(logging): add a logging abstraction and move the job logger onto it (#1295)

Add `act/common/logging`: a printf-style `Logger` on a slog-shaped `Handler`, with a logrus-backed handler as the only backend, plus a `logtest` handler for tests. Move the job logger onto it: `common.Logger` returns `*logging.Logger`, the job log formatters and the `Reporter` hook become handlers, and composite step IDs travel in the context.

Logging is tightly coupled to logrus types (hooks, formatters, `Entry.Data`). Going through our own interface first lets the remaining call sites move over with unchanged behavior, and lets the backend be swapped for `log/slog` in one place afterwards. The global process logger and its masking formatter stay on logrus for now.

Assisted-by: Codet
Co-authored-by: bircni <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1295
Reviewed-by: bircni <[email protected]>
This commit is contained in:
Lunny Xiaoandbircni committed 2026-10-11 23:26:00 +00:00
1 parent ad9eeb668a
commit de961b91eb
50 files changed
+1241 -438

No files matched your search

+51 -18
View File
@@ -8,11 +8,13 @@ import (
"errors"
"fmt"
"regexp"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
"gitea.com/gitea/runner/act/common/logging"
"gitea.com/gitea/runner/act/runner"
"gitea.com/gitea/runner/internal/pkg/client"
"gitea.com/gitea/runner/internal/pkg/config"
@@ -166,8 +168,30 @@ func (r *Reporter) ResetSteps(l int) {
}
}
func (r *Reporter) Levels() []log.Level {
return log.AllLevels
// Enabled makes the Reporter see every record, the job logger's level filter runs before it.
func (r *Reporter) Enabled(context.Context, logging.Level) bool { return true }
// WithAttrs returns a Handler feeding the same Reporter, with attrs added to every record.
func (r *Reporter) WithAttrs(attrs []logging.Attr) logging.Handler {
return &reporterHandler{r: r, attrs: attrs}
}
type reporterHandler struct {
r *Reporter
attrs []logging.Attr
}
func (h *reporterHandler) Enabled(ctx context.Context, level logging.Level) bool {
return h.r.Enabled(ctx, level)
}
func (h *reporterHandler) Handle(ctx context.Context, rec logging.Record) error {
rec.Attrs = slices.Concat(h.attrs, rec.Attrs)
return h.r.Handle(ctx, rec)
}
func (h *reporterHandler) WithAttrs(attrs []logging.Attr) logging.Handler {
return &reporterHandler{r: h.r, attrs: slices.Concat(h.attrs, attrs)}
}
// appendLogRow masks a row before buffering it for Gitea and the local job.log, the one point
@@ -181,11 +205,19 @@ func (r *Reporter) appendLogRow(row *runnerv1.LogRow) {
r.jobLog.write(row.Time.AsTime(), row.Content)
}
func attrArgs(attrs []logging.Attr) []any {
args := make([]any, len(attrs))
for i, a := range attrs {
args[i] = a
}
return args
}
// isJobStepEntry is used to not report composite step results incorrectly as step result
// returns true if the logentry is on job level
// returns false for composite action step messages
func isJobStepEntry(entry *log.Entry) bool {
if v, ok := entry.Data["stepID"]; ok {
func isJobStepEntry(rec logging.Record) bool {
if v, ok := rec.Attr("stepID"); ok {
if v, ok := v.([]string); ok && len(v) > 1 {
return false
}
@@ -219,15 +251,15 @@ func (r *Reporter) unlockAndNotify(urgentState bool) {
}
}
func (r *Reporter) Fire(entry *log.Entry) error {
func (r *Reporter) Handle(_ context.Context, entry logging.Record) error {
urgentState := false
r.stateMu.Lock()
r.stateChanged = true
if log.IsLevelEnabled(log.TraceLevel) {
log.WithFields(entry.Data).Trace(r.mask(entry.Message)) // the process masker has no ::add-mask:: value
if logging.Enabled(logging.LevelTrace) {
logging.With(attrArgs(entry.Attrs)...).Trace(r.mask(entry.Message)) // the process masker has no ::add-mask:: value
}
timestamp := entry.Time
@@ -235,10 +267,10 @@ func (r *Reporter) Fire(entry *log.Entry) error {
r.state.StartedAt = timestamppb.New(timestamp)
}
stage := entry.Data["stage"]
stage, _ := entry.Attr("stage")
if stage != "Main" {
if v, ok := entry.Data["jobResult"]; ok {
if v, ok := entry.Attr("jobResult"); ok {
if jobResult, ok := r.parseResult(v); ok {
// We need to ensure log upload before this upload
r.state.Result = jobResult
@@ -262,7 +294,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
}
var step *runnerv1.StepState
if v, ok := entry.Data["stepNumber"]; ok {
if v, ok := entry.Attr("stepNumber"); ok {
if v, ok := v.(int); ok && len(r.state.Steps) > v {
step = r.state.Steps[v]
}
@@ -284,11 +316,12 @@ func (r *Reporter) Fire(entry *log.Entry) error {
}
// Force reporting log errors as raw output to prevent silent failures
if entry.Level == log.ErrorLevel {
entry.Data["raw_output"] = true
v, ok := entry.Attr("raw_output")
if entry.Level >= logging.LevelError {
v, ok = true, true
}
if v, ok := entry.Data["raw_output"]; ok {
if ok {
if rawOutput, ok := v.(bool); ok && rawOutput {
if row := r.parseLogRow(entry); row != nil {
if step.LogLength == 0 {
@@ -301,7 +334,7 @@ func (r *Reporter) Fire(entry *log.Entry) error {
} else if r.shouldAppendLogRow(entry) {
r.appendLogRow(r.parseLogRow(entry))
}
if v, ok := entry.Data["stepResult"]; ok && isJobStepEntry(entry) {
if v, ok := entry.Attr("stepResult"); ok && isJobStepEntry(entry) {
if stepResult, ok := r.parseResult(v); ok {
if step.LogLength == 0 {
step.LogIndex = int64(r.logOffset + len(r.logRows))
@@ -671,7 +704,7 @@ func (r *Reporter) ReportState(reportResult bool) error {
r.outputsMu.Unlock()
// Consume stateChanged atomically with the snapshot; restored on error
// below so a concurrent Fire() during UpdateTask isn't silently lost.
// below so a log record handled during UpdateTask isn't silently lost.
// Heartbeat at stateReportInterval even when nothing changed, so the server
// doesn't time out long-running silent jobs as orphaned (#826).
last := r.lastReportedAtNanos.Load()
@@ -753,8 +786,8 @@ func (r *Reporter) duringSteps() bool {
// shouldAppendLogRow reports whether a non-raw_output entry should be written
// to the job log: only when we are between steps and the entry's level is
// within the globally configured log level.
func (r *Reporter) shouldAppendLogRow(entry *log.Entry) bool {
return !r.duringSteps() && entry.Level <= log.GetLevel()
func (r *Reporter) shouldAppendLogRow(entry logging.Record) bool {
return !r.duringSteps() && entry.Level >= logging.GetLevel()
}
var stringToResult = map[string]runnerv1.Result{
@@ -879,7 +912,7 @@ func cutPrefixFold(s, prefix string) (string, bool) {
return s[len(prefix):], true
}
func (r *Reporter) parseLogRow(entry *log.Entry) *runnerv1.LogRow {
func (r *Reporter) parseLogRow(entry logging.Record) *runnerv1.LogRow {
content := strings.TrimRight(entry.Message, "\r\n")
// cmdRegex only covers the ::cmd:: form, so the ##[add-mask] one would otherwise reach