# HG changeset patch
# Node ID 9d08ca692427
# Parent  2dc09584dc67
diff --git a/config/config.go b/config/config.go
--- a/config/config.go
+++ b/config/config.go
@@ -19,8 +19,7 @@ package config
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/runtime"
 )
@@ -41,7 +40,7 @@ func (c *Config) Valid() error {
 		}
 
 		if err := task.Deprecated(); err != nil {
-			log.Warnf("warning: task %s: %s", name, err.Error())
+			slog.Warn(fmt.Sprintf("warning: task %s: %s", name, err.Error()))
 		}
 	}
 
diff --git a/config/loader.go b/config/loader.go
--- a/config/loader.go
+++ b/config/loader.go
@@ -17,10 +17,9 @@
 package config
 
 import (
+	"log/slog"
 	"os"
 
-	log "github.com/sirupsen/logrus"
-
 	"gopkg.in/yaml.v2"
 )
 
@@ -41,7 +40,7 @@ func LoadFile(filename string) (*Config,
 		// If the user selected the default and it didn't exist, attempt the
 		// default with a .yaml extension.
 		if os.IsNotExist(err) && filename == "convey.yml" {
-			log.Infof("info: configuration file convey.yml not found, attempting convey.yaml instead.")
+			slog.Info("info: configuration file convey.yml not found, attempting convey.yaml instead.")
 			filename = "convey.yaml"
 			_, err = os.Stat(filename)
 			if err != nil {
diff --git a/docker/build.go b/docker/build.go
--- a/docker/build.go
+++ b/docker/build.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -23,7 +22,7 @@ type Build struct {
 	realBuild *podman.Build
 }
 
-func (b *Build) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (b *Build) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	return b.realBuild.Execute(name, logger, stageEnv, rt)
 }
 
diff --git a/docker/environment.go b/docker/environment.go
--- a/docker/environment.go
+++ b/docker/environment.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -22,7 +21,7 @@ func (e *Environment) Valid() error {
 }
 
 // Execute runs the task.
-func (e *Environment) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (e *Environment) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	return nil
 }
 
diff --git a/docker/export.go b/docker/export.go
--- a/docker/export.go
+++ b/docker/export.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -32,7 +31,7 @@ func (e *Export) Valid() error {
 }
 
 // Execute runs the task.
-func (e *Export) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (e *Export) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	return e.realExport.Execute(name, logger, env, rt)
 }
 
diff --git a/docker/import.go b/docker/import.go
--- a/docker/import.go
+++ b/docker/import.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -32,7 +31,7 @@ func (i *Import) Valid() error {
 }
 
 // Execute runs the task.
-func (i *Import) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (i *Import) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	return i.realImport.Execute(name, logger, env, rt)
 }
 
diff --git a/docker/login.go b/docker/login.go
--- a/docker/login.go
+++ b/docker/login.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -18,7 +17,7 @@ type Login struct {
 	realLogin *podman.Login
 }
 
-func (l *Login) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (l *Login) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	return l.realLogin.Execute(name, logger, stageEnv, rt)
 }
 
diff --git a/docker/logout.go b/docker/logout.go
--- a/docker/logout.go
+++ b/docker/logout.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -16,7 +15,7 @@ type Logout struct {
 	realLogout *podman.Logout
 }
 
-func (l *Logout) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (l *Logout) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	return l.realLogout.Execute(name, logger, stageEnv, rt)
 }
 
diff --git a/docker/pull.go b/docker/pull.go
--- a/docker/pull.go
+++ b/docker/pull.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -18,7 +17,7 @@ type Pull struct {
 	realPull *podman.Pull
 }
 
-func (p *Pull) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (p *Pull) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	return p.realPull.Execute(name, logger, stageEnv, rt)
 }
 
diff --git a/docker/push.go b/docker/push.go
--- a/docker/push.go
+++ b/docker/push.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -18,7 +17,7 @@ type Push struct {
 	realPush *podman.Push
 }
 
-func (p *Push) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (p *Push) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	return p.realPush.Execute(name, logger, stageEnv, rt)
 }
 
diff --git a/docker/remove.go b/docker/remove.go
--- a/docker/remove.go
+++ b/docker/remove.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -19,7 +18,7 @@ type Remove struct {
 	realRemove *podman.Remove
 }
 
-func (r *Remove) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (r *Remove) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	return r.realRemove.Execute(name, logger, stageEnv, rt)
 }
 
diff --git a/docker/run.go b/docker/run.go
--- a/docker/run.go
+++ b/docker/run.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -62,7 +61,7 @@ func (r *Run) Valid() error {
 }
 
 // Execute runs the task.
-func (r *Run) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (r *Run) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	return r.realRun.Execute(name, logger, env, rt)
 }
 
diff --git a/docker/tag.go b/docker/tag.go
--- a/docker/tag.go
+++ b/docker/tag.go
@@ -2,8 +2,7 @@ package docker
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/podman"
@@ -39,7 +38,7 @@ func (t *Tag) Valid() error {
 }
 
 // Execute runs the task.
-func (t *Tag) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (t *Tag) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	return t.realTag.Execute(name, logger, env, rt)
 }
 
diff --git a/environment/expand.go b/environment/expand.go
--- a/environment/expand.go
+++ b/environment/expand.go
@@ -1,9 +1,9 @@
 package environment
 
 import (
+	"fmt"
+	"log/slog"
 	"os"
-
-	log "github.com/sirupsen/logrus"
 )
 
 func (e Environment) expandMapper(name string) string {
@@ -40,7 +40,7 @@ func (e Environment) Expand(key string) 
 		last = next
 	}
 
-	log.Warnf("failed to fully expand %q after %d recursions", key, tries)
+	slog.Warn(fmt.Sprintf("failed to fully expand %q after %d recursions", key, tries))
 
 	return ""
 }
diff --git a/exec/collector.go b/exec/collector.go
--- a/exec/collector.go
+++ b/exec/collector.go
@@ -18,17 +18,17 @@ package exec
 
 import (
 	"bufio"
+	"context"
+	"log/slog"
 	"sync"
-
-	log "github.com/sirupsen/logrus"
 )
 
 type collector func(s *bufio.Scanner)
 
-func newLogCollector(adapter *log.Entry, level log.Level) collector {
+func newLogCollector(adapter *slog.Logger, level slog.Level) collector {
 	return func(s *bufio.Scanner) {
 		for s.Scan() {
-			adapter.Log(level, s.Text())
+			adapter.Log(context.TODO(), level, s.Text())
 		}
 	}
 }
diff --git a/exec/exec.go b/exec/exec.go
--- a/exec/exec.go
+++ b/exec/exec.go
@@ -21,12 +21,11 @@ import (
 	"bufio"
 	"fmt"
 	"io"
+	"log/slog"
 	"os/exec"
 	"sync"
 	"time"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/logging"
 )
 
@@ -35,8 +34,8 @@ import (
 func Run(name string, cmdv []string, timeout time.Duration) error {
 	var (
 		logger       = logging.NewAdapter(name)
-		outCollector = newLogCollector(logger, log.InfoLevel)
-		errCollector = newLogCollector(logger, log.ErrorLevel)
+		outCollector = newLogCollector(logger, slog.LevelInfo)
+		errCollector = newLogCollector(logger, slog.LevelError)
 	)
 
 	return run(name, cmdv, nil, timeout, &sync.WaitGroup{}, outCollector, errCollector)
@@ -47,8 +46,8 @@ func Run(name string, cmdv []string, tim
 func RunWithStdin(name string, cmdv []string, stdin io.Reader, timeout time.Duration) error {
 	var (
 		logger       = logging.NewAdapter(name)
-		outCollector = newLogCollector(logger, log.InfoLevel)
-		errCollector = newLogCollector(logger, log.ErrorLevel)
+		outCollector = newLogCollector(logger, slog.LevelInfo)
+		errCollector = newLogCollector(logger, slog.LevelError)
 	)
 
 	return run(name, cmdv, stdin, timeout, &sync.WaitGroup{}, outCollector, errCollector)
@@ -72,7 +71,7 @@ func RunOutput(name string, cmdv []strin
 func run(name string, cmdv []string, stdin io.Reader, timeout time.Duration, wg *sync.WaitGroup, outHandler, errHandler collector) error {
 	logger := logging.NewAdapter(name)
 
-	logger.Debugf("running command \"%v\"", cmdv)
+	logger.Debug(fmt.Sprintf("running command \"%v\"", cmdv))
 
 	cmd := exec.Command(cmdv[0], cmdv[1:]...) // nolint: gas
 
@@ -124,7 +123,7 @@ func run(name string, cmdv []string, std
 			}
 
 			// log that the process timed out
-			logger.Errorf("command %v timed out after %v", cmdv, timeout)
+			logger.Error(fmt.Sprintf("command %v timed out after %v", cmdv, timeout))
 		})
 	}
 
diff --git a/fs/directory.go b/fs/directory.go
--- a/fs/directory.go
+++ b/fs/directory.go
@@ -20,11 +20,11 @@
 package fs
 
 import (
+	"fmt"
+	"log/slog"
 	"os"
 	"path/filepath"
 	"strings"
-
-	log "github.com/sirupsen/logrus"
 )
 
 type Directory struct {
@@ -57,12 +57,12 @@ func (d *Directory) Import(src, dst stri
 	realDst := filepath.Join(d.path, dst)
 
 	if file.IsDir() {
-		log.Debugf("importing directory %q to %q", src, realDst)
+		slog.Debug(fmt.Sprintf("importing directory %q to %q", src, realDst))
 
 		return copyDir(src, realDst, file.Mode())
 	}
 
-	log.Debugf("importing file %q to %q", src, realDst)
+	slog.Debug(fmt.Sprintf("importing file %q to %q", src, realDst))
 
 	return copyFile(src, realDst, file.Mode())
 }
@@ -76,12 +76,12 @@ func (d *Directory) Export(src, dst stri
 	}
 
 	if file.IsDir() {
-		log.Debugf("exporting directory %q to %q", realSrc, dst)
+		slog.Debug(fmt.Sprintf("exporting directory %q to %q", realSrc, dst))
 
 		return copyDir(realSrc, dst, file.Mode())
 	}
 
-	log.Debugf("exporting file %q to %q", realSrc, dst)
+	slog.Debug(fmt.Sprintf("exporting file %q to %q", realSrc, dst))
 
 	return copyFile(realSrc, dst, file.Mode())
 }
diff --git a/logging/adapter.go b/logging/adapter.go
--- a/logging/adapter.go
+++ b/logging/adapter.go
@@ -17,16 +17,16 @@
 package logging
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/color"
 )
 
 // NewAdapter creates a new logger with attributes for the identifier
-// as well has a color for it.
-func NewAdapter(identifier string) *log.Entry {
-	return log.WithFields(log.Fields{
-		"id":      identifier,
-		"idColor": color.Ansi(identifier),
-	})
+// as well as a color for it.
+func NewAdapter(identifier string) *slog.Logger {
+	return slog.With(
+		"id", identifier,
+		"idColor", color.Ansi(identifier),
+	)
 }
diff --git a/logging/formatter.go b/logging/handler.go
rename from logging/formatter.go
rename to logging/handler.go
--- a/logging/formatter.go
+++ b/logging/handler.go
@@ -18,86 +18,132 @@ package logging
 
 import (
 	"bytes"
+	"context"
 	"fmt"
+	"log/slog"
 	"os"
+	"sync"
 	"time"
 
 	"github.com/mgutz/ansi"
-	log "github.com/sirupsen/logrus"
 )
 
 var (
 	baseTime time.Time = time.Now()
 
-	levelColors map[log.Level]string = map[log.Level]string{
-		log.PanicLevel: ansi.ColorCode("red+h"),
-		log.FatalLevel: ansi.ColorCode("red+b"),
-		log.ErrorLevel: ansi.ColorCode("red"),
-		log.WarnLevel:  ansi.ColorCode("yellow"),
-		log.InfoLevel:  ansi.ColorCode("green"),
-		log.DebugLevel: ansi.ColorCode("cyan"),
-		log.TraceLevel: ansi.ColorCode("white+h"),
+	levelColors map[slog.Level]string = map[slog.Level]string{
+		slog.LevelError: ansi.ColorCode("red"),
+		slog.LevelWarn:  ansi.ColorCode("yellow"),
+		slog.LevelInfo:  ansi.ColorCode("green"),
+		slog.LevelDebug: ansi.ColorCode("cyan"),
 	}
 )
 
-type Formatter struct {
-	disableColors bool
+type Handler struct {
+	level                slog.Level
+	disableColors        bool
+	mtx                  *sync.Mutex
+	recordId             string
+	recordColor          string
+	ignoreRemainingAttrs bool
 }
 
-func miniTimestamp() int {
-	return int(time.Since(baseTime) / time.Second)
-}
-
-func NewFormatter(color bool) log.Formatter {
+func NewHandler(color bool, level slog.Level) slog.Handler {
 	disableColors := false
 
 	if color {
 		switch os.Getenv("TERM") {
-		case "win":
-			fallthrough
-		case "dumb":
+		case "win", "dumb":
 			disableColors = true
 		}
 	} else {
 		disableColors = true
 	}
 
-	return &Formatter{
+	return &Handler{
 		disableColors: disableColors,
+		level:         level,
+		mtx:           &sync.Mutex{},
 	}
 }
 
-func (f *Formatter) Format(entry *log.Entry) ([]byte, error) {
-	var b *bytes.Buffer
+func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool {
+	return level >= h.level
+}
 
-	if entry.Buffer != nil {
-		b = entry.Buffer
+func (h *Handler) Handle(ctx context.Context, record slog.Record) error {
+	b := &bytes.Buffer{}
+
+	var miniTimestamp int
+	if record.Time.IsZero() {
+		miniTimestamp = int(time.Since(baseTime) / time.Second)
 	} else {
-		b = &bytes.Buffer{}
+		miniTimestamp = int(record.Time.Sub(baseTime) / time.Second)
 	}
-
-	fmt.Fprintf(b, "[%04d] ", miniTimestamp())
+	fmt.Fprintf(b, "[%04d] ", miniTimestamp)
 
-	id := entry.Data["id"]
-	if id != nil {
-		idColor := entry.Data["idColor"]
-
-		if f.disableColors && idColor != nil {
+	// Find some attributes from handler context or the record.
+	id := h.recordId
+	idColor := h.recordColor
+	if !h.ignoreRemainingAttrs {
+		record.Attrs(func(a slog.Attr) bool {
+			switch a.Key {
+			case "id":
+				id = a.Value.String()
+			case "idColor":
+				idColor = a.Value.String()
+			}
+			return true
+		})
+	}
+	if id != "" {
+		if h.disableColors && idColor != "" {
 			fmt.Fprintf(b, "%s: ", id)
 		} else {
 			fmt.Fprintf(b, "%s%s%s: ", idColor, id, ansi.Reset)
 		}
 	}
 
-	if entry.Message != "" {
-		if f.disableColors {
-			fmt.Fprintf(b, "%s", entry.Message)
+	if record.Message != "" {
+		if h.disableColors {
+			fmt.Fprintf(b, "%s", record.Message)
 		} else {
-			fmt.Fprintf(b, "%s%s%s", levelColors[entry.Level], entry.Message, ansi.Reset)
+			fmt.Fprintf(b, "%s%s%s", levelColors[record.Level], record.Message, ansi.Reset)
 		}
 	}
 
 	b.WriteByte('\n')
 
-	return b.Bytes(), nil
+	h.mtx.Lock()
+	defer h.mtx.Unlock()
+
+	_, err := os.Stdout.Write(b.Bytes())
+	return err
 }
+
+func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler {
+	if len(attrs) == 0 {
+		return h
+	}
+	h2 := *h
+	// We only care about a few attributes for now.
+	for _, a := range attrs {
+		switch a.Key {
+		case "id":
+			h2.recordId = a.Value.String()
+		case "idColor":
+			h2.recordColor = a.Value.String()
+		}
+	}
+	return &h2
+}
+
+func (h *Handler) WithGroup(name string) slog.Handler {
+	if name == "" {
+		return h
+	}
+	h2 := *h
+	// Once a group is added, the attributes become group.attr, so we want to ignore the rest.
+	h2.ignoreRemainingAttrs = true
+	return &h2
+}
diff --git a/logging/logging.go b/logging/logging.go
--- a/logging/logging.go
+++ b/logging/logging.go
@@ -18,20 +18,18 @@
 package logging
 
 import (
-	"os"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 )
 
 // Setup sets up the logging system based on the input parameters
 func Setup(color, verbose bool) {
-	log.SetOutput(os.Stdout)
-
+	var level slog.Level
 	if verbose {
-		log.SetLevel(log.DebugLevel)
+		level = slog.LevelDebug
 	} else {
-		log.SetLevel(log.InfoLevel)
+		level = slog.LevelInfo
 	}
 
-	log.SetFormatter(NewFormatter(color))
+	handler := NewHandler(color, level)
+	slog.SetDefault(slog.New(handler))
 }
diff --git a/podman/build.go b/podman/build.go
--- a/podman/build.go
+++ b/podman/build.go
@@ -1,11 +1,11 @@
 package podman
 
 import (
+	"fmt"
+	"log/slog"
 	"path/filepath"
 	"strings"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -20,7 +20,7 @@ type Build struct {
 	Target        string             `yaml:"target"`
 }
 
-func (b *Build) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (b *Build) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	ws, err := rt.Workspace()
@@ -32,8 +32,8 @@ func (b *Build) Execute(name string, log
 	if !strings.HasSuffix(context, string(filepath.Separator)) {
 		context += string(filepath.Separator)
 	}
-	log.Infof("resolved context: %q", context)
-	log.Infof("wspath: %q", ws.Path())
+	slog.Info(fmt.Sprintf("resolved context: %q", context))
+	slog.Info(fmt.Sprintf("wspath: %q", ws.Path()))
 	if !strings.HasPrefix(context, ws.Path()) {
 		return ErrContextOutsideOfWorkspace
 	}
diff --git a/podman/login.go b/podman/login.go
--- a/podman/login.go
+++ b/podman/login.go
@@ -1,10 +1,9 @@
 package podman
 
 import (
+	"log/slog"
 	"strings"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -16,7 +15,7 @@ type Login struct {
 	Password string `yaml:"password"`
 }
 
-func (l *Login) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (l *Login) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	registry := env.Expand(l.Registry)
diff --git a/podman/logout.go b/podman/logout.go
--- a/podman/logout.go
+++ b/podman/logout.go
@@ -1,7 +1,7 @@
 package podman
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
@@ -12,7 +12,7 @@ type Logout struct {
 	Registry string `yaml:"registry"`
 }
 
-func (l *Logout) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (l *Logout) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	registry := env.Expand(l.Registry)
diff --git a/podman/pull.go b/podman/pull.go
--- a/podman/pull.go
+++ b/podman/pull.go
@@ -1,7 +1,7 @@
 package podman
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
@@ -13,7 +13,7 @@ type Pull struct {
 	Tags yaml.StringOrSlice `yaml:"tags"`
 }
 
-func (p *Pull) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (p *Pull) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	tags := env.Expandv(p.Tags)
diff --git a/podman/push.go b/podman/push.go
--- a/podman/push.go
+++ b/podman/push.go
@@ -1,7 +1,7 @@
 package podman
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
@@ -13,7 +13,7 @@ type Push struct {
 	Tags yaml.StringOrSlice `yaml:"tags"`
 }
 
-func (p *Push) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (p *Push) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	tags := env.Expandv(p.Tags)
diff --git a/podman/remove.go b/podman/remove.go
--- a/podman/remove.go
+++ b/podman/remove.go
@@ -2,10 +2,9 @@ package podman
 
 import (
 	"fmt"
+	"log/slog"
 	"strings"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -17,7 +16,7 @@ type Remove struct {
 	Quiet bool               `yaml:"quiet"`
 }
 
-func (r *Remove) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (r *Remove) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	tags := env.Expandv(r.Tags)
diff --git a/podman/run.go b/podman/run.go
--- a/podman/run.go
+++ b/podman/run.go
@@ -1,13 +1,13 @@
 package podman
 
 import (
+	"log/slog"
 	"os"
 	"path/filepath"
 	"strings"
 
 	"github.com/kballard/go-shellquote"
 	"github.com/opencontainers/selinux/go-selinux"
-	log "github.com/sirupsen/logrus"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
@@ -54,7 +54,7 @@ func (r *Run) buildScript(cfgPath string
 	return fp.Name(), nil
 }
 
-func (r *Run) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (r *Run) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	// Create a new environment based on the stage's environment. Then merge
 	// the task's environment overriding anything from the stage. Finally merge
 	// the runtime environment which holds the environment from the command line.
diff --git a/podman/tag.go b/podman/tag.go
--- a/podman/tag.go
+++ b/podman/tag.go
@@ -1,7 +1,7 @@
 package podman
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
@@ -14,7 +14,7 @@ type Tag struct {
 	Targets yaml.StringOrSlice `yaml:"targets"`
 }
 
-func (t *Tag) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (t *Tag) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	env := stageEnv.Copy().Merge(rt.Environment)
 
 	generator := exec.NewGenerator(
diff --git a/runner/cmd.go b/runner/cmd.go
--- a/runner/cmd.go
+++ b/runner/cmd.go
@@ -19,14 +19,13 @@ package runner
 import (
 	"context"
 	"fmt"
+	"log/slog"
 	"os"
 	"os/signal"
 	"path/filepath"
 	"syscall"
 	"time"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/config"
 	"keep.imfreedom.org/grim/convey/globals"
 	"keep.imfreedom.org/grim/convey/logging"
@@ -105,7 +104,7 @@ func (c *RunnerCmd) Run(g *globals.Globa
 		case err := <-errChan:
 			return err
 		case s := <-signalChan:
-			log.Infof("caught signal %s, exiting...", s)
+			slog.Info(fmt.Sprintf("caught signal %s, exiting...", s))
 			cancel()
 			break
 		}
diff --git a/runtime/count_task.go b/runtime/count_task.go
--- a/runtime/count_task.go
+++ b/runtime/count_task.go
@@ -17,7 +17,7 @@
 package runtime
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 )
@@ -28,7 +28,7 @@ type Count struct {
 }
 
 // Execute runs the count task.
-func (c *Count) Execute(name string, logger *log.Entry, env environment.Environment, rt *Runtime) error {
+func (c *Count) Execute(name string, logger *slog.Logger, env environment.Environment, rt *Runtime) error {
 	c.Count++
 
 	return nil
diff --git a/runtime/fail.go b/runtime/fail.go
--- a/runtime/fail.go
+++ b/runtime/fail.go
@@ -18,8 +18,7 @@ package runtime
 
 import (
 	"fmt"
-
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 )
@@ -28,7 +27,7 @@ import (
 type Fail struct{}
 
 // Execute runs the fail task.
-func (f *Fail) Execute(name string, logger *log.Entry, env environment.Environment, rt *Runtime) error {
+func (f *Fail) Execute(name string, logger *slog.Logger, env environment.Environment, rt *Runtime) error {
 	return fmt.Errorf("convey/fail task always fails")
 }
 
diff --git a/runtime/plans.go b/runtime/plans.go
--- a/runtime/plans.go
+++ b/runtime/plans.go
@@ -20,10 +20,9 @@ package runtime
 import (
 	"errors"
 	"fmt"
+	"log/slog"
 	"time"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/logging"
 	"keep.imfreedom.org/grim/convey/yaml"
 )
@@ -38,7 +37,7 @@ type Plan struct {
 	Environment yaml.StringOrSlice `yaml:"environment"`
 	Stages      []Stage            `yaml:"stages"`
 
-	logger *log.Entry
+	logger *slog.Logger
 }
 
 // Execute runs the plan.
@@ -73,7 +72,7 @@ func (p *Plan) Execute(path string, rt *
 
 			elapsed := time.Since(start)
 			rt.Timeout -= elapsed
-			p.logger.Debugf("remaining plantime: %s", rt.Timeout)
+			p.logger.Debug(fmt.Sprintf("remaining plantime: %s", rt.Timeout))
 
 			if err != nil {
 				stageLogger.Error("stage failed")
@@ -83,7 +82,7 @@ func (p *Plan) Execute(path string, rt *
 				continue
 			}
 
-			stageLogger.Infof("stage finished [%s]", elapsed.Round(10*time.Millisecond))
+			stageLogger.Info(fmt.Sprintf("stage finished [%s]", elapsed.Round(10*time.Millisecond)))
 		}
 	}
 
diff --git a/runtime/runtime.go b/runtime/runtime.go
--- a/runtime/runtime.go
+++ b/runtime/runtime.go
@@ -17,11 +17,11 @@
 package runtime
 
 import (
+	"fmt"
+	"log/slog"
 	"os"
 	"time"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/fs"
 )
@@ -81,7 +81,7 @@ func (rt *Runtime) Workspace() (*fs.Dire
 
 		rt.workspace = dir
 
-		log.Debugf("created workspace %q", path)
+		slog.Debug(fmt.Sprintf("created workspace %q", path))
 	}
 
 	return rt.workspace, nil
@@ -92,16 +92,16 @@ func (rt *Runtime) Shutdown() {
 		if !rt.KeepWorkspace {
 			err := rt.workspace.Destroy()
 			if err != nil {
-				log.Errorf(
+				slog.Error(fmt.Sprintf(
 					"failed to remove workspace %q: %s",
 					rt.workspace.Path(),
 					err,
-				)
+				))
 			} else {
-				log.Debugf("removed workspace %q", rt.workspace.Path())
+				slog.Debug(fmt.Sprintf("removed workspace %q", rt.workspace.Path()))
 			}
 		} else {
-			log.Infof("keeping workspace %q as requested", rt.workspace.Path())
+			slog.Info(fmt.Sprintf("keeping workspace %q as requested", rt.workspace.Path()))
 		}
 	}
 }
diff --git a/runtime/stages.go b/runtime/stages.go
--- a/runtime/stages.go
+++ b/runtime/stages.go
@@ -19,10 +19,9 @@ package runtime
 
 import (
 	"fmt"
+	"log/slog"
 	"sync"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/logging"
 	"keep.imfreedom.org/grim/convey/yaml"
@@ -70,7 +69,7 @@ func (s *Stage) UnmarshalYAML(unmarshal 
 }
 
 // Execute runs the stage.
-func (s *Stage) Execute(path string, logger *log.Entry, env environment.Environment, rt *Runtime) error {
+func (s *Stage) Execute(path string, logger *slog.Logger, env environment.Environment, rt *Runtime) error {
 	stageEnv := env.Copy().Merge(env).MergeSlice(s.Environment)
 
 	if s.Concurrent && !rt.ForceSequential {
@@ -138,7 +137,7 @@ func (s *Stage) runTask(path, name strin
 
 	err := task.Execute(absTaskName, taskLogger, stageEnv, rt)
 	if err != nil {
-		taskLogger.Errorf("failed, %s", err)
+		taskLogger.Error(fmt.Sprintf("failed, %s", err))
 
 		return err
 	}
diff --git a/runtime/task.go b/runtime/task.go
--- a/runtime/task.go
+++ b/runtime/task.go
@@ -16,14 +16,14 @@
 package runtime
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 )
 
 // Task is an interface for defining a task that can be run.
 type Task interface {
-	Execute(name string, logger *log.Entry, env environment.Environment, rt *Runtime) error
+	Execute(name string, logger *slog.Logger, env environment.Environment, rt *Runtime) error
 	New() Task
 	Valid() error
 	Deprecated() error
diff --git a/script/shell.go b/script/shell.go
--- a/script/shell.go
+++ b/script/shell.go
@@ -1,13 +1,12 @@
 package script
 
 import (
+	"log/slog"
 	"os"
 	"path/filepath"
 	gort "runtime"
 	"strings"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/exec"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -21,7 +20,7 @@ type Shell struct {
 	Shell       string             `yaml:"shell"`
 }
 
-func (s *Shell) Execute(name string, logger *log.Entry, stageEnv environment.Environment, rt *runtime.Runtime) error {
+func (s *Shell) Execute(name string, logger *slog.Logger, stageEnv environment.Environment, rt *runtime.Runtime) error {
 	// Create a new environment based on the stage's environment. Then merge
 	// the task's environment overriding anything from the stage. Finally merge
 	// the runtime environment which holds the environment from the command line.
diff --git a/tasks/clean.go b/tasks/clean.go
--- a/tasks/clean.go
+++ b/tasks/clean.go
@@ -18,12 +18,12 @@ package tasks
 
 import (
 	"fmt"
+	"log/slog"
 	"os"
 	"path/filepath"
 	"strings"
 
 	"github.com/mattn/go-zglob"
-	log "github.com/sirupsen/logrus"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -54,7 +54,7 @@ func sanitizeFile(base, file string, env
 }
 
 // Execute runs the clean task.
-func (c *Clean) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (c *Clean) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	fullEnv := environment.New()
 	fullEnv.Merge(env)
 	fullEnv.Merge(rt.Environment)
@@ -82,7 +82,7 @@ func (c *Clean) Execute(name string, log
 				return err
 			}
 
-			logger.Debugf("removing \"%v\", expanded from \"%v\"", sanitized, pattern)
+			logger.Debug(fmt.Sprintf("removing \"%v\", expanded from \"%v\"", sanitized, pattern))
 
 			if err := os.RemoveAll(sanitized); err != nil {
 				return err
diff --git a/tasks/export.go b/tasks/export.go
--- a/tasks/export.go
+++ b/tasks/export.go
@@ -17,11 +17,10 @@
 package tasks
 
 import (
+	"log/slog"
 	"path/filepath"
 	"strings"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/fs"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -47,7 +46,7 @@ func (e *Export) Valid() error {
 }
 
 // Execute runs the task.
-func (e *Export) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (e *Export) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	fullEnv := env.Copy().Merge(rt.Environment)
 
 	workspace, err := rt.Workspace()
diff --git a/tasks/import.go b/tasks/import.go
--- a/tasks/import.go
+++ b/tasks/import.go
@@ -18,10 +18,9 @@ package tasks
 
 import (
 	"fmt"
+	"log/slog"
 	"path/filepath"
 
-	log "github.com/sirupsen/logrus"
-
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/fs"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -48,7 +47,7 @@ func (i *Import) Valid() error {
 }
 
 // Execute runs the task.
-func (i *Import) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (i *Import) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	fullEnv := env.Copy().Merge(rt.Environment)
 
 	for _, pattern := range i.Files {
diff --git a/tasks/noop.go b/tasks/noop.go
--- a/tasks/noop.go
+++ b/tasks/noop.go
@@ -17,7 +17,7 @@
 package tasks
 
 import (
-	log "github.com/sirupsen/logrus"
+	"log/slog"
 
 	"keep.imfreedom.org/grim/convey/environment"
 	"keep.imfreedom.org/grim/convey/runtime"
@@ -27,7 +27,7 @@ import (
 type Noop struct{}
 
 // Execute runs the noop task.
-func (n *Noop) Execute(name string, logger *log.Entry, env environment.Environment, rt *runtime.Runtime) error {
+func (n *Noop) Execute(name string, logger *slog.Logger, env environment.Environment, rt *runtime.Runtime) error {
 	return nil
 }
 
