Skip to content

Commit 9fb6f00

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. Changes: - internal/cmd/logrus_slog.go: slog.Handler backed by *logrus.Logger. Attribute keys are dot-namespaced under any active group (WithGroup). KindGroup attrs are recursively flattened. WithAttrs pre-qualifies keys at call time. - internal/cmd/launcher.go: use NewProviderWithLogger passing the above handler; remove the now-redundant manual provisioning Info log and its formatDependencies helper. Note: k6provider is currently pinned to a pre-release pseudo-version from the k6providerLogging branch (grafana/k6provider#116). This will be updated to the tagged release once that PR is merged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5e2a6e1 commit 9fb6f00

18 files changed

Lines changed: 527 additions & 1311 deletions

File tree

go.mod

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ require (
2121
github.com/google/uuid v1.6.0
2222
github.com/gorilla/websocket v1.5.3
2323
github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20260331193133-94d5832119b8
24-
github.com/grafana/k6provider v0.2.0
24+
github.com/grafana/k6provider v0.2.1-0.20260415100024-4948162ef922
2525
github.com/grafana/sobek v0.0.0-20260331145705-2272ac4993ef
2626
github.com/grafana/xk6-dashboard-assets v0.1.2
2727
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
@@ -82,7 +82,6 @@ require (
8282
github.com/go-logr/logr v1.4.3 // indirect
8383
github.com/go-logr/stdr v1.2.2 // indirect
8484
github.com/google/pprof v0.0.0-20230728192033-2ba5b33183c6 // indirect
85-
github.com/grafana/k6build v0.5.15 // indirect
8685
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
8786
github.com/inconshreveable/mousetrap v1.0.0 // indirect
8887
github.com/jhump/protoreflect/v2 v2.0.0-beta.1 // indirect

go.sum

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,12 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
8080
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
8181
github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20260331193133-94d5832119b8 h1:Lk4QeKB37qTT51wMA5QYukHBKCptVSjI1b4Se36Aa9U=
8282
github.com/grafana/k6-cloud-openapi-client-go v0.0.0-20260331193133-94d5832119b8/go.mod h1:RBPBP7qIR/K6qzQEQYESVhp/XJspiBTOyBEBCbPXrvI=
83-
github.com/grafana/k6build v0.5.15 h1:4I5dkAWSMvXsElS1OpLbHj6ZXnebXZGnmwDXy5vcwSQ=
84-
github.com/grafana/k6build v0.5.15/go.mod h1:Sk7SUiCnx2AgkirG3PrCtmJKYL+a8EeRdTzFfbxP0X8=
85-
github.com/grafana/k6foundry v0.4.7 h1:1YXkTBwO/2dSx0pqJrraJATsFlsIX0vEpaEjV7E35w4=
86-
github.com/grafana/k6foundry v0.4.7/go.mod h1:eLsr0whhH+5Y1y7YpDxJi3Jv5wHMuf+80vdRyMH10pg=
87-
github.com/grafana/k6provider v0.2.0 h1:Zu8FBnk6cJyTTkpCA+y+Ravc2YFeAQjsIfPpcbZtfB0=
88-
github.com/grafana/k6provider v0.2.0/go.mod h1:TJ6vzPm4yDQ2ji/Fet0dFOpdjktrKZp4hsnLZhRoZVA=
83+
github.com/grafana/k6provider v0.2.1-0.20260415091724-10fc9964f7de h1:wGGJlwTliMpecLfM97X2+y7QD1my57cdS41EhLwAVPs=
84+
github.com/grafana/k6provider v0.2.1-0.20260415091724-10fc9964f7de/go.mod h1:R9WKthnwk96E47m6w/mnC13hhBYTn5iMdP5caFt/t5k=
85+
github.com/grafana/k6provider v0.2.1-0.20260415094313-e3f84614d0c4 h1:MG1XjQVItvcV53bAUJgJeGYW053WP0Xg/TN+0QmTP6U=
86+
github.com/grafana/k6provider v0.2.1-0.20260415094313-e3f84614d0c4/go.mod h1:R9WKthnwk96E47m6w/mnC13hhBYTn5iMdP5caFt/t5k=
87+
github.com/grafana/k6provider v0.2.1-0.20260415100024-4948162ef922 h1:Fa1NO2t1NsuNVuDzBBPm89+9JUU29D4eB1iCtlfw3Rc=
88+
github.com/grafana/k6provider v0.2.1-0.20260415100024-4948162ef922/go.mod h1:R9WKthnwk96E47m6w/mnC13hhBYTn5iMdP5caFt/t5k=
8989
github.com/grafana/sobek v0.0.0-20260331145705-2272ac4993ef h1:onLtCR9w3Iqn57s39rYf1PtRl89uSznaM36TKcepxn4=
9090
github.com/grafana/sobek v0.0.0-20260331145705-2272ac4993ef/go.mod h1:YtuqiJX1W3XvRSilL/kUZzduJG3phPJWyzM9DiIEfBo=
9191
github.com/grafana/xk6-dashboard-assets v0.1.2 h1:n2wqytPICn2ZYsKa9HE6GlvFXk+WjByMfT4eM+ms3gE=
@@ -251,8 +251,6 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
251251
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
252252
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
253253
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
254-
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
255-
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
256254
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
257255
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
258256
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

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+
}

vendor/github.com/grafana/k6build/Dockerfile

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)