Skip to content

Commit 2f8b4ab

Browse files
mstoykovclaude
andcommitted
Wire k6provider structured logging into k6
Adds a slog.Handler bridge (logrusSlogHandler) that forwards k6provider's structured log events through k6's existing logrus logger, so provider operations (artifact resolution, cache hits, downloads, retries, pruning) appear in k6's log output at the correct level. Removes the now-redundant manual provisioning Info log and its formatDependencies helper from k6buildProvisioner. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 075e4af commit 2f8b4ab

3 files changed

Lines changed: 199 additions & 12 deletions

File tree

internal/cmd/launcher.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"errors"
77
"fmt"
8+
"log/slog"
89
"os"
910
"os/exec"
1011
"strings"
@@ -174,7 +175,8 @@ func newK6BuildProvisioner(gs *state.GlobalState) provisioner {
174175
func (p *k6buildProvisioner) provision(deps map[string]string) (commandExecutor, error) {
175176
config := getProviderConfig(p.gs)
176177

177-
provider, err := k6provider.NewProvider(config)
178+
logger := slog.New(newLogrusSlogHandler(p.gs.Logger))
179+
provider, err := k6provider.NewProviderWithLogger(config, logger)
178180
if err != nil {
179181
return nil, err
180182
}
@@ -184,9 +186,6 @@ func (p *k6buildProvisioner) provision(deps map[string]string) (commandExecutor,
184186
return nil, err
185187
}
186188

187-
p.gs.Logger.
188-
Info("A new k6 binary has been provisioned with version(s): ", formatDependencies(binary.Dependencies))
189-
190189
return &customBinary{binary.Path}, nil
191190
}
192191

@@ -208,14 +207,6 @@ func getProviderConfig(gs *state.GlobalState) k6provider.Config {
208207
return config
209208
}
210209

211-
func formatDependencies(deps map[string]string) string {
212-
buffer := &bytes.Buffer{}
213-
for dep, version := range deps {
214-
fmt.Fprintf(buffer, "%s:%s ", dep, version)
215-
}
216-
return strings.Trim(buffer.String(), " ")
217-
}
218-
219210
// extractToken gets the cloud token required to access the build service
220211
// from the environment or from the config file
221212
func extractToken(gs *state.GlobalState) (string, error) {

internal/cmd/logrus_slog.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
7+
"github.com/sirupsen/logrus"
8+
)
9+
10+
// logrusSlogHandler is a slog.Handler that forwards to a logrus logger.
11+
// Attribute keys are namespaced with any active group path (e.g. "group.key").
12+
type logrusSlogHandler struct {
13+
logger *logrus.Logger //nolint:forbidigo
14+
attrs []slog.Attr // pre-qualified attrs (keys already include group prefix)
15+
groupPath string // dot-joined group names from outer to inner, e.g. "a.b"
16+
}
17+
18+
func newLogrusSlogHandler(logger *logrus.Logger) slog.Handler { //nolint:forbidigo
19+
return &logrusSlogHandler{logger: logger}
20+
}
21+
22+
func (h *logrusSlogHandler) Enabled(_ context.Context, level slog.Level) bool {
23+
switch {
24+
case level >= slog.LevelError:
25+
return h.logger.IsLevelEnabled(logrus.ErrorLevel)
26+
case level >= slog.LevelWarn:
27+
return h.logger.IsLevelEnabled(logrus.WarnLevel)
28+
case level >= slog.LevelInfo:
29+
return h.logger.IsLevelEnabled(logrus.InfoLevel)
30+
default:
31+
return h.logger.IsLevelEnabled(logrus.DebugLevel)
32+
}
33+
}
34+
35+
func (h *logrusSlogHandler) Handle(_ context.Context, r slog.Record) error {
36+
fields := make(logrus.Fields, len(h.attrs)+r.NumAttrs())
37+
for _, a := range h.attrs {
38+
fields[a.Key] = a.Value.Any()
39+
}
40+
r.Attrs(func(a slog.Attr) bool {
41+
flattenAttr(h.groupPath, a, fields)
42+
return true
43+
})
44+
45+
entry := h.logger.WithFields(fields)
46+
switch {
47+
case r.Level >= slog.LevelError:
48+
entry.Error(r.Message)
49+
case r.Level >= slog.LevelWarn:
50+
entry.Warn(r.Message)
51+
case r.Level >= slog.LevelInfo:
52+
entry.Info(r.Message)
53+
default:
54+
entry.Debug(r.Message)
55+
}
56+
return nil
57+
}
58+
59+
func (h *logrusSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
60+
// Flatten and pre-qualify keys now so Handle doesn't repeat the work.
61+
fields := make(logrus.Fields, len(attrs))
62+
for _, a := range attrs {
63+
flattenAttr(h.groupPath, a, fields)
64+
}
65+
qualified := make([]slog.Attr, 0, len(fields))
66+
for k, v := range fields {
67+
qualified = append(qualified, slog.Any(k, v))
68+
}
69+
combined := make([]slog.Attr, len(h.attrs)+len(qualified))
70+
copy(combined, h.attrs)
71+
copy(combined[len(h.attrs):], qualified)
72+
return &logrusSlogHandler{logger: h.logger, attrs: combined, groupPath: h.groupPath}
73+
}
74+
75+
func (h *logrusSlogHandler) WithGroup(name string) slog.Handler {
76+
if name == "" {
77+
return h
78+
}
79+
path := name
80+
if h.groupPath != "" {
81+
path = h.groupPath + "." + name
82+
}
83+
return &logrusSlogHandler{logger: h.logger, attrs: h.attrs, groupPath: path}
84+
}
85+
86+
// flattenAttr writes a into fields with keys qualified by prefix.
87+
// If a is a group-kind attr, it recurses into its children.
88+
func flattenAttr(prefix string, a slog.Attr, fields logrus.Fields) {
89+
key := a.Key
90+
if prefix != "" {
91+
key = prefix + "." + key
92+
}
93+
if a.Value.Kind() == slog.KindGroup {
94+
for _, child := range a.Value.Group() {
95+
flattenAttr(key, child, fields)
96+
}
97+
return
98+
}
99+
if fields != nil {
100+
fields[key] = a.Value.Any()
101+
}
102+
}

internal/cmd/logrus_slog_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package cmd
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"log/slog"
7+
"testing"
8+
9+
"github.com/sirupsen/logrus"
10+
)
11+
12+
// captureLogrus returns a logrus logger that writes JSON to buf.
13+
func captureLogrus(buf *bytes.Buffer) *logrus.Logger { //nolint:forbidigo
14+
l := logrus.New()
15+
l.SetFormatter(&logrus.JSONFormatter{})
16+
l.SetLevel(logrus.DebugLevel)
17+
l.SetOutput(buf)
18+
return l
19+
}
20+
21+
func TestLogrusSlogHandler_Levels(t *testing.T) {
22+
t.Parallel()
23+
for _, tc := range []struct {
24+
level slog.Level
25+
want string
26+
}{
27+
{slog.LevelDebug, `"level":"debug"`},
28+
{slog.LevelInfo, `"level":"info"`},
29+
{slog.LevelWarn, `"level":"warning"`},
30+
{slog.LevelError, `"level":"error"`},
31+
} {
32+
t.Run(tc.level.String(), func(t *testing.T) {
33+
t.Parallel()
34+
var buf bytes.Buffer
35+
logger := slog.New(newLogrusSlogHandler(captureLogrus(&buf)))
36+
logger.Log(context.Background(), tc.level, "msg")
37+
if !bytes.Contains(buf.Bytes(), []byte(tc.want)) {
38+
t.Errorf("expected %q in output %q", tc.want, buf.String())
39+
}
40+
})
41+
}
42+
}
43+
44+
func TestLogrusSlogHandler_WithGroup(t *testing.T) {
45+
t.Parallel()
46+
47+
var buf bytes.Buffer
48+
logger := slog.New(newLogrusSlogHandler(captureLogrus(&buf)))
49+
50+
// single group
51+
logger.WithGroup("provider").Info("msg", "key", "val")
52+
if !bytes.Contains(buf.Bytes(), []byte(`"provider.key":"val"`)) {
53+
t.Errorf("expected provider.key in output: %s", buf.String())
54+
}
55+
buf.Reset()
56+
57+
// nested groups
58+
logger.WithGroup("a").WithGroup("b").Info("msg", "key", "val")
59+
if !bytes.Contains(buf.Bytes(), []byte(`"a.b.key":"val"`)) {
60+
t.Errorf("expected a.b.key in output: %s", buf.String())
61+
}
62+
buf.Reset()
63+
64+
// WithAttrs after WithGroup
65+
sub := logger.WithGroup("g").With("pre", "v")
66+
sub.Info("msg", "post", "w")
67+
out := buf.String()
68+
if !bytes.Contains(buf.Bytes(), []byte(`"g.pre":"v"`)) {
69+
t.Errorf("expected g.pre in output: %s", out)
70+
}
71+
if !bytes.Contains(buf.Bytes(), []byte(`"g.post":"w"`)) {
72+
t.Errorf("expected g.post in output: %s", out)
73+
}
74+
buf.Reset()
75+
76+
// empty group name is a no-op
77+
logger.WithGroup("").Info("msg", "key", "bare")
78+
if !bytes.Contains(buf.Bytes(), []byte(`"key":"bare"`)) {
79+
t.Errorf("expected bare key in output: %s", buf.String())
80+
}
81+
}
82+
83+
func TestLogrusSlogHandler_GroupKindAttr(t *testing.T) {
84+
t.Parallel()
85+
86+
var buf bytes.Buffer
87+
logger := slog.New(newLogrusSlogHandler(captureLogrus(&buf)))
88+
89+
// slog.Group() produces a KindGroup attr — should be flattened
90+
logger.Info("msg", slog.Group("outer", slog.String("inner", "val")))
91+
if !bytes.Contains(buf.Bytes(), []byte(`"outer.inner":"val"`)) {
92+
t.Errorf("expected outer.inner in output: %s", buf.String())
93+
}
94+
}

0 commit comments

Comments
 (0)