Repository: kubernetes-sigs/kwok
Pull request #558 (2023-05-04 05:22:23)

Update golang.org/x/exp
#### What type of PR is this?

<!--
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->/kind cleanup

#### What this PR does / why we need it:
Updates golang.org/x/exp. This package has seen some breaking changes as of late. Updating to latest alleviates dependency conflicts.

#### Which issue(s) this PR fixes:

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?
<!--
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

For more information on release notes see: https://git.k8s.io/community/contributors/guide/release-notes.md
-->
```release-note
NONE
```

#### Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.:

<!--
This section can be blank if this pull request does not require a release note.

When adding links which point to resources within git repositories, like
KEPs or supporting documentation, please reference a specific commit and avoid
linking directly to the master branch. This ensures that links reference a
specific point in time, rather than a document that may change over time.

See here for guidance on getting permanent links to files: https://help.github.com/en/articles/getting-permanent-links-to-files

Please use the following format for linking documentation:
- [KEP]: <link>
- [Usage]: <link>
- [Other doc]: <link>
-->



k8s-ci-robot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kwok/pull/558#" title="Author self-approved">muff1nman</a>*
**Once this PR has been reviewed and has the lgtm label**, please assign yibozhuang for approval. For more information see [the Kubernetes Code Review Process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process).

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkwok).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubernetes-sigs/kwok/blob/main/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["yibozhuang"]} -->
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *k8s-kwok* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 37e35fc7371a786631bca6efc7ab03ed41f874f3 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/k8s-kwok/deploys/64534111ce80f00008d921c7 |
k8s-ci-robot: Hi @muff1nman. Thanks for your PR.

I'm waiting for a [kubernetes-sigs](https://github.com/orgs/kubernetes-sigs/people) member to verify that this patch is reasonable to test. If it is, they should reply with `/ok-to-test` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](https://git.k8s.io/community/community-membership.md#member) to skip this step.

Once the patch is verified, the new status will be reflected by the `ok-to-test` label.

I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkwok).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>

k8s-ci-robot: Welcome @muff1nman! <br><br>It looks like this is your first PR to <a href='https://github.com/kubernetes-sigs/kwok'>kubernetes-sigs/kwok</a> 🎉. Please refer to our [pull request process documentation](https://git.k8s.io/community/contributors/guide/pull-requests.md) to help your PR have a smooth ride to approval. <br><br>You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. [Here is the bot commands documentation](https://go.k8s.io/bot-commands). <br><br>You can also check if kubernetes-sigs/kwok has [its own contribution guidelines](https://github.com/kubernetes-sigs/kwok/tree/master/CONTRIBUTING.md). <br><br>You may want to refer to our [testing guide](https://git.k8s.io/community/contributors/devel/sig-testing/testing.md) if you run into trouble with your tests not passing. <br><br>If you are having difficulty getting your pull request seen, please follow the [recommended escalation practices](https://github.com/kubernetes/community/blob/master/contributors/guide/pull-requests.md#why-is-my-pull-request-not-getting-reviewed). Also, for tips and tricks in the contribution process you may want to read the [Kubernetes contributor cheat sheet](https://git.k8s.io/community/contributors/guide/contributor-cheatsheet/README.md). We want to make sure your contribution gets all the attention it needs! <br><br>Thank you, and welcome to Kubernetes. :smiley:
wzshiming: /ok-to-test
k8s-ci-robot: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
muff1nman: I can squash these commits into one, I just figured I'd keep them separate to contrast the two approaches.
wzshiming: Thank you for your contribution!

/approve
/lgtm
/label tide/merge-method-squash



k8s-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubernetes-sigs/kwok/pull/558#" title="Author self-approved">muff1nman</a>*, *<a href="https://github.com/kubernetes-sigs/kwok/pull/558#issuecomment-1536989137" title="LGTM">wzshiming</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubernetes-sigs%2Fkwok).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubernetes-sigs/kwok/blob/main/OWNERS)~~ [wzshiming]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/go.mod b/go.mod
index ebd1f355..a9fb952b 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	github.com/wzshiming/cmux v0.3.2
 	github.com/wzshiming/cron v0.2.1
 	github.com/wzshiming/ctc v1.2.3
-	golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9
+	golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
 	golang.org/x/sync v0.1.0
 	golang.org/x/sys v0.7.0
 	golang.org/x/term v0.7.0
diff --git a/go.sum b/go.sum
index 869eca7e..13a8e2cd 100644
--- a/go.sum
+++ b/go.sum
@@ -218,8 +218,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
-golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
+golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
 golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
diff --git a/hack/gen_cmd_docs/main.go b/hack/gen_cmd_docs/main.go
index d9efd0ed..509b6c2f 100644
--- a/hack/gen_cmd_docs/main.go
+++ b/hack/gen_cmd_docs/main.go
@@ -57,7 +57,7 @@ func main() {
 		logger.Error("Init config flags", err)
 		os.Exit(1)
 	}
-	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.WarnLevel))
+	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.LevelWarn))
 
 	err = genKwok(ctx, flagset, basePath)
 	if err != nil {
diff --git a/pkg/kwokctl/components/kube_apiserver.go b/pkg/kwokctl/components/kube_apiserver.go
index 49eef51e..c026a19c 100644
--- a/pkg/kwokctl/components/kube_apiserver.go
+++ b/pkg/kwokctl/components/kube_apiserver.go
@@ -208,7 +208,7 @@ func BuildKubeApiserverComponent(conf BuildKubeApiserverComponentConfig) (compon
 		}
 	}
 
-	if conf.Verbosity != int(log.InfoLevel) {
+	if conf.Verbosity != int(log.LevelInfo) {
 		kubeApiserverArgs = append(kubeApiserverArgs, "--v="+strconv.Itoa(conf.Verbosity))
 	}
 
diff --git a/pkg/kwokctl/components/kube_controller_manager.go b/pkg/kwokctl/components/kube_controller_manager.go
index 973dad86..5b254377 100644
--- a/pkg/kwokctl/components/kube_controller_manager.go
+++ b/pkg/kwokctl/components/kube_controller_manager.go
@@ -187,7 +187,7 @@ func BuildKubeControllerManagerComponent(conf BuildKubeControllerManagerComponen
 		}
 	}
 
-	if conf.Verbosity != int(log.InfoLevel) {
+	if conf.Verbosity != int(log.LevelInfo) {
 		kubeControllerManagerArgs = append(kubeControllerManagerArgs, "--v="+strconv.Itoa(conf.Verbosity))
 	}
 
diff --git a/pkg/kwokctl/components/kube_scheduler.go b/pkg/kwokctl/components/kube_scheduler.go
index d5ea9643..c0c3857c 100644
--- a/pkg/kwokctl/components/kube_scheduler.go
+++ b/pkg/kwokctl/components/kube_scheduler.go
@@ -171,7 +171,7 @@ func BuildKubeSchedulerComponent(conf BuildKubeSchedulerComponentConfig) (compon
 		//	)
 	}
 
-	if conf.Verbosity != int(log.InfoLevel) {
+	if conf.Verbosity != int(log.LevelInfo) {
 		kubeSchedulerArgs = append(kubeSchedulerArgs, "--v="+strconv.Itoa(conf.Verbosity))
 	}
 
diff --git a/pkg/log/convert.go b/pkg/log/convert.go
index 8b286ff8..d9d35c9d 100644
--- a/pkg/log/convert.go
+++ b/pkg/log/convert.go
@@ -38,11 +38,11 @@ func ToKlogLevel(level Level) int {
 // ToLogSeverityLevel maps the current logging level to a severity level string
 func ToLogSeverityLevel(level Level) string {
 	switch {
-	case level < slog.InfoLevel:
+	case level < slog.LevelInfo:
 		return DebugLevelSecurity
-	case level < slog.WarnLevel:
+	case level < slog.LevelWarn:
 		return InfoLevelSecurity
-	case level < slog.ErrorLevel:
+	case level < slog.LevelError:
 		return WarnLevelSecurity
 	default:
 		return ErrorLevelSecurity
diff --git a/pkg/log/logger.go b/pkg/log/logger.go
index 9daacad9..c17c6112 100644
--- a/pkg/log/logger.go
+++ b/pkg/log/logger.go
@@ -37,22 +37,20 @@ type DurationFormat struct {
 	Human      string `json:"human"`
 }
 
-// contextKey is how we find Logger Level in a context.Context.
+// contextKey is how we find Logger in a context.Context.
 type contextKey struct{}
 
 // FromContext returns the Logger associated with ctx, or the default logger.
 func FromContext(ctx context.Context) *Logger {
-	level, ok := ctx.Value(contextKey{}).(Level)
-	if !ok {
-		level = InfoLevel
+	if l, ok := ctx.Value(contextKey{}).(*Logger); ok {
+		return l
 	}
-	return wrapSlog(slog.FromContext(ctx), level)
+	return wrapSlog(slog.Default().Handler(), slog.LevelInfo)
 }
 
 // NewContext returns a new context with the given logger.
 func NewContext(ctx context.Context, logger *Logger) context.Context {
-	ctx = context.WithValue(ctx, contextKey{}, logger.level)
-	return slog.NewContext(ctx, logger.log)
+	return context.WithValue(ctx, contextKey{}, logger)
 }
 
 // NewLogger returns a new Logger that writes to w.
@@ -64,15 +62,15 @@ func NewLogger(w io.Writer, level slog.Level) *Logger {
 	if file, ok := w.(*os.File); ok {
 		fd := int(file.Fd())
 		if IsTerminal(fd) {
-			return wrapSlog(slog.New(newCtlHandler(w, fd, level)), level)
+			return wrapSlog(newCtlHandler(w, fd, level), level)
 		}
 	}
 
 	handler := slog.HandlerOptions{
 		AddSource: true,
 		Level:     level,
-		ReplaceAttr: func(a slog.Attr) slog.Attr {
-			if a.Value.Kind() == slog.DurationKind {
+		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
+			if a.Value.Kind() == slog.KindDuration {
 				if t, ok := a.Value.Any().(time.Duration); ok {
 					return slog.Any(a.Key, DurationFormat{
 						Nanosecond: int64(t),
@@ -80,7 +78,7 @@ func NewLogger(w io.Writer, level slog.Level) *Logger {
 					})
 				}
 			}
-			if a.Value.Kind() == slog.AnyKind {
+			if a.Value.Kind() == slog.KindAny {
 				if t, ok := a.Value.Any().(fmt.Stringer); ok {
 					return slog.Attr{
 						Key:   a.Key,
@@ -91,5 +89,5 @@ func NewLogger(w io.Writer, level slog.Level) *Logger {
 			return a
 		},
 	}
-	return wrapSlog(slog.New(handler.NewJSONHandler(w)), level)
+	return wrapSlog(handler.NewJSONHandler(w), level)
 }
diff --git a/pkg/log/logger_ctl.go b/pkg/log/logger_ctl.go
index e7016d87..6fa5fabb 100644
--- a/pkg/log/logger_ctl.go
+++ b/pkg/log/logger_ctl.go
@@ -17,6 +17,7 @@ limitations under the License.
 package log
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -50,15 +51,15 @@ func newCtlHandler(w io.Writer, fd int, level slog.Level) *ctlHandler {
 	}
 }
 
-func (c *ctlHandler) Enabled(level slog.Level) bool {
+func (c *ctlHandler) Enabled(_ context.Context, level slog.Level) bool {
 	return level >= c.level
 }
 
 func formatValue(val slog.Value) string {
 	switch val.Kind() {
-	case slog.StringKind:
+	case slog.KindString:
 		return quoteIfNeed(val.String())
-	case slog.DurationKind:
+	case slog.KindDuration:
 		return format.HumanDuration(val.Duration())
 	default:
 		switch x := val.Any().(type) {
@@ -76,7 +77,7 @@ func formatValue(val slog.Value) string {
 	}
 }
 
-func (c *ctlHandler) Handle(r slog.Record) error {
+func (c *ctlHandler) Handle(_ context.Context, r slog.Record) error {
 	if r.Level < c.level {
 		return nil
 	}
@@ -94,13 +95,14 @@ func (c *ctlHandler) Handle(r slog.Record) error {
 	if c.attrsStr != nil {
 		attrs = append(attrs, *c.attrsStr)
 	}
-	r.Attrs(func(attr slog.Attr) {
+	r.Attrs(func(attr slog.Attr) bool {
 		value := formatValue(attr.Value)
 		if len(c.groups) == 0 {
 			attrs = append(attrs, attr.Key+"="+value)
 		} else {
 			attrs = append(attrs, strings.Join(append(c.groups, attr.Key), ".")+"="+value)
 		}
+		return true
 	})
 
 	attrsStr := ""
@@ -111,7 +113,7 @@ func (c *ctlHandler) Handle(r slog.Record) error {
 	msg := r.Message
 	var err error
 	if attrsStr == "" {
-		if r.Level != slog.InfoLevel {
+		if r.Level != slog.LevelInfo {
 			levelStr := r.Level.String()
 			c, ok := levelColor[strings.SplitN(levelStr, "+", 2)[0]]
 			if ok {
@@ -121,7 +123,7 @@ func (c *ctlHandler) Handle(r slog.Record) error {
 		_, err = fmt.Fprintf(c.output, "%s\n", msg)
 	} else {
 		msgWidth := stringWidth(msg)
-		if r.Level != slog.InfoLevel {
+		if r.Level != slog.LevelInfo {
 			levelStr := r.Level.String()
 			c, ok := levelColor[strings.SplitN(levelStr, "+", 2)[0]]
 			if ok {
@@ -152,9 +154,9 @@ func newColour(c ctc.Color, msg string) color {
 }
 
 var levelColor = map[string]color{
-	slog.ErrorLevel.String(): newColour(ctc.ForegroundRed, slog.ErrorLevel.String()),
-	slog.WarnLevel.String():  newColour(ctc.ForegroundYellow, slog.WarnLevel.String()),
-	slog.DebugLevel.String(): newColour(ctc.ForegroundCyan, slog.DebugLevel.String()),
+	slog.LevelError.String(): newColour(ctc.ForegroundRed, slog.LevelError.String()),
+	slog.LevelWarn.String():  newColour(ctc.ForegroundYellow, slog.LevelWarn.String()),
+	slog.LevelDebug.String(): newColour(ctc.ForegroundCyan, slog.LevelDebug.String()),
 }
 
 func (c *ctlHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
diff --git a/pkg/log/logger_noop.go b/pkg/log/logger_noop.go
index ec109b37..9186b890 100644
--- a/pkg/log/logger_noop.go
+++ b/pkg/log/logger_noop.go
@@ -17,21 +17,23 @@ limitations under the License.
 package log
 
 import (
+	"context"
+
 	//nolint:depguard
 	"golang.org/x/exp/slog"
 )
 
-var noop = wrapSlog(slog.New(noopHandler{}), slog.InfoLevel)
+var noop = wrapSlog(noopHandler{}, slog.LevelInfo)
 
 type noopHandler struct{}
 
 var _ slog.Handler = noopHandler{}
 
-func (noopHandler) Enabled(slog.Level) bool {
+func (noopHandler) Enabled(_ context.Context, _ slog.Level) bool {
 	return false
 }
 
-func (noopHandler) Handle(r slog.Record) error {
+func (noopHandler) Handle(_ context.Context, _ slog.Record) error {
 	return nil
 }
 
diff --git a/pkg/log/wrapper.go b/pkg/log/wrapper.go
index 3a57e282..6a5c1c5f 100644
--- a/pkg/log/wrapper.go
+++ b/pkg/log/wrapper.go
@@ -17,9 +17,12 @@ limitations under the License.
 package log
 
 import (
+	"context"
 	"fmt"
+	"runtime"
 	"strconv"
 	"strings"
+	"time"
 
 	//nolint:depguard
 	"golang.org/x/exp/slog"
@@ -30,64 +33,85 @@ type Level = slog.Level
 
 // The following is Level definitions copied from slog.
 const (
-	DebugLevel Level = slog.DebugLevel
-	InfoLevel  Level = slog.InfoLevel
-	WarnLevel  Level = slog.WarnLevel
-	ErrorLevel Level = slog.ErrorLevel
+	LevelDebug Level = slog.LevelDebug
+	LevelInfo  Level = slog.LevelInfo
+	LevelWarn  Level = slog.LevelWarn
+	LevelError Level = slog.LevelError
 )
 
-func wrapSlog(log *slog.Logger, level slog.Level) *Logger {
-	return &Logger{log, level}
+func wrapSlog(handler slog.Handler, level slog.Level) *Logger {
+	return &Logger{handler, level}
 }
 
-// Logger is a wrapper around slog.Logger.
+// Logger is a wrapper around slog.Handler.
 type Logger struct {
-	log   *slog.Logger
-	level Level // Level specifies a level of verbosity for V logs.
-}
-
-// LogDepth logs a message with the given level and depth.
-func (l *Logger) LogDepth(calldepth int, level Level, msg string, args ...any) {
-	l.log.LogDepth(calldepth+1, level, msg, args...)
+	handler slog.Handler
+	level   Level // Level specifies a level of verbosity for V logs.
 }
 
 // Log logs a message with the given level.
 func (l *Logger) Log(level Level, msg string, args ...any) {
-	l.LogDepth(0, level, msg, args...)
+	l.log(level, msg, args...)
 }
 
 // Debug logs a debug message.
 func (l *Logger) Debug(msg string, args ...any) {
-	l.LogDepth(0, DebugLevel, msg, args...)
+	l.log(LevelDebug, msg, args...)
 }
 
 // Info logs an informational message.
 func (l *Logger) Info(msg string, args ...any) {
-	l.LogDepth(0, InfoLevel, msg, args...)
+	l.log(LevelInfo, msg, args...)
 }
 
 // Warn logs a warning message.
 func (l *Logger) Warn(msg string, args ...any) {
-	l.LogDepth(0, WarnLevel, msg, args...)
+	l.log(LevelWarn, msg, args...)
 }
 
 // Error logs an error message.
 func (l *Logger) Error(msg string, err error, args ...any) {
 	if err != nil {
-		args = append(args[:len(args):len(args)], slog.Any(slog.ErrorKey, err))
+		args = append(args[:len(args):len(args)], slog.Any("err", err))
 	}
-	l.log.LogDepth(0, ErrorLevel, msg, args...)
+	l.log(LevelError, msg, args...)
+}
+
+// log is the low-level logging method for methods that take ...any.
+// It must always be called directly by an exported logging method
+// or function, because it uses a fixed call depth to obtain the pc.
+// copied from slog.Logger
+func (l *Logger) log(level Level, msg string, args ...any) {
+	if !l.handler.Enabled(context.Background(), level) {
+		return
+	}
+	var pc uintptr
+	var pcs [1]uintptr
+	// skip [runtime.Callers, this function, this function's caller]
+	runtime.Callers(3, pcs[:])
+	pc = pcs[0]
+	r := slog.NewRecord(time.Now(), level, msg, pc)
+	r.Add(args...)
+	_ = l.handler.Handle(context.Background(), r)
 }
 
 // With returns a new Logger that includes the given arguments.
 func (l *Logger) With(args ...any) *Logger {
-	return wrapSlog(l.log.With(args...), l.level)
+	var (
+		attr  slog.Attr
+		attrs []slog.Attr
+	)
+	for len(args) > 0 {
+		attr, args = argsToAttr(args)
+		attrs = append(attrs, attr)
+	}
+	return wrapSlog(l.handler.WithAttrs(attrs), l.level)
 }
 
 // WithGroup returns a new Logger that starts a group. The keys of all
 // attributes added to the Logger will be qualified by the given name.
 func (l *Logger) WithGroup(name string) *Logger {
-	return wrapSlog(l.log.WithGroup(name), l.level)
+	return wrapSlog(l.handler.WithGroup(name), l.level)
 }
 
 // Level returns
@@ -111,13 +135,13 @@ func ParseLevel(s string) (l Level, err error) {
 
 	switch strings.ToUpper(name) {
 	case "DEBUG":
-		l = DebugLevel
+		l = LevelDebug
 	case "INFO":
-		l = InfoLevel
+		l = LevelInfo
 	case "WARN":
-		l = WarnLevel
+		l = LevelWarn
 	case "ERROR":
-		l = ErrorLevel
+		l = LevelError
 	default:
 		return 0, fmt.Errorf("ParseLevel %q: invalid level name", s)
 	}
@@ -132,3 +156,28 @@ func ParseLevel(s string) (l Level, err error) {
 
 	return l, nil
 }
+
+const badKey = "!BADKEY"
+
+// argsToAttr turns a prefix of the nonempty args slice into an Attr
+// and returns the unconsumed portion of the slice.
+// If args[0] is an Attr, it returns it.
+// If args[0] is a string, it treats the first two elements as
+// a key-value pair.
+// Otherwise, it treats args[0] as a value with a missing key.
+// copied from slog.Logger
+func argsToAttr(args []any) (slog.Attr, []any) {
+	switch x := args[0].(type) {
+	case string:
+		if len(args) == 1 {
+			return slog.String(badKey, x), nil
+		}
+		return slog.Any(x, args[1]), args[2:]
+
+	case slog.Attr:
+		return x, args[1:]
+
+	default:
+		return slog.Any(badKey, x), args[1:]
+	}
+}


Test patch:
diff --git a/pkg/kwok/controllers/controller_test.go b/pkg/kwok/controllers/controller_test.go
index a4a1c54f..4307041b 100644
--- a/pkg/kwok/controllers/controller_test.go
+++ b/pkg/kwok/controllers/controller_test.go
@@ -127,7 +127,7 @@ func TestController(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.DebugLevel))
+	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.LevelDebug))
 	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
 	t.Cleanup(func() {
 		cancel()
diff --git a/pkg/kwok/controllers/node_controller_test.go b/pkg/kwok/controllers/node_controller_test.go
index b17dbbac..bbb6c4a4 100644
--- a/pkg/kwok/controllers/node_controller_test.go
+++ b/pkg/kwok/controllers/node_controller_test.go
@@ -81,7 +81,7 @@ func TestNodeController(t *testing.T) {
 		t.Fatal(fmt.Errorf("new nodes controller error: %w", err))
 	}
 	ctx := context.Background()
-	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.DebugLevel))
+	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.LevelDebug))
 	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
 	t.Cleanup(func() {
 		cancel()
diff --git a/pkg/kwok/controllers/pod_controller_test.go b/pkg/kwok/controllers/pod_controller_test.go
index 3093fbed..6bf7bce8 100644
--- a/pkg/kwok/controllers/pod_controller_test.go
+++ b/pkg/kwok/controllers/pod_controller_test.go
@@ -177,7 +177,7 @@ func TestPodController(t *testing.T) {
 	}
 
 	ctx := context.Background()
-	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.DebugLevel))
+	ctx = log.NewContext(ctx, log.NewLogger(os.Stderr, log.LevelDebug))
 	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
 	t.Cleanup(func() {
 		cancel()
diff --git a/pkg/log/wrapper_test.go b/pkg/log/wrapper_test.go
index 733863da..eac2b22c 100644
--- a/pkg/log/wrapper_test.go
+++ b/pkg/log/wrapper_test.go
@@ -35,56 +35,56 @@ func TestParseLevel(t *testing.T) {
 			args: args{
 				s: "debug",
 			},
-			wantL: DebugLevel,
+			wantL: LevelDebug,
 		},
 		{
 			name: "info",
 			args: args{
 				s: "info",
 			},
-			wantL: InfoLevel,
+			wantL: LevelInfo,
 		},
 		{
 			name: "-4",
 			args: args{
 				s: "-4",
 			},
-			wantL: DebugLevel,
+			wantL: LevelDebug,
 		},
 		{
 			name: "0",
 			args: args{
 				s: "0",
 			},
-			wantL: InfoLevel,
+			wantL: LevelInfo,
 		},
 		{
 			name: "4",
 			args: args{
 				s: "4",
 			},
-			wantL: WarnLevel,
+			wantL: LevelWarn,
 		},
 		{
 			name: "8",
 			args: args{
 				s: "8",
 			},
-			wantL: ErrorLevel,
+			wantL: LevelError,
 		},
 		{
 			name: "info+1",
 			args: args{
 				s: "info+1",
 			},
-			wantL: InfoLevel + 1,
+			wantL: LevelInfo + 1,
 		},
 		{
 			name: "info-1",
 			args: args{
 				s: "info-1",
 			},
-			wantL: InfoLevel - 1,
+			wantL: LevelInfo - 1,
 		},
 	}
 	for _, tt := range tests {