diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 15a89ac..9436b52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,11 @@ jobs: restore-keys: | ${{ runner.os }}-go1.22- + - name: golangci-lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 #v9.2.0 + with: + version: v2.11 + - name: Run unit tests run: make testvv env: diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..789e43d --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,36 @@ +version: "2" +linters: + default: none + enable: + - sloglint + settings: + sloglint: + # Enforce key-value pair form for Info/Debug/Warn/Error/Log/With and + # the package-level slog equivalents. Use l.Log(ctx, level, ...) for + # custom levels instead of LogAttrs when you can. + # + # LogAttrs is also flagged by this rule because it takes ...slog.Attr; + # the few legitimate sites (where attrs is built up as a []slog.Attr) + # carry a //nolint:sloglint with rationale. + kv-only: true + # no-mixed-args is on by default: forbids mixing kv and attrs in one call. + # discard-handler is on by default (since Go 1.24): suggests + # slog.DiscardHandler over slog.NewTextHandler(io.Discard, nil). + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/client.go b/client.go index 415c188..bbdee26 100644 --- a/client.go +++ b/client.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net" "net/http" "net/url" @@ -19,7 +20,6 @@ import ( "github.com/DefinedNet/dnapi/keys" "github.com/DefinedNet/dnapi/message" - "github.com/sirupsen/logrus" ) // Client communicates with the API server. @@ -127,8 +127,8 @@ func mergeIPAddresses(plural []string, singular string) []string { // generated DH X25519 public key to be signed by the CA, and an Ed 25519 public key for future API call authentication. // On success it returns the Nebula config generated by the server, a Nebula private key PEM to be inserted into the // config (see api.InsertConfigPrivateKey), credentials to be used in DNClient API requests, and a meta object. -func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code string) ([]byte, []byte, *keys.Credentials, *ConfigMeta, error) { - logger.WithFields(logrus.Fields{"server": c.dnServer}).Debug("Making enrollment request to API") +func (c *Client) Enroll(ctx context.Context, logger *slog.Logger, code string) ([]byte, []byte, *keys.Credentials, *ConfigMeta, error) { + logger.Debug("Making enrollment request to API", "server", c.dnServer) // Generate newKeys for the enrollment request newKeys, err := keys.New() @@ -157,7 +157,7 @@ func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code str } reqID, r, err := callAPI[message.EnrollResponseData](ctx, c, "POST", message.EnrollEndpoint, payload) - l := logger.WithFields(logrus.Fields{"reqID": reqID}) + l := logger.With("reqID", reqID) if err != nil { var apiErrors message.APIErrors if errors.As(err, &apiErrors) && len(apiErrors) == 1 { @@ -174,7 +174,7 @@ func (c *Client) Enroll(ctx context.Context, logger logrus.FieldLogger, code str } } - l.WithError(err).Error("Enrollment request failed with unexpected error") + l.Error("Enrollment request failed with unexpected error", "error", err) return nil, nil, nil, nil, &APIError{e: fmt.Errorf("unexpected error during enrollment: %w", err), ReqID: reqID} } l.Info("Enrollment request succeeded") diff --git a/client_test.go b/client_test.go index e8d73a3..85d5af7 100644 --- a/client_test.go +++ b/client_test.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "log/slog" "net/http" "net/http/httptest" "testing" @@ -20,7 +21,6 @@ import ( "github.com/DefinedNet/dnapi/internal/testutil" "github.com/DefinedNet/dnapi/keys" "github.com/DefinedNet/dnapi/message" - "github.com/sirupsen/logrus" "github.com/slackhq/nebula/cert" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,6 +29,15 @@ import ( type m map[string]interface{} +// dropTimeAttr is a slog.HandlerOptions.ReplaceAttr that drops the time +// attribute from a record, so JSON output is deterministic in tests. +func dropTimeAttr(_ []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a +} + func TestEnroll(t *testing.T) { t.Parallel() @@ -988,10 +997,10 @@ func TestStreamCommandResponse(t *testing.T) { require.NoError(t, err) // Configure a logger to write to a buffer and the stream - logger := logrus.New() - logger.SetFormatter(&logrus.JSONFormatter{}) - logger.SetOutput(io.MultiWriter(sc, &buf)) - logger.SetLevel(logrus.DebugLevel) + logger := slog.New(slog.NewJSONHandler(io.MultiWriter(sc, &buf), &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: dropTimeAttr, + })) logger.Info("Hello, world! info!") logger.Warn("Hello, world! warning!") @@ -1018,7 +1027,10 @@ func TestStreamCommandResponse(t *testing.T) { sc, err = c.StreamCommandResponse(context.Background(), *creds, "responseToken") require.NoError(t, err) - logger.SetOutput(io.MultiWriter(sc, &buf)) + logger = slog.New(slog.NewJSONHandler(io.MultiWriter(sc, &buf), &slog.HandlerOptions{ + Level: slog.LevelDebug, + ReplaceAttr: dropTimeAttr, + })) logger.Info("Hello, world! info!") logger.Warn("Hello, world! warning!") diff --git a/examples/simple/main.go b/examples/simple/main.go index 8862767..3c3ffe0 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -4,11 +4,11 @@ import ( "context" "flag" "fmt" + "log/slog" "os" "time" "github.com/DefinedNet/dnapi" - "github.com/sirupsen/logrus" ) func main() { @@ -22,18 +22,20 @@ func main() { os.Exit(1) } - logger := logrus.New() + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) c := dnapi.NewClient("api-example/1.0", *server) // initial enrollment example config, pkey, creds, meta, err := c.Enroll(context.Background(), logger, *code) if err != nil { - logger.WithError(err).Fatal("Failed to enroll") + logger.Error("Failed to enroll", "error", err) + os.Exit(1) } config, err = dnapi.InsertConfigPrivateKey(config, pkey) if err != nil { - logger.WithError(err).Fatal("Failed to insert private key into config") + logger.Error("Failed to insert private key into config", "error", err) + os.Exit(1) } fmt.Printf( @@ -53,7 +55,7 @@ func main() { // check for an update and perform the update if available updateAvailable, err := c.CheckForUpdate(context.Background(), *creds) if err != nil { - logger.WithError(err).Error("Failed to check for update") + logger.Error("Failed to check for update", "error", err) continue } @@ -63,13 +65,13 @@ func main() { // this makes it less obvious to the caller that they need to save the new credentials to disk config, pkey, newCreds, meta, err := c.DoUpdate(context.Background(), *creds) if err != nil { - logger.WithError(err).Error("Failed to perform update") + logger.Error("Failed to perform update", "error", err) continue } config, err = dnapi.InsertConfigPrivateKey(config, pkey) if err != nil { - logger.WithError(err).Error("Failed to insert private key into config") + logger.Error("Failed to insert private key into config", "error", err) continue } diff --git a/go.mod b/go.mod index 9b67f02..c5f3d27 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/DefinedNet/dnapi go 1.25 require ( - github.com/sirupsen/logrus v1.9.4 github.com/slackhq/nebula v1.10.3 github.com/stretchr/testify v1.11.1 golang.org/x/crypto v0.47.0 diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 9bed3fe..765928b 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -1,37 +1,34 @@ package testutil import ( - "io/ioutil" + "io" + "log/slog" "os" - - "github.com/sirupsen/logrus" ) -// NewTestLogger returns a *logrus.Logger struct configured for testing (e.g. end-to-end tests, unit tests, etc.) -func NewTestLogger() *logrus.Logger { - l := logrus.New() - l.SetFormatter(&logrus.JSONFormatter{ - DisableTimestamp: true, - FieldMap: logrus.FieldMap{ - logrus.FieldKeyMsg: "message", - }, - }) - - v := os.Getenv("TEST_LOGS") - if v == "" { - l.SetOutput(ioutil.Discard) - return l - } +// NewTestLogger returns a *slog.Logger configured for testing (e.g. +// end-to-end tests, unit tests, etc.). Set the TEST_LOGS environment +// variable to 1/2/3 to raise the verbosity from info to debug/trace. +func NewTestLogger() *slog.Logger { + level := slog.LevelInfo + out := io.Discard - switch v { + switch os.Getenv("TEST_LOGS") { + case "": + // Keep the discard writer and info level default. case "1": - // This is the default level but we are being explicit - l.SetLevel(logrus.InfoLevel) + out = os.Stdout case "2": - l.SetLevel(logrus.DebugLevel) + out = os.Stdout + level = slog.LevelDebug case "3": - l.SetLevel(logrus.TraceLevel) + out = os.Stdout + level = slog.Level(-8) // trace-equivalent; below Debug + default: + out = os.Stdout } - return l + return slog.New(slog.NewJSONHandler(out, &slog.HandlerOptions{ + Level: level, + })) }