diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index d40c3f4b..2f30e69b 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -40,8 +40,8 @@ fields. - The process-wide logger masks the secrets of every live task, the job logger masks its own. Going around either leaks them. -- Libraries that log through logrus's global logger are forwarded to the process-wide one, so they - follow its level and masking too. +- 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 dependency that calls `logrus.SetOutput` or `SetLevel` breaks it, so check `logging.LogrusBridgeIntact()` when adding or upgrading one. diff --git a/act/common/logging/logging_test.go b/act/common/logging/logging_test.go index ae1c4aec..4aa31bdb 100644 --- a/act/common/logging/logging_test.go +++ b/act/common/logging/logging_test.go @@ -169,6 +169,23 @@ func TestLogrusBridge(t *testing.T) { assert.Empty(t, h.AllRecords()) } +func TestSlogBridge(t *testing.T) { + h := logtest.SetDefault(t) + slog.With("lib", "actionslib").Warn("from a library", "n", 1) + _, _, line, _ := runtime.Caller(0) + slog.Debug("dropped") + + recs := h.AllRecords() + require.Len(t, recs, 1) + assert.Equal(t, "from a library", recs[0].Message) + assert.Equal(t, logging.LevelWarn, recs[0].Level) + lib, _ := recs[0].Attr("lib") + assert.Equal(t, "actionslib", lib) + n, _ := recs[0].Attr("n") + assert.EqualValues(t, 1, n) + assert.Equal(t, line-1, frameOf(t, recs[0].PC).Line) +} + func TestLogrusBridgeIntact(t *testing.T) { assert.True(t, logging.LogrusBridgeIntact()) diff --git a/act/common/logging/logrus_bridge.go b/act/common/logging/logrus_bridge.go index a5d3ac88..295f594c 100644 --- a/act/common/logging/logrus_bridge.go +++ b/act/common/logging/logrus_bridge.go @@ -10,7 +10,7 @@ import ( "github.com/sirupsen/logrus" ) -// Libraries the runner depends on, such as the workflow model and the Docker client, log through +// Libraries the runner depends on, such as docker/cli and moby/go-archive, log through // logrus's global logger. Hand what they log to the process-wide Logger, so it follows the same // level, format and secret masking as the rest of the output. func init() { diff --git a/act/common/logging/slog.go b/act/common/logging/slog.go index 8f269861..450f677f 100644 --- a/act/common/logging/slog.go +++ b/act/common/logging/slog.go @@ -6,8 +6,49 @@ package logging import ( "context" "log/slog" + "slices" ) +func init() { + slog.SetDefault(slog.New(slogBridge{})) +} + +// slogBridge hands what libraries log through slog's global logger to the process-wide Logger. +type slogBridge struct { + attrs []Attr +} + +func (b slogBridge) Enabled(ctx context.Context, level slog.Level) bool { + return Default().h.Enabled(ctx, Level(level)) +} + +func (b slogBridge) Handle(ctx context.Context, r slog.Record) error { + h := Default().h + if len(b.attrs) > 0 { + h = h.WithAttrs(b.attrs) + } + attrs := make([]Attr, 0, r.NumAttrs()) + r.Attrs(func(a slog.Attr) bool { + attrs = append(attrs, fromSlogAttr(a)) + return true + }) + return h.Handle(ctx, Record{Time: r.Time, Level: Level(r.Level), Message: r.Message, PC: r.PC, Attrs: attrs}) +} + +func (b slogBridge) WithAttrs(attrs []slog.Attr) slog.Handler { + out := slices.Clone(b.attrs) + for _, a := range attrs { + out = append(out, fromSlogAttr(a)) + } + return slogBridge{attrs: out} +} + +func (b slogBridge) WithGroup(string) slog.Handler { return b } + +func fromSlogAttr(a slog.Attr) Attr { + return Attr{Key: a.Key, Value: a.Value.Resolve().Any()} +} + type slogHandler struct { h slog.Handler } diff --git a/act/runner/runner_test.go b/act/runner/runner_test.go index bab388a0..1d102d48 100644 --- a/act/runner/runner_test.go +++ b/act/runner/runner_test.go @@ -102,7 +102,7 @@ func TestGraphMissingEvent(t *testing.T) { assert.NotNil(t, plan) assert.Empty(t, plan.Stages) - assert.Contains(t, buf.String(), "no events found for workflow: no-event.yml") + assert.Contains(t, buf.String(), `msg="no events found for workflow" file=no-event.yml`) } func TestGraphMissingFirst(t *testing.T) { diff --git a/go.mod b/go.mod index 5de3b2f0..896d8832 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.27.2 require ( connectrpc.com/connect v1.21.0 dario.cat/mergo v1.0.2 - gitea.dev/actionslib v1.4.0 + gitea.dev/actionslib v1.4.1 github.com/avast/retry-go/v5 v5.0.0 github.com/bmatcuk/doublestar/v4 v4.10.2 github.com/coder/websocket v1.8.15 diff --git a/go.sum b/go.sum index 8e818ebb..a0841295 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik= cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -gitea.dev/actionslib v1.4.0 h1:k31smj22tnUZrrM5kkntXN8lKAe7dkoTEoiP1Fk5KJc= -gitea.dev/actionslib v1.4.0/go.mod h1:svCefEsavx4jmn8vJ4wTG7Q0KBcGfrFmGUjrhxuNUAQ= +gitea.dev/actionslib v1.4.1 h1:DWlUnzDfuuMArQWS6yZ+vwJu9av7sZVMOox7li/kyu4= +gitea.dev/actionslib v1.4.1/go.mod h1:RbXaZudZ04YndjBBxZJF9rZAqU6SKGkaSD1sob5lQbE= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=