mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-14 03:43:42 +00:00
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:
1 parent
ad9eeb668a
commit
de961b91eb
50 files changed
+1241
-438
No files matched your search
@@ -17,6 +17,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.com/gitea/runner/act/common/logging"
|
||||
"gitea.com/gitea/runner/act/runner"
|
||||
"gitea.com/gitea/runner/internal/pkg/client/mocks"
|
||||
"gitea.com/gitea/runner/internal/pkg/config"
|
||||
@@ -25,7 +26,6 @@ import (
|
||||
connect_go "connectrpc.com/connect"
|
||||
"gitea.dev/actionslib/pkg/model"
|
||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
logrustest "github.com/sirupsen/logrus/hooks/test"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -206,7 +206,7 @@ func TestReporter_parseLogRow(t *testing.T) {
|
||||
debugOutputEnabled: tt.debugOutputEnabled,
|
||||
}
|
||||
for idx, arg := range tt.args {
|
||||
rv := r.parseLogRow(&log.Entry{Message: arg})
|
||||
rv := r.parseLogRow(logging.Record{Message: arg})
|
||||
got := "<nil>"
|
||||
|
||||
if rv != nil {
|
||||
@@ -225,13 +225,13 @@ func TestReporter_parseLogRowAddMask(t *testing.T) {
|
||||
for _, line := range []string{"::add-mask::supersecret", "##[add-mask]supersecret"} {
|
||||
r := &Reporter{logReplacer: strings.NewReplacer()}
|
||||
|
||||
assert.Nil(t, r.parseLogRow(&log.Entry{Message: line}), line)
|
||||
assert.Nil(t, r.parseLogRow(logging.Record{Message: line}), line)
|
||||
|
||||
assert.Equal(t, "using *** now", r.mask("using supersecret now"), line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReporter_Fire(t *testing.T) {
|
||||
func TestReporter_Handle(t *testing.T) {
|
||||
t.Run("ignore command lines", func(t *testing.T) {
|
||||
client := mocks.NewClient(t)
|
||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||
@@ -263,13 +263,13 @@ func TestReporter_Fire(t *testing.T) {
|
||||
"raw_output": true,
|
||||
}
|
||||
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "composite step result", Data: map[string]any{
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "::debug::debug log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "regular log line", Data: dataStep0}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "composite step result", Data: map[string]any{
|
||||
"stage": "Main",
|
||||
"stepID": []string{"0", "0"},
|
||||
"stepNumber": 0,
|
||||
@@ -277,7 +277,7 @@ func TestReporter_Fire(t *testing.T) {
|
||||
"stepResult": model.StepStatusFailure,
|
||||
}}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_UNSPECIFIED, reporter.state.Steps[0].Result)
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step result", Data: map[string]any{
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "step result", Data: map[string]any{
|
||||
"stage": "Main",
|
||||
"stepNumber": 0,
|
||||
"raw_output": true,
|
||||
@@ -291,9 +291,9 @@ func TestReporter_Fire(t *testing.T) {
|
||||
|
||||
func TestReporter_LogLevelFiltering(t *testing.T) {
|
||||
// Set global level to Info so Debug entries should be filtered.
|
||||
origLevel := log.GetLevel()
|
||||
log.SetLevel(log.InfoLevel)
|
||||
defer log.SetLevel(origLevel)
|
||||
origLevel := logging.GetLevel()
|
||||
logging.SetLevel(logging.LevelInfo)
|
||||
defer logging.SetLevel(origLevel)
|
||||
|
||||
client := mocks.NewClient(t)
|
||||
client.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect_go.Request[runnerv1.UpdateLogRequest]) (*connect_go.Response[runnerv1.UpdateLogResponse], error) {
|
||||
@@ -316,27 +316,27 @@ func TestReporter_LogLevelFiltering(t *testing.T) {
|
||||
}()
|
||||
reporter.ResetSteps(2)
|
||||
|
||||
dataStep0 := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||
dataStep0Internal := log.Fields{"stage": "Main", "stepNumber": 0}
|
||||
dataStep0 := map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||
dataStep0Internal := map[string]any{"stage": "Main", "stepNumber": 0}
|
||||
|
||||
// raw_output entries always appear in job log regardless of level.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step output", Data: dataStep0, Level: log.InfoLevel}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step debug output", Data: dataStep0, Level: log.DebugLevel}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "step output", Data: dataStep0, Level: logging.LevelInfo}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "step debug output", Data: dataStep0, Level: logging.LevelDebug}))
|
||||
assert.Equal(t, int64(2), reporter.state.Steps[0].LogLength, "raw_output entries must always be forwarded")
|
||||
|
||||
// Non-raw_output entries during steps are not added to logRows regardless of level.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal info", Data: dataStep0Internal, Level: log.InfoLevel}))
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "internal debug", Data: dataStep0Internal, Level: log.DebugLevel}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "internal info", Data: dataStep0Internal, Level: logging.LevelInfo}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "internal debug", Data: dataStep0Internal, Level: logging.LevelDebug}))
|
||||
|
||||
// stepResult at DebugLevel (skipped step) must still update state even when filtered from log.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "Skipping step",
|
||||
Data: log.Fields{
|
||||
Data: map[string]any{
|
||||
"stage": "Main",
|
||||
"stepNumber": 1,
|
||||
"stepResult": "skipped",
|
||||
},
|
||||
Level: log.DebugLevel,
|
||||
Level: logging.LevelDebug,
|
||||
}))
|
||||
assert.Equal(t, runnerv1.Result_RESULT_SKIPPED, reporter.state.Steps[1].Result,
|
||||
"stepResult at DebugLevel must update step state even when log entry is filtered from job log output")
|
||||
@@ -383,9 +383,9 @@ func TestReporter_EphemeralRunnerDeletion(t *testing.T) {
|
||||
reporter.ResetSteps(1)
|
||||
|
||||
// Fire a log entry to create pending data
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "build output",
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
}))
|
||||
|
||||
// Step 1: RunDaemon calls ReportLog(false) — runner is still alive
|
||||
@@ -500,9 +500,9 @@ func TestReporter_MaxLatencyTimer(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Fire a single log line — not enough to trigger batch flush
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "single log line",
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
}))
|
||||
|
||||
// maxLatencyTimer should flush within ~100ms. Wait up to 500ms.
|
||||
@@ -552,9 +552,9 @@ func TestReporter_BatchSizeFlush(t *testing.T) {
|
||||
|
||||
// Fire exactly batchSize log lines
|
||||
for i := range 5 {
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: fmt.Sprintf("log line %d", i),
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -565,7 +565,7 @@ func TestReporter_BatchSizeFlush(t *testing.T) {
|
||||
"batch size threshold should have triggered immediate flush")
|
||||
}
|
||||
|
||||
// TestReporter_StateChangedNotLostDuringReport asserts that a Fire() arriving
|
||||
// TestReporter_StateChangedNotLostDuringReport asserts that a log record arriving
|
||||
// mid-UpdateTask re-dirties the flag so the change is picked up by the next report.
|
||||
func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
||||
var updateTaskCalls atomic.Int64
|
||||
@@ -607,11 +607,11 @@ func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
||||
// Wait until UpdateTask is in flight (snapshot taken, flag consumed).
|
||||
<-inFlight
|
||||
|
||||
// Concurrent Fire() modifies state — must re-flip stateChanged so the
|
||||
// A concurrent log record modifies state — must re-flip stateChanged so the
|
||||
// change is not lost when the in-flight ReportState finishes.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "step starts",
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 1, "raw_output": true},
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 1, "raw_output": true},
|
||||
}))
|
||||
|
||||
// Release the in-flight UpdateTask and wait for it to return.
|
||||
@@ -619,15 +619,15 @@ func TestReporter_StateChangedNotLostDuringReport(t *testing.T) {
|
||||
require.NoError(t, <-done)
|
||||
|
||||
// stateChanged must still be true so the next ReportState picks up the
|
||||
// concurrent Fire()'s change instead of skipping via the early-return path.
|
||||
// concurrent record's change instead of skipping via the early-return path.
|
||||
reporter.stateMu.RLock()
|
||||
changed := reporter.stateChanged
|
||||
reporter.stateMu.RUnlock()
|
||||
assert.True(t, changed, "stateChanged must remain true after a concurrent Fire() during in-flight ReportState")
|
||||
assert.True(t, changed, "stateChanged must remain true after a concurrent log record during in-flight ReportState")
|
||||
|
||||
// And the next ReportState must actually send a second UpdateTask.
|
||||
require.NoError(t, reporter.ReportState(false))
|
||||
assert.Equal(t, int64(2), updateTaskCalls.Load(), "concurrent Fire() change must trigger a second UpdateTask, not be silently lost")
|
||||
assert.Equal(t, int64(2), updateTaskCalls.Load(), "concurrent log record change must trigger a second UpdateTask, not be silently lost")
|
||||
}
|
||||
|
||||
// TestReporter_StateChangedRestoredOnError verifies that when UpdateTask fails,
|
||||
@@ -709,9 +709,9 @@ func TestReporter_StateNotifyFlush(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Fire a log entry that starts a step — this triggers stateNotify
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "step starting",
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
}))
|
||||
|
||||
// stateNotify should trigger immediate UpdateTask call
|
||||
@@ -1054,8 +1054,8 @@ func TestAppendLogRow(t *testing.T) {
|
||||
assert.Equal(t, "***", r.logRows[0].Content)
|
||||
}
|
||||
|
||||
func TestReporter_Levels(t *testing.T) {
|
||||
assert.Equal(t, log.AllLevels, (&Reporter{}).Levels())
|
||||
func TestReporter_Enabled(t *testing.T) {
|
||||
assert.True(t, (&Reporter{}).Enabled(t.Context(), logging.LevelTrace))
|
||||
}
|
||||
|
||||
func TestReporter_Result(t *testing.T) {
|
||||
@@ -1218,10 +1218,10 @@ func TestReporter_AckIndexBeyondBuffer(t *testing.T) {
|
||||
reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg)
|
||||
reporter.ResetSteps(1)
|
||||
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "hello",
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Level: log.InfoLevel,
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Level: logging.LevelInfo,
|
||||
}))
|
||||
|
||||
require.NoError(t, reporter.ReportLog(false))
|
||||
@@ -1258,10 +1258,10 @@ func TestReporter_CloseWithStuckDaemon(t *testing.T) {
|
||||
reporter.RunDaemon()
|
||||
reporter.ResetSteps(1)
|
||||
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: "hello",
|
||||
Data: log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Level: log.InfoLevel,
|
||||
Data: map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true},
|
||||
Level: logging.LevelInfo,
|
||||
}))
|
||||
|
||||
select {
|
||||
@@ -1441,17 +1441,17 @@ func TestReporter_StepRangeCoversAckedRows(t *testing.T) {
|
||||
_ = reporter.Close("")
|
||||
}()
|
||||
|
||||
stepData := log.Fields{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||
stepData := map[string]any{"stage": "Main", "stepNumber": 0, "raw_output": true}
|
||||
|
||||
// The step's first row is a transition, so it flushes state as well as logs.
|
||||
require.NoError(t, reporter.Fire(&log.Entry{Message: "step starting", Data: stepData}))
|
||||
require.NoError(t, fire(reporter, logEntry{Message: "step starting", Data: stepData}))
|
||||
require.Eventually(t, func() bool {
|
||||
rows, claimed := server.snapshot()
|
||||
return rows == 1 && claimed == rows
|
||||
}, time.Second, 10*time.Millisecond, "the step transition should have flushed both log and state")
|
||||
|
||||
for i := range cfg.Runner.LogReportBatchSize {
|
||||
require.NoError(t, reporter.Fire(&log.Entry{
|
||||
require.NoError(t, fire(reporter, logEntry{
|
||||
Message: fmt.Sprintf("step output %d", i),
|
||||
Data: stepData,
|
||||
}))
|
||||
@@ -1489,3 +1489,18 @@ func TestReporter_FlushWithoutAckedRowsSkipsState(t *testing.T) {
|
||||
reporter.Logf("row")
|
||||
reporter.reportLogWithState()
|
||||
}
|
||||
|
||||
// logEntry is the record a job logger hands the Reporter, with the fields it accumulated as Data.
|
||||
type logEntry struct {
|
||||
Message string
|
||||
Data map[string]any
|
||||
Level logging.Level
|
||||
}
|
||||
|
||||
func fire(r *Reporter, e logEntry) error {
|
||||
attrs := make([]logging.Attr, 0, len(e.Data))
|
||||
for k, v := range e.Data {
|
||||
attrs = append(attrs, logging.Attr{Key: k, Value: v})
|
||||
}
|
||||
return r.WithAttrs(attrs).Handle(context.Background(), logging.Record{Message: e.Message, Level: e.Level})
|
||||
}
|
||||
Reference in new issue
Block a user