diff --git a/cmd/dex/config.go b/cmd/dex/config.go
index 33c0db4782abe882895facaf08552b46ccb29513..b6d34f37e2fa7728536734afe26d028c156375fc 100644
--- a/cmd/dex/config.go
+++ b/cmd/dex/config.go
@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"os"
 	"strings"
@@ -11,7 +12,6 @@ import (
 	"golang.org/x/crypto/bcrypt"
 
 	"github.com/dexidp/dex/pkg/featureflags"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent"
@@ -236,7 +236,7 @@ type Storage struct {
 
 // StorageConfig is a configuration that can create a storage.
 type StorageConfig interface {
-	Open(logger log.Logger) (storage.Storage, error)
+	Open(logger *slog.Logger) (storage.Storage, error)
 }
 
 var (
@@ -386,7 +386,7 @@ type Expiry struct {
 // Logger holds configuration required to customize logging for dex.
 type Logger struct {
 	// Level sets logging level severity.
-	Level string `json:"level"`
+	Level slog.Level `json:"level"`
 
 	// Format specifies the format to be used for logging.
 	Format string `json:"format"`
diff --git a/cmd/dex/config_test.go b/cmd/dex/config_test.go
index e3169650392acbb7191315bc0c77db0b31808062..95a79eb27f606813f87e859f50e7c8363c319f9a 100644
--- a/cmd/dex/config_test.go
+++ b/cmd/dex/config_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"log/slog"
 	"os"
 	"testing"
 
@@ -219,7 +220,7 @@ logger:
 			DeviceRequests: "10m",
 		},
 		Logger: Logger{
-			Level:  "debug",
+			Level:  slog.LevelDebug,
 			Format: "json",
 		},
 	}
@@ -426,7 +427,7 @@ logger:
 			AuthRequests: "25h",
 		},
 		Logger: Logger{
-			Level:  "debug",
+			Level:  slog.LevelDebug,
 			Format: "json",
 		},
 	}
diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go
index 9461a6220aeadf289fb7095447b7aec9fad51d03..b39354ec0b5049d96cea7935dfb1311f5aded623 100644
--- a/cmd/dex/serve.go
+++ b/cmd/dex/serve.go
@@ -6,6 +6,7 @@ import (
 	"crypto/x509"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net"
 	"net/http"
 	"net/http/pprof"
@@ -28,14 +29,12 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/collectors"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
-	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials"
 	"google.golang.org/grpc/reflection"
 
 	"github.com/dexidp/dex/api/v2"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server"
 	"github.com/dexidp/dex/storage"
 )
@@ -98,22 +97,24 @@ func runServe(options serveOptions) error {
 		return fmt.Errorf("invalid config: %v", err)
 	}
 
-	logger.Infof(
-		"Dex Version: %s, Go Version: %s, Go OS/ARCH: %s %s",
-		version,
-		runtime.Version(),
-		runtime.GOOS,
-		runtime.GOARCH,
+	logger.Info(
+		"Version info",
+		"dex_version", version,
+		slog.Group("go",
+			"version", runtime.Version(),
+			"os", runtime.GOOS,
+			"arch", runtime.GOARCH,
+		),
 	)
 
-	if c.Logger.Level != "" {
-		logger.Infof("config using log level: %s", c.Logger.Level)
+	if c.Logger.Level != slog.LevelInfo {
+		logger.Info("config using log level", "level", c.Logger.Level)
 	}
 	if err := c.Validate(); err != nil {
 		return err
 	}
 
-	logger.Infof("config issuer: %s", c.Issuer)
+	logger.Info("config issuer", "issuer", c.Issuer)
 
 	prometheusRegistry := prometheus.NewRegistry()
 	err = prometheusRegistry.Register(collectors.NewGoCollector())
@@ -188,7 +189,7 @@ func runServe(options serveOptions) error {
 	}
 	defer s.Close()
 
-	logger.Infof("config storage: %s", c.Storage.Type)
+	logger.Info("config storage", "storage_type", c.Storage.Type)
 
 	if len(c.StaticClients) > 0 {
 		for i, client := range c.StaticClients {
@@ -213,7 +214,7 @@ func runServe(options serveOptions) error {
 				}
 				c.StaticClients[i].Secret = os.Getenv(client.SecretEnv)
 			}
-			logger.Infof("config static client: %s", client.Name)
+			logger.Info("config static client", "client_name", client.Name)
 		}
 		s = storage.WithStaticClients(s, c.StaticClients)
 	}
@@ -233,7 +234,7 @@ func runServe(options serveOptions) error {
 		if c.Config == nil {
 			return fmt.Errorf("invalid config: no config field for connector %q", c.ID)
 		}
-		logger.Infof("config connector: %s", c.ID)
+		logger.Info("config connector", "connector_id", c.ID)
 
 		// convert to a storage connector object
 		conn, err := ToStorageConnector(c)
@@ -249,22 +250,22 @@ func runServe(options serveOptions) error {
 			Name: "Email",
 			Type: server.LocalConnector,
 		})
-		logger.Infof("config connector: local passwords enabled")
+		logger.Info("config connector: local passwords enabled")
 	}
 
 	s = storage.WithStaticConnectors(s, storageConnectors)
 
 	if len(c.OAuth2.ResponseTypes) > 0 {
-		logger.Infof("config response types accepted: %s", c.OAuth2.ResponseTypes)
+		logger.Info("config response types accepted", "response_types", c.OAuth2.ResponseTypes)
 	}
 	if c.OAuth2.SkipApprovalScreen {
-		logger.Infof("config skipping approval screen")
+		logger.Info("config skipping approval screen")
 	}
 	if c.OAuth2.PasswordConnector != "" {
-		logger.Infof("config using password grant connector: %s", c.OAuth2.PasswordConnector)
+		logger.Info("config using password grant connector", "password_connector", c.OAuth2.PasswordConnector)
 	}
 	if len(c.Web.AllowedOrigins) > 0 {
-		logger.Infof("config allowed origins: %s", c.Web.AllowedOrigins)
+		logger.Info("config allowed origins", "origins", c.Web.AllowedOrigins)
 	}
 
 	// explicitly convert to UTC.
@@ -294,7 +295,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for signing keys expiry: %v", c.Expiry.SigningKeys, err)
 		}
-		logger.Infof("config signing keys expire after: %v", signingKeys)
+		logger.Info("config signing keys", "expire_after", signingKeys)
 		serverConfig.RotateKeysAfter = signingKeys
 	}
 	if c.Expiry.IDTokens != "" {
@@ -302,7 +303,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for id token expiry: %v", c.Expiry.IDTokens, err)
 		}
-		logger.Infof("config id tokens valid for: %v", idTokens)
+		logger.Info("config id tokens", "valid_for", idTokens)
 		serverConfig.IDTokensValidFor = idTokens
 	}
 	if c.Expiry.AuthRequests != "" {
@@ -310,7 +311,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for auth request expiry: %v", c.Expiry.AuthRequests, err)
 		}
-		logger.Infof("config auth requests valid for: %v", authRequests)
+		logger.Info("config auth requests", "valid_for", authRequests)
 		serverConfig.AuthRequestsValidFor = authRequests
 	}
 	if c.Expiry.DeviceRequests != "" {
@@ -318,7 +319,7 @@ func runServe(options serveOptions) error {
 		if err != nil {
 			return fmt.Errorf("invalid config value %q for device request expiry: %v", c.Expiry.AuthRequests, err)
 		}
-		logger.Infof("config device requests valid for: %v", deviceRequests)
+		logger.Info("config device requests", "valid_for", deviceRequests)
 		serverConfig.DeviceRequestsValidFor = deviceRequests
 	}
 	refreshTokenPolicy, err := server.NewRefreshTokenPolicy(
@@ -368,7 +369,7 @@ func runServe(options serveOptions) error {
 	if c.Telemetry.HTTP != "" {
 		const name = "telemetry"
 
-		logger.Infof("listening (%s) on %s", name, c.Telemetry.HTTP)
+		logger.Info("listening on", "server", name, "address", c.Telemetry.HTTP)
 
 		l, err := net.Listen("tcp", c.Telemetry.HTTP)
 		if err != nil {
@@ -390,9 +391,9 @@ func runServe(options serveOptions) error {
 			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 			defer cancel()
 
-			logger.Debugf("starting graceful shutdown (%s)", name)
+			logger.Debug("starting graceful shutdown", "server", name)
 			if err := server.Shutdown(ctx); err != nil {
-				logger.Errorf("graceful shutdown (%s): %v", name, err)
+				logger.Error("graceful shutdown", "server", name, "err", err)
 			}
 		})
 	}
@@ -401,7 +402,7 @@ func runServe(options serveOptions) error {
 	if c.Web.HTTP != "" {
 		const name = "http"
 
-		logger.Infof("listening (%s) on %s", name, c.Web.HTTP)
+		logger.Info("listening on", "server", name, "address", c.Web.HTTP)
 
 		l, err := net.Listen("tcp", c.Web.HTTP)
 		if err != nil {
@@ -419,9 +420,9 @@ func runServe(options serveOptions) error {
 			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 			defer cancel()
 
-			logger.Debugf("starting graceful shutdown (%s)", name)
+			logger.Debug("starting graceful shutdown", "server", name)
 			if err := server.Shutdown(ctx); err != nil {
-				logger.Errorf("graceful shutdown (%s): %v", name, err)
+				logger.Error("graceful shutdown", "server", name, "err", err)
 			}
 		})
 	}
@@ -430,7 +431,7 @@ func runServe(options serveOptions) error {
 	if c.Web.HTTPS != "" {
 		const name = "https"
 
-		logger.Infof("listening (%s) on %s", name, c.Web.HTTPS)
+		logger.Info("listening on", "server", name, "address", c.Web.HTTPS)
 
 		l, err := net.Listen("tcp", c.Web.HTTPS)
 		if err != nil {
@@ -470,16 +471,16 @@ func runServe(options serveOptions) error {
 			ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 			defer cancel()
 
-			logger.Debugf("starting graceful shutdown (%s)", name)
+			logger.Debug("starting graceful shutdown", "server", name)
 			if err := server.Shutdown(ctx); err != nil {
-				logger.Errorf("graceful shutdown (%s): %v", name, err)
+				logger.Error("graceful shutdown", "server", name, "err", err)
 			}
 		})
 	}
 
 	// Set up grpc server
 	if c.GRPC.Addr != "" {
-		logger.Infof("listening (grpc) on %s", c.GRPC.Addr)
+		logger.Info("listening on", "server", "grpc", "address", c.GRPC.Addr)
 
 		grpcListener, err := net.Listen("tcp", c.GRPC.Addr)
 		if err != nil {
@@ -498,7 +499,7 @@ func runServe(options serveOptions) error {
 		group.Add(func() error {
 			return grpcSrv.Serve(grpcListener)
 		}, func(err error) {
-			logger.Debugf("starting graceful shutdown (grpc)")
+			logger.Debug("starting graceful shutdown", "server", "grpc")
 			grpcSrv.GracefulStop()
 		})
 	}
@@ -508,53 +509,29 @@ func runServe(options serveOptions) error {
 		if _, ok := err.(run.SignalError); !ok {
 			return fmt.Errorf("run groups: %w", err)
 		}
-		logger.Infof("%v, shutdown now", err)
+		logger.Info("shutdown now", "err", err)
 	}
 	return nil
 }
 
-var (
-	logLevels  = []string{"debug", "info", "error"}
-	logFormats = []string{"json", "text"}
-)
-
-type utcFormatter struct {
-	f logrus.Formatter
-}
-
-func (f *utcFormatter) Format(e *logrus.Entry) ([]byte, error) {
-	e.Time = e.Time.UTC()
-	return f.f.Format(e)
-}
+var logFormats = []string{"json", "text"}
 
-func newLogger(level string, format string) (log.Logger, error) {
-	var logLevel logrus.Level
-	switch strings.ToLower(level) {
-	case "debug":
-		logLevel = logrus.DebugLevel
-	case "", "info":
-		logLevel = logrus.InfoLevel
-	case "error":
-		logLevel = logrus.ErrorLevel
-	default:
-		return nil, fmt.Errorf("log level is not one of the supported values (%s): %s", strings.Join(logLevels, ", "), level)
-	}
-
-	var formatter utcFormatter
+func newLogger(level slog.Level, format string) (*slog.Logger, error) {
+	var handler slog.Handler
 	switch strings.ToLower(format) {
 	case "", "text":
-		formatter.f = &logrus.TextFormatter{DisableColors: true}
+		slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
+			Level: level,
+		})
 	case "json":
-		formatter.f = &logrus.JSONFormatter{}
+		slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
+			Level: level,
+		})
 	default:
 		return nil, fmt.Errorf("log format is not one of the supported values (%s): %s", strings.Join(logFormats, ", "), format)
 	}
 
-	return &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &formatter,
-		Level:     logLevel,
-	}, nil
+	return slog.New(handler), nil
 }
 
 func applyConfigOverrides(options serveOptions, config *Config) {
@@ -600,7 +577,7 @@ func pprofHandler(router *http.ServeMux) {
 
 // newTLSReloader returns a [tls.Config] with GetCertificate or GetConfigForClient set
 // to reload certificates from the given paths on SIGHUP or on file creates (atomic update via rename).
-func newTLSReloader(logger log.Logger, certFile, keyFile, caFile string, baseConfig *tls.Config) (*tls.Config, error) {
+func newTLSReloader(logger *slog.Logger, certFile, keyFile, caFile string, baseConfig *tls.Config) (*tls.Config, error) {
 	// trigger reload on channel
 	sigc := make(chan os.Signal, 1)
 	signal.Notify(sigc, syscall.SIGHUP)
@@ -631,7 +608,7 @@ func newTLSReloader(logger log.Logger, certFile, keyFile, caFile string, baseCon
 	// recommended by fsnotify: watch the dir to handle renames
 	// https://pkg.go.dev/github.com/fsnotify/fsnotify#hdr-Watching_files
 	for dir := range watchDirs {
-		logger.Debugf("watching dir: %v", dir)
+		logger.Debug("watching dir", "dir", dir)
 		err := watcher.Add(dir)
 		if err != nil {
 			return nil, fmt.Errorf("watch dir for TLS reloader: %v", err)
@@ -654,19 +631,19 @@ func newTLSReloader(logger log.Logger, certFile, keyFile, caFile string, baseCon
 		for {
 			select {
 			case sig := <-sigc:
-				logger.Debug("reloading cert from signal: %v", sig)
+				logger.Debug("reloading cert from signal", "signal", sig)
 			case evt := <-watcher.Events:
 				if _, ok := watchFiles[evt.Name]; !ok || !evt.Has(fsnotify.Create) {
 					continue loop
 				}
-				logger.Debug("reloading cert from fsnotify: %v %v", evt.Name, evt.Op.String())
+				logger.Debug("reloading cert from fsnotify", "event", evt.Name, "operation", evt.Op.String())
 			case err := <-watcher.Errors:
-				logger.Errorf("TLS reloader watch: %v", err)
+				logger.Error("TLS reloader watch", "err", err)
 			}
 
 			loaded, err := loadTLSConfig(certFile, keyFile, caFile, baseConfig)
 			if err != nil {
-				logger.Errorf("reload TLS config: %v", err)
+				logger.Error("reload TLS config", "err", err)
 			}
 			ptr.Store(loaded)
 		}
diff --git a/connector/atlassiancrowd/atlassiancrowd.go b/connector/atlassiancrowd/atlassiancrowd.go
index aa142203415cde3cdf6a8eeb82715997cf365313..d36832846ebdcd94b97ea3551afab3d39e9efc88 100644
--- a/connector/atlassiancrowd/atlassiancrowd.go
+++ b/connector/atlassiancrowd/atlassiancrowd.go
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net"
 	"net/http"
 	"strings"
@@ -14,7 +15,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds configuration options for Atlassian Crowd connector.
@@ -80,16 +80,16 @@ type crowdAuthenticationError struct {
 }
 
 // Open returns a strategy for logging in through Atlassian Crowd
-func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		return nil, fmt.Errorf("crowd: no baseURL provided for crowd connector")
 	}
-	return &crowdConnector{Config: *c, logger: logger}, nil
+	return &crowdConnector{Config: *c, logger: logger.With(slog.Group("connector", "type", "atlassiancrowd", "id", id))}, nil
 }
 
 type crowdConnector struct {
 	Config
-	logger log.Logger
+	logger *slog.Logger
 }
 
 var (
@@ -375,7 +375,7 @@ func (c *crowdConnector) identityFromCrowdUser(user crowdUser) connector.Identit
 		identity.PreferredUsername = user.Email
 	default:
 		if c.PreferredUsernameField != "" {
-			c.logger.Warnf("preferred_username left empty. Invalid crowd field mapped to preferred_username: %s", c.PreferredUsernameField)
+			c.logger.Warn("preferred_username left empty. Invalid crowd field mapped to preferred_username", "field", c.PreferredUsernameField)
 		}
 	}
 
@@ -436,12 +436,12 @@ func (c *crowdConnector) validateCrowdResponse(resp *http.Response) ([]byte, err
 	}
 
 	if resp.StatusCode == http.StatusForbidden && strings.Contains(string(body), "The server understood the request but refuses to authorize it.") {
-		c.logger.Debugf("crowd response validation failed: %s", string(body))
+		c.logger.Debug("crowd response validation failed", "response", string(body))
 		return nil, fmt.Errorf("dex is forbidden from making requests to the Atlassian Crowd application by URL %q", c.BaseURL)
 	}
 
 	if resp.StatusCode == http.StatusUnauthorized && string(body) == "Application failed to authenticate" {
-		c.logger.Debugf("crowd response validation failed: %s", string(body))
+		c.logger.Debug("crowd response validation failed", "response", string(body))
 		return nil, fmt.Errorf("dex failed to authenticate Crowd Application with ID %q", c.ClientID)
 	}
 	return body, nil
diff --git a/connector/atlassiancrowd/atlassiancrowd_test.go b/connector/atlassiancrowd/atlassiancrowd_test.go
index 36789a391971168ff273806606581b06eb168f72..b4c35a13cc8cc84a80b46e712e7b87c3b2193db5 100644
--- a/connector/atlassiancrowd/atlassiancrowd_test.go
+++ b/connector/atlassiancrowd/atlassiancrowd_test.go
@@ -7,12 +7,11 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
 	"testing"
-
-	"github.com/sirupsen/logrus"
 )
 
 func TestUserGroups(t *testing.T) {
@@ -151,11 +150,7 @@ type TestServerResponse struct {
 func newTestCrowdConnector(baseURL string) crowdConnector {
 	connector := crowdConnector{}
 	connector.BaseURL = baseURL
-	connector.logger = &logrus.Logger{
-		Out:       io.Discard,
-		Level:     logrus.DebugLevel,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-	}
+	connector.logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	return connector
 }
 
diff --git a/connector/authproxy/authproxy.go b/connector/authproxy/authproxy.go
index 465c3e3d9e9b195830126dcd4c8d68247400e529..61353382f58b60b8d0153fa223f1f3a04daaddf7 100644
--- a/connector/authproxy/authproxy.go
+++ b/connector/authproxy/authproxy.go
@@ -5,12 +5,12 @@ package authproxy
 
 import (
 	"fmt"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"strings"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds the configuration parameters for a connector which returns an
@@ -27,7 +27,7 @@ type Config struct {
 }
 
 // Open returns an authentication strategy which requires no user interaction.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	userIDHeader := c.UserIDHeader
 	if userIDHeader == "" {
 		userIDHeader = "X-Remote-User-Id"
@@ -51,7 +51,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		emailHeader:  emailHeader,
 		groupHeader:  groupHeader,
 		groups:       c.Groups,
-		logger:       logger,
+		logger:       logger.With(slog.Group("connector", "type", "authproxy", "id", id)),
 		pathSuffix:   "/" + id,
 	}, nil
 }
@@ -64,7 +64,7 @@ type callback struct {
 	emailHeader  string
 	groupHeader  string
 	groups       []string
-	logger       log.Logger
+	logger       *slog.Logger
 	pathSuffix   string
 }
 
diff --git a/connector/authproxy/authproxy_test.go b/connector/authproxy/authproxy_test.go
index 5e098722993329848fcb35cbaa815403561d3345..fdcf4038cf3b4765c9224a572b3d569e9314b054 100644
--- a/connector/authproxy/authproxy_test.go
+++ b/connector/authproxy/authproxy_test.go
@@ -2,12 +2,11 @@ package authproxy
 
 import (
 	"io"
+	"log/slog"
 	"net/http"
 	"reflect"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/connector"
 )
 
@@ -23,7 +22,7 @@ const (
 	testUserID       = "1234567890"
 )
 
-var logger = &logrus.Logger{Out: io.Discard, Formatter: &logrus.TextFormatter{}}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 func TestUser(t *testing.T) {
 	config := Config{
diff --git a/connector/bitbucketcloud/bitbucketcloud.go b/connector/bitbucketcloud/bitbucketcloud.go
index 27eafb529947be98faab3ff2866af075b5f11af3..5f802e341497649abad4f4aac05cdd3d1f7f2f4f 100644
--- a/connector/bitbucketcloud/bitbucketcloud.go
+++ b/connector/bitbucketcloud/bitbucketcloud.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"sync"
 	"time"
@@ -16,7 +17,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -42,7 +42,7 @@ type Config struct {
 }
 
 // Open returns a strategy for logging in through Bitbucket.
-func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	b := bitbucketConnector{
 		redirectURI:       c.RedirectURI,
 		teams:             c.Teams,
@@ -51,7 +51,7 @@ func (c *Config) Open(_ string, logger log.Logger) (connector.Connector, error)
 		includeTeamGroups: c.IncludeTeamGroups,
 		apiURL:            apiURL,
 		legacyAPIURL:      legacyAPIURL,
-		logger:            logger,
+		logger:            logger.With(slog.Group("connector", "type", "bitbucketcloud", "id", id)),
 	}
 
 	return &b, nil
@@ -73,7 +73,7 @@ type bitbucketConnector struct {
 	teams        []string
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	apiURL       string
 	legacyAPIURL string
 
diff --git a/connector/gitea/gitea.go b/connector/gitea/gitea.go
index 6b02099414d2391af86956fadcb9152c6a5ff39b..62523185d59ca68df5fd5dc13b1d20c7bd4cd9a7 100644
--- a/connector/gitea/gitea.go
+++ b/connector/gitea/gitea.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strconv"
 	"sync"
@@ -15,7 +16,6 @@ import (
 	"golang.org/x/oauth2"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds configuration options for gitea logins.
@@ -51,7 +51,7 @@ type giteaUser struct {
 }
 
 // Open returns a strategy for logging in through Gitea
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		c.BaseURL = "https://gitea.com"
 	}
@@ -61,7 +61,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		orgs:          c.Orgs,
 		clientID:      c.ClientID,
 		clientSecret:  c.ClientSecret,
-		logger:        logger,
+		logger:        logger.With(slog.Group("connector", "type", "gitea", "id", id)),
 		loadAllGroups: c.LoadAllGroups,
 		useLoginAsID:  c.UseLoginAsID,
 	}, nil
@@ -84,7 +84,7 @@ type giteaConnector struct {
 	orgs         []Org
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	httpClient   *http.Client
 	// if set to true and no orgs are configured then connector loads all user claims (all orgs and team)
 	loadAllGroups bool
diff --git a/connector/github/github.go b/connector/github/github.go
index 6cb0db09df4bb3ba2a73f4f6dde1bf5450802f21..a7818579e25e51b726da44356dbc68eb8e9cd0c0 100644
--- a/connector/github/github.go
+++ b/connector/github/github.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"regexp"
 	"strconv"
@@ -18,7 +19,6 @@ import (
 	"github.com/dexidp/dex/connector"
 	groups_pkg "github.com/dexidp/dex/pkg/groups"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -66,7 +66,7 @@ type Org struct {
 }
 
 // Open returns a strategy for logging in through GitHub.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.Org != "" {
 		// Return error if both 'org' and 'orgs' fields are used.
 		if len(c.Orgs) > 0 {
@@ -82,7 +82,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		clientID:             c.ClientID,
 		clientSecret:         c.ClientSecret,
 		apiURL:               apiURL,
-		logger:               logger,
+		logger:               logger.With(slog.Group("connector", "type", "github", "id", id)),
 		useLoginAsID:         c.UseLoginAsID,
 		preferredEmailDomain: c.PreferredEmailDomain,
 	}
@@ -142,7 +142,7 @@ type githubConnector struct {
 	orgs         []Org
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	// apiURL defaults to "https://api.github.com"
 	apiURL string
 	// hostName of the GitHub enterprise account.
@@ -362,7 +362,7 @@ func (c *githubConnector) groupsForOrgs(ctx context.Context, client *http.Client
 		if len(org.Teams) == 0 {
 			inOrgNoTeams = true
 		} else if teams = groups_pkg.Filter(teams, org.Teams); len(teams) == 0 {
-			c.logger.Infof("github: user %q in org %q but no teams", userName, org.Name)
+			c.logger.Info("user in org but no teams", "user", userName, "org", org.Name)
 		}
 
 		for _, teamName := range teams {
@@ -667,7 +667,7 @@ func (c *githubConnector) userInOrg(ctx context.Context, client *http.Client, us
 	switch resp.StatusCode {
 	case http.StatusNoContent:
 	case http.StatusFound, http.StatusNotFound:
-		c.logger.Infof("github: user %q not in org %q or application not authorized to read org data", userName, orgName)
+		c.logger.Info("user not in org or application not authorized to read org data", "user", userName, "org", orgName)
 	default:
 		err = fmt.Errorf("github: unexpected return status: %q", resp.Status)
 	}
diff --git a/connector/github/github_test.go b/connector/github/github_test.go
index af8099e9c76e3ac84e0431eb60c57df35d8132a4..088cbb238c736e292ad4cde3a296c1cbeaf5352f 100644
--- a/connector/github/github_test.go
+++ b/connector/github/github_test.go
@@ -6,6 +6,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -449,6 +451,7 @@ func Test_isPreferredEmailDomain(t *testing.T) {
 }
 
 func Test_Open_PreferredDomainConfig(t *testing.T) {
+	log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	tests := []struct {
 		preferredEmailDomain string
 		email                string
@@ -476,7 +479,7 @@ func Test_Open_PreferredDomainConfig(t *testing.T) {
 			c := Config{
 				PreferredEmailDomain: test.preferredEmailDomain,
 			}
-			_, err := c.Open("id", nil)
+			_, err := c.Open("id", log)
 
 			expectEquals(t, err, test.expected)
 		})
diff --git a/connector/gitlab/gitlab.go b/connector/gitlab/gitlab.go
index 099cd2ef17f595ee03d363b97d67cb32e95bb6a7..fdb2c48204c1342a632396f99b402b801d87343d 100644
--- a/connector/gitlab/gitlab.go
+++ b/connector/gitlab/gitlab.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strconv"
 	"time"
@@ -15,7 +16,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -46,7 +46,7 @@ type gitlabUser struct {
 }
 
 // Open returns a strategy for logging in through GitLab.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.BaseURL == "" {
 		c.BaseURL = "https://gitlab.com"
 	}
@@ -55,7 +55,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		redirectURI:  c.RedirectURI,
 		clientID:     c.ClientID,
 		clientSecret: c.ClientSecret,
-		logger:       logger,
+		logger:       logger.With(slog.Group("connector", "type", "gitlab", "id", id)),
 		groups:       c.Groups,
 		useLoginAsID: c.UseLoginAsID,
 	}, nil
@@ -78,7 +78,7 @@ type gitlabConnector struct {
 	groups       []string
 	clientID     string
 	clientSecret string
-	logger       log.Logger
+	logger       *slog.Logger
 	httpClient   *http.Client
 	// if set to true will use the user's handle rather than their numeric id as the ID
 	useLoginAsID bool
diff --git a/connector/google/google.go b/connector/google/google.go
index c3042970dd1076aeab86e1021d95043bb64a7888..a370b93bbf8c92b73114bca0716d937d69038892 100644
--- a/connector/google/google.go
+++ b/connector/google/google.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"os"
 	"strings"
@@ -21,7 +22,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	pkg_groups "github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -67,9 +67,10 @@ type Config struct {
 }
 
 // Open returns a connector which can be used to login users through Google.
-func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) {
+	logger = logger.With(slog.Group("connector", "type", "google", "id", id))
 	if c.AdminEmail != "" {
-		log.Deprecated(logger, `google: use "domainToAdminEmail.*: %s" option instead of "adminEmail: %s".`, c.AdminEmail, c.AdminEmail)
+		logger.Warn(`use "domainToAdminEmail.*" option instead of "adminEmail"`, "deprecated", true)
 		if c.DomainToAdminEmail == nil {
 			c.DomainToAdminEmail = make(map[string]string)
 		}
@@ -152,7 +153,7 @@ type googleConnector struct {
 	oauth2Config                   *oauth2.Config
 	verifier                       *oidc.IDTokenVerifier
 	cancel                         context.CancelFunc
-	logger                         log.Logger
+	logger                         *slog.Logger
 	hostedDomains                  []string
 	groups                         []string
 	serviceAccountFilePath         string
@@ -340,7 +341,7 @@ func (c *googleConnector) findAdminService(domain string) (*admin.Service, error
 	adminSrv, ok := c.adminSrv[domain]
 	if !ok {
 		adminSrv, ok = c.adminSrv[wildcardDomainToAdminEmail]
-		c.logger.Debugf("using wildcard (%s) admin email to fetch groups", c.domainToAdminEmail[wildcardDomainToAdminEmail])
+		c.logger.Debug("using wildcard admin email to fetch groups", "admin_email", c.domainToAdminEmail[wildcardDomainToAdminEmail])
 	}
 
 	if !ok {
@@ -377,7 +378,7 @@ func getCredentialsFromFilePath(serviceAccountFilePath string) ([]byte, error) {
 // If the default credential is empty, it attempts to create a new service with metadata credentials.
 // If successful, it returns the service and nil error.
 // If unsuccessful, it returns the error and a nil service.
-func getCredentialsFromDefault(ctx context.Context, email string, logger log.Logger) ([]byte, *admin.Service, error) {
+func getCredentialsFromDefault(ctx context.Context, email string, logger *slog.Logger) ([]byte, *admin.Service, error) {
 	credential, err := google.FindDefaultCredentials(ctx)
 	if err != nil {
 		return nil, nil, fmt.Errorf("failed to fetch application default credentials: %w", err)
@@ -397,9 +398,9 @@ func getCredentialsFromDefault(ctx context.Context, email string, logger log.Log
 
 // createServiceWithMetadataServer creates a new service using metadata server.
 // If an error occurs during the process, it is returned along with a nil service.
-func createServiceWithMetadataServer(ctx context.Context, adminEmail string, logger log.Logger) (*admin.Service, error) {
+func createServiceWithMetadataServer(ctx context.Context, adminEmail string, logger *slog.Logger) (*admin.Service, error) {
 	serviceAccountEmail, err := metadata.Email("default")
-	logger.Infof("discovered serviceAccountEmail: %s", serviceAccountEmail)
+	logger.Info("discovered serviceAccountEmail", "email", serviceAccountEmail)
 
 	if err != nil {
 		return nil, fmt.Errorf("unable to get service account email from metadata server: %v", err)
@@ -423,7 +424,7 @@ func createServiceWithMetadataServer(ctx context.Context, adminEmail string, log
 // createDirectoryService sets up super user impersonation and creates an admin client for calling
 // the google admin api. If no serviceAccountFilePath is defined, the application default credential
 // is used.
-func createDirectoryService(serviceAccountFilePath, email string, logger log.Logger) (service *admin.Service, err error) {
+func createDirectoryService(serviceAccountFilePath, email string, logger *slog.Logger) (service *admin.Service, err error) {
 	var jsonCredentials []byte
 
 	ctx := context.Background()
diff --git a/connector/google/google_test.go b/connector/google/google_test.go
index 2fa2b783ef5d77e6ae67de2d61988d8e47899d99..bafcadc8ff0697b63197f27a1908505b6d19ca6b 100644
--- a/connector/google/google_test.go
+++ b/connector/google/google_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -11,7 +13,6 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 	admin "google.golang.org/api/admin/directory/v1"
 	"google.golang.org/api/option"
@@ -51,7 +52,7 @@ func testSetup() *httptest.Server {
 }
 
 func newConnector(config *Config) (*googleConnector, error) {
-	log := logrus.New()
+	log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	conn, err := config.Open("id", log)
 	if err != nil {
 		return nil, err
diff --git a/connector/keystone/keystone.go b/connector/keystone/keystone.go
index 03f473310b6f41b45f5c4b0bffb9bb6f52dde6b6..f8dff9e3c9eedd37d4673d1db7b0adfcb44c98a1 100644
--- a/connector/keystone/keystone.go
+++ b/connector/keystone/keystone.go
@@ -7,10 +7,10 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 type conn struct {
@@ -19,7 +19,7 @@ type conn struct {
 	AdminUsername string
 	AdminPassword string
 	client        *http.Client
-	Logger        log.Logger
+	Logger        *slog.Logger
 }
 
 type userKeystone struct {
@@ -111,13 +111,13 @@ var (
 )
 
 // Open returns an authentication strategy using Keystone.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	return &conn{
 		Domain:        c.Domain,
 		Host:          c.Host,
 		AdminUsername: c.AdminUsername,
 		AdminPassword: c.AdminPassword,
-		Logger:        logger,
+		Logger:        logger.With(slog.Group("connector", "type", "keystone", "id", id)),
 		client:        http.DefaultClient,
 	}, nil
 }
@@ -287,7 +287,7 @@ func (p *conn) getUserGroups(ctx context.Context, userID string, token string) (
 	req = req.WithContext(ctx)
 	resp, err := p.client.Do(req)
 	if err != nil {
-		p.Logger.Errorf("keystone: error while fetching user %q groups\n", userID)
+		p.Logger.Error("error while fetching user groups", "user_id", userID, "err", err)
 		return nil, err
 	}
 
diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go
index bb434a6cb2970c117be5aa4ba09f435bf672ea74..b5729fe876ef952f3d5dc02a5825a4c6dc616f20 100644
--- a/connector/ldap/ldap.go
+++ b/connector/ldap/ldap.go
@@ -7,6 +7,7 @@ import (
 	"crypto/x509"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"net"
 	"os"
 	"strings"
@@ -14,7 +15,6 @@ import (
 	"github.com/go-ldap/ldap/v3"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds the configuration parameters for the LDAP connector. The LDAP
@@ -188,12 +188,12 @@ func parseScope(s string) (int, bool) {
 // Function exists here to allow backward compatibility between old and new
 // group to user matching implementations.
 // See "Config.GroupSearch.UserMatchers" comments for the details
-func userMatchers(c *Config, logger log.Logger) []UserMatcher {
+func userMatchers(c *Config, logger *slog.Logger) []UserMatcher {
 	if len(c.GroupSearch.UserMatchers) > 0 && c.GroupSearch.UserMatchers[0].UserAttr != "" {
 		return c.GroupSearch.UserMatchers
 	}
 
-	log.Deprecated(logger, `LDAP: use groupSearch.userMatchers option instead of "userAttr/groupAttr" fields.`)
+	logger.Warn(`use "groupSearch.userMatchers" option instead of "userAttr/groupAttr" fields`, "deprecated", true)
 	return []UserMatcher{
 		{
 			UserAttr:  c.GroupSearch.UserAttr,
@@ -203,7 +203,8 @@ func userMatchers(c *Config, logger log.Logger) []UserMatcher {
 }
 
 // Open returns an authentication strategy using LDAP.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	logger = logger.With(slog.Group("connector", "type", "ldap", "id", id))
 	conn, err := c.OpenConnector(logger)
 	if err != nil {
 		return nil, err
@@ -217,7 +218,7 @@ type refreshData struct {
 }
 
 // OpenConnector is the same as Open but returns a type with all implemented connector interfaces.
-func (c *Config) OpenConnector(logger log.Logger) (interface {
+func (c *Config) OpenConnector(logger *slog.Logger) (interface {
 	connector.Connector
 	connector.PasswordConnector
 	connector.RefreshConnector
@@ -226,7 +227,7 @@ func (c *Config) OpenConnector(logger log.Logger) (interface {
 	return c.openConnector(logger)
 }
 
-func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
+func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) {
 	requiredFields := []struct {
 		name string
 		val  string
@@ -300,7 +301,7 @@ type ldapConnector struct {
 
 	tlsConfig *tls.Config
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 var (
@@ -359,7 +360,7 @@ func (c *ldapConnector) getAttrs(e ldap.Entry, name string) []string {
 		return []string{e.DN}
 	}
 
-	c.logger.Debugf("%q attribute is not fround in entry", name)
+	c.logger.Debug("attribute is not fround in entry", "attribute", name)
 	return nil
 }
 
@@ -438,8 +439,8 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
 		req.Attributes = append(req.Attributes, c.UserSearch.PreferredUsernameAttrAttr)
 	}
 
-	c.logger.Infof("performing ldap search %s %s %s",
-		req.BaseDN, scopeString(req.Scope), req.Filter)
+	c.logger.Info("performing ldap search",
+		"base_dn", req.BaseDN, "scope", scopeString(req.Scope), "filter", req.Filter)
 	resp, err := conn.Search(req)
 	if err != nil {
 		return ldap.Entry{}, false, fmt.Errorf("ldap: search with filter %q failed: %v", req.Filter, err)
@@ -447,11 +448,11 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
 
 	switch n := len(resp.Entries); n {
 	case 0:
-		c.logger.Errorf("ldap: no results returned for filter: %q", filter)
+		c.logger.Error("no results returned for filter", "filter", filter)
 		return ldap.Entry{}, false, nil
 	case 1:
 		user = *resp.Entries[0]
-		c.logger.Infof("username %q mapped to entry %s", username, user.DN)
+		c.logger.Info("username mapped to entry", "username", username, "user_dn", user.DN)
 		return user, true, nil
 	default:
 		return ldap.Entry{}, false, fmt.Errorf("ldap: filter returned multiple (%d) results: %q", n, filter)
@@ -491,11 +492,11 @@ func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username,
 			if ldapErr, ok := err.(*ldap.Error); ok {
 				switch ldapErr.ResultCode {
 				case ldap.LDAPResultInvalidCredentials:
-					c.logger.Errorf("ldap: invalid password for user %q", user.DN)
+					c.logger.Error("invalid password for user", "user_dn", user.DN)
 					incorrectPass = true
 					return nil
 				case ldap.LDAPResultConstraintViolation:
-					c.logger.Errorf("ldap: constraint violation for user %q: %s", user.DN, ldapErr.Error())
+					c.logger.Error("constraint violation for user", "user_dn", user.DN, "err", ldapErr.Error())
 					incorrectPass = true
 					return nil
 				}
@@ -581,7 +582,7 @@ func (c *ldapConnector) Refresh(ctx context.Context, s connector.Scopes, ident c
 
 func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string, error) {
 	if c.GroupSearch.BaseDN == "" {
-		c.logger.Debugf("No groups returned for %q because no groups baseDN has been configured.", c.getAttr(user, c.UserSearch.NameAttr))
+		c.logger.Debug("No groups returned because no groups baseDN has been configured.", "base_dn", c.getAttr(user, c.UserSearch.NameAttr))
 		return nil, nil
 	}
 
@@ -602,8 +603,8 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
 
 			gotGroups := false
 			if err := c.do(ctx, func(conn *ldap.Conn) error {
-				c.logger.Infof("performing ldap search %s %s %s",
-					req.BaseDN, scopeString(req.Scope), req.Filter)
+				c.logger.Info("performing ldap search",
+					"base_dn", req.BaseDN, "scope", scopeString(req.Scope), "filter", req.Filter)
 				resp, err := conn.Search(req)
 				if err != nil {
 					return fmt.Errorf("ldap: search failed: %v", err)
@@ -616,7 +617,7 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
 			}
 			if !gotGroups {
 				// TODO(ericchiang): Is this going to spam the logs?
-				c.logger.Errorf("ldap: groups search with filter %q returned no groups", filter)
+				c.logger.Error("groups search returned no groups", "filter", filter)
 			}
 		}
 	}
diff --git a/connector/ldap/ldap_test.go b/connector/ldap/ldap_test.go
index f00f1ead99a0c6883cf7a3ab8f2a5e9811d6c013..de85b6a256ea6c4cb24074cb7831787f26d80734 100644
--- a/connector/ldap/ldap_test.go
+++ b/connector/ldap/ldap_test.go
@@ -4,11 +4,11 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"log/slog"
 	"os"
 	"testing"
 
 	"github.com/kylelemons/godebug/pretty"
-	"github.com/sirupsen/logrus"
 
 	"github.com/dexidp/dex/connector"
 )
@@ -567,7 +567,7 @@ func runTests(t *testing.T, connMethod connectionMethod, config *Config, tests [
 	c.BindDN = "cn=admin,dc=example,dc=org"
 	c.BindPW = "admin"
 
-	l := &logrus.Logger{Out: io.Discard, Formatter: &logrus.TextFormatter{}}
+	l := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	conn, err := c.openConnector(l)
 	if err != nil {
diff --git a/connector/linkedin/linkedin.go b/connector/linkedin/linkedin.go
index f79f1c49d8a57723b55809b08dde6c017c1fcd30..f17d17cca1581fb2f03c50556633898a7b728e44 100644
--- a/connector/linkedin/linkedin.go
+++ b/connector/linkedin/linkedin.go
@@ -6,13 +6,13 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 
 	"golang.org/x/oauth2"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 const (
@@ -29,7 +29,7 @@ type Config struct {
 }
 
 // Open returns a strategy for logging in through LinkedIn
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	return &linkedInConnector{
 		oauth2Config: &oauth2.Config{
 			ClientID:     c.ClientID,
@@ -41,7 +41,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 			Scopes:      []string{"r_liteprofile", "r_emailaddress"},
 			RedirectURL: c.RedirectURI,
 		},
-		logger: logger,
+		logger: logger.With(slog.Group("connector", "type", "linkedin", "id", id)),
 	}, nil
 }
 
@@ -51,7 +51,7 @@ type connectorData struct {
 
 type linkedInConnector struct {
 	oauth2Config *oauth2.Config
-	logger       log.Logger
+	logger       *slog.Logger
 }
 
 // LinkedIn doesn't provide refresh tokens, so refresh tokens issued by Dex
diff --git a/connector/microsoft/microsoft.go b/connector/microsoft/microsoft.go
index bbc3d6c6b52f5147151cd8edb51918ab9eb71e4c..2fcf6a75152c96edcb8ab4b2ec968168cefba205 100644
--- a/connector/microsoft/microsoft.go
+++ b/connector/microsoft/microsoft.go
@@ -8,6 +8,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 	"sync"
@@ -17,7 +18,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	groups_pkg "github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // GroupNameFormat represents the format of the group identifier
@@ -66,7 +66,7 @@ type Config struct {
 }
 
 // Open returns a strategy for logging in through Microsoft.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	m := microsoftConnector{
 		apiURL:               strings.TrimSuffix(c.APIURL, "/"),
 		graphURL:             strings.TrimSuffix(c.GraphURL, "/"),
@@ -78,7 +78,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		groups:               c.Groups,
 		groupNameFormat:      c.GroupNameFormat,
 		useGroupsAsWhitelist: c.UseGroupsAsWhitelist,
-		logger:               logger,
+		logger:               logger.With(slog.Group("connector", "type", "microsoft", "id", id)),
 		emailToLowercase:     c.EmailToLowercase,
 		promptType:           c.PromptType,
 		domainHint:           c.DomainHint,
@@ -133,7 +133,7 @@ type microsoftConnector struct {
 	groupNameFormat      GroupNameFormat
 	groups               []string
 	useGroupsAsWhitelist bool
-	logger               log.Logger
+	logger               *slog.Logger
 	emailToLowercase     bool
 	promptType           string
 	domainHint           string
diff --git a/connector/mock/connectortest.go b/connector/mock/connectortest.go
index e97f9865744f4c63a3c14a9618f39bacca6ad001..7e5979a9923f3471e0ece95b485d656769018776 100644
--- a/connector/mock/connectortest.go
+++ b/connector/mock/connectortest.go
@@ -5,16 +5,16 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"net/url"
 
 	"github.com/dexidp/dex/connector"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // NewCallbackConnector returns a mock connector which requires no user interaction. It always returns
 // the same (fake) identity.
-func NewCallbackConnector(logger log.Logger) connector.Connector {
+func NewCallbackConnector(logger *slog.Logger) connector.Connector {
 	return &Callback{
 		Identity: connector.Identity{
 			UserID:        "0-385-28089-0",
@@ -39,7 +39,7 @@ var (
 type Callback struct {
 	// The returned identity.
 	Identity connector.Identity
-	Logger   log.Logger
+	Logger   *slog.Logger
 }
 
 // LoginURL returns the URL to redirect the user to login with.
@@ -74,7 +74,8 @@ func (m *Callback) TokenIdentity(ctx context.Context, subjectTokenType, subjectT
 type CallbackConfig struct{}
 
 // Open returns an authentication strategy which requires no user interaction.
-func (c *CallbackConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *CallbackConfig) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	logger = logger.With(slog.Group("connector", "type", "callback", "id", id))
 	return NewCallbackConnector(logger), nil
 }
 
@@ -86,7 +87,7 @@ type PasswordConfig struct {
 }
 
 // Open returns an authentication strategy which prompts for a predefined username and password.
-func (c *PasswordConfig) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *PasswordConfig) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	if c.Username == "" {
 		return nil, errors.New("no username supplied")
 	}
@@ -99,7 +100,7 @@ func (c *PasswordConfig) Open(id string, logger log.Logger) (connector.Connector
 type passwordConnector struct {
 	username string
 	password string
-	logger   log.Logger
+	logger   *slog.Logger
 }
 
 func (p passwordConnector) Close() error { return nil }
diff --git a/connector/oauth/oauth.go b/connector/oauth/oauth.go
index 2fe39fd467dd7189b1eb5b7070a1b70f9a7981cc..413a813a08963877fc7f569e576930bfd1a0167d 100644
--- a/connector/oauth/oauth.go
+++ b/connector/oauth/oauth.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"strings"
 
@@ -13,7 +14,6 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 type oauthConnector struct {
@@ -31,7 +31,7 @@ type oauthConnector struct {
 	emailVerifiedKey     string
 	groupsKey            string
 	httpClient           *http.Client
-	logger               log.Logger
+	logger               *slog.Logger
 }
 
 type connectorData struct {
@@ -58,7 +58,7 @@ type Config struct {
 	} `json:"claimMapping"`
 }
 
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
 	var err error
 
 	userIDKey := c.UserIDKey
@@ -99,7 +99,7 @@ func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error)
 		userInfoURL:          c.UserInfoURL,
 		scopes:               c.Scopes,
 		redirectURI:          c.RedirectURI,
-		logger:               logger,
+		logger:               logger.With(slog.Group("connector", "type", "oauth", "id", id)),
 		userIDKey:            userIDKey,
 		userNameKey:          userNameKey,
 		preferredUsernameKey: preferredUsernameKey,
diff --git a/connector/oauth/oauth_test.go b/connector/oauth/oauth_test.go
index 62cbd8d59ad5dc237a1732c294f40d12f352d778..d06c0c0840b79a89c6906e7c0aadea2736afbbb5 100644
--- a/connector/oauth/oauth_test.go
+++ b/connector/oauth/oauth_test.go
@@ -6,6 +6,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -13,7 +15,6 @@ import (
 	"testing"
 
 	"github.com/go-jose/go-jose/v4"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/assert"
 
 	"github.com/dexidp/dex/connector"
@@ -270,7 +271,7 @@ func newConnector(t *testing.T, serverURL string) *oauthConnector {
 	testConfig.ClaimMapping.EmailKey = "mail"
 	testConfig.ClaimMapping.EmailVerifiedKey = "has_verified_email"
 
-	log := logrus.New()
+	log := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	conn, err := testConfig.Open("id", log)
 	if err != nil {
diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go
index 578480ece3897c95c9bd6e51974ed90e556488b0..6213e06833d2e0fbfe3d01fe35bba0a9eb70cd31 100644
--- a/connector/oidc/oidc.go
+++ b/connector/oidc/oidc.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"strings"
@@ -17,7 +18,6 @@ import (
 	"github.com/dexidp/dex/connector"
 	groups_pkg "github.com/dexidp/dex/pkg/groups"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Config holds configuration options for OpenID Connect logins.
@@ -206,7 +206,7 @@ func knownBrokenAuthHeaderProvider(issuerURL string) bool {
 
 // Open returns a connector which can be used to login users through an upstream
 // OpenID Connect provider.
-func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) {
 	if len(c.HostedDomains) > 0 {
 		return nil, fmt.Errorf("support for the Hosted domains option had been deprecated and removed, consider switching to the Google connector")
 	}
@@ -225,7 +225,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 		return nil, err
 	}
 	if !c.ProviderDiscoveryOverrides.Empty() {
-		logger.Warnf("overrides for connector %q are set, this can be a vulnerability when not properly configured", id)
+		logger.Warn("overrides for connector are set, this can be a vulnerability when not properly configured", "connector_id", id)
 	}
 
 	endpoint := provider.Endpoint()
@@ -266,7 +266,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 		verifier: provider.Verifier(
 			&oidc.Config{ClientID: clientID},
 		),
-		logger:                    logger,
+		logger:                    logger.With(slog.Group("connector", "type", "oidc", "id", id)),
 		cancel:                    cancel,
 		httpClient:                httpClient,
 		insecureSkipEmailVerified: c.InsecureSkipEmailVerified,
@@ -296,7 +296,7 @@ type oidcConnector struct {
 	oauth2Config              *oauth2.Config
 	verifier                  *oidc.IDTokenVerifier
 	cancel                    context.CancelFunc
-	logger                    log.Logger
+	logger                    *slog.Logger
 	httpClient                *http.Client
 	insecureSkipEmailVerified bool
 	insecureEnableGroups      bool
diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go
index e621a55ffb0a54be3bf139ed9b9b69a558e0c9b9..07291f7e5b931b0ea457feb2eb5c5861912f0471 100644
--- a/connector/oidc/oidc_test.go
+++ b/connector/oidc/oidc_test.go
@@ -10,6 +10,8 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"reflect"
@@ -18,7 +20,6 @@ import (
 	"time"
 
 	"github.com/go-jose/go-jose/v4"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/connector"
@@ -765,7 +766,7 @@ func newToken(key *jose.JSONWebKey, claims map[string]interface{}) (string, erro
 }
 
 func newConnector(config Config) (*oidcConnector, error) {
-	logger := logrus.New()
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	conn, err := config.Open("id", logger)
 	if err != nil {
 		return nil, fmt.Errorf("unable to open: %v", err)
diff --git a/connector/openshift/openshift.go b/connector/openshift/openshift.go
index 99d1b5b2f0a32f35dc7b6ee5bd358bbcefeaece1..4519a85b6dfd683bfd6d593e2b234aa54ac72317 100644
--- a/connector/openshift/openshift.go
+++ b/connector/openshift/openshift.go
@@ -5,6 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"strings"
 
@@ -13,7 +14,6 @@ import (
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
 	"github.com/dexidp/dex/pkg/httpclient"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
 )
 
@@ -44,7 +44,7 @@ type openshiftConnector struct {
 	clientID     string
 	clientSecret string
 	cancel       context.CancelFunc
-	logger       log.Logger
+	logger       *slog.Logger
 	httpClient   *http.Client
 	oauth2Config *oauth2.Config
 	insecureCA   bool
@@ -62,7 +62,7 @@ type user struct {
 
 // Open returns a connector which can be used to login users through an upstream
 // OpenShift OAuth2 provider.
-func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, err error) {
+func (c *Config) Open(id string, logger *slog.Logger) (conn connector.Connector, err error) {
 	var rootCAs []string
 	if c.RootCA != "" {
 		rootCAs = append(rootCAs, c.RootCA)
@@ -78,7 +78,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
 
 // OpenWithHTTPClient returns a connector which can be used to login users through an upstream
 // OpenShift OAuth2 provider. It provides the ability to inject a http.Client.
-func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
+func (c *Config) OpenWithHTTPClient(id string, logger *slog.Logger,
 	httpClient *http.Client,
 ) (conn connector.Connector, err error) {
 	ctx, cancel := context.WithCancel(context.Background())
@@ -96,7 +96,7 @@ func (c *Config) OpenWithHTTPClient(id string, logger log.Logger,
 		clientID:     c.ClientID,
 		clientSecret: c.ClientSecret,
 		insecureCA:   c.InsecureCA,
-		logger:       logger,
+		logger:       logger.With(slog.Group("connector", "type", "openshift", "id", id)),
 		redirectURI:  c.RedirectURI,
 		rootCA:       c.RootCA,
 		groups:       c.Groups,
diff --git a/connector/openshift/openshift_test.go b/connector/openshift/openshift_test.go
index 1a2c7a4840a9750a1b02945a3c0955bd74147cdb..89ec0e25a9f5e7a5113433a07519d215cf4f2d7b 100644
--- a/connector/openshift/openshift_test.go
+++ b/connector/openshift/openshift_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/url"
@@ -11,7 +13,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"golang.org/x/oauth2"
 
 	"github.com/dexidp/dex/connector"
@@ -37,7 +38,7 @@ func TestOpen(t *testing.T) {
 		InsecureCA:   true,
 	}
 
-	logger := logrus.New()
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	oconfig, err := c.Open("id", logger)
 
diff --git a/connector/saml/saml.go b/connector/saml/saml.go
index fbfb1986a956919daad241d65dd274abf77ba76f..1ab8e54411cd0d3e2adf27e170061f78bb1f638b 100644
--- a/connector/saml/saml.go
+++ b/connector/saml/saml.go
@@ -8,6 +8,7 @@ import (
 	"encoding/pem"
 	"encoding/xml"
 	"fmt"
+	"log/slog"
 	"os"
 	"strings"
 	"sync"
@@ -21,10 +22,8 @@ import (
 
 	"github.com/dexidp/dex/connector"
 	"github.com/dexidp/dex/pkg/groups"
-	"github.com/dexidp/dex/pkg/log"
 )
 
-//nolint
 const (
 	bindingRedirect = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
 	bindingPOST     = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
@@ -120,11 +119,12 @@ func (c certStore) Certificates() (roots []*x509.Certificate, err error) {
 
 // Open validates the config and returns a connector. It does not actually
 // validate connectivity with the provider.
-func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
+func (c *Config) Open(id string, logger *slog.Logger) (connector.Connector, error) {
+	logger = logger.With(slog.Group("connector", "type", "saml", "id", id))
 	return c.openConnector(logger)
 }
 
-func (c *Config) openConnector(logger log.Logger) (*provider, error) {
+func (c *Config) openConnector(logger *slog.Logger) (*provider, error) {
 	requiredFields := []struct {
 		name, val string
 	}{
@@ -252,7 +252,7 @@ type provider struct {
 
 	nameIDPolicyFormat string
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 func (p *provider) POSTData(s connector.Scopes, id string) (action, value string, err error) {
@@ -389,7 +389,7 @@ func (p *provider) HandlePOST(s connector.Scopes, samlResponse, inResponseTo str
 	// Log the actual attributes we got back from the server. This helps debug
 	// configuration errors on the server side, where the SAML server doesn't
 	// send us the correct attributes.
-	p.logger.Infof("parsed and verified saml response attributes %s", attributes)
+	p.logger.Info("parsed and verified saml response attributes", "attributes", attributes)
 
 	// Grab the email.
 	if ident.Email, _ = attributes.get(p.emailAttr); ident.Email == "" {
diff --git a/connector/saml/saml_test.go b/connector/saml/saml_test.go
index 68c0cb1a93d7b0062255c752d0404cff0c572c25..f67e3e8bc9c0c60ec7bcb4c1ec1d0836ef62d283 100644
--- a/connector/saml/saml_test.go
+++ b/connector/saml/saml_test.go
@@ -5,6 +5,8 @@ import (
 	"encoding/base64"
 	"encoding/pem"
 	"errors"
+	"io"
+	"log/slog"
 	"os"
 	"sort"
 	"testing"
@@ -12,7 +14,6 @@ import (
 
 	"github.com/kylelemons/godebug/pretty"
 	dsig "github.com/russellhaering/goxmldsig"
-	"github.com/sirupsen/logrus"
 
 	"github.com/dexidp/dex/connector"
 )
@@ -420,7 +421,7 @@ func (r responseTest) run(t *testing.T) {
 		t.Fatalf("parse test time: %v", err)
 	}
 
-	conn, err := c.openConnector(logrus.New())
+	conn, err := c.openConnector(slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})))
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -454,7 +455,7 @@ func (r responseTest) run(t *testing.T) {
 }
 
 func TestConfigCAData(t *testing.T) {
-	logger := logrus.New()
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	validPEM, err := os.ReadFile("testdata/ca.crt")
 	if err != nil {
 		t.Fatal(err)
diff --git a/go.mod b/go.mod
index 05c44c1db700b8b90ae7dbfb2c12b2f9a8a430a0..2667cf8afc22eb18de8a4fb3f7ed91bbd8d7587b 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,6 @@ require (
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.19.1
 	github.com/russellhaering/goxmldsig v1.4.0
-	github.com/sirupsen/logrus v1.9.3
 	github.com/spf13/cobra v1.8.0
 	github.com/stretchr/testify v1.9.0
 	go.etcd.io/etcd/client/pkg/v3 v3.5.14
diff --git a/go.sum b/go.sum
index 6964c1553e59542782fbe8be0bda303abe12d983..0c546bb419835f488a3798fe76a52ff5a76ee2e2 100644
--- a/go.sum
+++ b/go.sum
@@ -191,8 +191,6 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
 github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
 github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
@@ -304,7 +302,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
diff --git a/pkg/log/deprecated.go b/pkg/log/deprecated.go
deleted file mode 100644
index f20e8b4cb824d4ca554f55bec2a4ecae05a519df..0000000000000000000000000000000000000000
--- a/pkg/log/deprecated.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package log
-
-func Deprecated(logger Logger, f string, args ...interface{}) {
-	logger.Warnf("Deprecated: "+f, args...)
-}
diff --git a/pkg/log/logger.go b/pkg/log/logger.go
deleted file mode 100644
index 4f3cdd38510ce16b099972b29aea4d1ba946947f..0000000000000000000000000000000000000000
--- a/pkg/log/logger.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// Package log provides a logger interface for logger libraries
-// so that dex does not depend on any of them directly.
-// It also includes a default implementation using Logrus (used by dex previously).
-package log
-
-// Logger serves as an adapter interface for logger libraries
-// so that dex does not depend on any of them directly.
-type Logger interface {
-	Debug(args ...interface{})
-	Info(args ...interface{})
-	Warn(args ...interface{})
-	Error(args ...interface{})
-
-	Debugf(format string, args ...interface{})
-	Infof(format string, args ...interface{})
-	Warnf(format string, args ...interface{})
-	Errorf(format string, args ...interface{})
-}
diff --git a/server/api.go b/server/api.go
index c0eacb8f800df80ccaacdc988ec04b2920884f52..6a0071efb8b5944b8b80538caa815f3812b1bbb4 100644
--- a/server/api.go
+++ b/server/api.go
@@ -4,11 +4,11 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 
 	"golang.org/x/crypto/bcrypt"
 
 	"github.com/dexidp/dex/api/v2"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server/internal"
 	"github.com/dexidp/dex/storage"
 )
@@ -29,10 +29,10 @@ const (
 )
 
 // NewAPI returns a server which implements the gRPC API interface.
-func NewAPI(s storage.Storage, logger log.Logger, version string) api.DexServer {
+func NewAPI(s storage.Storage, logger *slog.Logger, version string) api.DexServer {
 	return dexAPI{
 		s:       s,
-		logger:  logger,
+		logger:  logger.With("component", "api"),
 		version: version,
 	}
 }
@@ -41,7 +41,7 @@ type dexAPI struct {
 	api.UnimplementedDexServer
 
 	s       storage.Storage
-	logger  log.Logger
+	logger  *slog.Logger
 	version string
 }
 
@@ -89,7 +89,7 @@ func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*ap
 		if err == storage.ErrAlreadyExists {
 			return &api.CreateClientResp{AlreadyExists: true}, nil
 		}
-		d.logger.Errorf("api: failed to create client: %v", err)
+		d.logger.Error("failed to create client", "err", err)
 		return nil, fmt.Errorf("create client: %v", err)
 	}
 
@@ -122,7 +122,7 @@ func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*ap
 		if err == storage.ErrNotFound {
 			return &api.UpdateClientResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to update the client: %v", err)
+		d.logger.Error("failed to update the client", "err", err)
 		return nil, fmt.Errorf("update client: %v", err)
 	}
 	return &api.UpdateClientResp{}, nil
@@ -134,7 +134,7 @@ func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*ap
 		if err == storage.ErrNotFound {
 			return &api.DeleteClientResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to delete client: %v", err)
+		d.logger.Error("failed to delete client", "err", err)
 		return nil, fmt.Errorf("delete client: %v", err)
 	}
 	return &api.DeleteClientResp{}, nil
@@ -181,7 +181,7 @@ func (d dexAPI) CreatePassword(ctx context.Context, req *api.CreatePasswordReq)
 		if err == storage.ErrAlreadyExists {
 			return &api.CreatePasswordResp{AlreadyExists: true}, nil
 		}
-		d.logger.Errorf("api: failed to create password: %v", err)
+		d.logger.Error("failed to create password", "err", err)
 		return nil, fmt.Errorf("create password: %v", err)
 	}
 
@@ -218,7 +218,7 @@ func (d dexAPI) UpdatePassword(ctx context.Context, req *api.UpdatePasswordReq)
 		if err == storage.ErrNotFound {
 			return &api.UpdatePasswordResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to update password: %v", err)
+		d.logger.Error("failed to update password", "err", err)
 		return nil, fmt.Errorf("update password: %v", err)
 	}
 
@@ -235,7 +235,7 @@ func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq)
 		if err == storage.ErrNotFound {
 			return &api.DeletePasswordResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to delete password: %v", err)
+		d.logger.Error("failed to delete password", "err", err)
 		return nil, fmt.Errorf("delete password: %v", err)
 	}
 	return &api.DeletePasswordResp{}, nil
@@ -251,7 +251,7 @@ func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.Versi
 func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) {
 	passwordList, err := d.s.ListPasswords()
 	if err != nil {
-		d.logger.Errorf("api: failed to list passwords: %v", err)
+		d.logger.Error("failed to list passwords", "err", err)
 		return nil, fmt.Errorf("list passwords: %v", err)
 	}
 
@@ -286,12 +286,12 @@ func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq)
 				NotFound: true,
 			}, nil
 		}
-		d.logger.Errorf("api: there was an error retrieving the password: %v", err)
+		d.logger.Error("there was an error retrieving the password", "err", err)
 		return nil, fmt.Errorf("verify password: %v", err)
 	}
 
 	if err := bcrypt.CompareHashAndPassword(password.Hash, []byte(req.Password)); err != nil {
-		d.logger.Infof("api: password check failed: %v", err)
+		d.logger.Info("password check failed", "err", err)
 		return &api.VerifyPasswordResp{
 			Verified: false,
 		}, nil
@@ -304,7 +304,7 @@ func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq)
 func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, error) {
 	id := new(internal.IDTokenSubject)
 	if err := internal.Unmarshal(req.UserId, id); err != nil {
-		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
+		d.logger.Error("failed to unmarshal ID Token subject", "err", err)
 		return nil, err
 	}
 
@@ -315,7 +315,7 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.
 			// An empty list should be returned instead of an error.
 			return &api.ListRefreshResp{}, nil
 		}
-		d.logger.Errorf("api: failed to list refresh tokens %t here : %v", err == storage.ErrNotFound, err)
+		d.logger.Error("failed to list refresh tokens here", "err", err)
 		return nil, err
 	}
 
@@ -338,7 +338,7 @@ func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.
 func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) {
 	id := new(internal.IDTokenSubject)
 	if err := internal.Unmarshal(req.UserId, id); err != nil {
-		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
+		d.logger.Error("failed to unmarshal ID Token subject", "err", err)
 		return nil, err
 	}
 
@@ -349,7 +349,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 	updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
 		refreshRef := old.Refresh[req.ClientId]
 		if refreshRef == nil || refreshRef.ID == "" {
-			d.logger.Errorf("api: refresh token issued to client %q for user %q not found for deletion", req.ClientId, id.UserId)
+			d.logger.Error("refresh token issued to client not found for deletion", "client_id", req.ClientId, "user_id", id.UserId)
 			notFound = true
 			return old, storage.ErrNotFound
 		}
@@ -366,7 +366,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 		if err == storage.ErrNotFound {
 			return &api.RevokeRefreshResp{NotFound: true}, nil
 		}
-		d.logger.Errorf("api: failed to update offline session object: %v", err)
+		d.logger.Error("failed to update offline session object", "err", err)
 		return nil, err
 	}
 
@@ -379,7 +379,7 @@ func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*
 	// TODO(ericchiang): we don't have any good recourse if this call fails.
 	// Consider garbage collection of refresh tokens with no associated ref.
 	if err := d.s.DeleteRefresh(refreshID); err != nil {
-		d.logger.Errorf("failed to delete refresh token: %v", err)
+		d.logger.Error("failed to delete refresh token", "err", err)
 		return nil, err
 	}
 
diff --git a/server/api_test.go b/server/api_test.go
index 1ed43168dda344a31cab09cf30b3a450ef041f9c..e4150f1f961fdd0cbc9d0b6f57ad7e5fc4e529a5 100644
--- a/server/api_test.go
+++ b/server/api_test.go
@@ -2,17 +2,16 @@ package server
 
 import (
 	"context"
+	"io"
+	"log/slog"
 	"net"
-	"os"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
 
 	"github.com/dexidp/dex/api/v2"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/server/internal"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/memory"
@@ -30,7 +29,7 @@ type apiClient struct {
 }
 
 // newAPI constructs a gRCP client connected to a backing server.
-func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient {
+func newAPI(s storage.Storage, logger *slog.Logger, t *testing.T) *apiClient {
 	l, err := net.Listen("tcp", "127.0.0.1:0")
 	if err != nil {
 		t.Fatal(err)
@@ -59,11 +58,7 @@ func newAPI(s storage.Storage, logger log.Logger, t *testing.T) *apiClient {
 
 // Attempts to create, update and delete a test Password
 func TestPassword(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
@@ -172,11 +167,7 @@ func TestPassword(t *testing.T) {
 
 // Ensures checkCost returns expected values
 func TestCheckCost(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
@@ -229,11 +220,7 @@ func TestCheckCost(t *testing.T) {
 
 // Attempts to list and revoke an existing refresh token.
 func TestRefreshToken(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
@@ -342,11 +329,7 @@ func TestRefreshToken(t *testing.T) {
 }
 
 func TestUpdateClient(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	s := memory.New(logger)
 	client := newAPI(s, logger, t)
diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go
index 5683e9441a9e23937c3989403e4bd162fc6210e8..6f8aae0306d9a841491e1e3ba2b2a942efbed0fa 100644
--- a/server/deviceflowhandlers.go
+++ b/server/deviceflowhandlers.go
@@ -13,7 +13,6 @@ import (
 
 	"golang.org/x/net/html"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -49,7 +48,7 @@ func (s *Server) handleDeviceExchange(w http.ResponseWriter, r *http.Request) {
 			invalidAttempt = false
 		}
 		if err := s.templates.device(r, w, s.getDeviceVerificationURI(), userCode, invalidAttempt); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("server template error", "err", err)
 			s.renderError(r, w, http.StatusNotFound, "Page not found")
 		}
 	default:
@@ -65,7 +64,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 	case http.MethodPost:
 		err := r.ParseForm()
 		if err != nil {
-			s.logger.Errorf("Could not parse Device Request body: %v", err)
+			s.logger.Error("could not parse Device Request body", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusNotFound)
 			return
 		}
@@ -86,7 +85,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 			return
 		}
 
-		s.logger.Infof("Received device request for client %v with scopes %v", clientID, scopes)
+		s.logger.Info("received device request", "client_id", clientID, "scoped", scopes)
 
 		// Make device code
 		deviceCode := storage.NewDeviceCode()
@@ -108,7 +107,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 		}
 
 		if err := s.storage.CreateDeviceRequest(ctx, deviceReq); err != nil {
-			s.logger.Errorf("Failed to store device request; %v", err)
+			s.logger.Error("failed to store device request", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError)
 			return
 		}
@@ -127,14 +126,14 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 		}
 
 		if err := s.storage.CreateDeviceToken(ctx, deviceToken); err != nil {
-			s.logger.Errorf("Failed to store device token %v", err)
+			s.logger.Error("failed to store device token", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError)
 			return
 		}
 
 		u, err := url.Parse(s.issuerURL.String())
 		if err != nil {
-			s.logger.Errorf("Could not parse issuer URL %v", err)
+			s.logger.Error("could not parse issuer URL", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusInternalServerError)
 			return
 		}
@@ -175,14 +174,14 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) {
 }
 
 func (s *Server) handleDeviceTokenDeprecated(w http.ResponseWriter, r *http.Request) {
-	log.Deprecated(s.logger, `The /device/token endpoint was called. It will be removed, use /token instead.`)
+	s.logger.Warn(`the /device/token endpoint was called. It will be removed, use /token instead.`, "deprecated", true)
 
 	w.Header().Set("Content-Type", "application/json")
 	switch r.Method {
 	case http.MethodPost:
 		err := r.ParseForm()
 		if err != nil {
-			s.logger.Warnf("Could not parse Device Token Request body: %v", err)
+			s.logger.Warn("could not parse Device Token Request body", "err", err)
 			s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
 			return
 		}
@@ -212,7 +211,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 	deviceToken, err := s.storage.GetDeviceToken(deviceCode)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get device code: %v", err)
+			s.logger.Error("failed to get device code", "err", err)
 		}
 		s.tokenErrHelper(w, errInvalidRequest, "Invalid Device code.", http.StatusBadRequest)
 		return
@@ -242,7 +241,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 		}
 		// Update device token last request time in storage
 		if err := s.storage.UpdateDeviceToken(deviceCode, updater); err != nil {
-			s.logger.Errorf("failed to update device token: %v", err)
+			s.logger.Error("failed to update device token", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "")
 			return
 		}
@@ -259,7 +258,7 @@ func (s *Server) handleDeviceToken(w http.ResponseWriter, r *http.Request) {
 		case providedCodeVerifier != "" && codeChallengeFromStorage != "":
 			calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, deviceToken.PKCE.CodeChallengeMethod)
 			if err != nil {
-				s.logger.Error(err)
+				s.logger.Error("failed to calculate code challenge", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				return
 			}
@@ -304,7 +303,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		if err != nil || s.now().After(authCode.Expiry) {
 			errCode := http.StatusBadRequest
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get auth code: %v", err)
+				s.logger.Error("failed to get auth code", "err", err)
 				errCode = http.StatusInternalServerError
 			}
 			s.renderError(r, w, errCode, "Invalid or expired auth code.")
@@ -316,7 +315,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		if err != nil || s.now().After(deviceReq.Expiry) {
 			errCode := http.StatusBadRequest
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get device code: %v", err)
+				s.logger.Error("failed to get device code", "err", err)
 				errCode = http.StatusInternalServerError
 			}
 			s.renderError(r, w, errCode, "Invalid or expired user code.")
@@ -326,7 +325,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		client, err := s.storage.GetClient(deviceReq.ClientID)
 		if err != nil {
 			if err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get client: %v", err)
+				s.logger.Error("failed to get client", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			} else {
 				s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
@@ -340,7 +339,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 
 		resp, err := s.exchangeAuthCode(ctx, w, authCode, client)
 		if err != nil {
-			s.logger.Errorf("Could not exchange auth code for client %q: %v", deviceReq.ClientID, err)
+			s.logger.Error("could not exchange auth code for clien", "client_id", deviceReq.ClientID, "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Failed to exchange auth code.")
 			return
 		}
@@ -350,7 +349,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 		if err != nil || s.now().After(old.Expiry) {
 			errCode := http.StatusBadRequest
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get device token: %v", err)
+				s.logger.Error("failed to get device token", "err", err)
 				errCode = http.StatusInternalServerError
 			}
 			s.renderError(r, w, errCode, "Invalid or expired device code.")
@@ -363,7 +362,7 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 			}
 			respStr, err := json.MarshalIndent(resp, "", "  ")
 			if err != nil {
-				s.logger.Errorf("failed to marshal device token response: %v", err)
+				s.logger.Error("failed to marshal device token response", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "")
 				return old, err
 			}
@@ -375,13 +374,13 @@ func (s *Server) handleDeviceCallback(w http.ResponseWriter, r *http.Request) {
 
 		// Update refresh token in the storage, store the token and mark as complete
 		if err := s.storage.UpdateDeviceToken(deviceReq.DeviceCode, updater); err != nil {
-			s.logger.Errorf("failed to update device token: %v", err)
+			s.logger.Error("failed to update device token", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "")
 			return
 		}
 
 		if err := s.templates.deviceSuccess(r, w, client.Name); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("Server template error", "err", err)
 			s.renderError(r, w, http.StatusNotFound, "Page not found")
 		}
 
@@ -396,7 +395,7 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) {
 	case http.MethodPost:
 		err := r.ParseForm()
 		if err != nil {
-			s.logger.Warnf("Could not parse user code verification request body : %v", err)
+			s.logger.Warn("could not parse user code verification request body", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "")
 			return
 		}
@@ -413,10 +412,10 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) {
 		deviceRequest, err := s.storage.GetDeviceRequest(userCode)
 		if err != nil || s.now().After(deviceRequest.Expiry) {
 			if err != nil && err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get device request: %v", err)
+				s.logger.Error("failed to get device request", "err", err)
 			}
 			if err := s.templates.device(r, w, s.getDeviceVerificationURI(), userCode, true); err != nil {
-				s.logger.Errorf("Server template error: %v", err)
+				s.logger.Error("Server template error", "err", err)
 				s.renderError(r, w, http.StatusNotFound, "Page not found")
 			}
 			return
diff --git a/server/handlers.go b/server/handlers.go
index ccd534d9910d27941ea970971a7e4d34b0b3b922..42f3ebe5d581e4369bbdb29690420996763ca38c 100644
--- a/server/handlers.go
+++ b/server/handlers.go
@@ -35,13 +35,13 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
 	// TODO(ericchiang): Cache this.
 	keys, err := s.storage.GetKeys()
 	if err != nil {
-		s.logger.Errorf("failed to get keys: %v", err)
+		s.logger.Error("failed to get keys", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		return
 	}
 
 	if keys.SigningKeyPub == nil {
-		s.logger.Errorf("No public keys found.")
+		s.logger.Error("no public keys found.")
 		s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		return
 	}
@@ -56,7 +56,7 @@ func (s *Server) handlePublicKeys(w http.ResponseWriter, r *http.Request) {
 
 	data, err := json.MarshalIndent(jwks, "", "  ")
 	if err != nil {
-		s.logger.Errorf("failed to marshal discovery data: %v", err)
+		s.logger.Error("failed to marshal discovery data", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		return
 	}
@@ -132,7 +132,7 @@ func (s *Server) discoveryHandler() (http.HandlerFunc, error) {
 func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 	// Extract the arguments
 	if err := r.ParseForm(); err != nil {
-		s.logger.Errorf("Failed to parse arguments: %v", err)
+		s.logger.Error("failed to parse arguments", "err", err)
 
 		s.renderError(r, w, http.StatusBadRequest, err.Error())
 		return
@@ -142,7 +142,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 
 	connectors, err := s.storage.ListConnectors()
 	if err != nil {
-		s.logger.Errorf("Failed to get list of connectors: %v", err)
+		s.logger.Error("failed to get list of connectors", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve connector list.")
 		return
 	}
@@ -185,7 +185,7 @@ func (s *Server) handleAuthorization(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if err := s.templates.login(r, w, connectorInfos); err != nil {
-		s.logger.Errorf("Server template error: %v", err)
+		s.logger.Error("server template error", "err", err)
 	}
 }
 
@@ -193,7 +193,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 	ctx := r.Context()
 	authReq, err := s.parseAuthorizationRequest(r)
 	if err != nil {
-		s.logger.Errorf("Failed to parse authorization request: %v", err)
+		s.logger.Error("failed to parse authorization request", "err", err)
 
 		switch authErr := err.(type) {
 		case *redirectedAuthErr:
@@ -209,22 +209,22 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 
 	connID, err := url.PathUnescape(mux.Vars(r)["connector"])
 	if err != nil {
-		s.logger.Errorf("Failed to parse connector: %v", err)
+		s.logger.Error("failed to parse connector", "err", err)
 		s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
 		return
 	}
 
 	conn, err := s.getConnector(connID)
 	if err != nil {
-		s.logger.Errorf("Failed to get connector: %v", err)
+		s.logger.Error("Failed to get connector", "err", err)
 		s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
 		return
 	}
 
 	// Set the connector being used for the login.
 	if authReq.ConnectorID != "" && authReq.ConnectorID != connID {
-		s.logger.Errorf("Mismatched connector ID in auth request: %s vs %s",
-			authReq.ConnectorID, connID)
+		s.logger.Error("mismatched connector ID in auth request",
+			"auth_request_connector_id", authReq.ConnectorID, "connector_id", connID)
 		s.renderError(r, w, http.StatusBadRequest, "Bad connector ID")
 		return
 	}
@@ -234,7 +234,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 	// Actually create the auth request
 	authReq.Expiry = s.now().Add(s.authRequestsValidFor)
 	if err := s.storage.CreateAuthRequest(ctx, *authReq); err != nil {
-		s.logger.Errorf("Failed to create authorization request: %v", err)
+		s.logger.Error("failed to create authorization request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Failed to connect to the database.")
 		return
 	}
@@ -260,7 +260,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 			// TODO(ericchiang): Is this appropriate or should we also be using a nonce?
 			callbackURL, err := conn.LoginURL(scopes, s.absURL("/callback"), authReq.ID)
 			if err != nil {
-				s.logger.Errorf("Connector %q returned error when creating callback: %v", connID, err)
+				s.logger.Error("connector returned error when creating callback", "connector_id", connID, "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 				return
 			}
@@ -278,7 +278,7 @@ func (s *Server) handleConnectorLogin(w http.ResponseWriter, r *http.Request) {
 		case connector.SAMLConnector:
 			action, value, err := conn.POSTData(scopes, authReq.ID)
 			if err != nil {
-				s.logger.Errorf("Creating SAML data: %v", err)
+				s.logger.Error("creating SAML data", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Connector Login Error")
 				return
 			}
@@ -321,36 +321,36 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 	authReq, err := s.storage.GetAuthRequest(authID)
 	if err != nil {
 		if err == storage.ErrNotFound {
-			s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
+			s.logger.Error("invalid 'state' parameter provided", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
 			return
 		}
-		s.logger.Errorf("Failed to get auth request: %v", err)
+		s.logger.Error("failed to get auth request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Database error.")
 		return
 	}
 
 	connID, err := url.PathUnescape(mux.Vars(r)["connector"])
 	if err != nil {
-		s.logger.Errorf("Failed to parse connector: %v", err)
+		s.logger.Error("failed to parse connector", "err", err)
 		s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist")
 		return
 	} else if connID != "" && connID != authReq.ConnectorID {
-		s.logger.Errorf("Connector mismatch: authentication started with id %q, but password login for id %q was triggered", authReq.ConnectorID, connID)
+		s.logger.Error("connector mismatch: password login triggered for different connector from authentication start", "start_connector_id", authReq.ConnectorID, "password_connector_id", connID)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
 
 	conn, err := s.getConnector(authReq.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
+		s.logger.Error("failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
 
 	pwConn, ok := conn.Connector.(connector.PasswordConnector)
 	if !ok {
-		s.logger.Errorf("Expected password connector in handlePasswordLogin(), but got %v", pwConn)
+		s.logger.Error("expected password connector in handlePasswordLogin()", "password_connector", pwConn)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
@@ -358,7 +358,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 	switch r.Method {
 	case http.MethodGet:
 		if err := s.templates.password(r, w, r.URL.String(), "", usernamePrompt(pwConn), false, backLink); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("server template error", "err", err)
 		}
 	case http.MethodPost:
 		username := r.FormValue("login")
@@ -367,20 +367,20 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 
 		identity, ok, err := pwConn.Login(ctx, scopes, username, password)
 		if err != nil {
-			s.logger.Errorf("Failed to login user: %v", err)
+			s.logger.Error("failed to login user", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Login error: %v", err))
 			return
 		}
 		if !ok {
 			if err := s.templates.password(r, w, r.URL.String(), username, usernamePrompt(pwConn), true, backLink); err != nil {
-				s.logger.Errorf("Server template error: %v", err)
+				s.logger.Error("server template error", "err", err)
 			}
-			s.logger.Errorf("Failed login attempt for user: %q. Invalid credentials.", username)
+			s.logger.Error("failed login attempt: Invalid credentials.", "user", username)
 			return
 		}
 		redirectURL, canSkipApproval, err := s.finalizeLogin(ctx, identity, authReq, conn.Connector)
 		if err != nil {
-			s.logger.Errorf("Failed to finalize login: %v", err)
+			s.logger.Error("failed to finalize login", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 			return
 		}
@@ -388,7 +388,7 @@ func (s *Server) handlePasswordLogin(w http.ResponseWriter, r *http.Request) {
 		if canSkipApproval {
 			authReq, err = s.storage.GetAuthRequest(authReq.ID)
 			if err != nil {
-				s.logger.Errorf("Failed to get finalized auth request: %v", err)
+				s.logger.Error("failed to get finalized auth request", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 				return
 			}
@@ -424,29 +424,29 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	authReq, err := s.storage.GetAuthRequest(authID)
 	if err != nil {
 		if err == storage.ErrNotFound {
-			s.logger.Errorf("Invalid 'state' parameter provided: %v", err)
+			s.logger.Error("invalid 'state' parameter provided", "err", err)
 			s.renderError(r, w, http.StatusBadRequest, "Requested resource does not exist.")
 			return
 		}
-		s.logger.Errorf("Failed to get auth request: %v", err)
+		s.logger.Error("failed to get auth request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Database error.")
 		return
 	}
 
 	connID, err := url.PathUnescape(mux.Vars(r)["connector"])
 	if err != nil {
-		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
+		s.logger.Error("failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	} else if connID != "" && connID != authReq.ConnectorID {
-		s.logger.Errorf("Connector mismatch: authentication started with id %q, but callback for id %q was triggered", authReq.ConnectorID, connID)
+		s.logger.Error("connector mismatch: callback triggered for different connector than authentication start", "authentication_start_connector_id", authReq.ConnectorID, "connector_id", connID)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
 
 	conn, err := s.getConnector(authReq.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("Failed to get connector with id %q : %v", authReq.ConnectorID, err)
+		s.logger.Error("failed to get connector", "connector_id", authReq.ConnectorID, "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Requested resource does not exist.")
 		return
 	}
@@ -455,14 +455,14 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	switch conn := conn.Connector.(type) {
 	case connector.CallbackConnector:
 		if r.Method != http.MethodGet {
-			s.logger.Errorf("SAML request mapped to OAuth2 connector")
+			s.logger.Error("SAML request mapped to OAuth2 connector")
 			s.renderError(r, w, http.StatusBadRequest, "Invalid request")
 			return
 		}
 		identity, err = conn.HandleCallback(parseScopes(authReq.Scopes), r)
 	case connector.SAMLConnector:
 		if r.Method != http.MethodPost {
-			s.logger.Errorf("OAuth2 request mapped to SAML connector")
+			s.logger.Error("OAuth2 request mapped to SAML connector")
 			s.renderError(r, w, http.StatusBadRequest, "Invalid request")
 			return
 		}
@@ -473,14 +473,14 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	}
 
 	if err != nil {
-		s.logger.Errorf("Failed to authenticate: %v", err)
+		s.logger.Error("failed to authenticate", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, fmt.Sprintf("Failed to authenticate: %v", err))
 		return
 	}
 
 	redirectURL, canSkipApproval, err := s.finalizeLogin(ctx, identity, authReq, conn.Connector)
 	if err != nil {
-		s.logger.Errorf("Failed to finalize login: %v", err)
+		s.logger.Error("failed to finalize login", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 		return
 	}
@@ -488,7 +488,7 @@ func (s *Server) handleConnectorCallback(w http.ResponseWriter, r *http.Request)
 	if canSkipApproval {
 		authReq, err = s.storage.GetAuthRequest(authReq.ID)
 		if err != nil {
-			s.logger.Errorf("Failed to get finalized auth request: %v", err)
+			s.logger.Error("failed to get finalized auth request", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Login error.")
 			return
 		}
@@ -526,8 +526,9 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 		email += " (unverified)"
 	}
 
-	s.logger.Infof("login successful: connector %q, username=%q, preferred_username=%q, email=%q, groups=%q",
-		authReq.ConnectorID, claims.Username, claims.PreferredUsername, email, claims.Groups)
+	s.logger.Info("login successful",
+		"connector_id", authReq.ConnectorID, "username", claims.Username,
+		"preferred_username", claims.PreferredUsername, "email", email, "groups", claims.Groups)
 
 	// we can skip the redirect to /approval and go ahead and send code if it's not required
 	if s.skipApproval && !authReq.ForceApprovalPrompt {
@@ -561,7 +562,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 	session, err := s.storage.GetOfflineSessions(identity.UserID, authReq.ConnectorID)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get offline session: %v", err)
+			s.logger.Error("failed to get offline session", "err", err)
 			return "", false, err
 		}
 		offlineSessions := storage.OfflineSessions{
@@ -574,7 +575,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 		// Create a new OfflineSession object for the user and add a reference object for
 		// the newly received refreshtoken.
 		if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
-			s.logger.Errorf("failed to create offline session: %v", err)
+			s.logger.Error("failed to create offline session", "err", err)
 			return "", false, err
 		}
 
@@ -588,7 +589,7 @@ func (s *Server) finalizeLogin(ctx context.Context, identity connector.Identity,
 		}
 		return old, nil
 	}); err != nil {
-		s.logger.Errorf("failed to update offline session: %v", err)
+		s.logger.Error("failed to update offline session", "err", err)
 		return "", false, err
 	}
 
@@ -609,12 +610,12 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 
 	authReq, err := s.storage.GetAuthRequest(r.FormValue("req"))
 	if err != nil {
-		s.logger.Errorf("Failed to get auth request: %v", err)
+		s.logger.Error("failed to get auth request", "err", err)
 		s.renderError(r, w, http.StatusInternalServerError, "Database error.")
 		return
 	}
 	if !authReq.LoggedIn {
-		s.logger.Errorf("Auth request does not have an identity for approval")
+		s.logger.Error("auth request does not have an identity for approval")
 		s.renderError(r, w, http.StatusInternalServerError, "Login process not yet finalized.")
 		return
 	}
@@ -633,12 +634,12 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
 	case http.MethodGet:
 		client, err := s.storage.GetClient(authReq.ClientID)
 		if err != nil {
-			s.logger.Errorf("Failed to get client %q: %v", authReq.ClientID, err)
+			s.logger.Error("Failed to get client", "client_id", authReq.ClientID, "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Failed to retrieve client.")
 			return
 		}
 		if err := s.templates.approval(r, w, authReq.ID, authReq.Claims.Username, client.Name, authReq.Scopes); err != nil {
-			s.logger.Errorf("Server template error: %v", err)
+			s.logger.Error("server template error", "err", err)
 		}
 	case http.MethodPost:
 		if r.FormValue("approval") != "approve" {
@@ -658,7 +659,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 
 	if err := s.storage.DeleteAuthRequest(authReq.ID); err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("Failed to delete authorization request: %v", err)
+			s.logger.Error("Failed to delete authorization request", "err", err)
 			s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 		} else {
 			s.renderError(r, w, http.StatusBadRequest, "User session error.")
@@ -704,7 +705,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 				PKCE:          authReq.PKCE,
 			}
 			if err := s.storage.CreateAuthCode(ctx, code); err != nil {
-				s.logger.Errorf("Failed to create auth code: %v", err)
+				s.logger.Error("Failed to create auth code", "err", err)
 				s.renderError(r, w, http.StatusInternalServerError, "Internal server error.")
 				return
 			}
@@ -713,7 +714,7 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 			// rejected earlier. If we got here we're using the code flow.
 			if authReq.RedirectURI == redirectURIOOB {
 				if err := s.templates.oob(r, w, code.ID); err != nil {
-					s.logger.Errorf("Server template error: %v", err)
+					s.logger.Error("server template error", "err", err)
 				}
 				return
 			}
@@ -725,14 +726,14 @@ func (s *Server) sendCodeResponse(w http.ResponseWriter, r *http.Request, authRe
 
 			accessToken, _, err = s.newAccessToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, authReq.ConnectorID)
 			if err != nil {
-				s.logger.Errorf("failed to create new access token: %v", err)
+				s.logger.Error("failed to create new access token", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				return
 			}
 
 			idToken, idTokenExpiry, err = s.newIDToken(authReq.ClientID, authReq.Claims, authReq.Scopes, authReq.Nonce, accessToken, code.ID, authReq.ConnectorID)
 			if err != nil {
-				s.logger.Errorf("failed to create ID token: %v", err)
+				s.logger.Error("failed to create ID token", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				return
 			}
@@ -807,7 +808,7 @@ func (s *Server) withClientFromStorage(w http.ResponseWriter, r *http.Request, h
 	client, err := s.storage.GetClient(clientID)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get client: %v", err)
+			s.logger.Error("failed to get client", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		} else {
 			s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
@@ -817,9 +818,9 @@ func (s *Server) withClientFromStorage(w http.ResponseWriter, r *http.Request, h
 
 	if subtle.ConstantTimeCompare([]byte(client.Secret), []byte(clientSecret)) != 1 {
 		if clientSecret == "" {
-			s.logger.Infof("missing client_secret on token request for client: %s", client.ID)
+			s.logger.Info("missing client_secret on token request", "client_id", client.ID)
 		} else {
-			s.logger.Infof("invalid client_secret on token request for client: %s", client.ID)
+			s.logger.Info("invalid client_secret on token request", "client_id", client.ID)
 		}
 		s.tokenErrHelper(w, errInvalidClient, "Invalid client credentials.", http.StatusUnauthorized)
 		return
@@ -837,14 +838,14 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
 
 	err := r.ParseForm()
 	if err != nil {
-		s.logger.Errorf("Could not parse request body: %v", err)
+		s.logger.Error("could not parse request body", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
 		return
 	}
 
 	grantType := r.PostFormValue("grant_type")
 	if !contains(s.supportedGrantTypes, grantType) {
-		s.logger.Errorf("unsupported grant type: %v", grantType)
+		s.logger.Error("unsupported grant type", "grant_type", grantType)
 		s.tokenErrHelper(w, errUnsupportedGrantType, "", http.StatusBadRequest)
 		return
 	}
@@ -890,7 +891,7 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 	authCode, err := s.storage.GetAuthCode(code)
 	if err != nil || s.now().After(authCode.Expiry) || authCode.ClientID != client.ID {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get auth code: %v", err)
+			s.logger.Error("failed to get auth code", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		} else {
 			s.tokenErrHelper(w, errInvalidGrant, "Invalid or expired code parameter.", http.StatusBadRequest)
@@ -906,7 +907,7 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 	case providedCodeVerifier != "" && codeChallengeFromStorage != "":
 		calculatedCodeChallenge, err := s.calculateCodeChallenge(providedCodeVerifier, authCode.PKCE.CodeChallengeMethod)
 		if err != nil {
-			s.logger.Error(err)
+			s.logger.Error("failed to calculate code challenge", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return
 		}
@@ -940,20 +941,20 @@ func (s *Server) handleAuthCode(w http.ResponseWriter, r *http.Request, client s
 func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, authCode storage.AuthCode, client storage.Client) (*accessTokenResponse, error) {
 	accessToken, _, err := s.newAccessToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, authCode.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create new access token: %v", err)
+		s.logger.Error("failed to create new access token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return nil, err
 	}
 
 	idToken, expiry, err := s.newIDToken(client.ID, authCode.Claims, authCode.Scopes, authCode.Nonce, accessToken, authCode.ID, authCode.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create ID token: %v", err)
+		s.logger.Error("failed to create ID token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return nil, err
 	}
 
 	if err := s.storage.DeleteAuthCode(authCode.ID); err != nil {
-		s.logger.Errorf("failed to delete auth code: %v", err)
+		s.logger.Error("failed to delete auth code", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return nil, err
 	}
@@ -964,7 +965,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 		// Connectors like `saml` do not implement RefreshConnector.
 		conn, err := s.getConnector(authCode.ConnectorID)
 		if err != nil {
-			s.logger.Errorf("connector with ID %q not found: %v", authCode.ConnectorID, err)
+			s.logger.Error("connector not found", "connector_id", authCode.ConnectorID, "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return false
 		}
@@ -1000,13 +1001,13 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			Token:     refresh.Token,
 		}
 		if refreshToken, err = internal.Marshal(token); err != nil {
-			s.logger.Errorf("failed to marshal refresh token: %v", err)
+			s.logger.Error("failed to marshal refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return nil, err
 		}
 
 		if err := s.storage.CreateRefresh(ctx, refresh); err != nil {
-			s.logger.Errorf("failed to create refresh token: %v", err)
+			s.logger.Error("failed to create refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return nil, err
 		}
@@ -1019,7 +1020,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			if deleteToken {
 				// Delete newly created refresh token from storage.
 				if err := s.storage.DeleteRefresh(refresh.ID); err != nil {
-					s.logger.Errorf("failed to delete refresh token: %v", err)
+					s.logger.Error("failed to delete refresh token", "err", err)
 					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 					return
 				}
@@ -1036,7 +1037,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 		// Try to retrieve an existing OfflineSession object for the corresponding user.
 		if session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID); err != nil {
 			if err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get offline session: %v", err)
+				s.logger.Error("failed to get offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return nil, err
@@ -1051,7 +1052,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			// Create a new OfflineSession object for the user and add a reference object for
 			// the newly received refreshtoken.
 			if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
-				s.logger.Errorf("failed to create offline session: %v", err)
+				s.logger.Error("failed to create offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return nil, err
@@ -1060,7 +1061,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 			if oldTokenRef, ok := session.Refresh[tokenRef.ClientID]; ok {
 				// Delete old refresh token from storage.
 				if err := s.storage.DeleteRefresh(oldTokenRef.ID); err != nil && err != storage.ErrNotFound {
-					s.logger.Errorf("failed to delete refresh token: %v", err)
+					s.logger.Error("failed to delete refresh token", "err", err)
 					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 					deleteToken = true
 					return nil, err
@@ -1072,7 +1073,7 @@ func (s *Server) exchangeAuthCode(ctx context.Context, w http.ResponseWriter, au
 				old.Refresh[tokenRef.ClientID] = &tokenRef
 				return old, nil
 			}); err != nil {
-				s.logger.Errorf("failed to update offline session: %v", err)
+				s.logger.Error("failed to update offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return nil, err
@@ -1184,7 +1185,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 	password := q.Get("password")
 	identity, ok, err := passwordConnector.Login(ctx, parseScopes(scopes), username, password)
 	if err != nil {
-		s.logger.Errorf("Failed to login user: %v", err)
+		s.logger.Error("failed to login user", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "Could not login user", http.StatusBadRequest)
 		return
 	}
@@ -1205,14 +1206,14 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 
 	accessToken, _, err := s.newAccessToken(client.ID, claims, scopes, nonce, connID)
 	if err != nil {
-		s.logger.Errorf("password grant failed to create new access token: %v", err)
+		s.logger.Error("password grant failed to create new access token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
 
 	idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, nonce, accessToken, "", connID)
 	if err != nil {
-		s.logger.Errorf("password grant failed to create new ID token: %v", err)
+		s.logger.Error("password grant failed to create new ID token", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
@@ -1252,13 +1253,13 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 			Token:     refresh.Token,
 		}
 		if refreshToken, err = internal.Marshal(token); err != nil {
-			s.logger.Errorf("failed to marshal refresh token: %v", err)
+			s.logger.Error("failed to marshal refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return
 		}
 
 		if err := s.storage.CreateRefresh(ctx, refresh); err != nil {
-			s.logger.Errorf("failed to create refresh token: %v", err)
+			s.logger.Error("failed to create refresh token", "err", err)
 			s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 			return
 		}
@@ -1271,7 +1272,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 			if deleteToken {
 				// Delete newly created refresh token from storage.
 				if err := s.storage.DeleteRefresh(refresh.ID); err != nil {
-					s.logger.Errorf("failed to delete refresh token: %v", err)
+					s.logger.Error("failed to delete refresh token", "err", err)
 					s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 					return
 				}
@@ -1288,7 +1289,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 		// Try to retrieve an existing OfflineSession object for the corresponding user.
 		if session, err := s.storage.GetOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID); err != nil {
 			if err != storage.ErrNotFound {
-				s.logger.Errorf("failed to get offline session: %v", err)
+				s.logger.Error("failed to get offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return
@@ -1304,7 +1305,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 			// Create a new OfflineSession object for the user and add a reference object for
 			// the newly received refreshtoken.
 			if err := s.storage.CreateOfflineSessions(ctx, offlineSessions); err != nil {
-				s.logger.Errorf("failed to create offline session: %v", err)
+				s.logger.Error("failed to create offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return
@@ -1314,9 +1315,9 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 				// Delete old refresh token from storage.
 				if err := s.storage.DeleteRefresh(oldTokenRef.ID); err != nil {
 					if err == storage.ErrNotFound {
-						s.logger.Warnf("database inconsistent, refresh token missing: %v", oldTokenRef.ID)
+						s.logger.Warn("database inconsistent, refresh token missing", "token_id", oldTokenRef.ID)
 					} else {
-						s.logger.Errorf("failed to delete refresh token: %v", err)
+						s.logger.Error("failed to delete refresh token", "err", err)
 						s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 						deleteToken = true
 						return
@@ -1330,7 +1331,7 @@ func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, cli
 				old.ConnectorData = identity.ConnectorData
 				return old, nil
 			}); err != nil {
-				s.logger.Errorf("failed to update offline session: %v", err)
+				s.logger.Error("failed to update offline session", "err", err)
 				s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 				deleteToken = true
 				return
@@ -1346,7 +1347,7 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
 	ctx := r.Context()
 
 	if err := r.ParseForm(); err != nil {
-		s.logger.Errorf("could not parse request body: %v", err)
+		s.logger.Error("could not parse request body", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "", http.StatusBadRequest)
 		return
 	}
@@ -1375,19 +1376,19 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
 
 	conn, err := s.getConnector(connID)
 	if err != nil {
-		s.logger.Errorf("failed to get connector: %v", err)
+		s.logger.Error("failed to get connector", "err", err)
 		s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest)
 		return
 	}
 	teConn, ok := conn.Connector.(connector.TokenIdentityConnector)
 	if !ok {
-		s.logger.Errorf("connector doesn't implement token exchange: %v", connID)
+		s.logger.Error("connector doesn't implement token exchange", "connector_id", connID)
 		s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest)
 		return
 	}
 	identity, err := teConn.TokenIdentity(ctx, subjectTokenType, subjectToken)
 	if err != nil {
-		s.logger.Errorf("failed to verify subject token: %v", err)
+		s.logger.Error("failed to verify subject token", "err", err)
 		s.tokenErrHelper(w, errAccessDenied, "", http.StatusUnauthorized)
 		return
 	}
@@ -1415,7 +1416,7 @@ func (s *Server) handleTokenExchange(w http.ResponseWriter, r *http.Request, cli
 		return
 	}
 	if err != nil {
-		s.logger.Errorf("token exchange failed to create new %v token: %v", requestedTokenType, err)
+		s.logger.Error("token exchange failed to create new token", "requested_token_type", requestedTokenType, "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
@@ -1451,7 +1452,7 @@ func (s *Server) toAccessTokenResponse(idToken, accessToken, refreshToken string
 func (s *Server) writeAccessToken(w http.ResponseWriter, resp *accessTokenResponse) {
 	data, err := json.Marshal(resp)
 	if err != nil {
-		s.logger.Errorf("failed to marshal access token response: %v", err)
+		s.logger.Error("failed to marshal access token response", "err", err)
 		s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError)
 		return
 	}
@@ -1466,13 +1467,13 @@ func (s *Server) writeAccessToken(w http.ResponseWriter, resp *accessTokenRespon
 
 func (s *Server) renderError(r *http.Request, w http.ResponseWriter, status int, description string) {
 	if err := s.templates.err(r, w, status, description); err != nil {
-		s.logger.Errorf("server template error: %v", err)
+		s.logger.Error("server template error", "err", err)
 	}
 }
 
 func (s *Server) tokenErrHelper(w http.ResponseWriter, typ string, description string, statusCode int) {
 	if err := tokenErr(w, typ, description, statusCode); err != nil {
-		s.logger.Errorf("token error response: %v", err)
+		s.logger.Error("token error response", "err", err)
 	}
 }
 
diff --git a/server/introspectionhandler.go b/server/introspectionhandler.go
index f0d1f807cace513772fc44847c52b0507148a0b8..8c6e4419f3871f084d7129aed894c10714a38f24 100644
--- a/server/introspectionhandler.go
+++ b/server/introspectionhandler.go
@@ -179,14 +179,14 @@ func (s *Server) getTokenFromRequest(r *http.Request) (string, TokenTypeEnum, er
 	token := r.PostForm.Get("token")
 	tokenType, err := s.guessTokenType(r.Context(), token)
 	if err != nil {
-		s.logger.Error(err)
+		s.logger.Error("failed to guess token type", "err", err)
 		return "", 0, newIntrospectInternalServerError()
 	}
 
 	requestTokenType := r.PostForm.Get("token_type_hint")
 	if requestTokenType != "" {
 		if tokenType.String() != requestTokenType {
-			s.logger.Warnf("Token type hint doesn't match token type: %s != %s", requestTokenType, tokenType)
+			s.logger.Warn("token type hint doesn't match token type", "request_token_type", requestTokenType, "token_type", tokenType)
 		}
 	}
 
@@ -211,13 +211,13 @@ func (s *Server) introspectRefreshToken(_ context.Context, token string) (*Intro
 			return nil, newIntrospectInactiveTokenError()
 		}
 
-		s.logger.Errorf("failed to get refresh token: %v", err)
+		s.logger.Error("failed to get refresh token", "err", err)
 		return nil, newIntrospectInternalServerError()
 	}
 
 	subjectString, sErr := genSubject(rCtx.storageToken.Claims.UserID, rCtx.storageToken.ConnectorID)
 	if sErr != nil {
-		s.logger.Errorf("failed to marshal offline session ID: %v", err)
+		s.logger.Error("failed to marshal offline session ID", "err", err)
 		return nil, newIntrospectInternalServerError()
 	}
 
@@ -253,19 +253,19 @@ func (s *Server) introspectAccessToken(ctx context.Context, token string) (*Intr
 
 	var claims IntrospectionExtra
 	if err := idToken.Claims(&claims); err != nil {
-		s.logger.Errorf("Error while fetching token claims: %s", err.Error())
+		s.logger.Error("error while fetching token claims", "err", err.Error())
 		return nil, newIntrospectInternalServerError()
 	}
 
 	clientID, err := getClientID(idToken.Audience, claims.AuthorizingParty)
 	if err != nil {
-		s.logger.Error("Error while fetching client_id from token: %s", err.Error())
+		s.logger.Error("error while fetching client_id from token:", "err", err.Error())
 		return nil, newIntrospectInternalServerError()
 	}
 
 	client, err := s.storage.GetClient(clientID)
 	if err != nil {
-		s.logger.Error("Error while fetching client from storage: %s", err.Error())
+		s.logger.Error("error while fetching client from storage", "err", err.Error())
 		return nil, newIntrospectInternalServerError()
 	}
 
@@ -299,7 +299,7 @@ func (s *Server) handleIntrospect(w http.ResponseWriter, r *http.Request) {
 			introspect, err = s.introspectRefreshToken(ctx, token)
 		default:
 			// Token type is neither handled token types.
-			s.logger.Errorf("Unknown token type: %s", tokenType)
+			s.logger.Error("unknown token type", "token_type", tokenType)
 			introspectInactiveErr(w)
 			return
 		}
@@ -309,7 +309,7 @@ func (s *Server) handleIntrospect(w http.ResponseWriter, r *http.Request) {
 		if intErr, ok := err.(*introspectionError); ok {
 			s.introspectErrHelper(w, intErr.typ, intErr.desc, intErr.code)
 		} else {
-			s.logger.Errorf("An unknown error occurred: %s", err.Error())
+			s.logger.Error("an unknown error occurred", "err", err.Error())
 			s.introspectErrHelper(w, errServerError, "An unknown error occurred", http.StatusInternalServerError)
 		}
 
@@ -332,7 +332,7 @@ func (s *Server) introspectErrHelper(w http.ResponseWriter, typ string, descript
 	}
 
 	if err := tokenErr(w, typ, description, statusCode); err != nil {
-		s.logger.Errorf("introspect error response: %v", err)
+		s.logger.Error("introspect error response", "err", err)
 	}
 }
 
diff --git a/server/oauth2.go b/server/oauth2.go
index 3589e493ea7f13c451c55f9b81c67ebab68bc917..3d9cfc8fe7992c073770bfc96338db0940374c3a 100644
--- a/server/oauth2.go
+++ b/server/oauth2.go
@@ -353,7 +353,7 @@ func genSubject(userID string, connID string) (string, error) {
 func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []string, nonce, accessToken, code, connID string) (idToken string, expiry time.Time, err error) {
 	keys, err := s.storage.GetKeys()
 	if err != nil {
-		s.logger.Errorf("Failed to get keys: %v", err)
+		s.logger.Error("failed to get keys", "err", err)
 		return "", expiry, err
 	}
 
@@ -371,7 +371,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 
 	subjectString, err := genSubject(claims.UserID, connID)
 	if err != nil {
-		s.logger.Errorf("failed to marshal offline session ID: %v", err)
+		s.logger.Error("failed to marshal offline session ID", "err", err)
 		return "", expiry, fmt.Errorf("failed to marshal offline session ID: %v", err)
 	}
 
@@ -386,7 +386,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 	if accessToken != "" {
 		atHash, err := accessTokenHash(signingAlg, accessToken)
 		if err != nil {
-			s.logger.Errorf("error computing at_hash: %v", err)
+			s.logger.Error("error computing at_hash", "err", err)
 			return "", expiry, fmt.Errorf("error computing at_hash: %v", err)
 		}
 		tok.AccessTokenHash = atHash
@@ -395,7 +395,7 @@ func (s *Server) newIDToken(clientID string, claims storage.Claims, scopes []str
 	if code != "" {
 		cHash, err := accessTokenHash(signingAlg, code)
 		if err != nil {
-			s.logger.Errorf("error computing c_hash: %v", err)
+			s.logger.Error("error computing c_hash", "err", err)
 			return "", expiry, fmt.Errorf("error computing c_hash: #{err}")
 		}
 		tok.CodeHash = cHash
@@ -482,7 +482,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
 		if err == storage.ErrNotFound {
 			return nil, newDisplayedErr(http.StatusNotFound, "Invalid client_id (%q).", clientID)
 		}
-		s.logger.Errorf("Failed to get client: %v", err)
+		s.logger.Error("failed to get client", "err", err)
 		return nil, newDisplayedErr(http.StatusInternalServerError, "Database error.")
 	}
 
@@ -501,7 +501,7 @@ func (s *Server) parseAuthorizationRequest(r *http.Request) (*storage.AuthReques
 	if connectorID != "" {
 		connectors, err := s.storage.ListConnectors()
 		if err != nil {
-			s.logger.Errorf("Failed to list connectors: %v", err)
+			s.logger.Error("failed to list connectors", "err", err)
 			return nil, newRedirectedErr(errServerError, "Unable to retrieve connectors")
 		}
 		if !validateConnectorID(connectors, connectorID) {
@@ -637,7 +637,7 @@ func (s *Server) validateCrossClientTrust(clientID, peerID string) (trusted bool
 	peer, err := s.storage.GetClient(peerID)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("Failed to get client: %v", err)
+			s.logger.Error("failed to get client", "err", err)
 			return false, err
 		}
 		return false, nil
diff --git a/server/refreshhandlers.go b/server/refreshhandlers.go
index cb53802b87d4947d0cb8dac0b751e4b0e160617c..01a0f435b6422597f18e57388a88aad8632e99e4 100644
--- a/server/refreshhandlers.go
+++ b/server/refreshhandlers.go
@@ -87,7 +87,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 	refresh, err := s.storage.GetRefresh(token.RefreshId)
 	if err != nil {
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get refresh token: %v", err)
+			s.logger.Error("failed to get refresh token", "err", err)
 			return nil, newInternalServerError()
 		}
 		return nil, invalidErr
@@ -95,7 +95,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 
 	// Only check ClientID if it was provided;
 	if clientID != nil && (refresh.ClientID != *clientID) {
-		s.logger.Errorf("client %s trying to claim token for client %s", clientID, refresh.ClientID)
+		s.logger.Error("trying to claim token for different client", "client_id", clientID, "refresh_client_id", refresh.ClientID)
 		// According to https://datatracker.ietf.org/doc/html/rfc6749#section-5.2 Dex should respond with an
 		//  invalid grant error if token has already been claimed by another client.
 		return nil, &refreshError{msg: errInvalidGrant, desc: invalidErr.desc, code: http.StatusBadRequest}
@@ -108,18 +108,18 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 		case refresh.ObsoleteToken != token.Token:
 			fallthrough
 		case refresh.ObsoleteToken == "":
-			s.logger.Errorf("refresh token with id %s claimed twice", refresh.ID)
+			s.logger.Error("refresh token claimed twice", "token_id", refresh.ID)
 			return nil, invalidErr
 		}
 	}
 
 	if s.refreshTokenPolicy.CompletelyExpired(refresh.CreatedAt) {
-		s.logger.Errorf("refresh token with id %s expired", refresh.ID)
+		s.logger.Error("refresh token expired", "token_id", refresh.ID)
 		return nil, expiredErr
 	}
 
 	if s.refreshTokenPolicy.ExpiredBecauseUnused(refresh.LastUsed) {
-		s.logger.Errorf("refresh token with id %s expired due to inactivity", refresh.ID)
+		s.logger.Error("refresh token expired due to inactivity", "token_id", refresh.ID)
 		return nil, expiredErr
 	}
 
@@ -128,7 +128,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 	// Get Connector
 	refreshCtx.connector, err = s.getConnector(refresh.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("connector with ID %q not found: %v", refresh.ConnectorID, err)
+		s.logger.Error("connector not found", "connector_id", refresh.ConnectorID, "err", err)
 		return nil, newInternalServerError()
 	}
 
@@ -137,7 +137,7 @@ func (s *Server) getRefreshTokenFromStorage(clientID *string, token *internal.Re
 	switch {
 	case err != nil:
 		if err != storage.ErrNotFound {
-			s.logger.Errorf("failed to get offline session: %v", err)
+			s.logger.Error("failed to get offline session", "err", err)
 			return nil, newInternalServerError()
 		}
 	case len(refresh.ConnectorData) > 0:
@@ -191,11 +191,11 @@ func (s *Server) refreshWithConnector(ctx context.Context, rCtx *refreshContext,
 	if refreshConn, ok := rCtx.connector.Connector.(connector.RefreshConnector); ok {
 		// Set connector data to the one received from an offline session
 		ident.ConnectorData = rCtx.connectorData
-		s.logger.Debugf("connector data before refresh: %s", ident.ConnectorData)
+		s.logger.Debug("connector data before refresh", "connector_data", ident.ConnectorData)
 
 		newIdent, err := refreshConn.Refresh(ctx, parseScopes(rCtx.scopes), ident)
 		if err != nil {
-			s.logger.Errorf("failed to refresh identity: %v", err)
+			s.logger.Error("failed to refresh identity", "err", err)
 			return ident, newInternalServerError()
 		}
 
@@ -216,7 +216,7 @@ func (s *Server) updateOfflineSession(refresh *storage.RefreshToken, ident conne
 			old.ConnectorData = ident.ConnectorData
 		}
 
-		s.logger.Debugf("saved connector data: %s %s", ident.UserID, ident.ConnectorData)
+		s.logger.Debug("saved connector data", "user_id", ident.UserID, "connector_data", ident.ConnectorData)
 
 		return old, nil
 	}
@@ -225,7 +225,7 @@ func (s *Server) updateOfflineSession(refresh *storage.RefreshToken, ident conne
 	// in offline session for the user.
 	err := s.storage.UpdateOfflineSessions(refresh.Claims.UserID, refresh.ConnectorID, offlineSessionUpdater)
 	if err != nil {
-		s.logger.Errorf("failed to update offline session: %v", err)
+		s.logger.Error("failed to update offline session", "err", err)
 		return newInternalServerError()
 	}
 
@@ -316,7 +316,7 @@ func (s *Server) updateRefreshToken(ctx context.Context, rCtx *refreshContext) (
 	// Update refresh token in the storage.
 	err := s.storage.UpdateRefreshToken(rCtx.storageToken.ID, refreshTokenUpdater)
 	if err != nil {
-		s.logger.Errorf("failed to update refresh token: %v", err)
+		s.logger.Error("failed to update refresh token", "err", err)
 		return nil, ident, newInternalServerError()
 	}
 
@@ -366,21 +366,21 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie
 
 	accessToken, _, err := s.newAccessToken(client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, rCtx.storageToken.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create new access token: %v", err)
+		s.logger.Error("failed to create new access token", "err", err)
 		s.refreshTokenErrHelper(w, newInternalServerError())
 		return
 	}
 
 	idToken, expiry, err := s.newIDToken(client.ID, claims, rCtx.scopes, rCtx.storageToken.Nonce, accessToken, "", rCtx.storageToken.ConnectorID)
 	if err != nil {
-		s.logger.Errorf("failed to create ID token: %v", err)
+		s.logger.Error("failed to create ID token", "err", err)
 		s.refreshTokenErrHelper(w, newInternalServerError())
 		return
 	}
 
 	rawNewToken, err := internal.Marshal(newToken)
 	if err != nil {
-		s.logger.Errorf("failed to marshal refresh token: %v", err)
+		s.logger.Error("failed to marshal refresh token", "err", err)
 		s.refreshTokenErrHelper(w, newInternalServerError())
 		return
 	}
diff --git a/server/rotation.go b/server/rotation.go
index 77a9926ee3cb3a3c8e6049a4342f76cd89ac2898..dfd776d67773b3fa51be7c569d1272495305ff56 100644
--- a/server/rotation.go
+++ b/server/rotation.go
@@ -8,11 +8,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"time"
 
 	"github.com/go-jose/go-jose/v4"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -61,7 +61,7 @@ type keyRotator struct {
 	strategy rotationStrategy
 	now      func() time.Time
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 // startKeyRotation begins key rotation in a new goroutine, closing once the context is canceled.
@@ -74,9 +74,9 @@ func (s *Server) startKeyRotation(ctx context.Context, strategy rotationStrategy
 	// Try to rotate immediately so properly configured storages will have keys.
 	if err := rotator.rotate(); err != nil {
 		if err == errAlreadyRotated {
-			s.logger.Infof("Key rotation not needed: %v", err)
+			s.logger.Info("key rotation not needed", "err", err)
 		} else {
-			s.logger.Errorf("failed to rotate keys: %v", err)
+			s.logger.Error("failed to rotate keys", "err", err)
 		}
 	}
 
@@ -87,7 +87,7 @@ func (s *Server) startKeyRotation(ctx context.Context, strategy rotationStrategy
 				return
 			case <-time.After(time.Second * 30):
 				if err := rotator.rotate(); err != nil {
-					s.logger.Errorf("failed to rotate keys: %v", err)
+					s.logger.Error("failed to rotate keys", "err", err)
 				}
 			}
 		}
@@ -102,7 +102,7 @@ func (k keyRotator) rotate() error {
 	if k.now().Before(keys.NextRotation) {
 		return nil
 	}
-	k.logger.Infof("keys expired, rotating")
+	k.logger.Info("keys expired, rotating")
 
 	// Generate the key outside of a storage transaction.
 	key, err := k.strategy.key()
@@ -174,7 +174,7 @@ func (k keyRotator) rotate() error {
 	if err != nil {
 		return err
 	}
-	k.logger.Infof("keys rotated, next rotation: %s", nextRotation)
+	k.logger.Info("keys rotated", "next_rotation", nextRotation)
 	return nil
 }
 
@@ -187,10 +187,10 @@ type RefreshTokenPolicy struct {
 
 	now func() time.Time
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
-func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor, absoluteLifetime, reuseInterval string) (*RefreshTokenPolicy, error) {
+func NewRefreshTokenPolicy(logger *slog.Logger, rotation bool, validIfNotUsedFor, absoluteLifetime, reuseInterval string) (*RefreshTokenPolicy, error) {
 	r := RefreshTokenPolicy{now: time.Now, logger: logger}
 	var err error
 
@@ -199,7 +199,7 @@ func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor,
 		if err != nil {
 			return nil, fmt.Errorf("invalid config value %q for refresh token valid if not used for: %v", validIfNotUsedFor, err)
 		}
-		logger.Infof("config refresh tokens valid if not used for: %v", validIfNotUsedFor)
+		logger.Info("config refresh tokens", "valid_if_not_used_for", validIfNotUsedFor)
 	}
 
 	if absoluteLifetime != "" {
@@ -207,7 +207,7 @@ func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor,
 		if err != nil {
 			return nil, fmt.Errorf("invalid config value %q for refresh tokens absolute lifetime: %v", absoluteLifetime, err)
 		}
-		logger.Infof("config refresh tokens absolute lifetime: %v", absoluteLifetime)
+		logger.Info("config refresh tokens", "absolute_lifetime", absoluteLifetime)
 	}
 
 	if reuseInterval != "" {
@@ -215,11 +215,11 @@ func NewRefreshTokenPolicy(logger log.Logger, rotation bool, validIfNotUsedFor,
 		if err != nil {
 			return nil, fmt.Errorf("invalid config value %q for refresh tokens reuse interval: %v", reuseInterval, err)
 		}
-		logger.Infof("config refresh tokens reuse interval: %v", reuseInterval)
+		logger.Info("config refresh tokens", "reuse_interval", reuseInterval)
 	}
 
 	r.rotateRefreshTokens = !rotation
-	logger.Infof("config refresh tokens rotation enabled: %v", r.rotateRefreshTokens)
+	logger.Info("config refresh tokens rotation", "enabled", r.rotateRefreshTokens)
 	return &r, nil
 }
 
diff --git a/server/rotation_test.go b/server/rotation_test.go
index e279bf543e5347b4edc89d998abed018bfb93e1a..1d0d2f100a67ad0c78c05334ccd786fbd77cb3e2 100644
--- a/server/rotation_test.go
+++ b/server/rotation_test.go
@@ -1,12 +1,12 @@
 package server
 
 import (
-	"os"
+	"io"
+	"log/slog"
 	"sort"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage"
@@ -68,11 +68,7 @@ func TestKeyRotator(t *testing.T) {
 	// Only the last 5 verification keys are expected to be kept around.
 	maxVerificationKeys := 5
 
-	l := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	l := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	r := &keyRotator{
 		Storage:  memory.New(l),
@@ -104,11 +100,7 @@ func TestKeyRotator(t *testing.T) {
 
 func TestRefreshTokenPolicy(t *testing.T) {
 	lastTime := time.Now()
-	l := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	l := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	r, err := NewRefreshTokenPolicy(l, true, "1m", "1m", "1m")
 	require.NoError(t, err)
diff --git a/server/server.go b/server/server.go
index dddbb137e9c04ce7b5d8660f2a9debdeef4b2c58..5c1a97b89623a63930ea14e6edf991133cc3590d 100644
--- a/server/server.go
+++ b/server/server.go
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"fmt"
 	"io/fs"
+	"log/slog"
 	"net/http"
 	"net/url"
 	"os"
@@ -42,7 +43,6 @@ import (
 	"github.com/dexidp/dex/connector/oidc"
 	"github.com/dexidp/dex/connector/openshift"
 	"github.com/dexidp/dex/connector/saml"
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/web"
 )
@@ -108,7 +108,7 @@ type Config struct {
 
 	Web WebConfig
 
-	Logger log.Logger
+	Logger *slog.Logger
 
 	PrometheusRegistry *prometheus.Registry
 
@@ -189,7 +189,7 @@ type Server struct {
 
 	refreshTokenPolicy *RefreshTokenPolicy
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 // NewServer constructs a server from the provided config.
@@ -556,10 +556,11 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura
 				return
 			case <-time.After(frequency):
 				if r, err := s.storage.GarbageCollect(now()); err != nil {
-					s.logger.Errorf("garbage collection failed: %v", err)
+					s.logger.ErrorContext(ctx, "garbage collection failed", "err", err)
 				} else if !r.IsEmpty() {
-					s.logger.Infof("garbage collection run, delete auth requests=%d, auth codes=%d, device requests=%d, device tokens=%d",
-						r.AuthRequests, r.AuthCodes, r.DeviceRequests, r.DeviceTokens)
+					s.logger.InfoContext(ctx, "garbage collection run, delete auth",
+						"requests", r.AuthRequests, "auth_codes", r.AuthCodes,
+						"device_requests", r.DeviceRequests, "device_tokens", r.DeviceTokens)
 				}
 			}
 		}
@@ -568,7 +569,7 @@ func (s *Server) startGarbageCollection(ctx context.Context, frequency time.Dura
 
 // ConnectorConfig is a configuration that can open a connector.
 type ConnectorConfig interface {
-	Open(id string, logger log.Logger) (connector.Connector, error)
+	Open(id string, logger *slog.Logger) (connector.Connector, error)
 }
 
 // ConnectorsConfig variable provides an easy way to return a config struct
@@ -596,7 +597,7 @@ var ConnectorsConfig = map[string]func() ConnectorConfig{
 }
 
 // openConnector will parse the connector config and open the connector.
-func openConnector(logger log.Logger, conn storage.Connector) (connector.Connector, error) {
+func openConnector(logger *slog.Logger, conn storage.Connector) (connector.Connector, error) {
 	var c connector.Connector
 
 	f, ok := ConnectorsConfig[conn.Type]
diff --git a/server/server_test.go b/server/server_test.go
index 25d1909dc239566145b820404f7555bd1eca6c9a..8936c90a075bdf2c6d72181a138a890d25c5fe02 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -9,11 +9,11 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httputil"
 	"net/url"
-	"os"
 	"path"
 	"reflect"
 	"sort"
@@ -26,7 +26,6 @@ import (
 	"github.com/go-jose/go-jose/v4"
 	"github.com/kylelemons/godebug/pretty"
 	"github.com/prometheus/client_golang/prometheus"
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/crypto/bcrypt"
 	"golang.org/x/oauth2"
@@ -77,11 +76,7 @@ FDWV28nTP9sqbtsmU8Tem2jzMvZ7C/Q0AuDoKELFUpux8shm8wfIhyaPnXUGZoAZ
 Np4vUwMSYV5mopESLWOg3loBxKyLGFtgGKVCjGiQvy6zISQ4fQo=
 -----END RSA PRIVATE KEY-----`)
 
-var logger = &logrus.Logger{
-	Out:       os.Stderr,
-	Formatter: &logrus.TextFormatter{DisableColors: true},
-	Level:     logrus.DebugLevel,
-}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Config)) (*httptest.Server, *Server) {
 	var server *Server
diff --git a/storage/ent/mysql.go b/storage/ent/mysql.go
index 4a9407f95c88c13fb4877959d460cbbc2f522f52..008f7bad333650579e3ae3630a57b7063d356fc6 100644
--- a/storage/ent/mysql.go
+++ b/storage/ent/mysql.go
@@ -7,6 +7,7 @@ import (
 	"crypto/x509"
 	"database/sql"
 	"fmt"
+	"log/slog"
 	"net"
 	"os"
 	"strconv"
@@ -15,7 +16,6 @@ import (
 	entSQL "entgo.io/ent/dialect/sql"
 	"github.com/go-sql-driver/mysql" // Register mysql driver.
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/client"
 	"github.com/dexidp/dex/storage/ent/db"
@@ -39,7 +39,7 @@ type MySQL struct {
 }
 
 // Open always returns a new in sqlite3 storage.
-func (m *MySQL) Open(logger log.Logger) (storage.Storage, error) {
+func (m *MySQL) Open(logger *slog.Logger) (storage.Storage, error) {
 	logger.Debug("experimental ent-based storage driver is enabled")
 	drv, err := m.driver()
 	if err != nil {
diff --git a/storage/ent/mysql_test.go b/storage/ent/mysql_test.go
index 6c2dfa1ddf7da43ef02274057786f16c4600263b..f3e198aa720b9a72881e6f0782e5040ad0c44438 100644
--- a/storage/ent/mysql_test.go
+++ b/storage/ent/mysql_test.go
@@ -1,11 +1,12 @@
 package ent
 
 import (
+	"io"
+	"log/slog"
 	"os"
 	"strconv"
 	"testing"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage"
@@ -39,11 +40,7 @@ func mysqlTestConfig(host string, port uint64) *MySQL {
 }
 
 func newMySQLStorage(host string, port uint64) storage.Storage {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	cfg := mysqlTestConfig(host, port)
 	s, err := cfg.Open(logger)
diff --git a/storage/ent/postgres.go b/storage/ent/postgres.go
index ac091e702cf51d1b38eb6410172958ee7382d63e..dad81df445a864659bdecc39d9ddb2912985f6fc 100644
--- a/storage/ent/postgres.go
+++ b/storage/ent/postgres.go
@@ -5,6 +5,7 @@ import (
 	"crypto/sha256"
 	"database/sql"
 	"fmt"
+	"log/slog"
 	"net"
 	"regexp"
 	"strconv"
@@ -14,13 +15,11 @@ import (
 	entSQL "entgo.io/ent/dialect/sql"
 	_ "github.com/lib/pq" // Register postgres driver.
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/client"
 	"github.com/dexidp/dex/storage/ent/db"
 )
 
-//nolint
 const (
 	// postgres SSL modes
 	pgSSLDisable    = "disable"
@@ -37,7 +36,7 @@ type Postgres struct {
 }
 
 // Open always returns a new in sqlite3 storage.
-func (p *Postgres) Open(logger log.Logger) (storage.Storage, error) {
+func (p *Postgres) Open(logger *slog.Logger) (storage.Storage, error) {
 	logger.Debug("experimental ent-based storage driver is enabled")
 	drv, err := p.driver()
 	if err != nil {
diff --git a/storage/ent/postgres_test.go b/storage/ent/postgres_test.go
index c8e3a54df2fc76f19a3a63305eca8c453044f513..baf0172bb0f091049f225050213ab0c99bca9a8a 100644
--- a/storage/ent/postgres_test.go
+++ b/storage/ent/postgres_test.go
@@ -1,11 +1,12 @@
 package ent
 
 import (
+	"io"
+	"log/slog"
 	"os"
 	"strconv"
 	"testing"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage"
@@ -36,11 +37,7 @@ func postgresTestConfig(host string, port uint64) *Postgres {
 }
 
 func newPostgresStorage(host string, port uint64) storage.Storage {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	cfg := postgresTestConfig(host, port)
 	s, err := cfg.Open(logger)
diff --git a/storage/ent/sqlite.go b/storage/ent/sqlite.go
index c0b442f400f52702320ef02ce588a17105d9e9ed..8c5287ef5073b1acdf5e52e1ac168e6c1e888ae5 100644
--- a/storage/ent/sqlite.go
+++ b/storage/ent/sqlite.go
@@ -3,12 +3,12 @@ package ent
 import (
 	"context"
 	"crypto/sha256"
+	"log/slog"
 	"strings"
 
 	"entgo.io/ent/dialect/sql"
 	_ "github.com/mattn/go-sqlite3" // Register sqlite driver.
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/ent/client"
 	"github.com/dexidp/dex/storage/ent/db"
@@ -20,7 +20,7 @@ type SQLite3 struct {
 }
 
 // Open always returns a new in sqlite3 storage.
-func (s *SQLite3) Open(logger log.Logger) (storage.Storage, error) {
+func (s *SQLite3) Open(logger *slog.Logger) (storage.Storage, error) {
 	logger.Debug("experimental ent-based storage driver is enabled")
 
 	// Implicitly set foreign_keys pragma to "on" because it is required by ent
diff --git a/storage/ent/sqlite_test.go b/storage/ent/sqlite_test.go
index 301d769b4a1d3bafb962181feb88eee589d4ffc6..d88097c225813237c60eeb17776aac663244ed12 100644
--- a/storage/ent/sqlite_test.go
+++ b/storage/ent/sqlite_test.go
@@ -1,21 +1,16 @@
 package ent
 
 import (
-	"os"
+	"io"
+	"log/slog"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/conformance"
 )
 
 func newSQLiteStorage() storage.Storage {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	cfg := SQLite3{File: ":memory:"}
 	s, err := cfg.Open(logger)
diff --git a/storage/etcd/config.go b/storage/etcd/config.go
index 7f1a7b4fb7f3ae00b2c85a9ae8bb16c7012c8efa..a8aee39acae7eb6dfd077448c0e9b341e359f4ee 100644
--- a/storage/etcd/config.go
+++ b/storage/etcd/config.go
@@ -1,13 +1,13 @@
 package etcd
 
 import (
+	"log/slog"
 	"time"
 
 	"go.etcd.io/etcd/client/pkg/v3/transport"
 	clientv3 "go.etcd.io/etcd/client/v3"
 	"go.etcd.io/etcd/client/v3/namespace"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -34,11 +34,11 @@ type Etcd struct {
 }
 
 // Open creates a new storage implementation backed by Etcd
-func (p *Etcd) Open(logger log.Logger) (storage.Storage, error) {
+func (p *Etcd) Open(logger *slog.Logger) (storage.Storage, error) {
 	return p.open(logger)
 }
 
-func (p *Etcd) open(logger log.Logger) (*conn, error) {
+func (p *Etcd) open(logger *slog.Logger) (*conn, error) {
 	cfg := clientv3.Config{
 		Endpoints:   p.Endpoints,
 		DialTimeout: defaultDialTimeout,
diff --git a/storage/etcd/etcd.go b/storage/etcd/etcd.go
index e4b24b4a4a98d7b8ce5f3391424a6a5b86feee1f..f65701ff1fb5bf1757d3f14f413c6f146195dbfa 100644
--- a/storage/etcd/etcd.go
+++ b/storage/etcd/etcd.go
@@ -4,12 +4,12 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"strings"
 	"time"
 
 	clientv3 "go.etcd.io/etcd/client/v3"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -33,7 +33,7 @@ var _ storage.Storage = (*conn)(nil)
 
 type conn struct {
 	db     *clientv3.Client
-	logger log.Logger
+	logger *slog.Logger
 }
 
 func (c *conn) Close() error {
@@ -52,7 +52,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, authRequest := range authRequests {
 		if now.After(authRequest.Expiry) {
 			if err := c.deleteKey(ctx, keyID(authRequestPrefix, authRequest.ID)); err != nil {
-				c.logger.Errorf("failed to delete auth request: %v", err)
+				c.logger.Error("failed to delete auth request", "err", err)
 				delErr = fmt.Errorf("failed to delete auth request: %v", err)
 			}
 			result.AuthRequests++
@@ -70,7 +70,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, authCode := range authCodes {
 		if now.After(authCode.Expiry) {
 			if err := c.deleteKey(ctx, keyID(authCodePrefix, authCode.ID)); err != nil {
-				c.logger.Errorf("failed to delete auth code %v", err)
+				c.logger.Error("failed to delete auth code", "err", err)
 				delErr = fmt.Errorf("failed to delete auth code: %v", err)
 			}
 			result.AuthCodes++
@@ -85,7 +85,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, deviceRequest := range deviceRequests {
 		if now.After(deviceRequest.Expiry) {
 			if err := c.deleteKey(ctx, keyID(deviceRequestPrefix, deviceRequest.UserCode)); err != nil {
-				c.logger.Errorf("failed to delete device request %v", err)
+				c.logger.Error("failed to delete device request", "err", err)
 				delErr = fmt.Errorf("failed to delete device request: %v", err)
 			}
 			result.DeviceRequests++
@@ -100,7 +100,7 @@ func (c *conn) GarbageCollect(now time.Time) (result storage.GCResult, err error
 	for _, deviceToken := range deviceTokens {
 		if now.After(deviceToken.Expiry) {
 			if err := c.deleteKey(ctx, keyID(deviceTokenPrefix, deviceToken.DeviceCode)); err != nil {
-				c.logger.Errorf("failed to delete device token %v", err)
+				c.logger.Error("failed to delete device token", "err", err)
 				delErr = fmt.Errorf("failed to delete device token: %v", err)
 			}
 			result.DeviceTokens++
diff --git a/storage/etcd/etcd_test.go b/storage/etcd/etcd_test.go
index 8a9af5c9d5e9c7388d6dbae01cec1e16b688f0d8..5a568e8c3f61f8c1973a09ab61b03f50bc8a65ab 100644
--- a/storage/etcd/etcd_test.go
+++ b/storage/etcd/etcd_test.go
@@ -3,13 +3,14 @@ package etcd
 import (
 	"context"
 	"fmt"
+	"io"
+	"log/slog"
 	"os"
 	"runtime"
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	clientv3 "go.etcd.io/etcd/client/v3"
 
 	"github.com/dexidp/dex/storage"
@@ -55,11 +56,7 @@ func cleanDB(c *conn) error {
 	return nil
 }
 
-var logger = &logrus.Logger{
-	Out:       os.Stderr,
-	Formatter: &logrus.TextFormatter{DisableColors: true},
-	Level:     logrus.DebugLevel,
-}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 func TestEtcd(t *testing.T) {
 	testEtcdEnv := "DEX_ETCD_ENDPOINTS"
diff --git a/storage/kubernetes/client.go b/storage/kubernetes/client.go
index af0a2338fae576b00a9c0b829a14086ca335b094..1a1653b345d1cea593f979726055b811ac0042f7 100644
--- a/storage/kubernetes/client.go
+++ b/storage/kubernetes/client.go
@@ -13,6 +13,7 @@ import (
 	"hash"
 	"hash/fnv"
 	"io"
+	"log/slog"
 	"net"
 	"net/http"
 	"net/url"
@@ -27,7 +28,6 @@ import (
 	"github.com/ghodss/yaml"
 	"golang.org/x/net/http2"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
 )
@@ -36,7 +36,7 @@ type client struct {
 	client    *http.Client
 	baseURL   string
 	namespace string
-	logger    log.Logger
+	logger    *slog.Logger
 
 	// Hash function to map IDs (which could span a large range) to Kubernetes names.
 	// While this is not currently upgradable, it could be in the future.
@@ -268,7 +268,7 @@ func (cli *client) detectKubernetesVersion() error {
 
 	clusterVersion, err := semver.NewVersion(version.GitVersion)
 	if err != nil {
-		cli.logger.Warnf("cannot detect Kubernetes version (%s): %v", clusterVersion, err)
+		cli.logger.Warn("cannot detect Kubernetes version", "version", clusterVersion, "err", err)
 		return nil
 	}
 
@@ -358,7 +358,7 @@ func defaultTLSConfig() *tls.Config {
 	}
 }
 
-func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, logger log.Logger, inCluster bool) (*client, error) {
+func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, logger *slog.Logger, inCluster bool) (*client, error) {
 	tlsConfig := defaultTLSConfig()
 	data := func(b string, file string) ([]byte, error) {
 		if b != "" {
@@ -418,7 +418,7 @@ func newClient(cluster k8sapi.Cluster, user k8sapi.AuthInfo, namespace string, l
 
 	apiVersion := "dex.coreos.com/v1"
 
-	logger.Infof("kubernetes client apiVersion = %s", apiVersion)
+	logger.Info("kubernetes client", "api_version", apiVersion)
 	return &client{
 		client: &http.Client{
 			Transport: t,
diff --git a/storage/kubernetes/client_test.go b/storage/kubernetes/client_test.go
index cfd04857b6ea58f302bc9c4fca14a485fb97fc4e..c8fc8db11bb954d6ee739f09caebbec11dfe127f 100644
--- a/storage/kubernetes/client_test.go
+++ b/storage/kubernetes/client_test.go
@@ -3,6 +3,8 @@ package kubernetes
 import (
 	"hash"
 	"hash/fnv"
+	"io"
+	"log/slog"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -10,7 +12,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
@@ -52,11 +53,7 @@ func TestOfflineTokenName(t *testing.T) {
 }
 
 func TestInClusterTransport(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	user := k8sapi.AuthInfo{Token: "abc"}
 	cli, err := newClient(
diff --git a/storage/kubernetes/lock.go b/storage/kubernetes/lock.go
index 12075e81db6f1dcb4859bb421350770ccc6fc319..c67380dcc0a7f1bcdef65b1d674b4a38f652af6a 100644
--- a/storage/kubernetes/lock.go
+++ b/storage/kubernetes/lock.go
@@ -53,14 +53,14 @@ func (l *refreshTokenLock) Unlock(id string) {
 
 	r, err := l.cli.getRefreshToken(id)
 	if err != nil {
-		l.cli.logger.Debugf("failed to get resource to release lock for refresh token %s: %v", id, err)
+		l.cli.logger.Debug("failed to get resource to release lock for refresh token", "token_id", id, "err", err)
 		return
 	}
 
 	r.Annotations = nil
 	err = l.cli.put(resourceRefreshToken, r.ObjectMeta.Name, r)
 	if err != nil {
-		l.cli.logger.Debugf("failed to release lock for refresh token %s: %v", id, err)
+		l.cli.logger.Debug("failed to release lock for refresh token", "token_id", id, "err", err)
 	}
 }
 
@@ -114,7 +114,7 @@ func (l *refreshTokenLock) setLockAnnotation(id string) (bool, error) {
 		return false, nil
 	}
 
-	l.cli.logger.Debugf("break lock annotation error: %v", err)
+	l.cli.logger.Debug("break lock annotation", "error", err)
 	if isKubernetesAPIConflictError(err) {
 		l.waitingState = true
 		// after breaking error waiting for the lock to be released
diff --git a/storage/kubernetes/storage.go b/storage/kubernetes/storage.go
index c08362b8eb3803f53a4d7920edfd8bc47f2da3e6..8b6d5c9c2e8ee1050de1a4bd9d6a338e4a92fecc 100644
--- a/storage/kubernetes/storage.go
+++ b/storage/kubernetes/storage.go
@@ -4,12 +4,12 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log/slog"
 	"math/rand"
 	"net/http"
 	"strings"
 	"time"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/kubernetes/k8sapi"
 )
@@ -53,7 +53,7 @@ type Config struct {
 }
 
 // Open returns a storage using Kubernetes third party resource.
-func (c *Config) Open(logger log.Logger) (storage.Storage, error) {
+func (c *Config) Open(logger *slog.Logger) (storage.Storage, error) {
 	cli, err := c.open(logger, false)
 	if err != nil {
 		return nil, err
@@ -66,7 +66,7 @@ func (c *Config) Open(logger log.Logger) (storage.Storage, error) {
 //
 // waitForResources controls if errors creating the resources cause this method to return
 // immediately (used during testing), or if the client will asynchronously retry.
-func (c *Config) open(logger log.Logger, waitForResources bool) (*client, error) {
+func (c *Config) open(logger *slog.Logger, waitForResources bool) (*client, error) {
 	if c.InCluster && (c.KubeConfigFile != "") {
 		return nil, errors.New("cannot specify both 'inCluster' and 'kubeConfigFile'")
 	}
@@ -155,12 +155,12 @@ func (cli *client) registerCustomResources() (ok bool) {
 
 		r := definitions[i]
 		var i interface{}
-		cli.logger.Infof("checking if custom resource %s has already been created...", r.ObjectMeta.Name)
+		cli.logger.Info("checking if custom resource has already been created...", "object", r.ObjectMeta.Name)
 		if err := cli.list(r.Spec.Names.Plural, &i); err == nil {
-			cli.logger.Infof("The custom resource %s already available, skipping create", r.ObjectMeta.Name)
+			cli.logger.Info("the custom resource already available, skipping create", "object", r.ObjectMeta.Name)
 			continue
 		} else {
-			cli.logger.Infof("failed to list custom resource %s, attempting to create: %v", r.ObjectMeta.Name, err)
+			cli.logger.Info("failed to list custom resource, attempting to create", "object", r.ObjectMeta.Name, "err", err)
 		}
 
 		err = cli.postResource(cli.crdAPIVersion, "", "customresourcedefinitions", r)
@@ -169,17 +169,17 @@ func (cli *client) registerCustomResources() (ok bool) {
 		if err != nil {
 			switch err {
 			case storage.ErrAlreadyExists:
-				cli.logger.Infof("custom resource already created %s", resourceName)
+				cli.logger.Info("custom resource already created", "object", resourceName)
 			case storage.ErrNotFound:
-				cli.logger.Errorf("custom resources not found, please enable the respective API group")
+				cli.logger.Error("custom resources not found, please enable the respective API group")
 				ok = false
 			default:
-				cli.logger.Errorf("creating custom resource %s: %v", resourceName, err)
+				cli.logger.Error("creating custom resource", "object", resourceName, "err", err)
 				ok = false
 			}
 			continue
 		}
-		cli.logger.Errorf("create custom resource %s", resourceName)
+		cli.logger.Error("create custom resource", "object", resourceName)
 	}
 	return ok
 }
@@ -197,7 +197,7 @@ func (cli *client) waitForCRDs(ctx context.Context) error {
 				break
 			}
 
-			cli.logger.Errorf("checking CRD: %v", err)
+			cli.logger.ErrorContext(ctx, "checking CRD", "err", err)
 
 			select {
 			case <-ctx.Done():
@@ -556,7 +556,7 @@ func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, erro
 		err = cli.post(resourceKeys, newKeys)
 		if err != nil && errors.Is(err, storage.ErrAlreadyExists) {
 			// We need to tolerate conflicts here in case of HA mode.
-			cli.logger.Debugf("Keys creation failed: %v. It is possible that keys have already been created by another dex instance.", err)
+			cli.logger.Debug("Keys creation failed. It is possible that keys have already been created by another dex instance.", "err", err)
 			return errors.New("keys already created by another server instance")
 		}
 
@@ -569,7 +569,7 @@ func (cli *client) UpdateKeys(updater func(old storage.Keys) (storage.Keys, erro
 	if isKubernetesAPIConflictError(err) {
 		// We need to tolerate conflicts here in case of HA mode.
 		// Dex instances run keys rotation at the same time because they use SigningKey.nextRotation CR field as a trigger.
-		cli.logger.Debugf("Keys rotation failed: %v. It is possible that keys have already been rotated by another dex instance.", err)
+		cli.logger.Debug("Keys rotation failed. It is possible that keys have already been rotated by another dex instance.", "err", err)
 		return errors.New("keys already rotated by another server instance")
 	}
 
@@ -622,7 +622,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, authRequest := range authRequests.AuthRequests {
 		if now.After(authRequest.Expiry) {
 			if err := cli.delete(resourceAuthRequest, authRequest.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete auth request: %v", err)
+				cli.logger.Error("failed to delete auth request", "err", err)
 				delErr = fmt.Errorf("failed to delete auth request: %v", err)
 			}
 			result.AuthRequests++
@@ -640,7 +640,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, authCode := range authCodes.AuthCodes {
 		if now.After(authCode.Expiry) {
 			if err := cli.delete(resourceAuthCode, authCode.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete auth code %v", err)
+				cli.logger.Error("failed to delete auth code", "err", err)
 				delErr = fmt.Errorf("failed to delete auth code: %v", err)
 			}
 			result.AuthCodes++
@@ -655,7 +655,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, deviceRequest := range deviceRequests.DeviceRequests {
 		if now.After(deviceRequest.Expiry) {
 			if err := cli.delete(resourceDeviceRequest, deviceRequest.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete device request: %v", err)
+				cli.logger.Error("failed to delete device request", "err", err)
 				delErr = fmt.Errorf("failed to delete device request: %v", err)
 			}
 			result.DeviceRequests++
@@ -670,7 +670,7 @@ func (cli *client) GarbageCollect(now time.Time) (result storage.GCResult, err e
 	for _, deviceToken := range deviceTokens.DeviceTokens {
 		if now.After(deviceToken.Expiry) {
 			if err := cli.delete(resourceDeviceToken, deviceToken.ObjectMeta.Name); err != nil {
-				cli.logger.Errorf("failed to delete device token: %v", err)
+				cli.logger.Error("failed to delete device token", "err", err)
 				delErr = fmt.Errorf("failed to delete device token: %v", err)
 			}
 			result.DeviceTokens++
diff --git a/storage/kubernetes/storage_test.go b/storage/kubernetes/storage_test.go
index b4b42688e25fed10d162d25ab8e94ea3b4926ee7..d8bfd1f6897652471201eb987b66da08fc4e76d9 100644
--- a/storage/kubernetes/storage_test.go
+++ b/storage/kubernetes/storage_test.go
@@ -5,6 +5,8 @@ import (
 	"crypto/tls"
 	"errors"
 	"fmt"
+	"io"
+	"log/slog"
 	"net/http"
 	"net/http/httptest"
 	"os"
@@ -13,7 +15,6 @@ import (
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
 
@@ -57,11 +58,7 @@ func (s *StorageTestSuite) SetupTest() {
 		KubeConfigFile: kubeconfigPath,
 	}
 
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	kubeClient, err := config.open(logger, true)
 	s.Require().NoError(err)
@@ -253,11 +250,7 @@ func newStatusCodesResponseTestClient(getResponseCode, actionResponseCode int) *
 	return &client{
 		client:  &http.Client{Transport: tr},
 		baseURL: s.URL,
-		logger: &logrus.Logger{
-			Out:       os.Stderr,
-			Formatter: &logrus.TextFormatter{DisableColors: true},
-			Level:     logrus.DebugLevel,
-		},
+		logger:  slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})),
 	}
 }
 
@@ -314,11 +307,7 @@ func TestRefreshTokenLock(t *testing.T) {
 		KubeConfigFile: kubeconfigPath,
 	}
 
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	kubeClient, err := config.open(logger, true)
 	require.NoError(t, err)
diff --git a/storage/memory/memory.go b/storage/memory/memory.go
index 8e080c9faa536c5f20d47592f2b07e31d6fb83f4..4399c61df1d90efe71052042a87f7e420dcdd2a7 100644
--- a/storage/memory/memory.go
+++ b/storage/memory/memory.go
@@ -3,18 +3,18 @@ package memory
 
 import (
 	"context"
+	"log/slog"
 	"strings"
 	"sync"
 	"time"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
 var _ storage.Storage = (*memStorage)(nil)
 
 // New returns an in memory storage.
-func New(logger log.Logger) storage.Storage {
+func New(logger *slog.Logger) storage.Storage {
 	return &memStorage{
 		clients:         make(map[string]storage.Client),
 		authCodes:       make(map[string]storage.AuthCode),
@@ -36,7 +36,7 @@ type Config struct { // The in memory implementation has no config.
 }
 
 // Open always returns a new in memory storage.
-func (c *Config) Open(logger log.Logger) (storage.Storage, error) {
+func (c *Config) Open(logger *slog.Logger) (storage.Storage, error) {
 	return New(logger), nil
 }
 
@@ -55,7 +55,7 @@ type memStorage struct {
 
 	keys storage.Keys
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 type offlineSessionID struct {
diff --git a/storage/memory/memory_test.go b/storage/memory/memory_test.go
index 84a8826ef2c72753c82cb7a201c82f7415c97a26..75a17ac62cf7b9dceddda910e98301c1c1a988a7 100644
--- a/storage/memory/memory_test.go
+++ b/storage/memory/memory_test.go
@@ -1,21 +1,16 @@
 package memory
 
 import (
-	"os"
+	"io"
+	"log/slog"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/conformance"
 )
 
 func TestStorage(t *testing.T) {
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	newStorage := func() storage.Storage {
 		return New(logger)
diff --git a/storage/memory/static_test.go b/storage/memory/static_test.go
index 4be23a1e6a1c4eeff10872a2cb0087cbae2768a8..b9138742315e849aca5a06c24f0c43aad8de8764 100644
--- a/storage/memory/static_test.go
+++ b/storage/memory/static_test.go
@@ -3,22 +3,17 @@ package memory
 import (
 	"context"
 	"fmt"
-	"os"
+	"io"
+	"log/slog"
 	"strings"
 	"testing"
 
-	"github.com/sirupsen/logrus"
-
 	"github.com/dexidp/dex/storage"
 )
 
 func TestStaticClients(t *testing.T) {
 	ctx := context.Background()
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	backing := New(logger)
 
 	c1 := storage.Client{ID: "foo", Secret: "foo_secret"}
@@ -102,11 +97,7 @@ func TestStaticClients(t *testing.T) {
 
 func TestStaticPasswords(t *testing.T) {
 	ctx := context.Background()
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	backing := New(logger)
 
 	p1 := storage.Password{Email: "foo@example.com", Username: "foo_secret"}
@@ -215,11 +206,7 @@ func TestStaticPasswords(t *testing.T) {
 
 func TestStaticConnectors(t *testing.T) {
 	ctx := context.Background()
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 	backing := New(logger)
 
 	config1 := []byte(`{"issuer": "https://accounts.google.com"}`)
diff --git a/storage/sql/config.go b/storage/sql/config.go
index 8b782425631db56bc938a820411b2d03376c9be2..5379aeb6b259f8914eb31e417236edfd4b3ec635 100644
--- a/storage/sql/config.go
+++ b/storage/sql/config.go
@@ -5,6 +5,7 @@ import (
 	"crypto/x509"
 	"database/sql"
 	"fmt"
+	"log/slog"
 	"net"
 	"os"
 	"regexp"
@@ -15,7 +16,6 @@ import (
 	"github.com/go-sql-driver/mysql"
 	"github.com/lib/pq"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -31,7 +31,6 @@ const (
 	mysqlErrUnknownSysVar       = 1193
 )
 
-//nolint
 const (
 	// postgres SSL modes
 	pgSSLDisable    = "disable"
@@ -40,7 +39,6 @@ const (
 	pgSSLVerifyFull = "verify-full"
 )
 
-//nolint
 const (
 	// MySQL SSL modes
 	mysqlSSLTrue       = "true"
@@ -84,7 +82,7 @@ type Postgres struct {
 }
 
 // Open creates a new storage implementation backed by Postgres.
-func (p *Postgres) Open(logger log.Logger) (storage.Storage, error) {
+func (p *Postgres) Open(logger *slog.Logger) (storage.Storage, error) {
 	conn, err := p.open(logger)
 	if err != nil {
 		return nil, err
@@ -164,7 +162,7 @@ func (p *Postgres) createDataSourceName() string {
 	return strings.Join(parameters, " ")
 }
 
-func (p *Postgres) open(logger log.Logger) (*conn, error) {
+func (p *Postgres) open(logger *slog.Logger) (*conn, error) {
 	dataSourceName := p.createDataSourceName()
 
 	db, err := sql.Open("postgres", dataSourceName)
@@ -216,7 +214,7 @@ type MySQL struct {
 }
 
 // Open creates a new storage implementation backed by MySQL.
-func (s *MySQL) Open(logger log.Logger) (storage.Storage, error) {
+func (s *MySQL) Open(logger *slog.Logger) (storage.Storage, error) {
 	conn, err := s.open(logger)
 	if err != nil {
 		return nil, err
@@ -224,7 +222,7 @@ func (s *MySQL) Open(logger log.Logger) (storage.Storage, error) {
 	return conn, nil
 }
 
-func (s *MySQL) open(logger log.Logger) (*conn, error) {
+func (s *MySQL) open(logger *slog.Logger) (*conn, error) {
 	cfg := mysql.Config{
 		User:                 s.User,
 		Passwd:               s.Password,
diff --git a/storage/sql/config_test.go b/storage/sql/config_test.go
index 1178728c1a24d79601a367723750b2a1e075f567..b1037e64e9aed1f1820cbf950d758b8fc3407abe 100644
--- a/storage/sql/config_test.go
+++ b/storage/sql/config_test.go
@@ -2,15 +2,14 @@ package sql
 
 import (
 	"fmt"
+	"io"
+	"log/slog"
 	"os"
 	"runtime"
 	"strconv"
 	"testing"
 	"time"
 
-	"github.com/sirupsen/logrus"
-
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 	"github.com/dexidp/dex/storage/conformance"
 )
@@ -48,14 +47,10 @@ func cleanDB(c *conn) error {
 	return nil
 }
 
-var logger = &logrus.Logger{
-	Out:       os.Stderr,
-	Formatter: &logrus.TextFormatter{DisableColors: true},
-	Level:     logrus.DebugLevel,
-}
+var logger = slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 type opener interface {
-	open(logger log.Logger) (*conn, error)
+	open(logger *slog.Logger) (*conn, error)
 }
 
 func testDB(t *testing.T, o opener, withTransactions bool) {
diff --git a/storage/sql/migrate_test.go b/storage/sql/migrate_test.go
index 4b77eb2fbe52a9aa07403a9fa4d45abd60f22f14..bf9f0ddb4dc8e4ee206106c9cc047ca49acd1e04 100644
--- a/storage/sql/migrate_test.go
+++ b/storage/sql/migrate_test.go
@@ -5,11 +5,11 @@ package sql
 
 import (
 	"database/sql"
-	"os"
+	"io"
+	"log/slog"
 	"testing"
 
 	sqlite3 "github.com/mattn/go-sqlite3"
-	"github.com/sirupsen/logrus"
 )
 
 func TestMigrate(t *testing.T) {
@@ -19,11 +19,7 @@ func TestMigrate(t *testing.T) {
 	}
 	defer db.Close()
 
-	logger := &logrus.Logger{
-		Out:       os.Stderr,
-		Formatter: &logrus.TextFormatter{DisableColors: true},
-		Level:     logrus.DebugLevel,
-	}
+	logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{}))
 
 	errCheck := func(err error) bool {
 		sqlErr, ok := err.(sqlite3.Error)
diff --git a/storage/sql/sql.go b/storage/sql/sql.go
index 0a2921693649174964ae9d46cda49a3338374687..d671021fcafad1b5b16fc8dfee2522b4c0f681c3 100644
--- a/storage/sql/sql.go
+++ b/storage/sql/sql.go
@@ -3,14 +3,13 @@ package sql
 
 import (
 	"database/sql"
+	"log/slog"
 	"regexp"
 	"time"
 
 	// import third party drivers
 	_ "github.com/lib/pq"
 	_ "github.com/mattn/go-sqlite3"
-
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // flavor represents a specific SQL implementation, and is used to translate query strings
@@ -131,7 +130,7 @@ func (c *conn) translateArgs(args []interface{}) []interface{} {
 type conn struct {
 	db                 *sql.DB
 	flavor             *flavor
-	logger             log.Logger
+	logger             *slog.Logger
 	alreadyExistsCheck func(err error) bool
 }
 
diff --git a/storage/sql/sqlite.go b/storage/sql/sqlite.go
index 43df671a7cbbfef68abc91e64f349ea61f3e5721..2d29e607dcc42f8a5ece678d6eb2cb51de248b95 100644
--- a/storage/sql/sqlite.go
+++ b/storage/sql/sqlite.go
@@ -6,10 +6,10 @@ package sql
 import (
 	"database/sql"
 	"fmt"
+	"log/slog"
 
 	sqlite3 "github.com/mattn/go-sqlite3"
 
-	"github.com/dexidp/dex/pkg/log"
 	"github.com/dexidp/dex/storage"
 )
 
@@ -20,7 +20,7 @@ type SQLite3 struct {
 }
 
 // Open creates a new storage implementation backed by SQLite3
-func (s *SQLite3) Open(logger log.Logger) (storage.Storage, error) {
+func (s *SQLite3) Open(logger *slog.Logger) (storage.Storage, error) {
 	conn, err := s.open(logger)
 	if err != nil {
 		return nil, err
@@ -28,7 +28,7 @@ func (s *SQLite3) Open(logger log.Logger) (storage.Storage, error) {
 	return conn, nil
 }
 
-func (s *SQLite3) open(logger log.Logger) (*conn, error) {
+func (s *SQLite3) open(logger *slog.Logger) (*conn, error) {
 	db, err := sql.Open("sqlite3", s.File)
 	if err != nil {
 		return nil, err
diff --git a/storage/static.go b/storage/static.go
index e8902b9b5955131109f23feb9fff2650f30cc95a..ca04937acfe612dc02aa01cd58114d693be686b7 100644
--- a/storage/static.go
+++ b/storage/static.go
@@ -3,9 +3,8 @@ package storage
 import (
 	"context"
 	"errors"
+	"log/slog"
 	"strings"
-
-	"github.com/dexidp/dex/pkg/log"
 )
 
 // Tests for this code are in the "memory" package, since this package doesn't
@@ -90,17 +89,17 @@ type staticPasswordsStorage struct {
 	// A map of passwords that is indexed by lower-case email ids
 	passwordsByEmail map[string]Password
 
-	logger log.Logger
+	logger *slog.Logger
 }
 
 // WithStaticPasswords returns a storage with a read-only set of passwords.
-func WithStaticPasswords(s Storage, staticPasswords []Password, logger log.Logger) Storage {
+func WithStaticPasswords(s Storage, staticPasswords []Password, logger *slog.Logger) Storage {
 	passwordsByEmail := make(map[string]Password, len(staticPasswords))
 	for _, p := range staticPasswords {
 		// Enable case insensitive email comparison.
 		lowerEmail := strings.ToLower(p.Email)
 		if _, ok := passwordsByEmail[lowerEmail]; ok {
-			logger.Errorf("Attempting to create StaticPasswords with the same email id: %s", p.Email)
+			logger.Error("attempting to create StaticPasswords with the same email id", "email", p.Email)
 		}
 		passwordsByEmail[lowerEmail] = p
 	}