Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 3 additions & 13 deletions internal/cmd/launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io/fs"
"log/slog"
"os"
"os/exec"
"strings"
Expand Down Expand Up @@ -322,7 +323,8 @@ func newCacheProvisioner(gs *state.GlobalState) provisioner {
func (p *buildProvisioner) provision(ctx context.Context, deps map[string]string) (commandExecutor, error) {
config := getProviderConfig(p.gs)

provider, err := k6provider.NewProvider(config)
logger := slog.New(newLogrusSlogHandler(p.gs.Logger))
provider, err := k6provider.NewProviderWithLogger(config, logger)
if err != nil {
return nil, err
}
Expand All @@ -336,10 +338,6 @@ func (p *buildProvisioner) provision(ctx context.Context, deps map[string]string
if err != nil {
return nil, err
}
if !p.cachedOnly {
p.gs.Logger.Info("A new k6 binary has been provisioned with version(s): ",
formatDependencies(binary.Dependencies))
}

return &customBinary{binary.Path}, nil
}
Expand All @@ -362,14 +360,6 @@ func getProviderConfig(gs *state.GlobalState) k6provider.Config {
return config
}

func formatDependencies(deps map[string]string) string {
buffer := &bytes.Buffer{}
for dep, version := range deps {
fmt.Fprintf(buffer, "%s:%s ", dep, version)
}
return strings.Trim(buffer.String(), " ")
}

// extractToken gets the cloud token required to access the build service
// from the environment or from the config file
func extractToken(gs *state.GlobalState) (string, error) {
Expand Down
103 changes: 103 additions & 0 deletions internal/cmd/logrus_slog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package cmd

import (
"context"
"log/slog"

"github.com/sirupsen/logrus"
)

// logrusSlogHandler is a slog.Handler that forwards to a logrus logger.
// Attribute keys are namespaced with any active group path (e.g. "group.key").
type logrusSlogHandler struct {
logger *logrus.Logger //nolint:forbidigo
attrs []slog.Attr // pre-qualified attrs (keys already include group prefix)
groupPath string // dot-joined group names from outer to inner, e.g. "a.b"
}

func newLogrusSlogHandler(logger *logrus.Logger) slog.Handler { //nolint:forbidigo
return &logrusSlogHandler{logger: logger}
}

func (h *logrusSlogHandler) Enabled(_ context.Context, level slog.Level) bool {
switch {
case level >= slog.LevelError:
return h.logger.IsLevelEnabled(logrus.ErrorLevel)
case level >= slog.LevelWarn:
return h.logger.IsLevelEnabled(logrus.WarnLevel)
case level >= slog.LevelInfo:
return h.logger.IsLevelEnabled(logrus.InfoLevel)
default:
return h.logger.IsLevelEnabled(logrus.DebugLevel)
}
}

func (h *logrusSlogHandler) Handle(_ context.Context, r slog.Record) error {
fields := make(logrus.Fields, len(h.attrs)+r.NumAttrs())
for _, a := range h.attrs {
fields[a.Key] = a.Value.Any()
}
r.Attrs(func(a slog.Attr) bool {
flattenAttr(h.groupPath, a, fields)
return true
})

entry := h.logger.WithFields(fields)
switch {
case r.Level >= slog.LevelError:
entry.Error(r.Message)
case r.Level >= slog.LevelWarn:
entry.Warn(r.Message)
case r.Level >= slog.LevelInfo:
entry.Info(r.Message)
default:
entry.Debug(r.Message)
}
return nil
}

func (h *logrusSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
// Flatten and pre-qualify keys now so Handle doesn't repeat the work.
fields := make(logrus.Fields, len(attrs))
for _, a := range attrs {
flattenAttr(h.groupPath, a, fields)
}
qualified := make([]slog.Attr, 0, len(fields))
for k, v := range fields {
qualified = append(qualified, slog.Any(k, v))
}
combined := make([]slog.Attr, len(h.attrs)+len(qualified))
copy(combined, h.attrs)
copy(combined[len(h.attrs):], qualified)
return &logrusSlogHandler{logger: h.logger, attrs: combined, groupPath: h.groupPath}
}

func (h *logrusSlogHandler) WithGroup(name string) slog.Handler {
if name == "" {
return h
}
path := name
if h.groupPath != "" {
path = h.groupPath + "." + name
}
return &logrusSlogHandler{logger: h.logger, attrs: h.attrs, groupPath: path}
}

// flattenAttr copies a slog attribute into fields using dot-separated keys.
// Group attributes are expanded recursively, so prefix + group + child becomes
// a single logrus field key such as "provider.request.id".
func flattenAttr(prefix string, a slog.Attr, fields logrus.Fields) {
key := a.Key
if prefix != "" {
key = prefix + "." + key
}
if a.Value.Kind() == slog.KindGroup {
for _, child := range a.Value.Group() {
flattenAttr(key, child, fields)
}
return
}
if fields != nil {
fields[key] = a.Value.Any()
}
}
94 changes: 94 additions & 0 deletions internal/cmd/logrus_slog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package cmd

import (
"bytes"
"context"
"log/slog"
"testing"

"github.com/sirupsen/logrus"
)

// captureLogrus returns a logrus logger that writes JSON to buf.
func captureLogrus(buf *bytes.Buffer) *logrus.Logger { //nolint:forbidigo
l := logrus.New()
l.SetFormatter(&logrus.JSONFormatter{})
l.SetLevel(logrus.DebugLevel)
l.SetOutput(buf)
return l
}

func TestLogrusSlogHandler_Levels(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
level slog.Level
want string
}{
{slog.LevelDebug, `"level":"debug"`},
{slog.LevelInfo, `"level":"info"`},
{slog.LevelWarn, `"level":"warning"`},
{slog.LevelError, `"level":"error"`},
} {
t.Run(tc.level.String(), func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
logger := slog.New(newLogrusSlogHandler(captureLogrus(&buf)))
logger.Log(context.Background(), tc.level, "msg")
if !bytes.Contains(buf.Bytes(), []byte(tc.want)) {
t.Errorf("expected %q in output %q", tc.want, buf.String())
}
})
}
}

func TestLogrusSlogHandler_WithGroup(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
logger := slog.New(newLogrusSlogHandler(captureLogrus(&buf)))

// single group
logger.WithGroup("provider").Info("msg", "key", "val")
if !bytes.Contains(buf.Bytes(), []byte(`"provider.key":"val"`)) {
t.Errorf("expected provider.key in output: %s", buf.String())
}
buf.Reset()

// nested groups
logger.WithGroup("a").WithGroup("b").Info("msg", "key", "val")
if !bytes.Contains(buf.Bytes(), []byte(`"a.b.key":"val"`)) {
t.Errorf("expected a.b.key in output: %s", buf.String())
}
buf.Reset()

// WithAttrs after WithGroup
sub := logger.WithGroup("g").With("pre", "v")
sub.Info("msg", "post", "w")
out := buf.String()
if !bytes.Contains(buf.Bytes(), []byte(`"g.pre":"v"`)) {
t.Errorf("expected g.pre in output: %s", out)
}
if !bytes.Contains(buf.Bytes(), []byte(`"g.post":"w"`)) {
t.Errorf("expected g.post in output: %s", out)
}
buf.Reset()

// empty group name is a no-op
logger.WithGroup("").Info("msg", "key", "bare")
if !bytes.Contains(buf.Bytes(), []byte(`"key":"bare"`)) {
t.Errorf("expected bare key in output: %s", buf.String())
}
}

func TestLogrusSlogHandler_GroupKindAttr(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
logger := slog.New(newLogrusSlogHandler(captureLogrus(&buf)))

// slog.Group() produces a KindGroup attr — should be flattened
logger.Info("msg", slog.Group("outer", slog.String("inner", "val")))
if !bytes.Contains(buf.Bytes(), []byte(`"outer.inner":"val"`)) {
t.Errorf("expected outer.inner in output: %s", buf.String())
}
}
Loading