From 9c4c66e70f8a46db0952c315caa3514ec34066ec Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 14 Oct 2026 14:15:26 +0000 Subject: [PATCH] enhance(report): add runner.log_report_level for the uploaded runner messages (#1310) The runner's own messages between steps (not step output) were uploaded to the job log at `log.level`, so raising `log.level` for local debugging also changed what workflow authors see in Gitea, and lowering it hid messages from them. Add `runner.log_report_level` to set that independently. Unset, it still follows `log.level`, so existing setups behave as before. An invalid value is rejected when the config loads. Assisted-by: Codet Assisted-by: Claude Code:claude-opus-5-5 Co-authored-by: silverwind Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com> Reviewed-on: https://gitea.com/gitea/runner/pulls/1310 Co-authored-by: Lunny Xiao --- DEVELOPMENT.md | 3 +++ internal/pkg/config/config.example.yaml | 3 +++ internal/pkg/config/config.go | 6 ++++++ internal/pkg/config/config_test.go | 5 +++++ internal/pkg/report/reporter.go | 10 ++++++++-- internal/pkg/report/reporter_test.go | 3 +++ 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2f30e69b..e18051f8 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -40,6 +40,9 @@ fields. - The process-wide logger masks the secrets of every live task, the job logger masks its own. Going around either leaks them. +- The job logger's output can reach the job log workflow authors see, the process-wide logger's + stays on the runner host, so keep operator-only detail (host paths, internal addresses) off the + job logger. - Libraries that log through logrus's or slog's global logger are forwarded to the process-wide one, so they follow its level and masking too. - That forwarding only works while logrus's global logger is left as `logging` configures it. A diff --git a/internal/pkg/config/config.example.yaml b/internal/pkg/config/config.example.yaml index 5a90db19..b91a49d2 100644 --- a/internal/pkg/config/config.example.yaml +++ b/internal/pkg/config/config.example.yaml @@ -76,6 +76,9 @@ runner: # Flush logs immediately when the buffer reaches this many rows. # This ensures bursty output (e.g., npm install) is delivered promptly. #log_report_batch_size: 100 + # The lowest severity of the runner's own messages (not step output) that are sent to the + # job log between steps: trace, debug, info, warn, error. Default if omitted: follow log.level. + #log_report_level: info # The interval for reporting task state (step status, timing) to the Gitea instance. # State is also reported on step transitions (start/stop) and after each log flush. #state_report_interval: 5s diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index c6a8f06d..0ff35ff2 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -76,6 +76,7 @@ type Runner struct { LogReportInterval time.Duration `yaml:"log_report_interval"` // LogReportInterval specifies the base interval for periodic log flush. LogReportMaxLatency time.Duration `yaml:"log_report_max_latency"` // LogReportMaxLatency specifies the max time a log row can wait before being sent. LogReportBatchSize int `yaml:"log_report_batch_size"` // LogReportBatchSize triggers immediate log flush when buffer reaches this size. + LogReportLevel string `yaml:"log_report_level"` // LogReportLevel is the lowest level of the runner's own messages sent to the job log between steps. Empty, the default, follows log.level. StateReportInterval time.Duration `yaml:"state_report_interval"` // StateReportInterval specifies the interval for state reporting. ReportCloseTimeout time.Duration `yaml:"report_close_timeout"` // ReportCloseTimeout caps each RPC attempt when flushing the final logs and task state at job completion, on a detached context so a server cancel can't block the acknowledgement. Labels []string `yaml:"labels"` // Labels specify the labels of the runner. Labels are declared on each startup @@ -348,6 +349,11 @@ func LoadDefault(file string) (*Config, error) { if !slices.Contains(ToolCacheModes, cfg.Runner.ToolCacheMode) { return nil, fmt.Errorf("invalid runner.tool_cache_mode %q: must be one of %q", cfg.Runner.ToolCacheMode, ToolCacheModes) } + if cfg.Runner.LogReportLevel != "" { + if _, err := logging.ParseLevel(cfg.Runner.LogReportLevel); err != nil { + return nil, fmt.Errorf("invalid runner.log_report_level: %w", err) + } + } if err := validateExtraHeaders(cfg.Runner.ExtraHeaders); err != nil { return nil, err } diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go index 230d8c1d..d1de61c0 100644 --- a/internal/pkg/config/config_test.go +++ b/internal/pkg/config/config_test.go @@ -172,6 +172,11 @@ func TestLoadDefault_ToolCacheMode(t *testing.T) { assert.Contains(t, err.Error(), "tool_cache_mode") } +func TestLoadDefault_RejectsInvalidLogReportLevel(t *testing.T) { + _, err := LoadDefault(write(t, "runner:\n log_report_level: loud\n")) + require.ErrorContains(t, err, "runner.log_report_level") +} + func TestLoadDefault_LoadsExtraHeaders(t *testing.T) { path := filepath.Join(t.TempDir(), "config.yaml") require.NoError(t, os.WriteFile(path, []byte("runner:\n extra_headers:\n X-Proxy-Token: proxy-token\n"), 0o600)) diff --git a/internal/pkg/report/reporter.go b/internal/pkg/report/reporter.go index f7546ab8..59600f98 100644 --- a/internal/pkg/report/reporter.go +++ b/internal/pkg/report/reporter.go @@ -89,6 +89,7 @@ type Reporter struct { logReportInterval time.Duration logReportMaxLatency time.Duration logBatchSize int + logReportLevel logging.Level stateReportInterval time.Duration // closeTimeout bounds each RPC attempt in the final flush, on a context // detached from r.ctx so a server cancel can't abort the acknowledgement. @@ -133,6 +134,7 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C logReportInterval: cfg.Runner.LogReportInterval, logReportMaxLatency: cfg.Runner.LogReportMaxLatency, logBatchSize: cfg.Runner.LogReportBatchSize, + logReportLevel: logging.GetLevel(), stateReportInterval: cfg.Runner.StateReportInterval, closeTimeout: cfg.Runner.ReportCloseTimeout, logNotify: make(chan struct{}, 1), @@ -146,6 +148,10 @@ func NewReporter(ctx context.Context, cancel context.CancelFunc, client client.C jobLog: openJobLog(cfg.Log.Job, task.Id, time.Now()), } + if level, err := logging.ParseLevel(cfg.Runner.LogReportLevel); err == nil { // unset keeps log.level + rv.logReportLevel = level + } + rv.daemonWait = 6 * rv.effectiveCloseTimeout() registerGlobalMasks(rv) @@ -954,9 +960,9 @@ 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. +// within runner.log_report_level. func (r *Reporter) shouldAppendLogRow(entry logging.Record) bool { - return !r.duringSteps() && entry.Level >= logging.GetLevel() + return !r.duringSteps() && entry.Level >= r.logReportLevel } var stringToResult = map[string]runnerv1.Result{ diff --git a/internal/pkg/report/reporter_test.go b/internal/pkg/report/reporter_test.go index 442c48c9..cf5dd257 100644 --- a/internal/pkg/report/reporter_test.go +++ b/internal/pkg/report/reporter_test.go @@ -309,7 +309,10 @@ func TestReporter_LogLevelFiltering(t *testing.T) { taskCtx, err := structpb.NewStruct(map[string]any{}) require.NoError(t, err) cfg, _ := config.LoadDefault("") + cfg.Runner.LogReportLevel = "warn" reporter := NewReporter(ctx, cancel, client, &runnerv1.Task{Context: taskCtx}, cfg) + assert.False(t, reporter.shouldAppendLogRow(logging.Record{Level: logging.LevelInfo})) + assert.True(t, reporter.shouldAppendLogRow(logging.Record{Level: logging.LevelWarn})) reporter.RunDaemon() defer func() { require.NoError(t, reporter.Close(""))