From 82a2266316d02645931f0582642752cec4279e93 Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Tue, 30 Jul 2024 17:10:44 -0700 Subject: [PATCH 01/10] switch to slog --- cmd/aws-sso/cache_cmd.go | 8 +- cmd/aws-sso/console_cmd.go | 8 +- cmd/aws-sso/ecs_client_cmd.go | 8 +- cmd/aws-sso/ecs_server_cmd.go | 8 +- cmd/aws-sso/exec_cmd.go | 10 +- cmd/aws-sso/interactive.go | 10 +- cmd/aws-sso/list_cmd.go | 10 +- cmd/aws-sso/list_sso_roles_cmd.go | 4 +- cmd/aws-sso/login_cmd.go | 20 ++-- cmd/aws-sso/logout_cmd.go | 6 +- cmd/aws-sso/main.go | 40 ++++---- cmd/aws-sso/setup_wizard.go | 16 +-- internal/ecs/client/client.go | 12 +-- internal/ecs/server/default.go | 6 +- internal/ecs/server/httplog.go | 2 +- internal/ecs/server/profile.go | 4 +- internal/ecs/server/server.go | 4 +- internal/ecs/server/slotted.go | 2 +- internal/helper/helper.go | 6 +- internal/logger/logger.go | 156 ++++++++++++++++++++++++++++-- internal/predictor/predictor.go | 4 +- internal/sso/awssso.go | 34 +++---- internal/sso/awssso_auth.go | 43 ++++---- internal/sso/cache.go | 38 ++++---- internal/sso/config.go | 4 +- internal/sso/options.go | 2 +- internal/sso/roles.go | 13 ++- internal/sso/settings.go | 26 ++--- internal/storage/json_store.go | 4 +- internal/storage/keyring.go | 4 +- internal/storage/storage.go | 4 +- internal/tags/tags_list.go | 4 +- internal/url/url.go | 14 +-- internal/utils/fileedit.go | 2 +- internal/utils/utils.go | 8 +- 35 files changed, 336 insertions(+), 208 deletions(-) diff --git a/cmd/aws-sso/cache_cmd.go b/cmd/aws-sso/cache_cmd.go index 1d2bd6bf..e8de06a4 100644 --- a/cmd/aws-sso/cache_cmd.go +++ b/cmd/aws-sso/cache_cmd.go @@ -39,12 +39,12 @@ func (c CacheCmd) AfterApply(runCtx *RunContext) error { func (cc *CacheCmd) Run(ctx *RunContext) error { s, err := ctx.Settings.GetSelectedSSO(ctx.Cli.SSO) if err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal("unable to select SSO instance", "sso", ctx.Cli.SSO, "error", err.Error()) } ssoName, err := ctx.Settings.GetSelectedSSOName(ctx.Cli.SSO) if err != nil { - log.Fatalf(err.Error()) + log.Fatal("unable to get name for SSO instance", "sso", ctx.Cli.SSO, "error", err.Error()) } added, deleted, err := ctx.Settings.Cache.Refresh(AwsSSO, s, ssoName, ctx.Cli.Cache.Threads) @@ -59,13 +59,13 @@ func (cc *CacheCmd) Run(ctx *RunContext) error { } if added > 0 || deleted > 0 { - log.Infof("Updated cache: %d added, %d deleted", added, deleted) + log.Info("Updated cache", "added", added, "deleted", deleted) // should we update our config?? if !ctx.Cli.Cache.NoConfigCheck && ctx.Settings.AutoConfigCheck { if ctx.Settings.ConfigProfilesUrlAction != url.ConfigProfilesUndef { err := awsconfig.UpdateAwsConfig(ctx.Settings, "", true, false) if err != nil { - log.Errorf("Unable to auto-update aws config file: %s", err.Error()) + log.Error("Unable to auto-update aws config file", "error", err.Error()) } } } diff --git a/cmd/aws-sso/console_cmd.go b/cmd/aws-sso/console_cmd.go index 748877be..9a1023b1 100644 --- a/cmd/aws-sso/console_cmd.go +++ b/cmd/aws-sso/console_cmd.go @@ -241,7 +241,7 @@ func openConsole(ctx *RunContext, accountid int64, role string) error { ctx.Settings.Cache.AddHistory(utils.MakeRoleARN(accountid, role)) if err := ctx.Settings.Cache.Save(false); err != nil { - log.WithError(err).Warnf("Unable to update cache") + log.Warn("Unable to update cache", "error", err.Error()) } creds := GetRoleCredentials(ctx, AwsSSO, ctx.Cli.Console.STSRefresh, accountid, role) @@ -263,7 +263,7 @@ func openConsoleAccessKey(ctx *RunContext, creds *storage.RoleCredentials, resp, err := http.Get(signin.GetUrl()) if err != nil { - log.Debugf(err.Error()) + log.Debug("http get", "url", signin.GetUrl(), "error", err.Error()) // sanitize error and remove sensitive URL from normal output r := regexp.MustCompile(`Get "[^"]+": `) e := r.ReplaceAllString(err.Error(), "") @@ -279,7 +279,7 @@ func openConsoleAccessKey(ctx *RunContext, creds *storage.RoleCredentials, loginResponse := LoginResponse{} err = json.Unmarshal(body, &loginResponse) if err != nil { - log.Tracef("LoginResponse body: %s", body) + log.Trace("LoginResponse", "body", body) return fmt.Errorf("Error parsing Login response: %s", err.Error()) } @@ -298,7 +298,7 @@ func openConsoleAccessKey(ctx *RunContext, creds *storage.RoleCredentials, action, err := url.NewAction(ctx.Cli.Console.UrlAction) if err != nil { - log.Fatalf("Invalid --url-action %s", ctx.Cli.Console.UrlAction) + log.Fatal("Invalid --url-action", "action", ctx.Cli.Console.UrlAction) } if action == "" { action = ctx.Settings.UrlAction diff --git a/cmd/aws-sso/ecs_client_cmd.go b/cmd/aws-sso/ecs_client_cmd.go index 2713dfab..c3f81174 100644 --- a/cmd/aws-sso/ecs_client_cmd.go +++ b/cmd/aws-sso/ecs_client_cmd.go @@ -112,10 +112,10 @@ func ecsLoadCmd(ctx *RunContext, accountId int64, role string) error { // save history ctx.Settings.Cache.AddHistory(utils.MakeRoleARN(rFlat.AccountId, rFlat.RoleName)) if err := ctx.Settings.Cache.Save(false); err != nil { - log.WithError(err).Warnf("Unable to update cache") + log.Warn("Unable to update cache", "error", err.Error()) } - log.Debugf("%s", spew.Sdump(rFlat)) + log.Debug("role", "dump", spew.Sdump(rFlat)) return c.SubmitCreds(creds, rFlat.Profile, ctx.Cli.Ecs.Load.Slotted) } @@ -184,11 +184,11 @@ func listProfiles(profiles []ecs.ListProfilesResponse) error { func newClient(server string, ctx *RunContext) *client.ECSClient { certChain, err := ctx.Store.GetEcsSslCert() if err != nil { - log.Fatalf("Unable to get ECS SSL cert: %s", err) + log.Fatal("Unable to get ECS SSL cert", "error", err.Error()) } bearerToken, err := ctx.Store.GetEcsBearerToken() if err != nil { - log.Fatalf("Unable to get ECS bearer token: %s", err) + log.Fatal("Unable to get ECS bearer token", "error", err) } return client.NewECSClient(server, bearerToken, certChain) } diff --git a/cmd/aws-sso/ecs_server_cmd.go b/cmd/aws-sso/ecs_server_cmd.go index 22d393f7..6e3479f7 100644 --- a/cmd/aws-sso/ecs_server_cmd.go +++ b/cmd/aws-sso/ecs_server_cmd.go @@ -60,7 +60,7 @@ func (cc *EcsServerCmd) Run(ctx *RunContext) error { // fetch the creds from our temporary file mounted in the docker container f, err := ecs.OpenSecurityFile(ecs.READ_ONLY) if err != nil { - log.Warnf("Failed to open ECS credentials file: %s", err.Error()) + log.Warn("Failed to open ECS credentials file", "error", err.Error()) } else { creds, err := ecs.ReadSecurityConfig(f) if err != nil { @@ -100,15 +100,15 @@ func (cc *EcsServerCmd) Run(ctx *RunContext) error { } if bearerToken == "" { - log.Warnf("HTTP Auth: disabled. Use 'aws-sso ecs bearer-token' to enable") + log.Warn("HTTP Auth: disabled. Use 'aws-sso ecs bearer-token' to enable") } else { log.Info("HTTP Auth: enabled") } if privateKey != "" && certChain != "" { - log.Infof("SSL/TLS: enabled") + log.Info("SSL/TLS: enabled") } else { - log.Warnf("SSL/TLS: disabled. Use 'aws-sso ecs cert' to enable") + log.Warn("SSL/TLS: disabled. Use 'aws-sso ecs cert' to enable") } s, err := server.NewEcsServer(context.TODO(), bearerToken, l, privateKey, certChain) diff --git a/cmd/aws-sso/exec_cmd.go b/cmd/aws-sso/exec_cmd.go index 2dce5736..837273ca 100644 --- a/cmd/aws-sso/exec_cmd.go +++ b/cmd/aws-sso/exec_cmd.go @@ -52,7 +52,7 @@ func (e ExecCmd) AfterApply(runCtx *RunContext) error { func (cc *ExecCmd) Run(ctx *RunContext) error { err := checkAwsEnvironment() if err != nil { - log.WithError(err).Fatalf("Unable to continue") + log.Fatal("Unable to continue", "error", err.Error()) } if runtime.GOOS == "windows" && ctx.Cli.Exec.Cmd == "" { @@ -78,7 +78,7 @@ func execCmd(ctx *RunContext, accountid int64, role string) error { ctx.Settings.Cache.AddHistory(utils.MakeRoleARN(accountid, role)) if err := ctx.Settings.Cache.Save(false); err != nil { - log.WithError(err).Warnf("Unable to update cache") + log.Warn("Unable to update cache", "error", err.Error()) } // ready our command and connect everything up @@ -91,7 +91,7 @@ func execCmd(ctx *RunContext, accountid int64, role string) error { // add the variables we need for AWS to the executor without polluting our // own process for k, v := range execShellEnvs(ctx, accountid, role, region) { - log.Debugf("Setting %s = %s", k, v) + log.Debug("Setting", "variable", k, "value", v) cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v)) } // just do it! @@ -128,11 +128,11 @@ func execShellEnvs(ctx *RunContext, accountid int64, role, region string) map[st var roleInfo *sso.AWSRoleFlat if roleInfo, err = cache.Roles.GetRole(accountid, role); err != nil { // this error should never happen - log.Errorf("Unable to find role in cache. Unable to set AWS_SSO_PROFILE") + log.Error("Unable to find role in cache. Unable to set AWS_SSO_PROFILE") } else { shellVars["AWS_SSO_PROFILE"], err = roleInfo.ProfileName(ctx.Settings) if err != nil { - log.Errorf("Unable to generate AWS_SSO_PROFILE: %s", err.Error()) + log.Error("Unable to generate AWS_SSO_PROFILE", "error", err.Error()) } // and any EnvVarTags diff --git a/cmd/aws-sso/interactive.go b/cmd/aws-sso/interactive.go index 36894f89..81f6524d 100644 --- a/cmd/aws-sso/interactive.go +++ b/cmd/aws-sso/interactive.go @@ -41,7 +41,7 @@ func (ctx *RunContext) PromptExec(exec CompleterExec) error { return err } if err = ctx.Settings.Cache.Expired(sso); err != nil { - log.Infof(err.Error()) + log.Info("cache has expired", "error", err.Error()) c := &CacheCmd{} if err = c.Run(ctx); err != nil { return err @@ -138,21 +138,21 @@ func (tc *TagsCompleter) Executor(args string) { ssoRoles := tc.roleTags.GetMatchingRoles(argsMap) if len(ssoRoles) == 0 { - log.Fatalf("Invalid selection: No matching roles.") + log.Fatal("Invalid selection: No matching roles.") } else if len(ssoRoles) > 1 { - log.Fatalf("Invalid selection: Too many roles match selected values.") + log.Fatal("Invalid selection: Too many roles match selected values.") } roleArn = ssoRoles[0] } aId, rName, err := utils.ParseRoleARN(roleArn) if err != nil { - log.Fatalf("Unable to parse %s: %s", roleArn, err.Error()) + log.Fatal("Unable to parse", "arn", roleArn, "error", err.Error()) } err = tc.exec(tc.ctx, aId, rName) if err != nil { - log.Fatalf("Unable to exec: %s", err.Error()) + log.Fatal("Unable to exec TagsCompleter", "error", err.Error()) } } diff --git a/cmd/aws-sso/list_cmd.go b/cmd/aws-sso/list_cmd.go index 4c4ecd32..9cf68c1a 100644 --- a/cmd/aws-sso/list_cmd.go +++ b/cmd/aws-sso/list_cmd.go @@ -83,7 +83,7 @@ func (cc *ListCmd) Run(ctx *RunContext) error { if err = ctx.Settings.Cache.Expired(s); err != nil { c := &CacheCmd{} if err = c.Run(ctx); err != nil { - log.WithError(err).Errorf("Unable to refresh local cache") + log.Error("Unable to refresh local cache", "error", err.Error()) } } @@ -120,7 +120,7 @@ func (cc *DefaultCmd) Run(ctx *RunContext) error { if err = ctx.Settings.Cache.Expired(s); err != nil { c := &CacheCmd{} if err = c.Run(ctx); err != nil { - log.WithError(err).Errorf("Unable to refresh local cache") + log.Error("Unable to refresh local cache", "error", err.Error()) } } @@ -201,10 +201,10 @@ func printRoles(ctx *RunContext, fields []string, csv bool, prefixSearch []strin expires := "" ctr := storage.CreateTokenResponse{} if err := ctx.Store.GetCreateTokenResponse(AwsSSO.StoreKey(), &ctr); err != nil { - log.Debugf("Unable to get SSO session expire time: %s", err.Error()) + log.Debug("Unable to get SSO session expire time", "error", err.Error()) } else { if exp, err := utils.TimeRemain(ctr.ExpiresAt, true); err != nil { - log.Errorf("Unable to determine time remain for %d: %s", ctr.ExpiresAt, err) + log.Error("Unable to determine time remain", "expiresAt", ctr.ExpiresAt, "error", err.Error()) } else { expires = fmt.Sprintf(" [Expires in: %s]", strings.TrimSpace(exp)) } @@ -249,7 +249,7 @@ func listAllFields() { fields := []string{"Field", "Description"} if err := gotable.GenerateTable(ts, fields); err != nil { - log.WithError(err).Fatalf("Unable to generate report") + log.Fatal("Unable to generate report", "error", err.Error()) } fmt.Printf("\n") } diff --git a/cmd/aws-sso/list_sso_roles_cmd.go b/cmd/aws-sso/list_sso_roles_cmd.go index 4f6aa199..51b6987d 100644 --- a/cmd/aws-sso/list_sso_roles_cmd.go +++ b/cmd/aws-sso/list_sso_roles_cmd.go @@ -44,9 +44,9 @@ func (cc *ListSSORolesCmd) Run(ctx *RunContext) error { tr := []gotable.TableStruct{} for _, account := range accounts { - log.Debugf("Fetching roles for %s | %s (%s)...", account.AccountName, account.AccountId, account.EmailAddress) + log.Debug("Fetching roles for", "accountName", account.AccountName, "accountID", account.AccountId, "email", account.EmailAddress) roles, err := AwsSSO.GetRoles(account) - log.Debugf("AWS returned %d roles", len(roles)) + log.Debug("AWS returned roles", "count", len(roles)) if err != nil { return nil } diff --git a/cmd/aws-sso/login_cmd.go b/cmd/aws-sso/login_cmd.go index d81e399b..b880cf8d 100644 --- a/cmd/aws-sso/login_cmd.go +++ b/cmd/aws-sso/login_cmd.go @@ -46,7 +46,7 @@ func checkAuth(ctx *RunContext) bool { if AwsSSO == nil { s, err := ctx.Settings.GetSelectedSSO(ctx.Cli.SSO) if err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal("unable to select SSO", "sso", ctx.Cli.SSO, err.Error()) } AwsSSO = sso.NewAWSSSO(s, &ctx.Store) @@ -59,44 +59,44 @@ func checkAuth(ctx *RunContext) bool { func doAuth(ctx *RunContext) { if checkAuth(ctx) { // nothing to do here - log.Infof("You are already logged in. :)") + log.Info("You are already logged in. :)") return } action, err := url.NewAction(ctx.Cli.Login.UrlAction) if err != nil { - log.Fatalf("Invalid --url-action %s", ctx.Cli.Login.UrlAction) + log.Fatal("Invalid --url-action", "action", ctx.Cli.Login.UrlAction) } if action == "" { action = ctx.Settings.UrlAction } err = AwsSSO.Authenticate(action, ctx.Settings.Browser) if err != nil { - log.WithError(err).Fatalf("Unable to authenticate") + log.Fatal("Unable to authenticate", "error", err.Error()) } s, err := ctx.Settings.GetSelectedSSO(ctx.Cli.SSO) if err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal("unable to select SSO", "sso", ctx.Cli.SSO, "error", err.Error()) } if err = ctx.Settings.Cache.Expired(s); err != nil { ssoName, err := ctx.Settings.GetSelectedSSOName(ctx.Cli.SSO) - log.Infof("Refreshing AWS SSO role cache for %s, please wait...", ssoName) if err != nil { - log.Fatalf(err.Error()) + log.Fatal("unable to GetSelectedSSOName", "sso", ctx.Cli.SSO, "error", err.Error()) } + log.Info("Refreshing AWS SSO role cache, please wait...", "sso", ssoName) added, deleted, err := ctx.Settings.Cache.Refresh(AwsSSO, s, ssoName, ctx.Cli.Login.Threads) if err != nil { - log.WithError(err).Fatalf("Unable to refresh cache") + log.Fatal("Unable to refresh cache", "error", err.Error()) } if added > 0 || deleted > 0 { - log.Infof("Updated cache: %d added, %d deleted", added, deleted) + log.Info("Updated cache", "added", added, "deletd", deleted) } if err = ctx.Settings.Cache.Save(true); err != nil { - log.WithError(err).Errorf("Unable to save cache") + log.Error("Unable to save cache", "error", err.Error()) } } } diff --git a/cmd/aws-sso/logout_cmd.go b/cmd/aws-sso/logout_cmd.go index 50a54eee..1a4099c2 100644 --- a/cmd/aws-sso/logout_cmd.go +++ b/cmd/aws-sso/logout_cmd.go @@ -50,13 +50,13 @@ func flushSts(ctx *RunContext, awssso *sso.AWSSSO) { for _, role := range cache.Roles.GetAllRoles() { if !role.IsExpired() { if err := ctx.Store.DeleteRoleCredentials(role.Arn); err != nil { - log.WithError(err).Errorf("Unable to delete STS token for %s", role.Arn) + log.Error("Unable to delete STS token", "arn", role.Arn) } } } if err := ctx.Settings.Cache.MarkRolesExpired(); err != nil { - log.Errorf(err.Error()) + log.Error("failed to mark roles expired", "error", err.Error()) } else { - log.Infof("Deleted cached AWS STS credentials for %s", awssso.StoreKey()) + log.Info("Deleted cached AWS STS credentials", "sso", awssso.StoreKey()) } } diff --git a/cmd/aws-sso/main.go b/cmd/aws-sso/main.go index 0ae3abbe..0ebefac8 100644 --- a/cmd/aws-sso/main.go +++ b/cmd/aws-sso/main.go @@ -157,7 +157,7 @@ func main() { if runCtx.Auth == AUTH_NO_CONFIG { // side-step the rest of the setup... if err = runCtx.Kctx.Run(&runCtx); err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal(err.Error()) } } @@ -165,22 +165,22 @@ func main() { runCtx.Cli.ConfigFile = utils.GetHomePath(runCtx.Cli.ConfigFile) if _, err := os.Stat(cli.ConfigFile); errors.Is(err, os.ErrNotExist) { - log.Warnf("No config file found! Will now prompt you for a basic config...") + log.Warn("No config file found! Will now prompt you for a basic config...") if err = setupWizard(&runCtx, false, false, runCtx.Cli.Setup.Wizard.Advanced); err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal(err.Error()) } if runCtx.Kctx.Command() == "setup wizard" { // don't run the wizard again, we're done. return } } else if err != nil { - log.WithError(err).Fatalf("Unable to open config file: %s", cli.ConfigFile) + log.Fatal("Unable to open config file", "file", cli.ConfigFile, "error", err.Error()) } cacheFile := config.InsecureCacheFile(true) if runCtx.Settings, err = sso.LoadSettings(runCtx.Cli.ConfigFile, cacheFile, DEFAULT_CONFIG, override); err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal(err.Error()) } switch runCtx.Auth { @@ -188,7 +188,7 @@ func main() { // make sure we have authenticated via AWS SSO and init SecureStore loadSecureStore(&runCtx) if !checkAuth(&runCtx) { - log.Fatalf("Must run `aws-sso login` before running `aws-sso %s`", runCtx.Kctx.Command()) + log.Fatal(fmt.Sprintf("Must run `aws-sso login` before running `aws-sso %s`", runCtx.Kctx.Command())) } case AUTH_SKIP: @@ -196,7 +196,7 @@ func main() { c := &runCtx s, err := c.Settings.GetSelectedSSO(c.Cli.SSO) if err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal(err.Error()) } loadSecureStore(c) @@ -207,7 +207,7 @@ func main() { err = runCtx.Kctx.Run(&runCtx) if err != nil { - log.Fatalf("%s", err.Error()) + log.Fatal(err.Error()) } } @@ -222,17 +222,17 @@ func loadSecureStore(ctx *RunContext) { } ctx.Store, err = storage.OpenJsonStore(sfile) if err != nil { - log.WithError(err).Fatalf("Unable to open JsonStore %s", sfile) + log.Fatal("Unable to open JsonStore", "file", sfile, "error", err.Error()) } - log.Warnf("Using insecure json file for SecureStore: %s", sfile) + log.Warn("Using insecure json file for SecureStore", "file", sfile) default: cfg, err := storage.NewKeyringConfig(ctx.Settings.SecureStore, config.ConfigDir(true)) if err != nil { - log.WithError(err).Fatalf("Unable to create SecureStore") + log.Fatal("Unable to create SecureStore", "error", err.Error()) } ctx.Store, err = storage.OpenKeyring(cfg) if err != nil { - log.WithError(err).Fatalf("Unable to open SecureStore %s", ctx.Settings.SecureStore) + log.Fatal("Unable to open SecureStore", "file", ctx.Settings.SecureStore, "error", err.Error()) } } } @@ -340,41 +340,41 @@ func GetRoleCredentials(ctx *RunContext, awssso *sso.AWSSSO, refreshSTS bool, ac // First look for our creds in the secure store, if we're not forcing a refresh arn := utils.MakeRoleARN(accountid, role) - log.Debugf("Getting role credentials for %s", arn) + log.Debug("Getting role credentials", "arn", arn) if !refreshSTS { if roleFlat, err := ctx.Settings.Cache.GetRole(arn); err == nil { if !roleFlat.IsExpired() { if err := ctx.Store.GetRoleCredentials(arn, &creds); err == nil { if !creds.Expired() { - log.Debugf("Retrieved role credentials from the SecureStore") + log.Debug("Retrieved role credentials from the SecureStore") return &creds } } } } } else { - log.Infof("Forcing STS refresh for %s", arn) + log.Info("Forcing STS refresh", "arn", arn) } - log.Debugf("Fetching STS token from AWS SSO") + log.Debug("Fetching STS token from AWS SSO") // If we didn't use our secure store ask AWS SSO var err error creds, err = awssso.GetRoleCredentials(accountid, role) if err != nil { - log.WithError(err).Fatalf("Unable to get role credentials for %s", arn) + log.Fatal("Unable to get role credentials", "arn", arn, "error", err.Error()) } - log.Debugf("Retrieved role credentials from AWS SSO") + log.Debug("Retrieved role credentials from AWS SSO") // Cache our creds if err := ctx.Store.SaveRoleCredentials(arn, creds); err != nil { - log.WithError(err).Warnf("Unable to cache role credentials in secure store") + log.Warn("Unable to cache role credentials in secure store", "error", err.Error()) } // Update the cache if err := ctx.Settings.Cache.SetRoleExpires(arn, creds.ExpireEpoch()); err != nil { - log.WithError(err).Warnf("Unable to update cache") + log.Warn("Unable to update cache", "error", err.Error()) } return &creds } diff --git a/cmd/aws-sso/setup_wizard.go b/cmd/aws-sso/setup_wizard.go index 33355053..15bef96e 100644 --- a/cmd/aws-sso/setup_wizard.go +++ b/cmd/aws-sso/setup_wizard.go @@ -131,20 +131,20 @@ func checkPromptError(err error) { switch err.Error() { case "^D": // https://github.com/synfinatic/aws-sso-cli/issues/531 - log.Errorf("sorry, not supported") + log.Error("sorry, not supported") case "^C": - log.Fatalf("User aborted.") + log.Fatal("User aborted.") default: - log.Error(err) + log.Error(err.Error()) } } func checkSelectError(err error) { switch err.Error() { case "^C": - log.Fatalf("User aborted.") + log.Fatal("User aborted.") default: - log.Error(err) + log.Error(err.Error()) } } @@ -191,10 +191,10 @@ func promptStartUrl(defaultValue string) string { if _, err := net.LookupHost(val); err == nil { validFQDN = true } else if err != nil { - log.Errorf("unable to resolve %s", val) + log.Error("unable to resolve", "host", val) } } - log.Infof("Using %s", val) + log.Info(fmt.Sprintf("Using %s", val)) return val } @@ -224,7 +224,7 @@ func promptAwsSsoRegion(defaultValue string) string { Templates: makeSelectTemplate(label), } if i, _, err = sel.Run(); err != nil { - log.Error(err) + log.Error(err.Error()) } return items[i].Value diff --git a/internal/ecs/client/client.go b/internal/ecs/client/client.go index 7fc93341..67e30027 100644 --- a/internal/ecs/client/client.go +++ b/internal/ecs/client/client.go @@ -66,10 +66,10 @@ func NewECSClient(server, authToken, certChain string) *ECSClient { } if authToken == "" { - log.Warnf("no auth token provided, ECS server communication will be unauthenticated") + log.Warn("no auth token provided, ECS server communication will be unauthenticated") } if certChain == "" { - log.Warnf("no SSL cert provided, ECS server communication will be unencrypted") + log.Warn("no SSL cert provided, ECS server communication will be unencrypted") } hostPort := strings.Split(server, ":") @@ -117,12 +117,12 @@ func (c *ECSClient) newRequest(method, url string, body io.Reader) (*http.Reques if c.authToken != "" { req.Header.Set("Authorization", "Bearer "+c.authToken) } - log.Debugf("http req: %s", req.URL.String()) + log.Debug("http req", "url", req.URL.String()) return req, nil } func (c *ECSClient) SubmitCreds(creds *storage.RoleCredentials, profile string, slotted bool) error { - log.Debugf("loading %s in a slot: %v", profile, slotted) + log.Debug("loading in slot", "profile", profile, "slot", slotted) cr := ecs.ECSClientRequest{ Creds: creds, ProfileName: profile, @@ -164,7 +164,7 @@ func (c *ECSClient) GetProfile() (ecs.ListProfilesResponse, error) { if err = json.Unmarshal(body, &lpr); err != nil { return lpr, err } - log.Debugf("resp: %s", spew.Sdump(lpr)) + log.Debug("response", "body", spew.Sdump(lpr)) return lpr, nil } @@ -191,7 +191,7 @@ func (c *ECSClient) ListProfiles() ([]ecs.ListProfilesResponse, error) { if err = json.Unmarshal(body, &lpr); err != nil { return lpr, err } - log.Debugf("resp: %s", spew.Sdump(lpr)) + log.Debug("response", "body", spew.Sdump(lpr)) return lpr, nil } diff --git a/internal/ecs/server/default.go b/internal/ecs/server/default.go index 6395a62c..0d09a444 100644 --- a/internal/ecs/server/default.go +++ b/internal/ecs/server/default.go @@ -30,7 +30,7 @@ type DefaultHandler struct { func (p DefaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.String() != "/" { - log.Errorf("Invalid %s request: %s", r.Method, r.URL.String()) + log.Error("Invalid request", "method", r.Method, "url", r.URL.String()) ecs.Unavailable(w) return } @@ -43,13 +43,13 @@ func (p DefaultHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: p.Delete(w, r) default: - log.Errorf("Invalid %s request: %s", r.Method, r.URL.String()) + log.Error("Invalid request", "method", r.Method, "url", r.URL.String()) ecs.Invalid(w) } } func (p DefaultHandler) Get(w http.ResponseWriter, r *http.Request) { - log.Debugf("fetching default creds") + log.Debug("fetching default creds") ecs.WriteCreds(w, p.ecs.DefaultCreds.Creds) } diff --git a/internal/ecs/server/httplog.go b/internal/ecs/server/httplog.go index f03700e5..a2a9daad 100644 --- a/internal/ecs/server/httplog.go +++ b/internal/ecs/server/httplog.go @@ -43,6 +43,6 @@ func withLogging(handler http.Handler) http.Handler { requestStart := time.Now() w2 := &loggingMiddlewareResponseWriter{w, http.StatusOK} handler.ServeHTTP(w2, r) - log.Infof("http: %s: %d %s %s (%s)", r.RemoteAddr, w2.Code, r.Method, r.URL, time.Since(requestStart)) + log.Info("http", "remote", r.RemoteAddr, "code", w2.Code, "method", r.Method, "url", r.URL, "time", time.Since(requestStart)) }) } diff --git a/internal/ecs/server/profile.go b/internal/ecs/server/profile.go index 8ba600be..6183cd36 100644 --- a/internal/ecs/server/profile.go +++ b/internal/ecs/server/profile.go @@ -34,14 +34,14 @@ func (p ProfileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.Get(w, r) default: - log.Errorf("Invalid request: %s", r.URL.String()) + log.Error("Invalid request", "url", r.URL.String()) ecs.Invalid(w) } } func (p ProfileHandler) Get(w http.ResponseWriter, r *http.Request) { // get the details of the default profile - log.Debugf("fetching default profile") + log.Debug("fetching default profile") if p.ecs.DefaultCreds.ProfileName == "" { ecs.Unavailable(w) return diff --git a/internal/ecs/server/server.go b/internal/ecs/server/server.go index 1718fa84..4446ea9a 100644 --- a/internal/ecs/server/server.go +++ b/internal/ecs/server/server.go @@ -100,7 +100,7 @@ func (e *EcsServer) DeleteSlottedCreds(profile string) error { // getCreds fetches the named profile from the cache. func (e *EcsServer) GetSlottedCreds(profile string) (*ecs.ECSClientRequest, error) { - log.Debugf("fetching creds for profile: %s", profile) + log.Debug("fetching creds", "profile", profile) c, ok := e.slottedCreds[profile] if !ok { return c, fmt.Errorf("%s is not found", profile) @@ -124,7 +124,7 @@ func (e *EcsServer) ListSlottedCreds() []ecs.ListProfilesResponse { for _, cr := range e.slottedCreds { if cr.Creds.Expired() { - log.Errorf("Skipping expired creds for %s", cr.ProfileName) + log.Error("Skipping expired creds", "profile", cr.ProfileName) continue } diff --git a/internal/ecs/server/slotted.go b/internal/ecs/server/slotted.go index 297c9fec..f7bf4e88 100644 --- a/internal/ecs/server/slotted.go +++ b/internal/ecs/server/slotted.go @@ -39,7 +39,7 @@ func (p SlottedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { case http.MethodDelete: p.Delete(w, r) default: - log.Errorf("Invalid request: %s", r.URL.String()) + log.Error("Invalid request", "url", r.URL.String()) ecs.Invalid(w) } } diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 09b40219..0b86ab8e 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -83,7 +83,7 @@ func getScript(shell string) ([]byte, string, error) { return bytes, "", err } } - log.Debugf("using %s as our shell", shell) + log.Debug("detected our shell", "shell", shell) if shellFile, ok = SHELL_SCRIPTS[shell]; !ok { return bytes, "", fmt.Errorf("unsupported shell: %s", shell) @@ -221,7 +221,7 @@ func uninstallConfigFile(path string) error { _, _, err = fe.UpdateConfig(false, false, path) if err != nil { - log.Warnf("unable to remove config: %s", err.Error()) + log.Warn("unable to remove config", "error", err.Error()) } return nil @@ -237,7 +237,7 @@ func detectShell() (string, error) { } _, shell := path.Split(shellPath) - log.Debugf("detected configured shell as: %s", shell) + log.Debug("detected our shell", "shell", shell) return shell, nil } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 100a8414..3cbee402 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -19,26 +19,131 @@ package logger */ import ( - "github.com/sirupsen/logrus" + "context" + "fmt" + "log/slog" + "os" + "runtime" + "strings" ) var log *Logger +const ( + LevelTrace = slog.Level(-8) + LevelFatal = slog.Level(12) + LineKey = "_line" + FileKey = "_file" + FunctionKey = "_function" +) + +var LevelNames = map[slog.Leveler]string{ + LevelTrace: "TRACE", + LevelFatal: "FATAL", +} + +var LevelStrings = map[string]slog.Leveler{ + "TRACE": LevelTrace, + "FATAL": LevelFatal, + "INFO": slog.LevelInfo, + "WARN": slog.LevelWarn, + "ERROR": slog.LevelError, + "DEBUG": slog.LevelDebug, +} + type Logger struct { - *logrus.Logger + *slog.Logger + addSource bool + level *slog.LevelVar } -func NewLogger(l *logrus.Logger) *Logger { - return &Logger{l} +// NewLogger creates a new logger with the given log level and whether to add source information +func NewLogger(addSource bool, level slog.Leveler) *Logger { + lvl := new(slog.LevelVar) + lvl.Set(level.Level()) + + opts := &slog.HandlerOptions{ + Level: lvl, + AddSource: addSource, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Remove time from the output for predictable test output. + if a.Key == slog.TimeKey { + return slog.Attr{} + } + + // Fix level names and pad the names + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelLabel, exists := LevelNames[level] + if !exists { + levelLabel = level.String() + } + + // Pad the level name to 8 characters + a.Value = slog.StringValue(fmt.Sprintf("%8s", levelLabel)) + } + + // Rename the source attributes if they came from Trace/Fatal to the correct names + // so the old values get overwritten + if groups[0] == "source" { + switch a.Key { + case FileKey: + a.Key = "file" + case LineKey: + a.Key = "line" + case FunctionKey: + a.Key = "function" + default: + break // do nothing + } + } + + return a + }, + } + + var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts) + return &Logger{ + Logger: slog.New(handler), + addSource: addSource, + level: lvl, + } } +// SetLevel sets the log level for the logger +func (l *Logger) SetLevel(level slog.Leveler) { + l.level.Set(level.Level()) +} + +func (l *Logger) SetLevelString(level string) error { + if _, ok := LevelStrings[strings.ToUpper(level)]; !ok { + return fmt.Errorf("invalid log level: %s", level) + } + l.level.Set(LevelStrings[strings.ToUpper(level)].Level()) + return nil +} + +func (l *Logger) SetReportCaller(reportCaller bool) { + if l.addSource == reportCaller { + return // do nothing + } + l.addSource = reportCaller + l.Logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: l.level, + AddSource: reportCaller, + })) + slog.SetDefault(l.Logger) +} + +// GetLevel returns the current log level +func (l *Logger) GetLevel() slog.Leveler { + return slog.Level(l.level.Level()) +} + +// initialize the default logger to log to stderr and log at the warn level func init() { - log = &Logger{logrus.New()} - log.SetFormatter(&logrus.TextFormatter{ - DisableLevelTruncation: true, - PadLevelText: true, - DisableTimestamp: true, - }) + log = NewLogger(false, slog.LevelWarn) + slog.SetDefault(log.Logger) } func SetLogger(l *Logger) { @@ -48,3 +153,34 @@ func SetLogger(l *Logger) { func GetLogger() *Logger { return log } + +func SetDefaultLogger(l *Logger) { + slog.SetDefault(l.Logger) +} + +// Log a message at the Trace level +func (l *Logger) Trace(msg string, args ...interface{}) { + l.logWithSource(LevelTrace, msg, args...) +} + +// Log a message at the Fatal level and exit +func (l *Logger) Fatal(msg string, args ...interface{}) { + l.logWithSource(LevelFatal, msg, args...) + os.Exit(1) +} + +func (l *Logger) logWithSource(level slog.Level, msg string, args ...interface{}) { + ctx := context.Background() + var allArgs []interface{} + allArgs = append(allArgs, args...) + + if l.addSource { + var functionName string = "" + pc, file, line, ok := runtime.Caller(2) // go up two levels to get the caller + if ok { + functionName = runtime.FuncForPC(pc).Name() + allArgs = append(allArgs, slog.Group("source", slog.String(FileKey, file), slog.Int(LineKey, line), slog.String(FunctionKey, functionName))) + } + } + l.Logger.Log(ctx, level, msg, allArgs...) +} diff --git a/internal/predictor/predictor.go b/internal/predictor/predictor.go index 928c1eec..960607f7 100644 --- a/internal/predictor/predictor.go +++ b/internal/predictor/predictor.go @@ -97,7 +97,7 @@ func (p *Predictor) newPredictor(s *sso.Settings, c *sso.Cache) *Predictor { uniqueRoles[roleName] = true profile, err := rFlat.ProfileName(s) if err != nil { - log.Warnf(err.Error()) + log.Warn("unable to find Profile for ARN", "arn", rFlat.Arn, "error", err.Error()) continue } p.profiles = append(p.profiles, profile) @@ -165,7 +165,7 @@ func (p *Predictor) SsoComplete() complete.Predictor { ssos = append(ssos, sso) } } else { - log.Panicf("error: %s", err.Error()) + log.Fatal("unable to process file", "file", p.configFile, "error", err.Error()) } } return complete.PredictSet(ssos...) diff --git a/internal/sso/awssso.go b/internal/sso/awssso.go index 081ca449..4834b1f6 100644 --- a/internal/sso/awssso.go +++ b/internal/sso/awssso.go @@ -91,7 +91,7 @@ func NewAWSSSO(s *SSOConfig, store *storage.SecureStorage) *AWSSSO { if s.MaxBackoff > 0 { maxBackoff = s.MaxBackoff } - log.Debugf("loading SSO using %d retries and max %dsec backoff", maxRetry, maxBackoff) + log.Debug("loading SSO", "retries", maxRetry, "maxBackoff", maxBackoff) r := retry.NewStandard(func(o *retry.StandardOptions) { o.MaxAttempts = maxRetry @@ -154,10 +154,10 @@ func (ri RoleInfo) RoleArn() string { func (ri RoleInfo) GetAccountId64() int64 { i64, err := strconv.ParseInt(ri.AccountId, 10, 64) if err != nil { - log.WithError(err).Panicf("Invalid AWS AccountID from AWS SSO: %s", ri.AccountId) + panic(fmt.Sprintf("Invalid AWS AccountID from AWS SSO: %s", ri.AccountId)) } if i64 < 0 { - log.WithError(err).Panicf("AWS AccountID must be >= 0: %s", ri.AccountId) + panic(fmt.Sprintf("AWS AccountID must be >= 0: %s", ri.AccountId)) } return i64 } @@ -237,7 +237,7 @@ func (as *AWSSSO) ListAccounts(input *sso.ListAccountsInput) (*sso.ListAccountsO // sometimes our AccessToken is invalid so try a new one once? // if we have to re-auth, hold everyone else up since that will reduce other failures as.rolesLock.Lock() - log.Errorf("AccessToken Unauthorized Error; refreshing: %s", err.Error()) + log.Error("AccessToken Unauthorized Error; refreshing", "error", err.Error()) if err = as.reauthenticate(); err != nil { // fail hard now @@ -247,11 +247,11 @@ func (as *AWSSSO) ListAccounts(input *sso.ListAccountsInput) (*sso.ListAccountsO as.rolesLock.Unlock() case errors.As(err, &tmr): // try again - log.Warnf("Exceeded MaxRetry/MaxBackoff. Consider tuning values.") + log.Warn("Exceeded MaxRetry/MaxBackoff. Consider tuning values.") time.Sleep(time.Duration(MAX_BACKOFF_SECONDS) * time.Second) default: - log.WithError(err).Error("Unexpected error") + log.Error("Unexpected error", "error", err.Error()) } } } @@ -274,22 +274,22 @@ func (as *AWSSSO) ListAccountRoles(input *sso.ListAccountRolesInput) (*sso.ListA // sometimes our AccessToken is invalid so try a new one once? // if we have to re-auth, hold everyone else up since that will reduce other failures as.rolesLock.Lock() - log.Errorf("AccessToken Unauthorized Error; refreshing: %s", err.Error()) + log.Error("AccessToken Unauthorized Error; refreshing", "error", err.Error()) if err = as.reauthenticate(); err != nil { // fail hard now - log.WithError(err).Fatalf("Unexpected auth failure") + panic(fmt.Sprintf("Unexpected auth failure: %s", err.Error())) } input.AccessToken = aws.String(as.Token.AccessToken) as.rolesLock.Unlock() case errors.As(err, &tmr): // try again - log.Warnf("Exceeded MaxRetry/MaxBackoff. Consider tuning values.") + log.Warn("Exceeded MaxRetry/MaxBackoff. Consider tuning values.") time.Sleep(time.Duration(MAX_BACKOFF_SECONDS) * time.Second) default: - log.WithError(err).Error("Unexpected error") + log.Error("Unexpected error", "error", err.Error()) } } } @@ -336,10 +336,10 @@ func (ai AccountInfo) GetHeader(fieldName string) (string, error) { func (ai AccountInfo) GetAccountId64() int64 { i64, err := strconv.ParseInt(ai.AccountId, 10, 64) if err != nil { - log.WithError(err).Panicf("Invalid AWS AccountID from AWS SSO: %s", ai.AccountId) + panic(fmt.Sprintf("Invalid AWS AccountID from AWS SSO: %s", ai.AccountId)) } if i64 < 0 { - log.WithError(err).Panicf("AWS AccountID must be >= 0: %s", ai.AccountId) + panic(fmt.Sprintf("AWS AccountID must be >= 0: %s", ai.AccountId)) } return i64 } @@ -401,12 +401,12 @@ func (as *AWSSSO) GetRoleCredentials(accountId int64, role string) (storage.Role // is the role defined in the config file? configRole, err := as.SSOConfig.GetRole(accountId, role) if err != nil { - log.Debugf("SSOConfig.GetRole(): %s", err.Error()) + log.Debug("SSOConfig.GetRole()", "error", err.Error()) } // If not in config OR config does not require doing a Via if err != nil || configRole.Via == "" { - log.Debugf("Getting %s:%s directly", aId, role) + log.Debug("Getting role directly", "accountID", aId, "role", role) // This is the actual role creds requested through AWS SSO input := sso.GetRoleCredentialsInput{ AccessToken: aws.String(as.Token.AccessToken), @@ -434,7 +434,7 @@ func (as *AWSSSO) GetRoleCredentials(accountId int64, role string) (storage.Role roleChainMap[configRole.ARN] = true for k := range roleChainMap { if k == configRole.Via { - log.Fatalf("Detected role chain loop! Getting %s via %s", configRole.ARN, configRole.Via) + panic(fmt.Sprintf("Detected role chain loop! Getting %s via %s", configRole.ARN, configRole.Via)) } roleChainMap[k] = true } @@ -442,7 +442,7 @@ func (as *AWSSSO) GetRoleCredentials(accountId int64, role string) (storage.Role // Need to recursively call sts:AssumeRole in order to retrieve the STS creds for // the requested role // role has a Via - log.Debugf("Getting %s:%s via %s", aId, role, configRole.Via) + log.Debug("Calling AssumeRole", "role", fmt.Sprintf("%s:%s", aId, role), "via", configRole.Via) viaAccountId, viaRole, err := utils.ParseRoleARN(configRole.Via) if err != nil { return storage.RoleCredentials{}, fmt.Errorf("Invalid Via %s: %s", configRole.Via, err.Error()) @@ -489,7 +489,7 @@ func (as *AWSSSO) GetRoleCredentials(accountId int64, role string) (storage.Role if err != nil { return storage.RoleCredentials{}, err } - log.Debugf("%s", spew.Sdump(output)) + log.Debug("stsSession.AssumeRole", "output", spew.Sdump(output)) ret := storage.RoleCredentials{ AccountId: accountId, RoleName: role, diff --git a/internal/sso/awssso_auth.go b/internal/sso/awssso_auth.go index 2a4c47c9..20b957c9 100644 --- a/internal/sso/awssso_auth.go +++ b/internal/sso/awssso_auth.go @@ -46,7 +46,7 @@ func (as *AWSSSO) ValidAuthToken() bool { token := storage.CreateTokenResponse{} err := as.store.GetCreateTokenResponse(as.StoreKey(), &token) if err != nil { - log.Debugf(err.Error()) + log.Debug(err.Error()) return false } @@ -59,10 +59,10 @@ func (as *AWSSSO) ValidAuthToken() bool { if token.ExpiresAt != 0 { t := time.Unix(token.ExpiresAt, 0) - log.Infof("Cached SSO token expired at: %s. Reauthenticating...\n", - t.Format("Mon Jan 2 15:04:05 -0700 MST 2006")) + log.Info("Cached SSO token has expired. Reauthenticating...", + "time", t.Format("Mon Jan 2 15:04:05 -0700 MST 2006")) } else { - log.Infof("Cached SSO token has expired. Reauthenticating...\n") + log.Info("Cached SSO token has expired. Reauthenticating...") } return false } @@ -70,7 +70,7 @@ func (as *AWSSSO) ValidAuthToken() bool { // Authenticate retrieves an AWS SSO AccessToken from our cache or by // making the necessary AWS SSO calls. func (as *AWSSSO) Authenticate(urlAction url.Action, browser string) error { - log.Tracef("Authenticate(%s, %s)", urlAction, browser) + log.Trace("Authenticate", "urlAction", urlAction, "browser", browser) // cache urlAction and browser for subsequent calls if necessary if urlAction != "" { as.urlAction = urlAction @@ -94,17 +94,17 @@ func (as *AWSSSO) reauthenticate() error { as.authenticateLock.Lock() defer as.authenticateLock.Unlock() - log.Tracef("reauthenticate() for %s", as.StoreKey()) + log.Trace("reauthenticate()", "storeKey", as.StoreKey()) err := as.registerClient(false) if err != nil { return fmt.Errorf("unable to register client with AWS SSO: %s", err.Error()) } - log.Tracef("<- reauthenticate()") + log.Trace("<- reauthenticate()") err = as.startDeviceAuthorization() - log.Tracef("<- reauthenticate()") + log.Trace("<- reauthenticate()") if err != nil { - log.Debugf("startDeviceAuthorization failed. Forcing refresh of registerClient") + log.Debug("startDeviceAuthorization failed. Forcing refresh of registerClient") // startDeviceAuthorization can fail if our cached registerClient token is invalid if err = as.registerClient(true); err != nil { return fmt.Errorf("unable to register client with AWS SSO: %s", err.Error()) @@ -115,7 +115,7 @@ func (as *AWSSSO) reauthenticate() error { } auth, err := as.getDeviceAuthInfo() - log.Tracef("<- reauthenticate()") + log.Trace("<- reauthenticate()") if err != nil { return fmt.Errorf("unable to get device auth info from AWS SSO: %s", err.Error()) } @@ -133,7 +133,7 @@ func (as *AWSSSO) reauthenticate() error { return err } - log.Infof("Waiting for SSO authentication...") + log.Info("Waiting for SSO authentication...") err = as.createToken() if err != nil { @@ -156,12 +156,12 @@ const ( // registerClient does the needful to talk to AWS or read our cache to get the // RegisterClientData for later steps and saves it to our secret store func (as *AWSSSO) registerClient(force bool) error { - log.Tracef("registerClient()") + log.Trace("registerClient()") if !force { log.Trace("Checking cache for RegisterClientData") err := as.store.GetRegisterClientData(as.StoreKey(), &as.ClientData) if err == nil && !as.ClientData.Expired() { - log.Debugf("Using RegisterClient cache for %s", as.StoreKey()) + log.Debug("Using RegisterClient cache", "storeKey", as.StoreKey()) return nil } } @@ -189,7 +189,7 @@ func (as *AWSSSO) registerClient(force bool) error { } err = as.store.SaveRegisterClientData(as.StoreKey(), as.ClientData) if err != nil { - log.WithError(err).Errorf("unable to save RegisterClientData for %s", as.StoreKey()) + log.Error("unable to save RegisterClientData", "storeKey", as.StoreKey(), "error", err.Error()) } return nil } @@ -197,7 +197,7 @@ func (as *AWSSSO) registerClient(force bool) error { // startDeviceAuthorization makes the call to AWS to initiate the OIDC auth // to the SSO provider. func (as *AWSSSO) startDeviceAuthorization() error { - log.Tracef("startDeviceAuthorization() for %s", as.StoreKey()) + log.Trace("startDeviceAuthorization()", "storeKey", as.StoreKey()) input := ssooidc.StartDeviceAuthorizationInput{ StartUrl: aws.String(as.StartUrl), ClientId: aws.String(as.ClientData.ClientId), @@ -216,8 +216,7 @@ func (as *AWSSSO) startDeviceAuthorization() error { ExpiresIn: resp.ExpiresIn, Interval: resp.Interval, } - log.Debugf("Created OIDC device code for %s (expires in: %ds)", - as.StoreKey(), as.DeviceAuth.ExpiresIn) + log.Debug("Created OIDC device code", "storeKey", as.StoreKey(), "expires", as.DeviceAuth.ExpiresIn) fmt.Fprintf(os.Stderr, VERIFY_MSG, as.DeviceAuth.UserCode) @@ -232,7 +231,7 @@ type DeviceAuthInfo struct { // getDeviceAuthInfo generates a DeviceAuthInfo struct func (as *AWSSSO) getDeviceAuthInfo() (DeviceAuthInfo, error) { - log.Tracef("getDeviceAuthInfo()") + log.Trace("getDeviceAuthInfo()") if as.DeviceAuth.VerificationUri == "" { return DeviceAuthInfo{}, fmt.Errorf("no valid verification url is available for %s", as.StoreKey()) } @@ -248,7 +247,7 @@ func (as *AWSSSO) getDeviceAuthInfo() (DeviceAuthInfo, error) { // createToken blocks until we have a new SSO AccessToken and saves it // to our secret store func (as *AWSSSO) createToken() error { - log.Tracef("createToken()") + log.Trace("createToken()") input := ssooidc.CreateTokenInput{ ClientId: aws.String(as.ClientData.ClientId), ClientSecret: aws.String(as.ClientData.ClientSecret), @@ -277,7 +276,7 @@ func (as *AWSSSO) createToken() error { var ape *oidctypes.AuthorizationPendingException if errors.As(err, &sde) { - log.Debugf("Slowing down CreateToken()") + log.Debug("Slowing down CreateToken()") retryInterval += slowDown time.Sleep(retryInterval) } else if errors.As(err, &ape) { @@ -302,7 +301,7 @@ func (as *AWSSSO) createToken() error { err = as.store.SaveCreateTokenResponse(as.StoreKey(), as.Token) as.tokenLock.RUnlock() if err != nil { - log.WithError(err).Errorf("unable to save CreateTokenResponse") + log.Error("unable to save CreateTokenResponse", "error", err.Error()) } return nil @@ -322,7 +321,7 @@ func (as *AWSSSO) Logout() error { // delete the value from the store so we don't think we have a valid token if err := as.store.DeleteCreateTokenResponse(as.key); err != nil { - log.WithError(err).Errorf("unable to delete AccessToken from secure store") + log.Error("unable to delete AccessToken from secure store", "error", err.Error()) } } diff --git a/internal/sso/cache.go b/internal/sso/cache.go index a45500d3..4578b562 100644 --- a/internal/sso/cache.go +++ b/internal/sso/cache.go @@ -19,6 +19,7 @@ package sso */ import ( + "context" "encoding/json" "fmt" "os" @@ -223,7 +224,7 @@ func (c *Cache) deleteOldHistory() { for _, arn := range cache.History { id, role, err := utils.ParseRoleARN(arn) if err != nil { - log.Debugf("Unable to parse History ARN %s: %s", arn, err.Error()) + log.Debug("Unable to parse History ARN", "arn", arn, "error", err.Error()) c.deleteHistoryItem(arn) continue } @@ -235,20 +236,20 @@ func (c *Cache) deleteOldHistory() { history, ok := r.Tags["History"] if !ok || history == "" { // doesn't have anything to expires - log.Debugf("%s is in history list without a History tag in cache?", arn) + log.Debug("ARN in history list without a History tag in cache?", "arn", arn) c.deleteHistoryItem(arn) continue } values := strings.SplitN(history, ",", 2) if len(values) != 2 { - log.Debugf("Too few fields for %s History Tag: '%s'", r.Arn, history) + log.Debug("Too few fields for History Tag", "arn", r.Arn, "history", history) c.deleteHistoryItem(arn) continue } lastTime, err := strconv.ParseInt(values[1], 10, 64) if err != nil { - log.Debugf("Unable to parse %s History Tag '%s': %s", r.Arn, history, err.Error()) + log.Debug("Unable to parse History Tag", "arn", r.Arn, "history", history, "error", err.Error()) c.deleteHistoryItem(arn) continue } @@ -262,15 +263,15 @@ func (c *Cache) deleteOldHistory() { // not appending it to newHistoryItems delete(r.Tags, "History") c.deleteHistoryItem(arn) - log.Debugf("Removed expired history role: %s", r.Arn) + log.Debug("Removed expired history role", "arn", r.Arn) } } else { c.deleteHistoryItem(arn) - log.Debugf("History contains %s, but no role by that name", arn) + log.Debug("History contains but no role by that name", "arn", arn) } } else { c.deleteHistoryItem(arn) - log.Debugf("History contains %s, but no account by that name", arn) + log.Debug("History contains but no account by that name", "arn", arn) } } @@ -285,7 +286,7 @@ func (c *Cache) Refresh(sso *AWSSSO, config *SSOConfig, ssoName string, threads return 0, 0, nil } c.refreshed = true - log.Debugf("refreshing %s SSO cache", ssoName) + log.Debug("refreshing SSO cache", "SSOname", ssoName) // save role creds expires time expires := map[string]int64{} @@ -364,18 +365,18 @@ func (c *Cache) Refresh(sso *AWSSSO, config *SSOConfig, ssoName string, threads // pruneSSO removes any SSO instances that are no longer configured func (c *Cache) PruneSSO(settings *Settings) { - log.Debugf("pruning our cache of outdated SSO instances") + log.Debug("pruning our cache of outdated SSO instances") for sso := range c.SSO { hasSSO := false for s := range settings.SSO { if s == sso { - log.Debugf("keeping %s in cache", sso) + log.Debug("keeping in cache", "SSOName", sso) hasSSO = true break } } if !hasSSO { - log.Debugf("pruning %s from cache", sso) + log.Debug("pruning from cache", "SSOName", sso) delete(c.SSO, sso) } } @@ -485,10 +486,10 @@ func fetchSSORole(id int, as *AWSSSO, aInfo <-chan AccountInfo, rInfo chan<- []R // need some way to exit our worker... break } - log.Debugf("Worker %d processing AccountId: %s", id, a.AccountId) + log.Debug("Worker processing", "worker", id, "accountID", a.AccountId) roles, err := as.GetRoles(a) if err != nil { - log.Panicf("Unable to get AWS SSO roles: %s", err.Error()) + panic(fmt.Sprintf("Unable to get AWS SSO roles: %s", err.Error())) } rInfo <- roles } @@ -498,7 +499,7 @@ func fetchSSORole(id int, as *AWSSSO, aInfo <-chan AccountInfo, rInfo chan<- []R // and using our SSOCache func processSSORoles(roles []RoleInfo, cache *SSOCache, r *Roles) { for _, role := range roles { - log.Debugf("Processing %s:%s", role.AccountId, role.RoleName) + log.Debug("Processing %s:%s", role.AccountId, role.RoleName) accountId := role.GetAccountId64() if _, ok := r.Accounts[accountId]; !ok { @@ -588,9 +589,9 @@ func (c *Cache) addSSORoles(r *Roles, as *AWSSSO, threads int) error { case roles := <-results: processSSORoles(roles, cache, r) count++ // increment count only when processing results - log.Debugf("proccessed %d accounts, added %d roles, total %d", count, len(roles), len(r.GetAllRoles())) + log.Debug("proccessed", "accounts", count, "new_roles", len(roles), "total_roles", len(r.GetAllRoles())) case <-ticker.C: - log.Warnf("fetching roles for %d accounts, this might take a while...\n", len(accounts)+1) + log.Warn(fmt.Sprintf("fetching roles for %d accounts, this might take a while...", len(accounts)+1)) ticker.Stop() } } @@ -608,8 +609,9 @@ func (c *Cache) addConfigRoles(r *Roles, config *SSOConfig) error { if err != nil { return err } + ctx := context.WithValue(context.Background(), "accountID", id) if _, ok := r.Accounts[id]; !ok { - log.Debugf("config.yaml defines AWS AccountID %d, but you don't have access.", id) + log.DebugContext(ctx, "config.yaml defines an AWS AccountID, but you don't have access.") continue } r.Accounts[id].DefaultRegion = account.DefaultRegion @@ -638,7 +640,7 @@ func (c *Cache) addConfigRoles(r *Roles, config *SSOConfig) error { // set the tags from the config file for roleName, role := range config.Accounts[accountId].Roles { if _, ok := r.Accounts[id].Roles[roleName]; !ok { - log.Debugf("config.yaml has %s but you don't have access", utils.MakeRoleARN(id, roleName)) + log.DebugContext(ctx, "config.yaml defines a role but you don't have access", "role", roleName) continue } r.Accounts[id].Roles[roleName].Arn = utils.MakeRoleARN(id, roleName) diff --git a/internal/sso/config.go b/internal/sso/config.go index 1c16665f..29c07b41 100644 --- a/internal/sso/config.go +++ b/internal/sso/config.go @@ -249,7 +249,7 @@ func (r *SSORole) GetRoleName() string { func (r *SSORole) GetAccountId() string { a, err := utils.AccountIdToString(r.GetAccountId64()) if err != nil { - log.WithError(err).Errorf("Unable to parse AccountId '%s'", a) + log.Error("Unable to parse AccountId", "error", err.Error(), "accountID", r.GetAccountId64()) return "" } return a @@ -259,7 +259,7 @@ func (r *SSORole) GetAccountId() string { func (r *SSORole) GetAccountId64() int64 { a, _, err := utils.ParseRoleARN(r.ARN) if err != nil { - log.WithError(err).Panicf("Unable to parse %s", r.ARN) + log.Fatal("Unable to parse", "arn", r.ARN, "error", err.Error()) } return a } diff --git a/internal/sso/options.go b/internal/sso/options.go index 872ce33a..43272480 100644 --- a/internal/sso/options.go +++ b/internal/sso/options.go @@ -111,7 +111,7 @@ func (s *Settings) GetColorOptions() []prompt.Option { value := v.Field(i).String() field := t.Field(i).Name optionName := fmt.Sprintf("Option%s", field) - log.Debugf("%s => %s", field, value) + log.Trace("ColorOption", "field", field, "value", value) colorValue := PROMPT_COLORS[value] diff --git a/internal/sso/roles.go b/internal/sso/roles.go index 32a04e55..7681e9f1 100644 --- a/internal/sso/roles.go +++ b/internal/sso/roles.go @@ -211,8 +211,7 @@ func (r *Roles) GetRoleByProfile(profileName string, s *Settings) (*AWSRoleFlat, flat, _ := r.GetRole(aId, roleName) pName, err := flat.ProfileName(s) if err != nil { - log.WithError(err).Warnf( - "unable to generate Profile for %s", utils.MakeRoleARN(aId, roleName)) + log.Warn("unable to generate Profile", "arn", utils.MakeRoleARN(aId, roleName), "error", err.Error()) } if pName == profileName { return flat, nil @@ -228,17 +227,17 @@ func (r *Roles) GetRoleChain(accountId int64, roleName string) []*AWSRoleFlat { f, err := r.GetRole(accountId, roleName) if err != nil { - log.WithError(err).Fatalf("unable to get role: %s", utils.MakeRoleARN(accountId, roleName)) + log.Fatal("unable to fetch role", "arn", utils.MakeRoleARN(accountId, roleName), "error", err.Error()) } ret = append(ret, f) for f.Via != "" { aId, rName, err := utils.ParseRoleARN(f.Via) if err != nil { - log.WithError(err).Fatalf("unable to parse '%s'", f.Via) + log.Fatal("unable to parse", "via", f.Via, "error", err.Error()) } f, err = r.GetRole(aId, rName) if err != nil { - log.WithError(err).Fatalf("unable to get role: %s", utils.MakeRoleARN(aId, rName)) + log.Fatal("unable to get role", "role", utils.MakeRoleARN(aId, rName), "error", err.Error()) } ret = append([]*AWSRoleFlat{f}, ret...) // prepend } @@ -383,8 +382,8 @@ func (r *AWSRoleFlat) ProfileName(s *Settings) (string, error) { } buf := new(bytes.Buffer) - log.Tracef("RoleInfo: %s", spew.Sdump(r)) - log.Tracef("Template: %s", spew.Sdump(templ)) + log.Trace("RoleInfo", "dump", spew.Sdump(r)) + log.Trace("Template", "dump", spew.Sdump(templ)) if err := templ.Execute(buf, r); err != nil { return "", fmt.Errorf("unable to generate ProfileName: %s", err.Error()) } diff --git a/internal/sso/settings.go b/internal/sso/settings.go index e9670c85..7d4a58ec 100644 --- a/internal/sso/settings.go +++ b/internal/sso/settings.go @@ -33,7 +33,6 @@ import ( "github.com/knadh/koanf/parsers/yaml" "github.com/knadh/koanf/providers/confmap" "github.com/knadh/koanf/providers/file" - "github.com/sirupsen/logrus" "github.com/synfinatic/aws-sso-cli/internal/url" "github.com/synfinatic/aws-sso-cli/internal/utils" ) @@ -87,14 +86,14 @@ func (s *Settings) GetDefaultRegion(id int64, roleName string, noRegion bool) st accountId, err := utils.AccountIdToString(id) if err != nil { - log.WithError(err).Panicf("Unable to GetDefaultRegion()") + log.Fatal("Unable to GetDefaultRegion()", "error", err.Error()) } currentRegion := os.Getenv("AWS_DEFAULT_REGION") ssoManagedRegion := os.Getenv("AWS_SSO_DEFAULT_REGION") if len(currentRegion) > 0 && currentRegion != ssoManagedRegion { - log.Debugf("Will not override current AWS_DEFAULT_REGION=%s", currentRegion) + log.Debug("Will not override current AWS_DEFAULT_REGION", "region", currentRegion) return "" } @@ -206,7 +205,7 @@ func LoadSettings(configFile, cacheFile string, defaults map[string]interface{}, // load the cache if s.Cache, err = OpenCache(s.cacheFile, s); err != nil { - log.Infof("%s", err.Error()) + log.Info("unable to open cache file", "error", err.Error()) } return s, nil @@ -234,7 +233,7 @@ func (s *Settings) applyDeprecations() bool { if s.ConfigUrlAction != "" && s.ConfigProfilesUrlAction == "" { s.ConfigProfilesUrlAction, err = url.NewConfigProfilesAction(s.ConfigUrlAction) if err != nil { - log.Warnf("Invalid value for ConfigUrlAction: %s", s.ConfigUrlAction) + log.Warn("Invalid value for ConfigUrlAction", "value", s.ConfigUrlAction) } s.ConfigUrlAction = string(url.Undef) // disable old value so it is omitempty change = true @@ -310,16 +309,7 @@ func (s *Settings) setOverrides(override OverrideSettings) { s.LogLevel = override.LogLevel } - lvls := map[string]logrus.Level{ - "trace": logrus.TraceLevel, - "debug": logrus.DebugLevel, - "info": logrus.InfoLevel, - "warn": logrus.WarnLevel, - "error": logrus.ErrorLevel, - } - - lvl := lvls[s.LogLevel] - log.SetLevel(lvl) + log.SetLevelString(s.LogLevel) if override.LogLines { s.LogLines = true @@ -349,13 +339,13 @@ func (s *Settings) ConfigFile() string { func (s *Settings) CreatedAt() int64 { f, err := os.Open(s.configFile) if err != nil { - log.WithError(err).Panicf("Unable to open %s", s.configFile) + log.Fatal("Unable to open", "file", s.configFile, "error", err.Error()) } defer f.Close() info, err := f.Stat() if err != nil { - log.WithError(err).Panicf("Unable to Stat() %s", s.configFile) + log.Fatal("Unable to Stat()", "file", s.configFile, "error", err.Error()) } return info.ModTime().Unix() } @@ -420,7 +410,7 @@ var getExecutable func() (string, error) = func() (string, error) { return "", err } if strings.HasPrefix(exec, NIX_STORE_PREFIX) { - log.Warnf("Detected NIX. Using $PATH to find `aws-sso`. Override with `ConfigProfilesBinaryPath`") + log.Warn("Detected NIX. Using $PATH to find `aws-sso`. Override with `ConfigProfilesBinaryPath`") exec = "aws-sso" } return exec, nil diff --git a/internal/storage/json_store.go b/internal/storage/json_store.go index 99ab16fb..504ad5c1 100644 --- a/internal/storage/json_store.go +++ b/internal/storage/json_store.go @@ -72,10 +72,10 @@ func OpenJsonStore(filename string) (*JsonStore, error) { // save writes the JSON store file, creating the directory if necessary func (jc *JsonStore) save() error { - log.Debugf("Saving JSON Cache") + log.Debug("Saving JSON Cache") jbytes, err := json.MarshalIndent(jc, "", " ") if err != nil { - log.WithError(err).Errorf("Unable to marshal json") + log.Error("Unable to marshal json", "error", err) return err } diff --git a/internal/storage/keyring.go b/internal/storage/keyring.go index 1a60e7b3..1eddd7c7 100644 --- a/internal/storage/keyring.go +++ b/internal/storage/keyring.go @@ -154,7 +154,7 @@ func fileKeyringPassword(prompt string) (string, error) { s := string(b) if s == "" { fmt.Println() - log.Fatalf("Aborting with empty password") + panic("Aborting with empty password") } fmt.Println() return s, nil @@ -199,7 +199,7 @@ func (kr *KeyringStore) getStorageData(s *StorageData) error { } if err != nil { - log.Warn(err) + log.Warn("unable to load keyring data", "error", err.Error()) // Didn't find anything in our keyring *s = NewStorageData() return nil diff --git a/internal/storage/storage.go b/internal/storage/storage.go index aca1636e..4818e47b 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -110,7 +110,7 @@ func (r *RoleCredentials) ExpireString() string { func (r *RoleCredentials) AccountIdStr() string { s, err := utils.AccountIdToString(r.AccountId) if err != nil { - log.WithError(err).Fatalf("unable to parse accountId from AWS role credentials") + panic(fmt.Sprintf("unable to parse accountId from AWS role credentials: %s", err.Error())) } return s } @@ -167,7 +167,7 @@ func (sc *StaticCredentials) UserArn() string { func (sc *StaticCredentials) AccountIdStr() string { s, err := utils.AccountIdToString(sc.AccountId) if err != nil { - log.WithError(err).Panicf("Invalid AccountId from AWS static credentials: %d", sc.AccountId) + panic(fmt.Sprintf("Invalid AccountId from AWS static credentials: %d", sc.AccountId)) } return s } diff --git a/internal/tags/tags_list.go b/internal/tags/tags_list.go index 05c8c055..21cf150a 100644 --- a/internal/tags/tags_list.go +++ b/internal/tags/tags_list.go @@ -20,6 +20,7 @@ package tags import ( "fmt" + "os" "sort" "strconv" "strings" @@ -138,7 +139,8 @@ func ReformatHistory(value string) string { i, err := strconv.ParseInt(x[1], 10, 64) if err != nil { - log.WithError(err).Panicf("Unable to parse: %s", value) + log.Error("unable to parse epoch", "value", value, "epoch", x[1], "split", x, "error", err) + os.Exit(1) } d := time.Since(time.Unix(i, 0)).Truncate(time.Second) diff --git a/internal/url/url.go b/internal/url/url.go index 3b86aa7b..279210d1 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -178,7 +178,7 @@ func NewHandleUrl(action Action, url, browser string, command []string) *HandleU } if (action == Exec || action.IsContainer()) && len(command) == 0 { - log.Panicf("Unable to call exec or open firefox container with an empty command") + panic("Unable to call exec or open firefox container with an empty command") } h := &HandleUrl{ @@ -210,7 +210,7 @@ func (h *HandleUrl) Open() error { case Clip: err = clipboardWriter(h.Url) if err == nil { - log.Infof("Please open URL copied to clipboard.\n") + log.Info("Please open URL copied to clipboard.\n") } else { err = fmt.Errorf("unable to copy URL to clipboard: %s", err.Error()) } @@ -243,7 +243,7 @@ func (h *HandleUrl) Open() error { if err != nil { err = fmt.Errorf("unable to open URL with %s: %s", browser, err.Error()) } else { - log.Infof("Opening URL in: %s\n", browser) + log.Info("Opening URL", "browser", browser) } default: @@ -270,14 +270,14 @@ func selectElement(seed string, options []string) string { func formatContainerUrl(format, targetUrl, name, color, icon string) string { if !utils.StrListContains(color, FIREFOX_PLUGIN_COLORS) { if color != "" { - log.Warnf("Invalid Firefox Container color: %s", color) + log.Warn("Invalid Firefox Container color", "color", color) } color = selectElement(name, FIREFOX_PLUGIN_COLORS) } if !utils.StrListContains(icon, FIREFOX_PLUGIN_ICONS) { if icon != "" { - log.Warnf("Invalid Firefox Container icon: %s", icon) + log.Warn("Invalid Firefox Container icon", "icon", icon) } icon = selectElement(name, FIREFOX_PLUGIN_ICONS) } @@ -295,7 +295,7 @@ func execWithUrl(command []string, url string) error { } cmdStr := fmt.Sprintf("%s %s", program, strings.Join(cmdList, " ")) - log.Debugf("exec command as array: %s", cmdStr) + log.Debug("exec command as array", "command", cmdStr) cmd = exec.Command(program, cmdList...) // add $HOME to our environment @@ -307,7 +307,7 @@ func execWithUrl(command []string, url string) error { if err != nil { err = fmt.Errorf("unable to exec `%s`: %s", cmdStr, err) } - log.Debugf("Opened our URL with %s", command[0]) + log.Debug("Opened our URL", "command", command[0]) return err } diff --git a/internal/utils/fileedit.go b/internal/utils/fileedit.go index a093acb3..a3fbbbb9 100644 --- a/internal/utils/fileedit.go +++ b/internal/utils/fileedit.go @@ -116,7 +116,7 @@ func (f *FileEdit) UpdateConfig(printDiff, force bool, configFile string) (bool, if len(diff) == 0 { // do nothing if there is no diff - log.Infof("no changes made to %s", configFile) + log.Info("no changes made config file", "file", configFile) return false, "", nil } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index edf2d702..d2f29e45 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -49,7 +49,7 @@ func GetHomePath(path string) string { if strings.HasPrefix(p, "~") { home, err := os.UserHomeDir() if err != nil { - log.WithError(err).Fatalf("Unable to GetHomePath(%s)", path) + panic(fmt.Sprintf("unable to GetHomePath: %s", path)) } p = strings.Replace(p, "~", home, 1) @@ -97,7 +97,7 @@ func ParseUserARN(arn string) (int64, string, error) { func MakeRoleARN(account int64, name string) string { a, err := AccountIdToString(account) if err != nil { - log.WithError(err).Panicf("unable to MakeRoleARN") + panic(fmt.Sprintf("unable to MakeRoleARN: %s", err.Error())) } return fmt.Sprintf("arn:aws:iam::%s:role/%s", a, name) } @@ -106,7 +106,7 @@ func MakeRoleARN(account int64, name string) string { func MakeUserARN(account int64, name string) string { a, err := AccountIdToString(account) if err != nil { - log.WithError(err).Panicf("unable to MakeUserARN") + panic(fmt.Sprintf("unable to MakeUserARN: %s", err.Error())) } return fmt.Sprintf("arn:aws:iam::%s:user/%s", a, name) } @@ -115,7 +115,7 @@ func MakeUserARN(account int64, name string) string { func MakeRoleARNs(account, name string) string { x, err := AccountIdToInt64(account) if err != nil { - log.WithError(err).Panicf("unable to AccountIdToInt64 in MakeRoleARNs") + panic(fmt.Sprintf("unable to MakeRoleARNs: %s", err.Error())) } a, _ := AccountIdToString(x) From d4fd286a1c8a4af8b971ed65d56495a72a923631 Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Wed, 31 Jul 2024 13:30:46 -0700 Subject: [PATCH 02/10] add custom handlers --- internal/logger/console.go | 116 +++++++++++++++++++++++++++++++++++++ internal/logger/logger.go | 91 +++++++++++++++-------------- internal/logger/pretty.go | 92 +++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+), 43 deletions(-) create mode 100644 internal/logger/console.go create mode 100644 internal/logger/pretty.go diff --git a/internal/logger/console.go b/internal/logger/console.go new file mode 100644 index 00000000..80fd1aba --- /dev/null +++ b/internal/logger/console.go @@ -0,0 +1,116 @@ +package logger + +/* + * AWS SSO CLI + * Copyright (c) 2021-2024 Aaron Turner + * + * This program is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or with the authors permission any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import ( + "bytes" + "context" + "io" + "log/slog" + "sync" +) + +// impliment the slog.Handler interface +type ConsoleHandler struct { + writer io.Writer + opts slog.HandlerOptions + mu *sync.Mutex +} + +func NewConsoleHandler(w io.Writer, opts *slog.HandlerOptions) *ConsoleHandler { + if opts == nil { + opts = &slog.HandlerOptions{} + } + return &ConsoleHandler{ + writer: w, + opts: *opts, + mu: &sync.Mutex{}, + } +} + +// Enabled reports whether the handler handles records at the given level. +// The handler ignores records whose level is lower. +// It is called early, before any arguments are processed, +// to save effort if the log event should be discarded. +// If called from a Logger method, the first argument is the context +// passed to that method, or context.Background() if nil was passed +// or the method does not take a context. +// The context is passed so Enabled can use its values +// to make a decision. +func (h *ConsoleHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.opts.Level.Level() +} + +// Handle handles the Record. +// It will only be called when Enabled returns true. +// The Context argument is as for Enabled. +// It is present solely to provide Handlers access to the context's values. +// Canceling the context should not affect record processing. +// (Among other things, log messages may be necessary to debug a +// cancellation-related problem.) +// +// Handle methods that produce output should observe the following rules: +// - If r.Time is the zero time, ignore the time. +// - If r.PC is zero, ignore it. +// - Attr's values should be resolved. +// - If an Attr's key and value are both the zero value, ignore the Attr. +// This can be tested with attr.Equal(Attr{}). +// - If a group's key is empty, inline the group's Attrs. +// - If a group has no Attrs (even if it has a non-empty key), +// ignore it. +func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error { + buf := bytes.NewBuffer([]byte{}) + buf.WriteString(r.Level.String()) + buf.WriteByte(' ') + buf.WriteString(r.Message) + h.mu.Lock() + defer h.mu.Unlock() + _, err := h.writer.Write(buf.Bytes()) + return err +} + +// WithAttrs returns a new Handler whose attributes consist of +// both the receiver's attributes and the arguments. +// The Handler owns the slice: it may retain, modify or discard it. +func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) ConsoleHandler { + return *h +} + +// WithGroup returns a new Handler with the given group appended to +// the receiver's existing groups. +// The keys of all subsequent attributes, whether added by With or in a +// Record, should be qualified by the sequence of group names. +// +// How this qualification happens is up to the Handler, so long as +// this Handler's attribute keys differ from those of another Handler +// with a different sequence of group names. +// +// A Handler should treat WithGroup as starting a Group of Attrs that ends +// at the end of the log event. That is, +// +// logger.WithGroup("s").LogAttrs(ctx, level, msg, slog.Int("a", 1), slog.Int("b", 2)) +// +// should behave like +// +// logger.LogAttrs(ctx, level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) +// +// If the name is empty, WithGroup returns the receiver. +func (h *ConsoleHandler) WithGroup(name string) ConsoleHandler { + return *h +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 3cbee402..f6a9cf15 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -27,7 +27,7 @@ import ( "strings" ) -var log *Logger +var logger *Logger const ( LevelTrace = slog.Level(-8) @@ -57,52 +57,63 @@ type Logger struct { level *slog.LevelVar } +// initialize the default logger to log to stderr and log at the warn level +func init() { + logger = NewLogger(true, slog.LevelWarn) + slog.SetDefault(logger.Logger) +} + // NewLogger creates a new logger with the given log level and whether to add source information func NewLogger(addSource bool, level slog.Leveler) *Logger { lvl := new(slog.LevelVar) lvl.Set(level.Level()) - opts := &slog.HandlerOptions{ - Level: lvl, - AddSource: addSource, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - // Remove time from the output for predictable test output. - if a.Key == slog.TimeKey { - return slog.Attr{} - } - - // Fix level names and pad the names - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - levelLabel, exists := LevelNames[level] - if !exists { - levelLabel = level.String() + opts := PrettyHandlerOptions{ + TimeFormat: "", + HandlerOptions: &slog.HandlerOptions{ + AddSource: addSource, + Level: lvl, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + // Remove time from the output for predictable test output. + if a.Key == slog.TimeKey { + return slog.Attr{} + } + + // Fix level names and pad the names + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + + levelLabel, exists := LevelNames[level] + if !exists { + levelLabel = level.String() + } + + // Pad the level name to 8 characters + a.Value = slog.StringValue(levelLabel) // fmt.Sprintf("%8s", levelLabel)) } - // Pad the level name to 8 characters - a.Value = slog.StringValue(fmt.Sprintf("%8s", levelLabel)) - } - - // Rename the source attributes if they came from Trace/Fatal to the correct names - // so the old values get overwritten - if groups[0] == "source" { - switch a.Key { - case FileKey: - a.Key = "file" - case LineKey: - a.Key = "line" - case FunctionKey: - a.Key = "function" - default: - break // do nothing + // Rename the source attributes if they came from Trace/Fatal to the correct names + // so the old values get overwritten + if len(groups) > 0 && groups[0] == "source" { + switch a.Key { + case FileKey: + a.Key = "file" + case LineKey: + a.Key = "line" + case FunctionKey: + a.Key = "function" + default: + break // do nothing + } } - } - return a + return a + }, }, } - var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts) + // var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts) + var handler slog.Handler = NewPrettyHandler(os.Stderr, opts) return &Logger{ Logger: slog.New(handler), addSource: addSource, @@ -140,18 +151,12 @@ func (l *Logger) GetLevel() slog.Leveler { return slog.Level(l.level.Level()) } -// initialize the default logger to log to stderr and log at the warn level -func init() { - log = NewLogger(false, slog.LevelWarn) - slog.SetDefault(log.Logger) -} - func SetLogger(l *Logger) { - log = l + logger = l } func GetLogger() *Logger { - return log + return logger } func SetDefaultLogger(l *Logger) { diff --git a/internal/logger/pretty.go b/internal/logger/pretty.go new file mode 100644 index 00000000..465e275e --- /dev/null +++ b/internal/logger/pretty.go @@ -0,0 +1,92 @@ +package logger + +// Shamelessly stolen from https://betterstack.com/community/guides/logging/logging-in-go/#customizing-slog-handlers + +import ( + "context" + "encoding/json" + "io" + "log" + "log/slog" + + "github.com/fatih/color" +) + +type PrettyHandlerOptions struct { + *slog.HandlerOptions + TimeFormat string + NoColor bool +} + +type PrettyHandler struct { + slog.Handler + l *log.Logger + TimeFormat string + NoColor bool +} + +const ( + DefaultTimeFormat = "15:04:05.000" +) + +func (h *PrettyHandler) Handle(ctx context.Context, r slog.Record) error { + color.NoColor = h.NoColor // disable color if NoColor is set + + level, ok := LevelNames[r.Level] + if ok { + level = level + ":" + } else { + level = r.Level.String() + ":" + } + + switch r.Level { + case LevelTrace: + level = color.GreenString(level) + case slog.LevelDebug: + level = color.MagentaString(level) + case slog.LevelInfo: + level = color.BlueString(level) + case slog.LevelWarn: + level = color.YellowString(level) + case slog.LevelError, LevelFatal: + level = color.RedString(level) + default: + level = color.WhiteString(level) + } + + // figure out the line to generate + logLine := []any{} + if h.TimeFormat != "" { + logLine = append(logLine, r.Time.Format(h.TimeFormat)) + } + + logLine = append(logLine, level, color.CyanString(r.Message)) + + fields := make(map[string]interface{}, r.NumAttrs()) + if r.NumAttrs() > 0 { + r.Attrs(func(a slog.Attr) bool { + fields[a.Key] = a.Value.Any() + return true + }) + + b, err := json.MarshalIndent(fields, "", " ") + if err != nil { + return err + } + logLine = append(logLine, color.WhiteString(string(b))) + } + + h.l.Println(logLine...) + return nil +} + +func NewPrettyHandler(out io.Writer, opts PrettyHandlerOptions) *PrettyHandler { + h := &PrettyHandler{ + Handler: slog.NewJSONHandler(out, opts.HandlerOptions), + l: log.New(out, "", 0), + TimeFormat: opts.TimeFormat, + NoColor: opts.NoColor, + } + + return h +} From 5ace7983da52676b47057b93555400580ca8a773 Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Thu, 1 Aug 2024 15:53:56 -0700 Subject: [PATCH 03/10] Use tint for logging - currently uses my custom tint branch. Can't use this for reals yet --- go.mod | 10 ++-- go.sum | 14 ++++-- internal/logger/logger.go | 86 ++++++++++++--------------------- internal/logger/pretty.go | 28 +++++++++-- internal/logger/replace_attr.go | 48 ++++++++++++++++++ 5 files changed, 118 insertions(+), 68 deletions(-) create mode 100644 internal/logger/replace_attr.go diff --git a/go.mod b/go.mod index 761db156..3bd53238 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/synfinatic/aws-sso-cli go 1.21 +// FIXME: temporary development +replace github.com/lmittmann/tint v1.0.5 => ../tint + require ( github.com/99designs/keyring v1.2.2 github.com/Masterminds/sprig/v3 v3.2.3 @@ -34,7 +37,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect - github.com/fatih/color v1.10.0 // indirect + github.com/fatih/color v1.17.0 github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/google/uuid v1.6.0 // indirect @@ -43,8 +46,8 @@ require ( github.com/hashicorp/go-multierror v1.1.0 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect - github.com/mattn/go-colorable v0.1.8 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -75,6 +78,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 + github.com/lmittmann/tint v1.0.5 golang.org/x/net v0.27.0 ) diff --git a/go.sum b/go.sum index e46a9afb..94279427 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= @@ -270,14 +270,16 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -511,7 +513,9 @@ golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/logger/logger.go b/internal/logger/logger.go index f6a9cf15..3cea87b2 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -25,16 +25,17 @@ import ( "os" "runtime" "strings" + + "github.com/fatih/color" + "github.com/lmittmann/tint" + "github.com/mattn/go-isatty" ) var logger *Logger const ( - LevelTrace = slog.Level(-8) - LevelFatal = slog.Level(12) - LineKey = "_line" - FileKey = "_file" - FunctionKey = "_function" + LevelTrace = slog.Level(-8) + LevelFatal = slog.Level(12) ) var LevelNames = map[slog.Leveler]string{ @@ -59,63 +60,35 @@ type Logger struct { // initialize the default logger to log to stderr and log at the warn level func init() { - logger = NewLogger(true, slog.LevelWarn) - slog.SetDefault(logger.Logger) + w := os.Stderr + logger = NewTincLogger(w, false, slog.LevelWarn) + // slog.SetDefault(logger.Logger) } -// NewLogger creates a new logger with the given log level and whether to add source information -func NewLogger(addSource bool, level slog.Leveler) *Logger { +func NewTincLogger(w *os.File, addSource bool, level slog.Leveler) *Logger { lvl := new(slog.LevelVar) lvl.Set(level.Level()) - opts := PrettyHandlerOptions{ + opts := tint.Options{ + Level: lvl, + AddSource: addSource, + ReplaceAttr: replaceAttr, + // TimeFormat: time.Kitchen, TimeFormat: "", - HandlerOptions: &slog.HandlerOptions{ - AddSource: addSource, - Level: lvl, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - // Remove time from the output for predictable test output. - if a.Key == slog.TimeKey { - return slog.Attr{} - } - - // Fix level names and pad the names - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - - levelLabel, exists := LevelNames[level] - if !exists { - levelLabel = level.String() - } - - // Pad the level name to 8 characters - a.Value = slog.StringValue(levelLabel) // fmt.Sprintf("%8s", levelLabel)) - } - - // Rename the source attributes if they came from Trace/Fatal to the correct names - // so the old values get overwritten - if len(groups) > 0 && groups[0] == "source" { - switch a.Key { - case FileKey: - a.Key = "file" - case LineKey: - a.Key = "line" - case FunctionKey: - a.Key = "function" - default: - break // do nothing - } - } - - return a - }, + LevelColorsMap: tint.LevelColorsMapping{ + LevelTrace: {Name: "TRACE", Color: color.FgGreen}, + LevelFatal: {Name: "FATAL", Color: color.FgRed}, + slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, + slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, + slog.LevelError: {Name: "ERROR", Color: color.FgRed}, + slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, }, + NoColor: !isatty.IsTerminal(w.Fd()), } - // var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts) - var handler slog.Handler = NewPrettyHandler(os.Stderr, opts) + var handle slog.Handler = tint.NewHandler(w, &opts) return &Logger{ - Logger: slog.New(handler), + Logger: slog.New(handle), addSource: addSource, level: lvl, } @@ -180,11 +153,12 @@ func (l *Logger) logWithSource(level slog.Level, msg string, args ...interface{} allArgs = append(allArgs, args...) if l.addSource { - var functionName string = "" - pc, file, line, ok := runtime.Caller(2) // go up two levels to get the caller + pc, _, _, ok := runtime.Caller(2) // go up two levels to get the caller if ok { - functionName = runtime.FuncForPC(pc).Name() - allArgs = append(allArgs, slog.Group("source", slog.String(FileKey, file), slog.Int(LineKey, line), slog.String(FunctionKey, functionName))) + fs := runtime.CallersFrames([]uintptr{pc}) + f, _ := fs.Next() + allArgs = append(allArgs, slog.Group("source", + slog.String("file", f.File), slog.Int("line", f.Line), slog.String("func", f.Function))) } } l.Logger.Log(ctx, level, msg, allArgs...) diff --git a/internal/logger/pretty.go b/internal/logger/pretty.go index 465e275e..9c4b2e94 100644 --- a/internal/logger/pretty.go +++ b/internal/logger/pretty.go @@ -8,6 +8,7 @@ import ( "io" "log" "log/slog" + "os" "github.com/fatih/color" ) @@ -18,6 +19,29 @@ type PrettyHandlerOptions struct { NoColor bool } +// NewPrettyLogger creates a new logger with the given log level and whether to add source information +func NewPrettyLogger(addSource bool, level slog.Leveler) *Logger { + lvl := new(slog.LevelVar) + lvl.Set(level.Level()) + + opts := PrettyHandlerOptions{ + TimeFormat: "", + HandlerOptions: &slog.HandlerOptions{ + AddSource: addSource, + Level: lvl, + ReplaceAttr: replaceAttr, + }, + } + + // var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts) + var handler slog.Handler = NewPrettyHandler(os.Stderr, opts) + return &Logger{ + Logger: slog.New(handler), + addSource: addSource, + level: lvl, + } +} + type PrettyHandler struct { slog.Handler l *log.Logger @@ -25,10 +49,6 @@ type PrettyHandler struct { NoColor bool } -const ( - DefaultTimeFormat = "15:04:05.000" -) - func (h *PrettyHandler) Handle(ctx context.Context, r slog.Record) error { color.NoColor = h.NoColor // disable color if NoColor is set diff --git a/internal/logger/replace_attr.go b/internal/logger/replace_attr.go new file mode 100644 index 00000000..c54ab598 --- /dev/null +++ b/internal/logger/replace_attr.go @@ -0,0 +1,48 @@ +package logger + +import "log/slog" + +const ( + LineKey = "_line" + FileKey = "_file" + FunctionKey = "_function" +) + +func replaceAttr(groups []string, a slog.Attr) slog.Attr { + // Remove time from the output for predictable test output. + if a.Key == slog.TimeKey { + return slog.Attr{} + } + + // Fix level names and pad the names + /* + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + + levelLabel, exists := LevelNames[level] + if !exists { + levelLabel = level.String() + } + + // Pad the level name to 8 characters + a.Value = slog.StringValue(levelLabel) // fmt.Sprintf("%8s", levelLabel)) + } + */ + + // Rename the source attributes if they came from Trace/Fatal to the correct names + // so the old values get overwritten + if len(groups) > 0 && groups[0] == "source" { + switch a.Key { + case FileKey: + a.Key = "file" + case LineKey: + a.Key = "line" + case FunctionKey: + a.Key = "function" + default: + break // do nothing + } + } + + return a +} From e8f7393975d9841f1bcaeb553c76507705810e30 Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Fri, 2 Aug 2024 16:50:22 -0700 Subject: [PATCH 04/10] wrap tint to make pretty logs and hack line numbers --- go.mod | 4 +- go.sum | 2 + internal/logger/console.go | 139 +++++++++++++------------------- internal/logger/logger.go | 75 ++++++----------- internal/logger/pretty.go | 112 ------------------------- internal/logger/replace_attr.go | 37 +-------- internal/logger/tint.go | 35 ++++++++ internal/sso/settings.go | 12 +-- 8 files changed, 132 insertions(+), 284 deletions(-) delete mode 100644 internal/logger/pretty.go create mode 100644 internal/logger/tint.go diff --git a/go.mod b/go.mod index 3bd53238..4f6fb83f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/synfinatic/aws-sso-cli go 1.21 // FIXME: temporary development -replace github.com/lmittmann/tint v1.0.5 => ../tint +replace github.com/lmittmann/tint v1.0.5 => github.com/synfinatic/tint v0.0.0-20240801234705-50f64e87e8db require ( github.com/99designs/keyring v1.2.2 @@ -47,7 +47,7 @@ require ( github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index 94279427..6053e258 100644 --- a/go.sum +++ b/go.sum @@ -392,6 +392,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/synfinatic/gotable v0.0.3 h1:KI01OLECmOv7laXVNtw6T4kEHue09z9OuQwtNB8D5Mw= github.com/synfinatic/gotable v0.0.3/go.mod h1:kWXD1bxZY6tyu6tWK3CIbGAOrtF7teg0ZQotUMDvoMw= +github.com/synfinatic/tint v0.0.0-20240801234705-50f64e87e8db h1:tLDMrB9hhcOE8EstxtUx3EA/Ag2qQ1hGdkACxLIOyVA= +github.com/synfinatic/tint v0.0.0-20240801234705-50f64e87e8db/go.mod h1:U8412el5hDkno2/xAgVuq4+z5CXJQvRv3Nv81kIqByc= github.com/willabides/kongplete v0.2.0 h1:C6wYVn+IPyA8rAGRGLLkuxhhSQTEECX4t8u3gi+fuD0= github.com/willabides/kongplete v0.2.0/go.mod h1:kFVw+PkQsqkV7O4tfIBo6iJ9qY94PJC8sPfMgFG5AdM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/logger/console.go b/internal/logger/console.go index 80fd1aba..d6d84b24 100644 --- a/internal/logger/console.go +++ b/internal/logger/console.go @@ -19,98 +19,75 @@ package logger */ import ( - "bytes" "context" - "io" "log/slog" - "sync" + "os" + "runtime" + "time" + + "github.com/fatih/color" + "github.com/lmittmann/tint" + "github.com/mattn/go-isatty" ) -// impliment the slog.Handler interface -type ConsoleHandler struct { - writer io.Writer - opts slog.HandlerOptions - mu *sync.Mutex -} +const ( + FrameMarker = "__skip_frames" +) -func NewConsoleHandler(w io.Writer, opts *slog.HandlerOptions) *ConsoleHandler { - if opts == nil { - opts = &slog.HandlerOptions{} - } - return &ConsoleHandler{ - writer: w, - opts: *opts, - mu: &sync.Mutex{}, +// NewConsole creates a new slog.Handler for the ConsoleHandler, which wraps tint.NewHandler +// with some customizations. +func NewConsole(w *os.File, addSource bool, level slog.Leveler) (slog.Handler, *slog.LevelVar) { + lvl := new(slog.LevelVar) + lvl.Set(level.Level()) + + opts := tint.Options{ + Level: lvl, + AddSource: addSource, + ReplaceAttr: replaceAttr, + TimeFormat: time.Kitchen, + // TimeFormat: "", + LevelColorsMap: tint.LevelColorsMapping{ + LevelTrace: {Name: "TRACE", Color: color.FgGreen}, + LevelFatal: {Name: "FATAL", Color: color.FgRed}, + slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, + slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, + slog.LevelError: {Name: "ERROR", Color: color.FgRed}, + slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, + }, + NoColor: !isatty.IsTerminal(w.Fd()), } -} -// Enabled reports whether the handler handles records at the given level. -// The handler ignores records whose level is lower. -// It is called early, before any arguments are processed, -// to save effort if the log event should be discarded. -// If called from a Logger method, the first argument is the context -// passed to that method, or context.Background() if nil was passed -// or the method does not take a context. -// The context is passed so Enabled can use its values -// to make a decision. -func (h *ConsoleHandler) Enabled(_ context.Context, level slog.Level) bool { - return level >= h.opts.Level.Level() + return NewConsoleHandler(w, &opts), lvl } -// Handle handles the Record. -// It will only be called when Enabled returns true. -// The Context argument is as for Enabled. -// It is present solely to provide Handlers access to the context's values. -// Canceling the context should not affect record processing. -// (Among other things, log messages may be necessary to debug a -// cancellation-related problem.) -// -// Handle methods that produce output should observe the following rules: -// - If r.Time is the zero time, ignore the time. -// - If r.PC is zero, ignore it. -// - Attr's values should be resolved. -// - If an Attr's key and value are both the zero value, ignore the Attr. -// This can be tested with attr.Equal(Attr{}). -// - If a group's key is empty, inline the group's Attrs. -// - If a group has no Attrs (even if it has a non-empty key), -// ignore it. -func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error { - buf := bytes.NewBuffer([]byte{}) - buf.WriteString(r.Level.String()) - buf.WriteByte(' ') - buf.WriteString(r.Message) - h.mu.Lock() - defer h.mu.Unlock() - _, err := h.writer.Write(buf.Bytes()) - return err +// impliment the slog.Handler interface via the tint.Handler +type ConsoleHandler struct { + slog.Handler } -// WithAttrs returns a new Handler whose attributes consist of -// both the receiver's attributes and the arguments. -// The Handler owns the slice: it may retain, modify or discard it. -func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) ConsoleHandler { - return *h +// ConsoleHandler is a slog.Handler that wraps tint.Handler +func NewConsoleHandler(w *os.File, opts *tint.Options) slog.Handler { + return &ConsoleHandler{ + tint.NewHandler(w, opts), + } } -// WithGroup returns a new Handler with the given group appended to -// the receiver's existing groups. -// The keys of all subsequent attributes, whether added by With or in a -// Record, should be qualified by the sequence of group names. -// -// How this qualification happens is up to the Handler, so long as -// this Handler's attribute keys differ from those of another Handler -// with a different sequence of group names. -// -// A Handler should treat WithGroup as starting a Group of Attrs that ends -// at the end of the log event. That is, -// -// logger.WithGroup("s").LogAttrs(ctx, level, msg, slog.Int("a", 1), slog.Int("b", 2)) -// -// should behave like -// -// logger.LogAttrs(ctx, level, msg, slog.Group("s", slog.Int("a", 1), slog.Int("b", 2))) -// -// If the name is empty, WithGroup returns the receiver. -func (h *ConsoleHandler) WithGroup(name string) ConsoleHandler { - return *h +// Handle is a custom wrapper around the tint.Handler.Handle method which fixes up +// the PC value to be the correct caller for the Fatal/Trace methods +func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error { + var fixStack int64 = 0 + r.Attrs(func(a slog.Attr) bool { + if a.Key == FrameMarker { + fixStack = a.Value.Int64() + return false + } + return true + }) + + if fixStack > 0 { + rn := r.Clone() + rn.PC, _, _, _ = runtime.Caller(int(fixStack)) + return h.Handler.Handle(ctx, rn) + } + return h.Handler.Handle(ctx, r) } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 3cea87b2..0549a41a 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -23,16 +23,18 @@ import ( "fmt" "log/slog" "os" - "runtime" "strings" - - "github.com/fatih/color" - "github.com/lmittmann/tint" - "github.com/mattn/go-isatty" ) var logger *Logger +type Logger struct { + *slog.Logger + writer *os.File + addSource bool + level *slog.LevelVar +} + const ( LevelTrace = slog.Level(-8) LevelFatal = slog.Level(12) @@ -43,6 +45,10 @@ var LevelNames = map[slog.Leveler]string{ LevelFatal: "FATAL", } +type NewLoggerFunc func(w *os.File, addSource bool, level slog.Leveler) (slog.Handler, *slog.LevelVar) + +var NewLogger NewLoggerFunc = NewConsole + var LevelStrings = map[string]slog.Leveler{ "TRACE": LevelTrace, "FATAL": LevelFatal, @@ -52,46 +58,22 @@ var LevelStrings = map[string]slog.Leveler{ "DEBUG": slog.LevelDebug, } -type Logger struct { - *slog.Logger - addSource bool - level *slog.LevelVar -} - // initialize the default logger to log to stderr and log at the warn level func init() { w := os.Stderr - logger = NewTincLogger(w, false, slog.LevelWarn) - // slog.SetDefault(logger.Logger) -} - -func NewTincLogger(w *os.File, addSource bool, level slog.Leveler) *Logger { - lvl := new(slog.LevelVar) - lvl.Set(level.Level()) - - opts := tint.Options{ - Level: lvl, - AddSource: addSource, - ReplaceAttr: replaceAttr, - // TimeFormat: time.Kitchen, - TimeFormat: "", - LevelColorsMap: tint.LevelColorsMapping{ - LevelTrace: {Name: "TRACE", Color: color.FgGreen}, - LevelFatal: {Name: "FATAL", Color: color.FgRed}, - slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, - slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, - slog.LevelError: {Name: "ERROR", Color: color.FgRed}, - slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, - }, - NoColor: !isatty.IsTerminal(w.Fd()), - } + addSource := false + level := slog.LevelWarn + // logger = NewTincLogger(w, false, slog.LevelWarn) + handle, lvl := NewLogger(w, addSource, level) - var handle slog.Handler = tint.NewHandler(w, &opts) - return &Logger{ + logger = &Logger{ Logger: slog.New(handle), + writer: w, addSource: addSource, level: lvl, } + + slog.SetDefault(logger.Logger) } // SetLevel sets the log level for the logger @@ -112,11 +94,9 @@ func (l *Logger) SetReportCaller(reportCaller bool) { return // do nothing } l.addSource = reportCaller - l.Logger = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ - Level: l.level, - AddSource: reportCaller, - })) - slog.SetDefault(l.Logger) + handler, _ := NewLogger(l.writer, l.addSource, slog.LevelWarn) + logger.Logger = slog.New(handler) + slog.SetDefault(logger.Logger) } // GetLevel returns the current log level @@ -147,19 +127,16 @@ func (l *Logger) Fatal(msg string, args ...interface{}) { os.Exit(1) } +// logWithSource sets the __source attribute so that our Handler knows +// to modify the r.PC value to include the original caller. func (l *Logger) logWithSource(level slog.Level, msg string, args ...interface{}) { ctx := context.Background() var allArgs []interface{} allArgs = append(allArgs, args...) if l.addSource { - pc, _, _, ok := runtime.Caller(2) // go up two levels to get the caller - if ok { - fs := runtime.CallersFrames([]uintptr{pc}) - f, _ := fs.Next() - allArgs = append(allArgs, slog.Group("source", - slog.String("file", f.File), slog.Int("line", f.Line), slog.String("func", f.Function))) - } + // 5 is the number of stack frames to skip in Handler.Handle() + allArgs = append(allArgs, slog.Int(FrameMarker, 5)) } l.Logger.Log(ctx, level, msg, allArgs...) } diff --git a/internal/logger/pretty.go b/internal/logger/pretty.go deleted file mode 100644 index 9c4b2e94..00000000 --- a/internal/logger/pretty.go +++ /dev/null @@ -1,112 +0,0 @@ -package logger - -// Shamelessly stolen from https://betterstack.com/community/guides/logging/logging-in-go/#customizing-slog-handlers - -import ( - "context" - "encoding/json" - "io" - "log" - "log/slog" - "os" - - "github.com/fatih/color" -) - -type PrettyHandlerOptions struct { - *slog.HandlerOptions - TimeFormat string - NoColor bool -} - -// NewPrettyLogger creates a new logger with the given log level and whether to add source information -func NewPrettyLogger(addSource bool, level slog.Leveler) *Logger { - lvl := new(slog.LevelVar) - lvl.Set(level.Level()) - - opts := PrettyHandlerOptions{ - TimeFormat: "", - HandlerOptions: &slog.HandlerOptions{ - AddSource: addSource, - Level: lvl, - ReplaceAttr: replaceAttr, - }, - } - - // var handler slog.Handler = slog.NewTextHandler(os.Stderr, opts) - var handler slog.Handler = NewPrettyHandler(os.Stderr, opts) - return &Logger{ - Logger: slog.New(handler), - addSource: addSource, - level: lvl, - } -} - -type PrettyHandler struct { - slog.Handler - l *log.Logger - TimeFormat string - NoColor bool -} - -func (h *PrettyHandler) Handle(ctx context.Context, r slog.Record) error { - color.NoColor = h.NoColor // disable color if NoColor is set - - level, ok := LevelNames[r.Level] - if ok { - level = level + ":" - } else { - level = r.Level.String() + ":" - } - - switch r.Level { - case LevelTrace: - level = color.GreenString(level) - case slog.LevelDebug: - level = color.MagentaString(level) - case slog.LevelInfo: - level = color.BlueString(level) - case slog.LevelWarn: - level = color.YellowString(level) - case slog.LevelError, LevelFatal: - level = color.RedString(level) - default: - level = color.WhiteString(level) - } - - // figure out the line to generate - logLine := []any{} - if h.TimeFormat != "" { - logLine = append(logLine, r.Time.Format(h.TimeFormat)) - } - - logLine = append(logLine, level, color.CyanString(r.Message)) - - fields := make(map[string]interface{}, r.NumAttrs()) - if r.NumAttrs() > 0 { - r.Attrs(func(a slog.Attr) bool { - fields[a.Key] = a.Value.Any() - return true - }) - - b, err := json.MarshalIndent(fields, "", " ") - if err != nil { - return err - } - logLine = append(logLine, color.WhiteString(string(b))) - } - - h.l.Println(logLine...) - return nil -} - -func NewPrettyHandler(out io.Writer, opts PrettyHandlerOptions) *PrettyHandler { - h := &PrettyHandler{ - Handler: slog.NewJSONHandler(out, opts.HandlerOptions), - l: log.New(out, "", 0), - TimeFormat: opts.TimeFormat, - NoColor: opts.NoColor, - } - - return h -} diff --git a/internal/logger/replace_attr.go b/internal/logger/replace_attr.go index c54ab598..6efdd52a 100644 --- a/internal/logger/replace_attr.go +++ b/internal/logger/replace_attr.go @@ -2,46 +2,15 @@ package logger import "log/slog" -const ( - LineKey = "_line" - FileKey = "_file" - FunctionKey = "_function" -) - func replaceAttr(groups []string, a slog.Attr) slog.Attr { // Remove time from the output for predictable test output. if a.Key == slog.TimeKey { return slog.Attr{} } - // Fix level names and pad the names - /* - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - - levelLabel, exists := LevelNames[level] - if !exists { - levelLabel = level.String() - } - - // Pad the level name to 8 characters - a.Value = slog.StringValue(levelLabel) // fmt.Sprintf("%8s", levelLabel)) - } - */ - - // Rename the source attributes if they came from Trace/Fatal to the correct names - // so the old values get overwritten - if len(groups) > 0 && groups[0] == "source" { - switch a.Key { - case FileKey: - a.Key = "file" - case LineKey: - a.Key = "line" - case FunctionKey: - a.Key = "function" - default: - break // do nothing - } + // Remove the frame marker attribute flag if it's present + if a.Key == FrameMarker { + return slog.Attr{} } return a diff --git a/internal/logger/tint.go b/internal/logger/tint.go new file mode 100644 index 00000000..ff9ab2d6 --- /dev/null +++ b/internal/logger/tint.go @@ -0,0 +1,35 @@ +package logger + +import ( + "log/slog" + "os" + "time" + + "github.com/fatih/color" + "github.com/lmittmann/tint" + "github.com/mattn/go-isatty" +) + +func NewTinc(w *os.File, addSource bool, level slog.Leveler) (slog.Handler, *slog.LevelVar) { + lvl := new(slog.LevelVar) + lvl.Set(level.Level()) + + opts := tint.Options{ + Level: lvl, + AddSource: addSource, + ReplaceAttr: replaceAttr, + TimeFormat: time.Kitchen, + // TimeFormat: "", + LevelColorsMap: tint.LevelColorsMapping{ + LevelTrace: {Name: "TRACE", Color: color.FgGreen}, + LevelFatal: {Name: "FATAL", Color: color.FgRed}, + slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, + slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, + slog.LevelError: {Name: "ERROR", Color: color.FgRed}, + slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, + }, + NoColor: !isatty.IsTerminal(w.Fd()), + } + + return tint.NewHandler(w, &opts), lvl +} diff --git a/internal/sso/settings.go b/internal/sso/settings.go index 7d4a58ec..04a24535 100644 --- a/internal/sso/settings.go +++ b/internal/sso/settings.go @@ -305,12 +305,6 @@ func (s *Settings) Save(configFile string, overwrite bool) error { // configure our settings using the overrides func (s *Settings) setOverrides(override OverrideSettings) { // Setup Logging - if override.LogLevel != "" { - s.LogLevel = override.LogLevel - } - - log.SetLevelString(s.LogLevel) - if override.LogLines { s.LogLines = true } @@ -319,6 +313,12 @@ func (s *Settings) setOverrides(override OverrideSettings) { log.SetReportCaller(true) } + if override.LogLevel != "" { + s.LogLevel = override.LogLevel + } + + log.SetLevelString(s.LogLevel) + // Other overrides from CLI if override.Browser != "" { s.Browser = override.Browser From 23b3f68c6a557b4b93eea02c1f37afc18e49660c Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Sat, 3 Aug 2024 09:06:38 -0700 Subject: [PATCH 05/10] Use standard tint library and add json - No longer require changes to the tint library. - Add JSON output for server mode in the logger code --- internal/logger/console.go | 20 ++--- internal/logger/init.go | 41 +++++++++++ internal/logger/json.go | 64 ++++++++++++++++ internal/logger/levels.go | 125 ++++++++++++++++++++++++++++++++ internal/logger/logger.go | 48 ++++++------ internal/logger/replace_attr.go | 22 +++++- internal/logger/tint.go | 18 +---- 7 files changed, 283 insertions(+), 55 deletions(-) create mode 100644 internal/logger/init.go create mode 100644 internal/logger/json.go create mode 100644 internal/logger/levels.go diff --git a/internal/logger/console.go b/internal/logger/console.go index d6d84b24..c91e95b8 100644 --- a/internal/logger/console.go +++ b/internal/logger/console.go @@ -20,14 +20,12 @@ package logger import ( "context" + "io" "log/slog" - "os" "runtime" "time" - "github.com/fatih/color" "github.com/lmittmann/tint" - "github.com/mattn/go-isatty" ) const ( @@ -36,25 +34,17 @@ const ( // NewConsole creates a new slog.Handler for the ConsoleHandler, which wraps tint.NewHandler // with some customizations. -func NewConsole(w *os.File, addSource bool, level slog.Leveler) (slog.Handler, *slog.LevelVar) { +func NewConsole(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) { lvl := new(slog.LevelVar) lvl.Set(level.Level()) opts := tint.Options{ Level: lvl, AddSource: addSource, - ReplaceAttr: replaceAttr, + ReplaceAttr: replaceAttrConsole, TimeFormat: time.Kitchen, // TimeFormat: "", - LevelColorsMap: tint.LevelColorsMapping{ - LevelTrace: {Name: "TRACE", Color: color.FgGreen}, - LevelFatal: {Name: "FATAL", Color: color.FgRed}, - slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, - slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, - slog.LevelError: {Name: "ERROR", Color: color.FgRed}, - slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, - }, - NoColor: !isatty.IsTerminal(w.Fd()), + NoColor: true, // let the replaceAttr do the coloring } return NewConsoleHandler(w, &opts), lvl @@ -66,7 +56,7 @@ type ConsoleHandler struct { } // ConsoleHandler is a slog.Handler that wraps tint.Handler -func NewConsoleHandler(w *os.File, opts *tint.Options) slog.Handler { +func NewConsoleHandler(w io.Writer, opts *tint.Options) slog.Handler { return &ConsoleHandler{ tint.NewHandler(w, opts), } diff --git a/internal/logger/init.go b/internal/logger/init.go new file mode 100644 index 00000000..e5deca74 --- /dev/null +++ b/internal/logger/init.go @@ -0,0 +1,41 @@ +package logger + +import ( + "io" + "log/slog" + "os" + + "github.com/mattn/go-isatty" +) + +var logger *Logger + +type NewLoggerFunc func(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) + +// default to the console logger +var CreateLogger NewLoggerFunc = NewConsole + +func SetLoggerFunc(name string) { + var loggers = map[string]NewLoggerFunc{ + "console": NewConsole, + "json": NewJSON, + "tint": NewTinc, + } + var ok bool + CreateLogger, ok = loggers[name] + if !ok { + logger.Fatal("Invalid logger", "name", name) + } +} + +// initialize the default logger to log to stderr and log at the warn level +func init() { + w := os.Stderr + color := isatty.IsTerminal(w.Fd()) + addSource := false + level := slog.LevelWarn + + logger = NewLogger(CreateLogger, w, addSource, level, color) + + slog.SetDefault(logger.Logger) +} diff --git a/internal/logger/json.go b/internal/logger/json.go new file mode 100644 index 00000000..946b5c40 --- /dev/null +++ b/internal/logger/json.go @@ -0,0 +1,64 @@ +package logger + +import ( + "context" + "io" + "log/slog" + "runtime" + + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" + slogjson "github.com/veqryn/slog-json" +) + +func NewJSON(w io.Writer, addSource bool, level slog.Leveler, _ bool) (slog.Handler, *slog.LevelVar) { + lvl := new(slog.LevelVar) + lvl.Set(level.Level()) + + opts := slogjson.HandlerOptions{ + Level: lvl, + AddSource: addSource, + ReplaceAttr: replaceAttrJson, + JSONOptions: json.JoinOptions( + // Options from the json v2 library (these are the defaults) + json.Deterministic(true), + jsontext.AllowDuplicateNames(true), + jsontext.AllowInvalidUTF8(true), + jsontext.EscapeForJS(true), + jsontext.SpaceAfterColon(false), + jsontext.SpaceAfterComma(true), + ), + } + + return slogjson.NewHandler(w, &opts), lvl +} + +type JsonHandler struct { + slog.Handler +} + +func NewJSONHandler(w io.Writer, opts *slogjson.HandlerOptions) slog.Handler { + return &JsonHandler{ + slogjson.NewHandler(w, opts), + } +} + +// Handle is a custom wrapper around the slogjson.Handler.Handle method which fixes up +// the PC value to be the correct caller for the Fatal/Trace methods +func (h *JsonHandler) Handle(ctx context.Context, r slog.Record) error { + var fixStack int64 = 0 + r.Attrs(func(a slog.Attr) bool { + if a.Key == FrameMarker { + fixStack = a.Value.Int64() + return false + } + return true + }) + + if fixStack > 0 { + rn := r.Clone() + rn.PC, _, _, _ = runtime.Caller(int(fixStack)) + return h.Handler.Handle(ctx, rn) + } + return h.Handler.Handle(ctx, r) +} diff --git a/internal/logger/levels.go b/internal/logger/levels.go new file mode 100644 index 00000000..7bffeac0 --- /dev/null +++ b/internal/logger/levels.go @@ -0,0 +1,125 @@ +package logger + +import ( + "log/slog" + + "github.com/fatih/color" +) + +// LevelColors defines the name as displayed to the user and color of a log level. +type LevelColor struct { + // Name is the name of the log level + Name string + // Color is the color of the log level + Color color.Attribute + serialized string + colored bool +} + +// String returns the level name, optionally with color applied. +func (lc *LevelColor) String(colored bool) string { + if len(lc.serialized) == 0 || lc.colored != colored { + if colored { + lc.serialized = color.New(lc.Color).SprintFunc()(lc.Name) + } else { + lc.serialized = lc.Name + } + } + return lc.serialized +} + +// Copy returns a copy of the LevelColor. +func (lc *LevelColor) Copy() *LevelColor { + return &LevelColor{ + Name: lc.Name, + Color: lc.Color, + serialized: lc.serialized, + colored: lc.colored, + } +} + +// LevelColorsMapping is a map of log levels to their colors and is what +// the user defines in their configuration. +type LevelColorsMapping map[slog.Level]LevelColor + +// min returns the mapped minimum index +func (lm *LevelColorsMapping) min() int { + idx := 1000 + for check := range *lm { + if int(check) < idx { + idx = int(check) + } + } + return idx +} + +// size returns the size of the slice needed to store the LevelColors. +func (lm *LevelColorsMapping) size(offset int) int { + maxIdx := -1000 + for check := range *lm { + if int(check) > maxIdx { + maxIdx = int(check) + } + } + return offset + maxIdx + 1 +} + +// offset returns the index offset needed to map negative log levels. +func (lm *LevelColorsMapping) offset() int { + min := lm.min() + if min < 0 { + min = -min + } + return min +} + +// LevelColors returns the LevelColors for the LevelColorsMapping. +func (lm *LevelColorsMapping) LevelColors() *LevelColors { + lcList := make([]*LevelColor, lm.size(lm.offset())) + for idx, lc := range *lm { + lcList[int(idx)+lm.offset()] = lc.Copy() + } + lc := LevelColors{ + levels: lcList, + offset: lm.offset(), + } + return &lc +} + +// LevelColors is our internal representation of the user-defined LevelColorsMapping. +// We map the log levels via their slog.Level to their LevelColor using an offset +// to ensure we can map negative level values to our slice. +type LevelColors struct { + levels []*LevelColor + offset int +} + +// LevelColor returns the LevelColor for the given log level. +// Returns nil indicating if the log level was not found. +func (lc *LevelColors) LevelColor(level slog.Level) *LevelColor { + if len(lc.levels) == 0 { + return nil + } + + idx := int(level.Level()) + lc.offset + if len(lc.levels) < idx { + return &LevelColor{} + } + return lc.levels[idx] +} + +// Copy returns a copy of the LevelColors. +func (lc *LevelColors) Copy() *LevelColors { + if len(lc.levels) == 0 { + return &LevelColors{ + levels: []*LevelColor{}, + } + } + + lcCopy := LevelColors{ + levels: make([]*LevelColor, len(lc.levels)), + offset: lc.offset, + } + copy(lcCopy.levels, lc.levels) + return &lcCopy +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 0549a41a..03859a8d 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -21,18 +21,31 @@ package logger import ( "context" "fmt" + "io" "log/slog" "os" "strings" -) -var logger *Logger + "github.com/fatih/color" +) type Logger struct { *slog.Logger - writer *os.File addSource bool + color bool level *slog.LevelVar + writer io.Writer +} + +func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, color bool) *Logger { + handle, lvl := f(w, addSource, level, color) + return &Logger{ + Logger: slog.New(handle), + addSource: addSource, + color: color, + level: lvl, + writer: w, + } } const ( @@ -45,10 +58,6 @@ var LevelNames = map[slog.Leveler]string{ LevelFatal: "FATAL", } -type NewLoggerFunc func(w *os.File, addSource bool, level slog.Leveler) (slog.Handler, *slog.LevelVar) - -var NewLogger NewLoggerFunc = NewConsole - var LevelStrings = map[string]slog.Leveler{ "TRACE": LevelTrace, "FATAL": LevelFatal, @@ -58,22 +67,13 @@ var LevelStrings = map[string]slog.Leveler{ "DEBUG": slog.LevelDebug, } -// initialize the default logger to log to stderr and log at the warn level -func init() { - w := os.Stderr - addSource := false - level := slog.LevelWarn - // logger = NewTincLogger(w, false, slog.LevelWarn) - handle, lvl := NewLogger(w, addSource, level) - - logger = &Logger{ - Logger: slog.New(handle), - writer: w, - addSource: addSource, - level: lvl, - } - - slog.SetDefault(logger.Logger) +var LevelColorsMap map[slog.Level]LevelColor = map[slog.Level]LevelColor{ + LevelTrace: {Name: "TRACE", Color: color.FgGreen}, + LevelFatal: {Name: "FATAL", Color: color.FgRed}, + slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, + slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, + slog.LevelError: {Name: "ERROR", Color: color.FgRed}, + slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, } // SetLevel sets the log level for the logger @@ -94,7 +94,7 @@ func (l *Logger) SetReportCaller(reportCaller bool) { return // do nothing } l.addSource = reportCaller - handler, _ := NewLogger(l.writer, l.addSource, slog.LevelWarn) + handler, _ := CreateLogger(l.writer, l.addSource, slog.LevelWarn, l.color) logger.Logger = slog.New(handler) slog.SetDefault(logger.Logger) } diff --git a/internal/logger/replace_attr.go b/internal/logger/replace_attr.go index 6efdd52a..2e1bc5f5 100644 --- a/internal/logger/replace_attr.go +++ b/internal/logger/replace_attr.go @@ -2,8 +2,8 @@ package logger import "log/slog" -func replaceAttr(groups []string, a slog.Attr) slog.Attr { - // Remove time from the output for predictable test output. +func replaceAttrConsole(groups []string, a slog.Attr) slog.Attr { + // Remove time from the output on the console if a.Key == slog.TimeKey { return slog.Attr{} } @@ -13,5 +13,23 @@ func replaceAttr(groups []string, a slog.Attr) slog.Attr { return slog.Attr{} } + // Colorize and rename the log level + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelColor, ok := LevelColorsMap[level] + if ok { + a.Value = slog.StringValue(levelColor.String(logger.color)) + } + } + + return a +} + +func replaceAttrJson(groups []string, a slog.Attr) slog.Attr { + // Remove the frame marker attribute flag if it's present + if a.Key == FrameMarker { + return slog.Attr{} + } + return a } diff --git a/internal/logger/tint.go b/internal/logger/tint.go index ff9ab2d6..79f879c6 100644 --- a/internal/logger/tint.go +++ b/internal/logger/tint.go @@ -1,34 +1,24 @@ package logger import ( + "io" "log/slog" - "os" "time" - "github.com/fatih/color" "github.com/lmittmann/tint" - "github.com/mattn/go-isatty" ) -func NewTinc(w *os.File, addSource bool, level slog.Leveler) (slog.Handler, *slog.LevelVar) { +func NewTinc(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) { lvl := new(slog.LevelVar) lvl.Set(level.Level()) opts := tint.Options{ Level: lvl, AddSource: addSource, - ReplaceAttr: replaceAttr, + ReplaceAttr: replaceAttrConsole, TimeFormat: time.Kitchen, // TimeFormat: "", - LevelColorsMap: tint.LevelColorsMapping{ - LevelTrace: {Name: "TRACE", Color: color.FgGreen}, - LevelFatal: {Name: "FATAL", Color: color.FgRed}, - slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, - slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, - slog.LevelError: {Name: "ERROR", Color: color.FgRed}, - slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, - }, - NoColor: !isatty.IsTerminal(w.Fd()), + NoColor: !color, } return tint.NewHandler(w, &opts), lvl From 9bc1bf6282c023cbd1e7a0b3c62d75fa95cf03e8 Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Sat, 3 Aug 2024 09:48:29 -0700 Subject: [PATCH 06/10] reorg code and bump Go to v1.22 --- go.mod | 7 +-- go.sum | 8 +++- internal/logger/custom_levels.go | 63 +++++++++++++++++++++++++++ internal/logger/init.go | 13 ------ internal/logger/logger.go | 74 ++++++++++---------------------- internal/logger/tint.go | 5 +-- 6 files changed, 97 insertions(+), 73 deletions(-) create mode 100644 internal/logger/custom_levels.go diff --git a/go.mod b/go.mod index 4f6fb83f..9ee7b660 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ module github.com/synfinatic/aws-sso-cli -go 1.21 +go 1.22 -// FIXME: temporary development -replace github.com/lmittmann/tint v1.0.5 => github.com/synfinatic/tint v0.0.0-20240801234705-50f64e87e8db +toolchain go1.22.5 require ( github.com/99designs/keyring v1.2.2 @@ -78,7 +77,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 + github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b github.com/lmittmann/tint v1.0.5 + github.com/veqryn/slog-json v0.3.0 golang.org/x/net v0.27.0 ) diff --git a/go.sum b/go.sum index 6053e258..c180b881 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b h1:IM96IiRXFcd7l+mU8Sys9pcggoBLbH/dEgzOESrS8F8= +github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b/go.mod h1:uDEMZSTQMj7V6Lxdrx4ZwchmHEGdICbjuY+GQd7j9LM= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -264,6 +266,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw= +github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -392,8 +396,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/synfinatic/gotable v0.0.3 h1:KI01OLECmOv7laXVNtw6T4kEHue09z9OuQwtNB8D5Mw= github.com/synfinatic/gotable v0.0.3/go.mod h1:kWXD1bxZY6tyu6tWK3CIbGAOrtF7teg0ZQotUMDvoMw= -github.com/synfinatic/tint v0.0.0-20240801234705-50f64e87e8db h1:tLDMrB9hhcOE8EstxtUx3EA/Ag2qQ1hGdkACxLIOyVA= -github.com/synfinatic/tint v0.0.0-20240801234705-50f64e87e8db/go.mod h1:U8412el5hDkno2/xAgVuq4+z5CXJQvRv3Nv81kIqByc= +github.com/veqryn/slog-json v0.3.0 h1:jI2ORtKP1uQss4zmTR2uCpIDw/XnUvVdr5+0vDNl4Gk= +github.com/veqryn/slog-json v0.3.0/go.mod h1:L3fDxxDznYcFB1OwcMv/nziRltHO0/YeD1FkfSFeBIA= github.com/willabides/kongplete v0.2.0 h1:C6wYVn+IPyA8rAGRGLLkuxhhSQTEECX4t8u3gi+fuD0= github.com/willabides/kongplete v0.2.0/go.mod h1:kFVw+PkQsqkV7O4tfIBo6iJ9qY94PJC8sPfMgFG5AdM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/logger/custom_levels.go b/internal/logger/custom_levels.go new file mode 100644 index 00000000..39559bc5 --- /dev/null +++ b/internal/logger/custom_levels.go @@ -0,0 +1,63 @@ +package logger + +import ( + "context" + "log/slog" + "os" + + "github.com/fatih/color" +) + +// Define our custom levels +const ( + LevelTrace = slog.Level(-8) + LevelFatal = slog.Level(12) + StackFrames = 5 // number of stack frames to skip in Handler.Handle +) + +var LevelNames = map[slog.Leveler]string{ + LevelTrace: "TRACE", + LevelFatal: "FATAL", +} + +var LevelStrings = map[string]slog.Leveler{ + "TRACE": LevelTrace, + "FATAL": LevelFatal, + "INFO": slog.LevelInfo, + "WARN": slog.LevelWarn, + "ERROR": slog.LevelError, + "DEBUG": slog.LevelDebug, +} + +var LevelColorsMap map[slog.Level]LevelColor = map[slog.Level]LevelColor{ + LevelTrace: {Name: "TRACE", Color: color.FgGreen}, + LevelFatal: {Name: "FATAL", Color: color.FgRed}, + slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, + slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, + slog.LevelError: {Name: "ERROR", Color: color.FgRed}, + slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, +} + +// Log a message at the Trace level +func (l *Logger) Trace(msg string, args ...interface{}) { + l.logWithSource(LevelTrace, msg, args...) +} + +// Log a message at the Fatal level and exit +func (l *Logger) Fatal(msg string, args ...interface{}) { + l.logWithSource(LevelFatal, msg, args...) + os.Exit(1) +} + +// logWithSource sets the __source attribute so that our Handler knows +// to modify the r.PC value to include the original caller. +func (l *Logger) logWithSource(level slog.Level, msg string, args ...interface{}) { + ctx := context.Background() + var allArgs []interface{} + allArgs = append(allArgs, args...) + + if l.addSource { + allArgs = append(allArgs, slog.Int(FrameMarker, StackFrames)) + } + l.Logger.Log(ctx, level, msg, allArgs...) +} diff --git a/internal/logger/init.go b/internal/logger/init.go index e5deca74..e2ae8481 100644 --- a/internal/logger/init.go +++ b/internal/logger/init.go @@ -15,19 +15,6 @@ type NewLoggerFunc func(w io.Writer, addSource bool, level slog.Leveler, color b // default to the console logger var CreateLogger NewLoggerFunc = NewConsole -func SetLoggerFunc(name string) { - var loggers = map[string]NewLoggerFunc{ - "console": NewConsole, - "json": NewJSON, - "tint": NewTinc, - } - var ok bool - CreateLogger, ok = loggers[name] - if !ok { - logger.Fatal("Invalid logger", "name", name) - } -} - // initialize the default logger to log to stderr and log at the warn level func init() { w := os.Stderr diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 03859a8d..fe9c2bf7 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -19,16 +19,13 @@ package logger */ import ( - "context" "fmt" "io" "log/slog" - "os" "strings" - - "github.com/fatih/color" ) +// Our logger which wraps slog.Logger type Logger struct { *slog.Logger addSource bool @@ -37,6 +34,7 @@ type Logger struct { writer io.Writer } +// NewLoggerFunc creates a new Logger func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, color bool) *Logger { handle, lvl := f(w, addSource, level, color) return &Logger{ @@ -48,32 +46,27 @@ func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, } } -const ( - LevelTrace = slog.Level(-8) - LevelFatal = slog.Level(12) -) - -var LevelNames = map[slog.Leveler]string{ - LevelTrace: "TRACE", - LevelFatal: "FATAL", +// Copy returns a copy of the Logger current Logger +func (l *Logger) Copy() *Logger { + return NewLogger(CreateLogger, l.writer, l.addSource, l.level, l.color) } -var LevelStrings = map[string]slog.Leveler{ - "TRACE": LevelTrace, - "FATAL": LevelFatal, - "INFO": slog.LevelInfo, - "WARN": slog.LevelWarn, - "ERROR": slog.LevelError, - "DEBUG": slog.LevelDebug, -} +// SwitchLogger changes the current logger to the specified type +func SwitchLogger(name string) { + var loggers = map[string]NewLoggerFunc{ + "console": NewConsole, + "json": NewJSON, + "tint": NewTint, + } + var ok bool + CreateLogger, ok = loggers[name] + if !ok { + logger.Fatal("Invalid logger", "name", name) + } -var LevelColorsMap map[slog.Level]LevelColor = map[slog.Level]LevelColor{ - LevelTrace: {Name: "TRACE", Color: color.FgGreen}, - LevelFatal: {Name: "FATAL", Color: color.FgRed}, - slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, - slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, - slog.LevelError: {Name: "ERROR", Color: color.FgRed}, - slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, + // switch the logger + logger = NewLogger(CreateLogger, logger.writer, logger.addSource, logger.level, logger.color) + slog.SetDefault(logger.Logger) } // SetLevel sets the log level for the logger @@ -89,6 +82,8 @@ func (l *Logger) SetLevelString(level string) error { return nil } +// SetReportCaller sets whether to include the source file and line number in the log output +// Doing so will replace the current logger with a new one that has the new setting func (l *Logger) SetReportCaller(reportCaller bool) { if l.addSource == reportCaller { return // do nothing @@ -115,28 +110,3 @@ func GetLogger() *Logger { func SetDefaultLogger(l *Logger) { slog.SetDefault(l.Logger) } - -// Log a message at the Trace level -func (l *Logger) Trace(msg string, args ...interface{}) { - l.logWithSource(LevelTrace, msg, args...) -} - -// Log a message at the Fatal level and exit -func (l *Logger) Fatal(msg string, args ...interface{}) { - l.logWithSource(LevelFatal, msg, args...) - os.Exit(1) -} - -// logWithSource sets the __source attribute so that our Handler knows -// to modify the r.PC value to include the original caller. -func (l *Logger) logWithSource(level slog.Level, msg string, args ...interface{}) { - ctx := context.Background() - var allArgs []interface{} - allArgs = append(allArgs, args...) - - if l.addSource { - // 5 is the number of stack frames to skip in Handler.Handle() - allArgs = append(allArgs, slog.Int(FrameMarker, 5)) - } - l.Logger.Log(ctx, level, msg, allArgs...) -} diff --git a/internal/logger/tint.go b/internal/logger/tint.go index 79f879c6..c01fc63e 100644 --- a/internal/logger/tint.go +++ b/internal/logger/tint.go @@ -8,7 +8,7 @@ import ( "github.com/lmittmann/tint" ) -func NewTinc(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) { +func NewTint(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) { lvl := new(slog.LevelVar) lvl.Set(level.Level()) @@ -17,8 +17,7 @@ func NewTinc(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog. AddSource: addSource, ReplaceAttr: replaceAttrConsole, TimeFormat: time.Kitchen, - // TimeFormat: "", - NoColor: !color, + NoColor: !color, } return tint.NewHandler(w, &opts), lvl From 0cd9724d1d374e825dc3a5a404777e8f677cc316 Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Fri, 9 Aug 2024 16:49:19 -0700 Subject: [PATCH 07/10] switch to custom logger --- cmd/aws-sso/logger.go | 2 +- go.mod | 2 + go.sum | 4 ++ internal/ecs/client/client.go | 2 +- internal/ecs/server/server.go | 2 +- internal/helper/helper.go | 2 +- internal/logger/custom_levels.go | 18 ++++-- internal/logger/init.go | 36 +++++++++++- internal/logger/interface.go | 42 ++++++++++++++ internal/logger/logger.go | 62 ++++++++++---------- internal/logger/replace_attr.go | 11 +++- internal/logger/test/logger.go | 99 ++++++++++++++++++++++++++++++++ internal/predictor/predictor.go | 2 +- internal/sso/cache_test.go | 16 +++--- internal/sso/logger.go | 2 +- internal/storage/storage.go | 2 +- internal/tags/logger.go | 2 +- internal/tags/tags_list_test.go | 8 +-- internal/url/url.go | 2 +- internal/utils/utils.go | 2 +- 20 files changed, 258 insertions(+), 60 deletions(-) create mode 100644 internal/logger/interface.go create mode 100644 internal/logger/test/logger.go diff --git a/cmd/aws-sso/logger.go b/cmd/aws-sso/logger.go index cb70188a..682b1866 100644 --- a/cmd/aws-sso/logger.go +++ b/cmd/aws-sso/logger.go @@ -20,7 +20,7 @@ package main import "github.com/synfinatic/aws-sso-cli/internal/logger" -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/go.mod b/go.mod index 9ee7b660..b4854e08 100644 --- a/go.mod +++ b/go.mod @@ -78,6 +78,7 @@ require ( github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b + github.com/joeshaw/json-lossless v0.0.0-20181204200226-e0cd1ca6349b github.com/lmittmann/tint v1.0.5 github.com/veqryn/slog-json v0.3.0 golang.org/x/net v0.27.0 @@ -93,6 +94,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect github.com/aws/smithy-go v1.20.3 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect diff --git a/go.sum b/go.sum index c180b881..b226c472 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/c-bata/go-prompt v0.2.5 h1:3zg6PecEywxNn0xiqcXHD96fkbxghD+gdB2tbsYfl+Y= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -243,6 +245,8 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joeshaw/json-lossless v0.0.0-20181204200226-e0cd1ca6349b h1:tCtJ08SeJzFbI5UfgNBwvXPHiHK938r9lfdnrBsa1qI= +github.com/joeshaw/json-lossless v0.0.0-20181204200226-e0cd1ca6349b/go.mod h1:bPnXx4rALO5hbpPEp/hvkA5s5MFP5GuIXbLpt5e8QxM= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= diff --git a/internal/ecs/client/client.go b/internal/ecs/client/client.go index 67e30027..2d191b54 100644 --- a/internal/ecs/client/client.go +++ b/internal/ecs/client/client.go @@ -36,7 +36,7 @@ import ( "github.com/synfinatic/aws-sso-cli/internal/storage" ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/ecs/server/server.go b/internal/ecs/server/server.go index 4446ea9a..3dd96938 100644 --- a/internal/ecs/server/server.go +++ b/internal/ecs/server/server.go @@ -32,7 +32,7 @@ import ( "github.com/synfinatic/aws-sso-cli/internal/storage" ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 0b86ab8e..44d142d3 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -31,7 +31,7 @@ import ( "github.com/synfinatic/aws-sso-cli/internal/utils" ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/logger/custom_levels.go b/internal/logger/custom_levels.go index 39559bc5..26399834 100644 --- a/internal/logger/custom_levels.go +++ b/internal/logger/custom_levels.go @@ -40,19 +40,29 @@ var LevelColorsMap map[slog.Level]LevelColor = map[slog.Level]LevelColor{ // Log a message at the Trace level func (l *Logger) Trace(msg string, args ...interface{}) { - l.logWithSource(LevelTrace, msg, args...) + ctx := context.Background() + l.logWithSource(ctx, LevelTrace, msg, args...) +} + +func (l *Logger) TraceContext(ctx context.Context, msg string, args ...interface{}) { + l.logWithSource(ctx, LevelTrace, msg, args...) } // Log a message at the Fatal level and exit func (l *Logger) Fatal(msg string, args ...interface{}) { - l.logWithSource(LevelFatal, msg, args...) + ctx := context.Background() + l.logWithSource(ctx, LevelFatal, msg, args...) + os.Exit(1) +} + +func (l *Logger) FatalContext(ctx context.Context, msg string, args ...interface{}) { + l.logWithSource(ctx, LevelFatal, msg, args...) os.Exit(1) } // logWithSource sets the __source attribute so that our Handler knows // to modify the r.PC value to include the original caller. -func (l *Logger) logWithSource(level slog.Level, msg string, args ...interface{}) { - ctx := context.Background() +func (l *Logger) logWithSource(ctx context.Context, level slog.Level, msg string, args ...interface{}) { var allArgs []interface{} allArgs = append(allArgs, args...) diff --git a/internal/logger/init.go b/internal/logger/init.go index e2ae8481..62e07410 100644 --- a/internal/logger/init.go +++ b/internal/logger/init.go @@ -8,12 +8,12 @@ import ( "github.com/mattn/go-isatty" ) -var logger *Logger +var logger CustomLogger type NewLoggerFunc func(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) // default to the console logger -var CreateLogger NewLoggerFunc = NewConsole +var CreateLogger NewLoggerFunc = NewJSON // NewConsole // initialize the default logger to log to stderr and log at the warn level func init() { @@ -24,5 +24,35 @@ func init() { logger = NewLogger(CreateLogger, w, addSource, level, color) - slog.SetDefault(logger.Logger) + slog.SetDefault(logger.GetLogger()) +} + +func SetLogger(l CustomLogger) { + logger = l +} + +func GetLogger() CustomLogger { + return logger +} + +func SetDefaultLogger(l CustomLogger) { + slog.SetDefault(l.GetLogger()) +} + +// SwitchLogger changes the current logger to the specified type +func SwitchLogger(name string) { + var loggers = map[string]NewLoggerFunc{ + "console": NewConsole, + "json": NewJSON, + "tint": NewTint, + } + var ok bool + CreateLogger, ok = loggers[name] + if !ok { + logger.Fatal("Invalid logger", "name", name) + } + + // switch the logger + logger = NewLogger(CreateLogger, logger.Writer(), logger.AddSource(), logger.Level(), logger.Color()) + slog.SetDefault(logger.GetLogger()) } diff --git a/internal/logger/interface.go b/internal/logger/interface.go new file mode 100644 index 00000000..c393ef22 --- /dev/null +++ b/internal/logger/interface.go @@ -0,0 +1,42 @@ +package logger + +import ( + "context" + "io" + "log/slog" +) + +type CustomLogger interface { + // slog.Logger methods + Debug(msg string, args ...any) + DebugContext(ctx context.Context, msg string, args ...any) + Enabled(ctx context.Context, level slog.Level) bool + Error(msg string, args ...any) + ErrorContext(ctx context.Context, msg string, args ...any) + Handler() slog.Handler + Info(msg string, args ...any) + InfoContext(ctx context.Context, msg string, args ...any) + Log(ctx context.Context, level slog.Level, msg string, args ...any) + LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) + Warn(msg string, args ...any) + WarnContext(ctx context.Context, msg string, args ...any) + With(args ...any) *slog.Logger + WithGroup(name string) *slog.Logger + // custom methods + Copy() CustomLogger + // Clone(f NewLoggerFunc, w io.Writer) *CustomLogger + GetLevel() slog.Leveler + GetLogger() *slog.Logger + SetLevel(level slog.Leveler) + SetLevelString(level string) error + SetLogger(logger *slog.Logger) + SetReportCaller(reportCaller bool) + Trace(msg string, args ...any) + TraceContext(ctx context.Context, msg string, args ...any) + Fatal(msg string, args ...any) + FatalContext(ctx context.Context, msg string, args ...any) + Writer() io.Writer + AddSource() bool + Level() *slog.LevelVar + Color() bool +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index fe9c2bf7..f21d400c 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -25,7 +25,7 @@ import ( "strings" ) -// Our logger which wraps slog.Logger +// Our logger which wraps slog.Logger and impliments CustomLogger type Logger struct { *slog.Logger addSource bool @@ -47,26 +47,39 @@ func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, } // Copy returns a copy of the Logger current Logger -func (l *Logger) Copy() *Logger { +func (l *Logger) Copy() CustomLogger { return NewLogger(CreateLogger, l.writer, l.addSource, l.level, l.color) } -// SwitchLogger changes the current logger to the specified type -func SwitchLogger(name string) { - var loggers = map[string]NewLoggerFunc{ - "console": NewConsole, - "json": NewJSON, - "tint": NewTint, - } - var ok bool - CreateLogger, ok = loggers[name] - if !ok { - logger.Fatal("Invalid logger", "name", name) - } +func (l *Logger) Writer() io.Writer { + return l.writer +} + +func (l *Logger) AddSource() bool { + return l.addSource +} + +func (l *Logger) Level() *slog.LevelVar { + return l.level +} + +func (l *Logger) Color() bool { + return l.color +} + +/* +// Clone returns a clone of the current Logger with a new Logging function +func (l *Logger) Clone(f NewLoggerFunc, w io.Writer) *Logger { + return NewLogger(f, w, l.addSource, l.level, l.color) +} +*/ - // switch the logger - logger = NewLogger(CreateLogger, logger.writer, logger.addSource, logger.level, logger.color) - slog.SetDefault(logger.Logger) +func (l *Logger) GetLogger() *slog.Logger { + return l.Logger +} + +func (l *Logger) SetLogger(logger *slog.Logger) { + l.Logger = logger } // SetLevel sets the log level for the logger @@ -90,23 +103,10 @@ func (l *Logger) SetReportCaller(reportCaller bool) { } l.addSource = reportCaller handler, _ := CreateLogger(l.writer, l.addSource, slog.LevelWarn, l.color) - logger.Logger = slog.New(handler) - slog.SetDefault(logger.Logger) + logger.SetLogger(slog.New(handler)) } // GetLevel returns the current log level func (l *Logger) GetLevel() slog.Leveler { return slog.Level(l.level.Level()) } - -func SetLogger(l *Logger) { - logger = l -} - -func GetLogger() *Logger { - return logger -} - -func SetDefaultLogger(l *Logger) { - slog.SetDefault(l.Logger) -} diff --git a/internal/logger/replace_attr.go b/internal/logger/replace_attr.go index 2e1bc5f5..e29b4ca3 100644 --- a/internal/logger/replace_attr.go +++ b/internal/logger/replace_attr.go @@ -18,7 +18,7 @@ func replaceAttrConsole(groups []string, a slog.Attr) slog.Attr { level := a.Value.Any().(slog.Level) levelColor, ok := LevelColorsMap[level] if ok { - a.Value = slog.StringValue(levelColor.String(logger.color)) + a.Value = slog.StringValue(levelColor.String(logger.Color())) } } @@ -31,5 +31,14 @@ func replaceAttrJson(groups []string, a slog.Attr) slog.Attr { return slog.Attr{} } + // Rename the log level + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelColor, ok := LevelColorsMap[level] + if ok { + a.Value = slog.StringValue(levelColor.String(false)) + } + } + return a } diff --git a/internal/logger/test/logger.go b/internal/logger/test/logger.go new file mode 100644 index 00000000..6087300b --- /dev/null +++ b/internal/logger/test/logger.go @@ -0,0 +1,99 @@ +package test + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "reflect" + "regexp" + "strings" + + "github.com/davecgh/go-spew/spew" + lossless "github.com/joeshaw/json-lossless" + "github.com/synfinatic/aws-sso-cli/internal/logger" +) + +// TeslLogger impliments logger.CustomLogger +type TestLogger struct { + *logger.Logger + r *io.PipeReader + w *io.PipeWriter +} + +type LogMessages []LogMessage + +type LogMessage struct { + Level string `json:"level"` + Message string `json:"msg"` + Time string `json:"time"` + Source FileSource `json:"source"` + lossless.JSON `json:"-"` +} + +type FileSource struct { + File string `json:"file"` + Function string `json:"function"` + Line int `json:"line"` +} + +func NewTestLogger(level string) *TestLogger { + reader, writer := io.Pipe() + + l := logger.LevelStrings[strings.ToUpper(level)].Level() + + return &TestLogger{ + Logger: logger.NewLogger(logger.NewJSON, writer, false, l, false), + r: reader, + w: writer, + } +} + +func (tl *TestLogger) Close() { + tl.w.Close() + tl.r.Close() +} + +func (tl *TestLogger) GetLast(level slog.Level) (*LogMessage, error) { + messages := LogMessages{} + decoder := json.NewDecoder(tl.r) + if err := decoder.Decode(&messages); err != nil { + return nil, err + } + for i := len(messages) - 1; i >= 0; i-- { + msg := messages[i] + if msg.Level == level.String() { + return &msg, nil + } + } + + return nil, errors.New(fmt.Sprintf("No %s messages found", level.String())) +} + +func (tl *TestLogger) CheckLastEqual(level slog.Level, field, value string) (bool, error) { + msg, err := tl.GetLast(level) + if err != nil { + return false, err + } + + fmt.Printf("CheckLastEqual decoded msg: %s", spew.Sdump(msg)) + if reflect.ValueOf(msg).FieldByName(field).String() == value { + return true, nil + } + return false, nil +} + +func (tl *TestLogger) CheckLastMatch(level slog.Level, field string, match *regexp.Regexp) (bool, error) { + msg, err := tl.GetLast(level) + if err != nil { + return false, err + } + + fmt.Printf("CheckLastMatch decoded msg: %s", spew.Sdump(msg)) + value := reflect.ValueOf(msg).FieldByName(field).String() + if match.MatchString(value) { + return true, nil + } + return false, nil +} diff --git a/internal/predictor/predictor.go b/internal/predictor/predictor.go index 960607f7..c934d306 100644 --- a/internal/predictor/predictor.go +++ b/internal/predictor/predictor.go @@ -30,7 +30,7 @@ import ( "github.com/synfinatic/aws-sso-cli/internal/utils" ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/sso/cache_test.go b/internal/sso/cache_test.go index bc5718c0..600d217d 100644 --- a/internal/sso/cache_test.go +++ b/internal/sso/cache_test.go @@ -20,16 +20,18 @@ package sso import ( "fmt" + "io" + "log/slog" "os" "testing" "time" // "github.com/davecgh/go-spew/spew" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/synfinatic/aws-sso-cli/internal/logger" + "github.com/synfinatic/aws-sso-cli/internal/logger/test" ) const ( @@ -265,11 +267,11 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { c.SSO["Default"].Roles.Accounts[123456789012].Roles["Foo"].Tags) // setup logger for tests - logrusLogger, hook := test.NewNullLogger() - logrusLogger.SetLevel(logrus.DebugLevel) - oldLog := log - log = logger.NewLogger(logrusLogger) - defer func() { log = oldLog }() + oldLogger := log.Copy() + testLogger := test.NewTestLogger("DEBUG") + logger.SetLogger(testLogger) + + defer func() { logger.SetLogger(oldLogger) }() // remove one because of HistoryMinutes expires c = suite.setupDeleteOldHistory() diff --git a/internal/sso/logger.go b/internal/sso/logger.go index 7017d404..13dbcbde 100644 --- a/internal/sso/logger.go +++ b/internal/sso/logger.go @@ -20,7 +20,7 @@ package sso import "github.com/synfinatic/aws-sso-cli/internal/logger" -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 4818e47b..6e704c18 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -30,7 +30,7 @@ import ( "github.com/synfinatic/gotable" ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/tags/logger.go b/internal/tags/logger.go index e0036e5d..e7fea36c 100644 --- a/internal/tags/logger.go +++ b/internal/tags/logger.go @@ -20,7 +20,7 @@ package tags import "github.com/synfinatic/aws-sso-cli/internal/logger" -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/tags/tags_list_test.go b/internal/tags/tags_list_test.go index ddc66553..514252e2 100644 --- a/internal/tags/tags_list_test.go +++ b/internal/tags/tags_list_test.go @@ -26,12 +26,12 @@ const ( func TestTagsListSuite(t *testing.T) { info, err := os.Stat(TEST_TAGS_LIST_FILE) if err != nil { - log.WithError(err).Fatalf("os.Stat %s", TEST_TAGS_LIST_FILE) + log.Fatal("os.Stat", "file", TEST_TAGS_LIST_FILE, "error", err) } file, err := os.Open(TEST_TAGS_LIST_FILE) if err != nil { - log.WithError(err).Fatalf("os.Open %s", TEST_TAGS_LIST_FILE) + log.Fatal("os.Open", "file", TEST_TAGS_LIST_FILE, "error", err) } defer file.Close() @@ -39,13 +39,13 @@ func TestTagsListSuite(t *testing.T) { buf := make([]byte, info.Size()) _, err = file.Read(buf) if err != nil { - log.WithError(err).Fatalf("Error reading %d bytes from %s", info.Size(), TEST_TAGS_LIST_FILE) + log.Fatal("file.Read", "file", TEST_TAGS_LIST_FILE, "error", err, "byteLen", info.Size()) } s := &TagsListTestSuite{} err = yaml.Unmarshal(buf, &s.File) if err != nil { - log.WithError(err).Fatalf("Failed parsing %s", TEST_TAGS_LIST_FILE) + log.Fatal("yaml.Unmarshal", "file", TEST_TAGS_LIST_FILE, "error", err) } suite.Run(t, s) diff --git a/internal/url/url.go b/internal/url/url.go index 279210d1..1631a50e 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -33,7 +33,7 @@ import ( // default opener ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() diff --git a/internal/utils/utils.go b/internal/utils/utils.go index d2f29e45..ae32acf1 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -31,7 +31,7 @@ import ( "github.com/synfinatic/aws-sso-cli/internal/logger" ) -var log *logger.Logger +var log logger.CustomLogger func init() { log = logger.GetLogger() From 580c447c3cbddeaad9a3687ddca4504e4db3e0ec Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Tue, 13 Aug 2024 07:48:51 -0700 Subject: [PATCH 08/10] continue work on unittests --- internal/logger/console.go | 23 +++++ internal/logger/custom_levels.go | 4 +- internal/logger/json.go | 20 ++++- internal/logger/logger.go | 67 ++++++++++++++- internal/logger/replace_attr.go | 44 ---------- internal/logger/test/interface.go2 | 16 ++++ internal/logger/test/logger.go | 134 ++++++++++++++++++++++++----- internal/sso/cache_test.go | 67 ++++++++------- internal/sso/role_tags_test.go | 8 +- internal/sso/roles_test.go | 3 - internal/sso/settings_test.go | 2 +- internal/storage/keyring_test.go | 22 ++--- 12 files changed, 288 insertions(+), 122 deletions(-) delete mode 100644 internal/logger/replace_attr.go create mode 100644 internal/logger/test/interface.go2 diff --git a/internal/logger/console.go b/internal/logger/console.go index c91e95b8..efa9b302 100644 --- a/internal/logger/console.go +++ b/internal/logger/console.go @@ -81,3 +81,26 @@ func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error { } return h.Handler.Handle(ctx, r) } + +func replaceAttrConsole(groups []string, a slog.Attr) slog.Attr { + // Remove time from the output on the console + if a.Key == slog.TimeKey { + return slog.Attr{} + } + + // Remove the frame marker attribute flag if it's present + if a.Key == FrameMarker { + return slog.Attr{} + } + + // Colorize and rename the log level + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelColor, ok := LevelColorsMap[level] + if ok { + a.Value = slog.StringValue(levelColor.String(logger.Color())) + } + } + + return a +} diff --git a/internal/logger/custom_levels.go b/internal/logger/custom_levels.go index 26399834..a608e7a9 100644 --- a/internal/logger/custom_levels.go +++ b/internal/logger/custom_levels.go @@ -20,7 +20,7 @@ var LevelNames = map[slog.Leveler]string{ LevelFatal: "FATAL", } -var LevelStrings = map[string]slog.Leveler{ +var LevelStrings = map[string]slog.Level{ "TRACE": LevelTrace, "FATAL": LevelFatal, "INFO": slog.LevelInfo, @@ -69,5 +69,5 @@ func (l *Logger) logWithSource(ctx context.Context, level slog.Level, msg string if l.addSource { allArgs = append(allArgs, slog.Int(FrameMarker, StackFrames)) } - l.Logger.Log(ctx, level, msg, allArgs...) + l.logger.Log(ctx, level, msg, allArgs...) } diff --git a/internal/logger/json.go b/internal/logger/json.go index 946b5c40..f4f7fea3 100644 --- a/internal/logger/json.go +++ b/internal/logger/json.go @@ -30,7 +30,7 @@ func NewJSON(w io.Writer, addSource bool, level slog.Leveler, _ bool) (slog.Hand ), } - return slogjson.NewHandler(w, &opts), lvl + return NewJSONHandler(w, &opts), lvl } type JsonHandler struct { @@ -62,3 +62,21 @@ func (h *JsonHandler) Handle(ctx context.Context, r slog.Record) error { } return h.Handler.Handle(ctx, r) } + +func replaceAttrJson(groups []string, a slog.Attr) slog.Attr { + // Remove the frame marker attribute flag if it's present + if a.Key == FrameMarker { + return slog.Attr{} + } + + // Rename the log level + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + levelColor, ok := LevelColorsMap[level] + if ok { + a.Value = slog.StringValue(levelColor.String(false)) + } + } + + return a +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go index f21d400c..05d1d053 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -19,6 +19,7 @@ package logger */ import ( + "context" "fmt" "io" "log/slog" @@ -27,7 +28,7 @@ import ( // Our logger which wraps slog.Logger and impliments CustomLogger type Logger struct { - *slog.Logger + logger *slog.Logger addSource bool color bool level *slog.LevelVar @@ -38,7 +39,7 @@ type Logger struct { func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, color bool) *Logger { handle, lvl := f(w, addSource, level, color) return &Logger{ - Logger: slog.New(handle), + logger: slog.New(handle), addSource: addSource, color: color, level: lvl, @@ -46,6 +47,60 @@ func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, } } +// Debug logs a message at the debug level +func (l *Logger) Debug(msg string, args ...any) { + l.logger.Debug(msg, args...) +} + +// DebugContext logs a message at the debug level with context +func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any) { + l.logger.DebugContext(ctx, msg, args...) +} + +func (l *Logger) Error(msg string, args ...any) { + l.logger.Error(msg, args...) +} + +func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) { + l.logger.ErrorContext(ctx, msg, args...) +} + +func (l *Logger) Info(msg string, args ...any) { + l.logger.Info(msg, args...) +} + +func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) { + l.logger.InfoContext(ctx, msg, args...) +} + +func (l *Logger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { + l.logger.Log(ctx, level, msg, args...) +} + +func (l *Logger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { + l.logger.LogAttrs(ctx, level, msg, attrs...) +} + +func (l *Logger) Warn(msg string, args ...any) { + l.logger.Warn(msg, args...) +} + +func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any) { + l.logger.WarnContext(ctx, msg, args...) +} + +func (l *Logger) Handler() slog.Handler { + return l.logger.Handler() +} + +func (l *Logger) With(args ...any) *slog.Logger { + return l.logger.With(args...) +} + +func (l *Logger) WithGroup(name string) *slog.Logger { + return l.logger.WithGroup(name) +} + // Copy returns a copy of the Logger current Logger func (l *Logger) Copy() CustomLogger { return NewLogger(CreateLogger, l.writer, l.addSource, l.level, l.color) @@ -67,6 +122,10 @@ func (l *Logger) Color() bool { return l.color } +func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool { + return l.logger.Enabled(ctx, level) +} + /* // Clone returns a clone of the current Logger with a new Logging function func (l *Logger) Clone(f NewLoggerFunc, w io.Writer) *Logger { @@ -75,11 +134,11 @@ func (l *Logger) Clone(f NewLoggerFunc, w io.Writer) *Logger { */ func (l *Logger) GetLogger() *slog.Logger { - return l.Logger + return l.logger } func (l *Logger) SetLogger(logger *slog.Logger) { - l.Logger = logger + l.logger = logger } // SetLevel sets the log level for the logger diff --git a/internal/logger/replace_attr.go b/internal/logger/replace_attr.go deleted file mode 100644 index e29b4ca3..00000000 --- a/internal/logger/replace_attr.go +++ /dev/null @@ -1,44 +0,0 @@ -package logger - -import "log/slog" - -func replaceAttrConsole(groups []string, a slog.Attr) slog.Attr { - // Remove time from the output on the console - if a.Key == slog.TimeKey { - return slog.Attr{} - } - - // Remove the frame marker attribute flag if it's present - if a.Key == FrameMarker { - return slog.Attr{} - } - - // Colorize and rename the log level - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - levelColor, ok := LevelColorsMap[level] - if ok { - a.Value = slog.StringValue(levelColor.String(logger.Color())) - } - } - - return a -} - -func replaceAttrJson(groups []string, a slog.Attr) slog.Attr { - // Remove the frame marker attribute flag if it's present - if a.Key == FrameMarker { - return slog.Attr{} - } - - // Rename the log level - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - levelColor, ok := LevelColorsMap[level] - if ok { - a.Value = slog.StringValue(levelColor.String(false)) - } - } - - return a -} diff --git a/internal/logger/test/interface.go2 b/internal/logger/test/interface.go2 new file mode 100644 index 00000000..70086f66 --- /dev/null +++ b/internal/logger/test/interface.go2 @@ -0,0 +1,16 @@ +/* +package test + +import ( + "github.com/synfinatic/aws-sso-cli/internal/logger" +) + +func (tl *TestLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { + tl.Logger.Log(ctx, level, msg, args...) +} + +// TestLogger impliments logger.CustomLogger +func (tl *TestLogger) Debug(msg string, args ...any) { + tl.Logger.Debug(msg, args...) +} +*/ \ No newline at end of file diff --git a/internal/logger/test/logger.go b/internal/logger/test/logger.go index 6087300b..5df2b911 100644 --- a/internal/logger/test/logger.go +++ b/internal/logger/test/logger.go @@ -1,14 +1,17 @@ package test import ( + "bufio" "encoding/json" "errors" "fmt" "io" "log/slog" + "os" "reflect" "regexp" "strings" + "sync" "github.com/davecgh/go-spew/spew" lossless "github.com/joeshaw/json-lossless" @@ -18,14 +21,19 @@ import ( // TeslLogger impliments logger.CustomLogger type TestLogger struct { *logger.Logger - r *io.PipeReader - w *io.PipeWriter + r *io.PipeReader + w *io.PipeWriter + rch chan []byte + messages LogMessages + close bool + mutex sync.Mutex } type LogMessages []LogMessage type LogMessage struct { - Level string `json:"level"` + LevelStr string `json:"level"` + Level slog.Level Message string `json:"msg"` Time string `json:"time"` Source FileSource `json:"source"` @@ -43,49 +51,131 @@ func NewTestLogger(level string) *TestLogger { l := logger.LevelStrings[strings.ToUpper(level)].Level() - return &TestLogger{ - Logger: logger.NewLogger(logger.NewJSON, writer, false, l, false), - r: reader, - w: writer, + tl := TestLogger{ + Logger: logger.NewLogger(logger.NewJSON, writer, false, l, false), + w: writer, + r: reader, + messages: LogMessages{}, + close: false, + mutex: sync.Mutex{}, } + + // start a goroutine to read from the pipe and decode the log messages + go func() { + r := bufio.NewReader(tl.r) + + for !tl.close { + line, err := r.ReadString('\n') + if err != nil { + if err == io.EOF { + break + } else { + fmt.Fprintf(os.Stderr, "unable to read log message: %s", err.Error()) + break + } + } + msg := LogMessage{} + if err := json.Unmarshal([]byte(line), &msg); err != nil { + panic(fmt.Sprintf("unable to decode log message: %s", err.Error())) + } + + tl.mutex.Lock() + tl.messages = append(tl.messages, msg) + tl.mutex.Unlock() + } + }() + + return &tl } func (tl *TestLogger) Close() { + tl.close = true tl.w.Close() tl.r.Close() } -func (tl *TestLogger) GetLast(level slog.Level) (*LogMessage, error) { - messages := LogMessages{} - decoder := json.NewDecoder(tl.r) - if err := decoder.Decode(&messages); err != nil { - return nil, err +func (tl *TestLogger) ResetBuffer() { + tl.mutex.Lock() + defer tl.mutex.Unlock() + tl.messages = LogMessages{} +} + +func (tl *TestLogger) GetLast(msg *LogMessage) error { + if len(tl.messages) > 0 { + last := len(tl.messages) - 1 + msg.Level = tl.messages[last].Level + msg.LevelStr = tl.messages[last].LevelStr + msg.Message = tl.messages[last].Message + msg.Time = tl.messages[last].Time + msg.Source = tl.messages[last].Source + return nil } - for i := len(messages) - 1; i >= 0; i-- { - msg := messages[i] - if msg.Level == level.String() { - return &msg, nil + + return errors.New("no log messages found") +} + +// Returns the last log message of the given level +func (tl *TestLogger) GetLastLevel(level slog.Level, msg *LogMessage) error { + for i := len(tl.messages) - 1; i >= 0; i-- { + if tl.messages[i].Level == level { + msg.Level = tl.messages[i].Level + msg.LevelStr = tl.messages[i].LevelStr + msg.Message = tl.messages[i].Message + msg.Time = tl.messages[i].Time + msg.Source = tl.messages[i].Source + return nil } } - return nil, errors.New(fmt.Sprintf("No %s messages found", level.String())) + return fmt.Errorf("no log message found for level %s", level.String()) +} + +func (tl *TestLogger) CheckLastLevelEqual(level slog.Level, field, value string) (bool, error) { + msg := LogMessage{} + err := tl.GetLastLevel(level, &msg) + if err != nil { + return false, err + } + + fmt.Printf("CheckLastLevelEqual decoded msg: %s", spew.Sdump(msg)) + if reflect.ValueOf(msg).FieldByName(field).String() == value { + return true, nil + } + return false, nil +} + +func (tl *TestLogger) CheckLastLevelMatch(level slog.Level, field string, match *regexp.Regexp) (bool, error) { + msg := LogMessage{} + err := tl.GetLastLevel(level, &msg) + if err != nil { + return false, err + } + + fmt.Printf("CheckLastLevelMatch decoded msg: %s", spew.Sdump(msg)) + value := reflect.ValueOf(msg).FieldByName(field).String() + if match.MatchString(value) { + return true, nil + } + return false, nil } -func (tl *TestLogger) CheckLastEqual(level slog.Level, field, value string) (bool, error) { - msg, err := tl.GetLast(level) +func (tl *TestLogger) CheckLastEqual(field, value string) (bool, error) { + msg := LogMessage{} + err := tl.GetLast(&msg) if err != nil { return false, err } - fmt.Printf("CheckLastEqual decoded msg: %s", spew.Sdump(msg)) + fmt.Printf("CheckLast decoded msg: %s", spew.Sdump(msg)) if reflect.ValueOf(msg).FieldByName(field).String() == value { return true, nil } return false, nil } -func (tl *TestLogger) CheckLastMatch(level slog.Level, field string, match *regexp.Regexp) (bool, error) { - msg, err := tl.GetLast(level) +func (tl *TestLogger) CheckLastMatch(field string, match *regexp.Regexp) (bool, error) { + msg := LogMessage{} + err := tl.GetLast(&msg) if err != nil { return false, err } diff --git a/internal/sso/cache_test.go b/internal/sso/cache_test.go index 600d217d..75743de9 100644 --- a/internal/sso/cache_test.go +++ b/internal/sso/cache_test.go @@ -20,7 +20,6 @@ package sso import ( "fmt" - "io" "log/slog" "os" "testing" @@ -28,10 +27,10 @@ import ( // "github.com/davecgh/go-spew/spew" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/synfinatic/aws-sso-cli/internal/logger" - "github.com/synfinatic/aws-sso-cli/internal/logger/test" + testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" ) const ( @@ -268,52 +267,56 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { // setup logger for tests oldLogger := log.Copy() - testLogger := test.NewTestLogger("DEBUG") - logger.SetLogger(testLogger) + tLogger := testlogger.NewTestLogger("DEBUG") + log = tLogger - defer func() { logger.SetLogger(oldLogger) }() + defer func() { log = oldLogger }() // remove one because of HistoryMinutes expires c = suite.setupDeleteOldHistory() c.settings.HistoryMinutes = 1 c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "Removed expired history role") + + msg := testlogger.LogMessage{} + tLogger.RefreshBuffer() + assert.NoError(t, tLogger.GetLast(&msg)) + assert.NotEmpty(t, msg.Message) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, "Removed expired history role", msg.Message) assert.Equal(t, []string{"arn:aws:iam::123456789012:role/Test"}, c.GetSSO().History) c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam:") c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "Unable to parse History ARN") - hook.Reset() + tLogger.RefreshBuffer() + assert.NoError(t, tLogger.GetLast(&msg)) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, "Unable to parse History ARN", msg.Message) c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/NoHistoryTag") c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "but no role by that name") - hook.Reset() + tLogger.RefreshBuffer() + assert.NotNil(t, tLogger.GetLast(&msg)) + assert.Equal(t, logrus.DebugLevel, msg.Level) + assert.Contains(t, "but no role by that name", msg.Message) c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::1234567890:role/NoHistoryTag") c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "but no account by that name") - hook.Reset() + tLogger.RefreshBuffer() + assert.NotNil(t, tLogger.GetLast(&msg)) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, "but no account by that name", msg.Message) c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/NoHistoryTag") c.GetSSO().Roles.Accounts[123456789012].Roles["NoHistoryTag"] = &AWSRole{} c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "is in history list without a History tag") - hook.Reset() + tLogger.RefreshBuffer() + assert.NotNil(t, tLogger.GetLast(&msg)) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, "is in history list without a History tag", msg.Message) c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/MissingHistoryTag") @@ -323,9 +326,10 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { }, } c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "Too few fields for") + tLogger.RefreshBuffer() + assert.NotNil(t, tLogger.GetLast(&msg)) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, "Too few fields for", msg.Message) c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/MissingHistoryTag") @@ -335,9 +339,10 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { }, } c.deleteOldHistory() - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.DebugLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "Unable to parse") + tLogger.RefreshBuffer() + assert.NotNil(t, tLogger.GetLast(&msg)) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, "Unable to parse", msg.Message) } func (suite *CacheTestSuite) TestExpired() { diff --git a/internal/sso/role_tags_test.go b/internal/sso/role_tags_test.go index 9a533734..72ca395f 100644 --- a/internal/sso/role_tags_test.go +++ b/internal/sso/role_tags_test.go @@ -55,12 +55,12 @@ const ( func TestRoleTagsTestSuite(t *testing.T) { info, err := os.Stat(TEST_ROLE_TAGS_FILE) if err != nil { - log.WithError(err).Fatalf("os.Stat %s", TEST_ROLE_TAGS_FILE) + log.Fatal("os.Stat failure", "file", TEST_ROLE_TAGS_FILE, "error", err.Error()) } file, err := os.Open(TEST_ROLE_TAGS_FILE) if err != nil { - log.WithError(err).Fatalf("os.Open %s", TEST_ROLE_TAGS_FILE) + log.Fatal("os.Open failure", "file", TEST_ROLE_TAGS_FILE, "error", err.Error()) } defer file.Close() @@ -68,13 +68,13 @@ func TestRoleTagsTestSuite(t *testing.T) { buf := make([]byte, info.Size()) _, err = file.Read(buf) if err != nil { - log.WithError(err).Fatalf("Error reading %d bytes from %s", info.Size(), TEST_ROLE_TAGS_FILE) + log.Fatal("Error reading file", "bytes", info.Size(), "file", TEST_ROLE_TAGS_FILE, "error", err.Error()) } s := &RoleTagsTestSuite{} err = yaml.Unmarshal(buf, &s.File) if err != nil { - log.WithError(err).Fatalf("Failed parsing %s", TEST_ROLE_TAGS_FILE) + log.Fatal("Failed parsing", "file", TEST_ROLE_TAGS_FILE, "error", err.Error()) } suite.Run(t, s) diff --git a/internal/sso/roles_test.go b/internal/sso/roles_test.go index c2abeefa..a4543bed 100644 --- a/internal/sso/roles_test.go +++ b/internal/sso/roles_test.go @@ -415,9 +415,6 @@ func TestAWSRoleFlatHasPrefix(t *testing.T) { } for k, v := range valid { - if k == "Via" { - log.Errorf("%s = %s", k, v) - } ret, err := f.HasPrefix(k, v) assert.NoError(t, err) assert.True(t, ret) diff --git a/internal/sso/settings_test.go b/internal/sso/settings_test.go index 1df47975..a0a8cf00 100644 --- a/internal/sso/settings_test.go +++ b/internal/sso/settings_test.go @@ -329,7 +329,7 @@ func (suite *SettingsTestSuite) TestSetOverrides() { s.setOverrides(overrides) assert.Equal(t, logrus.DebugLevel, log.Level) - assert.True(t, log.ReportCaller) + // assert.True(t, log.ReportCaller) assert.Equal(t, "my-browser", s.Browser) assert.Equal(t, "hello", s.DefaultSSO) assert.Equal(t, 10, s.Threads) diff --git a/internal/storage/keyring_test.go b/internal/storage/keyring_test.go index 85bbc6c9..9c1196ef 100644 --- a/internal/storage/keyring_test.go +++ b/internal/storage/keyring_test.go @@ -21,6 +21,7 @@ package storage import ( "encoding/json" "fmt" + "log/slog" "os" "path" "strings" @@ -28,11 +29,10 @@ import ( "time" "github.com/99designs/keyring" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/synfinatic/aws-sso-cli/internal/logger" + testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" ) type KeyringSuite struct { @@ -494,11 +494,10 @@ func TestSplitCredentials(t *testing.T) { assert.NoError(t, err) // setup logger for testing - logrusLogger, hook := test.NewNullLogger() - logrusLogger.SetLevel(logrus.DebugLevel) - oldLog := log - log = logger.NewLogger(logrusLogger) - defer func() { log = oldLog }() + oldLogger := log.Copy() + tLogger := testlogger.NewTestLogger("DEBUG") + logger.SetLogger(tLogger) + defer func() { logger.SetLogger(oldLogger) }() defer func() { os.RemoveAll(d) @@ -578,7 +577,10 @@ func TestSplitCredentials(t *testing.T) { // but OpenKeyring is fine, just returns a warning _, err = OpenKeyring(c) assert.NoError(t, err) - assert.NotNil(t, hook.LastEntry()) - assert.Equal(t, logrus.WarnLevel, hook.LastEntry().Level) - assert.Contains(t, hook.LastEntry().Message, "unable to fetch") + + msg := testlogger.LogMessage{} + tLogger.RefreshBuffer() + assert.NoError(t, tLogger.GetLast(&msg)) + assert.Equal(t, slog.LevelWarn, msg.Level) + assert.Contains(t, "unable to fetch", msg.Message) } From d3d0a3719b6dd2afe93fe313b06544ec63395fdc Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Wed, 14 Aug 2024 21:36:38 -0700 Subject: [PATCH 09/10] unit tests now pass! --- .github/workflows/golangci-lint.yaml | 2 +- go.mod | 3 - go.sum | 5 - internal/logger/console.go | 2 +- internal/logger/custom_levels.go | 12 +- internal/logger/interface.go | 2 +- internal/logger/logger.go | 4 +- internal/logger/test/interface.go2 | 16 --- internal/logger/test/logger.go | 159 +++++++++++++++++---------- internal/predictor/predictor.go | 1 + internal/sso/cache.go | 8 +- internal/sso/cache_test.go | 43 ++++---- internal/sso/settings.go | 5 +- internal/sso/settings_test.go | 35 ++++-- internal/storage/keyring_test.go | 16 +-- internal/storage/storage.go | 4 +- internal/storage/storage_test.go | 16 ++- internal/tags/tags_list.go | 4 +- internal/tags/tags_list_test.go | 16 ++- 19 files changed, 209 insertions(+), 144 deletions(-) delete mode 100644 internal/logger/test/interface.go2 diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml index 045efdae..328a0e0d 100644 --- a/.github/workflows/golangci-lint.yaml +++ b/.github/workflows/golangci-lint.yaml @@ -30,7 +30,7 @@ jobs: uses: golangci/golangci-lint-action@v6 with: # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.54.2 + version: v${{ vars.GOLANGCI_LINT_VERSION }} # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/go.mod b/go.mod index b4854e08..60d32b6d 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/knadh/koanf v1.5.0 github.com/manifoldco/promptui v0.9.0 github.com/posener/complete v1.2.3 - github.com/sirupsen/logrus v1.9.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.9.0 github.com/synfinatic/gotable v0.0.3 @@ -78,7 +77,6 @@ require ( github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b - github.com/joeshaw/json-lossless v0.0.0-20181204200226-e0cd1ca6349b github.com/lmittmann/tint v1.0.5 github.com/veqryn/slog-json v0.3.0 golang.org/x/net v0.27.0 @@ -94,7 +92,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect github.com/aws/smithy-go v1.20.3 // indirect - github.com/bitly/go-simplejson v0.5.1 // indirect github.com/containerd/log v0.1.0 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect diff --git a/go.sum b/go.sum index b226c472..48b23117 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,6 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= -github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= github.com/c-bata/go-prompt v0.2.5 h1:3zg6PecEywxNn0xiqcXHD96fkbxghD+gdB2tbsYfl+Y= github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -245,8 +243,6 @@ github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joeshaw/json-lossless v0.0.0-20181204200226-e0cd1ca6349b h1:tCtJ08SeJzFbI5UfgNBwvXPHiHK938r9lfdnrBsa1qI= -github.com/joeshaw/json-lossless v0.0.0-20181204200226-e0cd1ca6349b/go.mod h1:bPnXx4rALO5hbpPEp/hvkA5s5MFP5GuIXbLpt5e8QxM= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -521,7 +517,6 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/logger/console.go b/internal/logger/console.go index efa9b302..c0b83d4e 100644 --- a/internal/logger/console.go +++ b/internal/logger/console.go @@ -50,7 +50,7 @@ func NewConsole(w io.Writer, addSource bool, level slog.Leveler, color bool) (sl return NewConsoleHandler(w, &opts), lvl } -// impliment the slog.Handler interface via the tint.Handler +// implement the slog.Handler interface via the tint.Handler type ConsoleHandler struct { slog.Handler } diff --git a/internal/logger/custom_levels.go b/internal/logger/custom_levels.go index a608e7a9..890aa0c3 100644 --- a/internal/logger/custom_levels.go +++ b/internal/logger/custom_levels.go @@ -41,33 +41,33 @@ var LevelColorsMap map[slog.Level]LevelColor = map[slog.Level]LevelColor{ // Log a message at the Trace level func (l *Logger) Trace(msg string, args ...interface{}) { ctx := context.Background() - l.logWithSource(ctx, LevelTrace, msg, args...) + l.LogWithSource(ctx, LevelTrace, StackFrames, msg, args...) } func (l *Logger) TraceContext(ctx context.Context, msg string, args ...interface{}) { - l.logWithSource(ctx, LevelTrace, msg, args...) + l.LogWithSource(ctx, LevelTrace, StackFrames, msg, args...) } // Log a message at the Fatal level and exit func (l *Logger) Fatal(msg string, args ...interface{}) { ctx := context.Background() - l.logWithSource(ctx, LevelFatal, msg, args...) + l.LogWithSource(ctx, LevelFatal, StackFrames, msg, args...) os.Exit(1) } func (l *Logger) FatalContext(ctx context.Context, msg string, args ...interface{}) { - l.logWithSource(ctx, LevelFatal, msg, args...) + l.LogWithSource(ctx, LevelFatal, StackFrames, msg, args...) os.Exit(1) } // logWithSource sets the __source attribute so that our Handler knows // to modify the r.PC value to include the original caller. -func (l *Logger) logWithSource(ctx context.Context, level slog.Level, msg string, args ...interface{}) { +func (l *Logger) LogWithSource(ctx context.Context, level slog.Level, frames int, msg string, args ...interface{}) { var allArgs []interface{} allArgs = append(allArgs, args...) if l.addSource { - allArgs = append(allArgs, slog.Int(FrameMarker, StackFrames)) + allArgs = append(allArgs, slog.Int(FrameMarker, frames)) } l.logger.Log(ctx, level, msg, allArgs...) } diff --git a/internal/logger/interface.go b/internal/logger/interface.go index c393ef22..6a4f28a7 100644 --- a/internal/logger/interface.go +++ b/internal/logger/interface.go @@ -25,7 +25,7 @@ type CustomLogger interface { // custom methods Copy() CustomLogger // Clone(f NewLoggerFunc, w io.Writer) *CustomLogger - GetLevel() slog.Leveler + GetLevel() slog.Level GetLogger() *slog.Logger SetLevel(level slog.Leveler) SetLevelString(level string) error diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 05d1d053..097a08f9 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -26,7 +26,7 @@ import ( "strings" ) -// Our logger which wraps slog.Logger and impliments CustomLogger +// Our logger which wraps slog.Logger and implements CustomLogger type Logger struct { logger *slog.Logger addSource bool @@ -166,6 +166,6 @@ func (l *Logger) SetReportCaller(reportCaller bool) { } // GetLevel returns the current log level -func (l *Logger) GetLevel() slog.Leveler { +func (l *Logger) GetLevel() slog.Level { return slog.Level(l.level.Level()) } diff --git a/internal/logger/test/interface.go2 b/internal/logger/test/interface.go2 deleted file mode 100644 index 70086f66..00000000 --- a/internal/logger/test/interface.go2 +++ /dev/null @@ -1,16 +0,0 @@ -/* -package test - -import ( - "github.com/synfinatic/aws-sso-cli/internal/logger" -) - -func (tl *TestLogger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { - tl.Logger.Log(ctx, level, msg, args...) -} - -// TestLogger impliments logger.CustomLogger -func (tl *TestLogger) Debug(msg string, args ...any) { - tl.Logger.Debug(msg, args...) -} -*/ \ No newline at end of file diff --git a/internal/logger/test/logger.go b/internal/logger/test/logger.go index 5df2b911..029dee76 100644 --- a/internal/logger/test/logger.go +++ b/internal/logger/test/logger.go @@ -1,43 +1,65 @@ package test +/* + // Example usage: + func TestMyFunction(t *testing.T) { + // setup the test logger + tLogger := testlogger.NewTestLogger("DEBUG") + defer tLogger.Close() + + oldLogger := log.Copy() + log = tLogger + defer func() { log = oldLogger }() + + // call the function(s) you want to test + + // check the log messages + msg := testlogger.LogMessage{} + assert.NoError(t, tLogger.GetNext(&msg)) + assert.Equal(t, "my message", msg.Message) + assert.Equal(t, logger.LevelDebug, msg.Level) + + // Clear any remaining log messages for the next test + tLogger.Reset() + } + +*/ + import ( "bufio" + "context" "encoding/json" "errors" "fmt" "io" "log/slog" - "os" "reflect" "regexp" "strings" - "sync" + "time" "github.com/davecgh/go-spew/spew" - lossless "github.com/joeshaw/json-lossless" "github.com/synfinatic/aws-sso-cli/internal/logger" ) -// TeslLogger impliments logger.CustomLogger +// TeslLogger implements logger.CustomLogger type TestLogger struct { *logger.Logger r *io.PipeReader w *io.PipeWriter - rch chan []byte - messages LogMessages + errors chan error + messages chan LogMessage close bool - mutex sync.Mutex } -type LogMessages []LogMessage - type LogMessage struct { - LevelStr string `json:"level"` - Level slog.Level - Message string `json:"msg"` - Time string `json:"time"` - Source FileSource `json:"source"` - lossless.JSON `json:"-"` + LevelStr string `json:"level"` + Level slog.Level + Message string `json:"msg"` + Time string `json:"time"` + Source FileSource `json:"source"` + Error string `json:"error"` // i standardized on this field name + // XXX: Problem is that all the extra fields are not being decoded } type FileSource struct { @@ -55,36 +77,37 @@ func NewTestLogger(level string) *TestLogger { Logger: logger.NewLogger(logger.NewJSON, writer, false, l, false), w: writer, r: reader, - messages: LogMessages{}, + errors: make(chan error, 10), + messages: make(chan LogMessage, 10), close: false, - mutex: sync.Mutex{}, } // start a goroutine to read from the pipe and decode the log messages go func() { + i := 0 r := bufio.NewReader(tl.r) for !tl.close { line, err := r.ReadString('\n') + i++ if err != nil { if err == io.EOF { break } else { - fmt.Fprintf(os.Stderr, "unable to read log message: %s", err.Error()) + tl.errors <- err break } } msg := LogMessage{} if err := json.Unmarshal([]byte(line), &msg); err != nil { - panic(fmt.Sprintf("unable to decode log message: %s", err.Error())) + tl.errors <- fmt.Errorf("unable to decode log message: %s", err.Error()) + break } + msg.Level = logger.LevelStrings[msg.LevelStr] - tl.mutex.Lock() - tl.messages = append(tl.messages, msg) - tl.mutex.Unlock() + tl.messages <- msg } }() - return &tl } @@ -94,64 +117,70 @@ func (tl *TestLogger) Close() { tl.r.Close() } -func (tl *TestLogger) ResetBuffer() { - tl.mutex.Lock() - defer tl.mutex.Unlock() - tl.messages = LogMessages{} +// Reset clears all the written log messages +func (tl *TestLogger) Reset() { + if tl.close { + return + } + r := bufio.NewReader(tl.r) + for r.Buffered() > 0 { + _, _ = r.ReadString('\n') + } } -func (tl *TestLogger) GetLast(msg *LogMessage) error { - if len(tl.messages) > 0 { - last := len(tl.messages) - 1 - msg.Level = tl.messages[last].Level - msg.LevelStr = tl.messages[last].LevelStr - msg.Message = tl.messages[last].Message - msg.Time = tl.messages[last].Time - msg.Source = tl.messages[last].Source - return nil +func (tl *TestLogger) GetNext(msg *LogMessage) error { + if tl.close { + return errors.New("logger closed") } - return errors.New("no log messages found") + t := time.NewTicker(100 * time.Millisecond) + select { + case err := <-tl.errors: + return err + case m := <-tl.messages: + *msg = m + return nil + case <-t.C: + return errors.New("no log messages found") + } } -// Returns the last log message of the given level -func (tl *TestLogger) GetLastLevel(level slog.Level, msg *LogMessage) error { - for i := len(tl.messages) - 1; i >= 0; i-- { - if tl.messages[i].Level == level { - msg.Level = tl.messages[i].Level - msg.LevelStr = tl.messages[i].LevelStr - msg.Message = tl.messages[i].Message - msg.Time = tl.messages[i].Time - msg.Source = tl.messages[i].Source +// GetNextLevel returns the next logged message of the given level +func (tl *TestLogger) GetNextLevel(level slog.Level, msg *LogMessage) error { + for { + err := tl.GetNext(msg) + if err != nil { + return err + } + + if msg.Level == level { return nil } } - - return fmt.Errorf("no log message found for level %s", level.String()) } -func (tl *TestLogger) CheckLastLevelEqual(level slog.Level, field, value string) (bool, error) { +func (tl *TestLogger) CheckNextLevelEqual(level slog.Level, field, value string) (bool, error) { msg := LogMessage{} - err := tl.GetLastLevel(level, &msg) + err := tl.GetNextLevel(level, &msg) if err != nil { return false, err } - fmt.Printf("CheckLastLevelEqual decoded msg: %s", spew.Sdump(msg)) + fmt.Printf("CheckNextLevelEqual decoded msg: %s", spew.Sdump(msg)) if reflect.ValueOf(msg).FieldByName(field).String() == value { return true, nil } return false, nil } -func (tl *TestLogger) CheckLastLevelMatch(level slog.Level, field string, match *regexp.Regexp) (bool, error) { +func (tl *TestLogger) CheckNextLevelMatch(level slog.Level, field string, match *regexp.Regexp) (bool, error) { msg := LogMessage{} - err := tl.GetLastLevel(level, &msg) + err := tl.GetNextLevel(level, &msg) if err != nil { return false, err } - fmt.Printf("CheckLastLevelMatch decoded msg: %s", spew.Sdump(msg)) + fmt.Printf("CheckNextLevelMatch decoded msg: %s", spew.Sdump(msg)) value := reflect.ValueOf(msg).FieldByName(field).String() if match.MatchString(value) { return true, nil @@ -159,31 +188,41 @@ func (tl *TestLogger) CheckLastLevelMatch(level slog.Level, field string, match return false, nil } -func (tl *TestLogger) CheckLastEqual(field, value string) (bool, error) { +func (tl *TestLogger) CheckNextEqual(field, value string) (bool, error) { msg := LogMessage{} - err := tl.GetLast(&msg) + err := tl.GetNext(&msg) if err != nil { return false, err } - fmt.Printf("CheckLast decoded msg: %s", spew.Sdump(msg)) + fmt.Printf("CheckNext decoded msg: %s", spew.Sdump(msg)) if reflect.ValueOf(msg).FieldByName(field).String() == value { return true, nil } return false, nil } -func (tl *TestLogger) CheckLastMatch(field string, match *regexp.Regexp) (bool, error) { +func (tl *TestLogger) CheckNextMatch(field string, match *regexp.Regexp) (bool, error) { msg := LogMessage{} - err := tl.GetLast(&msg) + err := tl.GetNext(&msg) if err != nil { return false, err } - fmt.Printf("CheckLastMatch decoded msg: %s", spew.Sdump(msg)) + fmt.Printf("CheckNextMatch decoded msg: %s", spew.Sdump(msg)) value := reflect.ValueOf(msg).FieldByName(field).String() if match.MatchString(value) { return true, nil } return false, nil } + +// Log a message at the Fatal level and exit +func (tl *TestLogger) Fatal(msg string, args ...interface{}) { + ctx := context.Background() + tl.Logger.LogWithSource(ctx, logger.LevelFatal, logger.StackFrames, msg, args...) +} + +func (tl *TestLogger) FatalContext(ctx context.Context, msg string, args ...interface{}) { + tl.Logger.LogWithSource(ctx, logger.LevelFatal, logger.StackFrames, msg, args...) +} diff --git a/internal/predictor/predictor.go b/internal/predictor/predictor.go index c934d306..477d5d1b 100644 --- a/internal/predictor/predictor.go +++ b/internal/predictor/predictor.go @@ -51,6 +51,7 @@ func NewPredictor(cacheFile, configFile string) *Predictor { // select our SSO from a CLI flag or env var, else use our default override := sso.OverrideSettings{ DefaultSSO: getSSOValue(), + LogLevel: "warn", } p := Predictor{ diff --git a/internal/sso/cache.go b/internal/sso/cache.go index 4578b562..e91d456f 100644 --- a/internal/sso/cache.go +++ b/internal/sso/cache.go @@ -600,6 +600,12 @@ func (c *Cache) addSSORoles(r *Roles, as *AWSSSO, threads int) error { return nil } +type contextKey string + +const ( + accountIdKey contextKey = "accountID" +) + // addConfigRoles decorates the provided Roles with the contents of our config func (c *Cache) addConfigRoles(r *Roles, config *SSOConfig) error { // The load all the Config file stuff. Normally this is just adding markup, but @@ -609,7 +615,7 @@ func (c *Cache) addConfigRoles(r *Roles, config *SSOConfig) error { if err != nil { return err } - ctx := context.WithValue(context.Background(), "accountID", id) + ctx := context.WithValue(context.Background(), accountIdKey, id) if _, ok := r.Accounts[id]; !ok { log.DebugContext(ctx, "config.yaml defines an AWS AccountID, but you don't have access.") continue diff --git a/internal/sso/cache_test.go b/internal/sso/cache_test.go index 75743de9..848ae294 100644 --- a/internal/sso/cache_test.go +++ b/internal/sso/cache_test.go @@ -27,7 +27,6 @@ import ( // "github.com/davecgh/go-spew/spew" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" @@ -268,6 +267,7 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { // setup logger for tests oldLogger := log.Copy() tLogger := testlogger.NewTestLogger("DEBUG") + defer tLogger.Close() log = tLogger defer func() { log = oldLogger }() @@ -278,46 +278,46 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { c.deleteOldHistory() msg := testlogger.LogMessage{} - tLogger.RefreshBuffer() - assert.NoError(t, tLogger.GetLast(&msg)) + assert.NoError(t, tLogger.GetNext(&msg)) assert.NotEmpty(t, msg.Message) assert.Equal(t, slog.LevelDebug, msg.Level) assert.Contains(t, "Removed expired history role", msg.Message) assert.Equal(t, []string{"arn:aws:iam::123456789012:role/Test"}, c.GetSSO().History) + tLogger.Reset() c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam:") c.deleteOldHistory() - tLogger.RefreshBuffer() - assert.NoError(t, tLogger.GetLast(&msg)) + assert.NoError(t, tLogger.GetNext(&msg)) assert.Equal(t, slog.LevelDebug, msg.Level) assert.Contains(t, "Unable to parse History ARN", msg.Message) + tLogger.Reset() c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/NoHistoryTag") c.deleteOldHistory() - tLogger.RefreshBuffer() - assert.NotNil(t, tLogger.GetLast(&msg)) - assert.Equal(t, logrus.DebugLevel, msg.Level) - assert.Contains(t, "but no role by that name", msg.Message) + assert.NoError(t, tLogger.GetNext(&msg)) + assert.Equal(t, slog.LevelDebug, msg.Level) + assert.Contains(t, msg.Message, "but no role by that name") + tLogger.Reset() c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::1234567890:role/NoHistoryTag") c.deleteOldHistory() - tLogger.RefreshBuffer() - assert.NotNil(t, tLogger.GetLast(&msg)) + assert.NoError(t, tLogger.GetNext(&msg)) assert.Equal(t, slog.LevelDebug, msg.Level) - assert.Contains(t, "but no account by that name", msg.Message) + assert.Contains(t, msg.Message, "but no account by that name") + tLogger.Reset() c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/NoHistoryTag") c.GetSSO().Roles.Accounts[123456789012].Roles["NoHistoryTag"] = &AWSRole{} c.deleteOldHistory() - tLogger.RefreshBuffer() - assert.NotNil(t, tLogger.GetLast(&msg)) + assert.NoError(t, tLogger.GetNext(&msg)) assert.Equal(t, slog.LevelDebug, msg.Level) - assert.Contains(t, "is in history list without a History tag", msg.Message) + assert.Contains(t, msg.Message, "in history list without a History tag") + tLogger.Reset() c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/MissingHistoryTag") c.GetSSO().Roles.Accounts[123456789012].Roles["MissingHistoryTag"] = &AWSRole{ @@ -326,11 +326,11 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { }, } c.deleteOldHistory() - tLogger.RefreshBuffer() - assert.NotNil(t, tLogger.GetLast(&msg)) + assert.NoError(t, tLogger.GetNext(&msg)) assert.Equal(t, slog.LevelDebug, msg.Level) - assert.Contains(t, "Too few fields for", msg.Message) + assert.Contains(t, msg.Message, "Too few fields for") + tLogger.Reset() c = suite.setupDeleteOldHistory() c.GetSSO().History = append(c.GetSSO().History, "arn:aws:iam::123456789012:role/MissingHistoryTag") c.GetSSO().Roles.Accounts[123456789012].Roles["MissingHistoryTag"] = &AWSRole{ @@ -339,10 +339,11 @@ func (suite *CacheTestSuite) TestDeleteOldHistory() { }, } c.deleteOldHistory() - tLogger.RefreshBuffer() - assert.NotNil(t, tLogger.GetLast(&msg)) + assert.NoError(t, tLogger.GetNext(&msg)) assert.Equal(t, slog.LevelDebug, msg.Level) - assert.Contains(t, "Unable to parse", msg.Message) + assert.Contains(t, msg.Message, "Unable to parse") + + tLogger.Reset() } func (suite *CacheTestSuite) TestExpired() { diff --git a/internal/sso/settings.go b/internal/sso/settings.go index 04a24535..a25d1581 100644 --- a/internal/sso/settings.go +++ b/internal/sso/settings.go @@ -317,7 +317,10 @@ func (s *Settings) setOverrides(override OverrideSettings) { s.LogLevel = override.LogLevel } - log.SetLevelString(s.LogLevel) + err := log.SetLevelString(s.LogLevel) + if err != nil { + log.Fatal("Invalid log level", "level", s.LogLevel, "error", err.Error()) + } // Other overrides from CLI if override.Browser != "" { diff --git a/internal/sso/settings_test.go b/internal/sso/settings_test.go index a0a8cf00..6193451d 100644 --- a/internal/sso/settings_test.go +++ b/internal/sso/settings_test.go @@ -20,13 +20,15 @@ package sso import ( "fmt" + "log/slog" "os" "path/filepath" "testing" - "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/synfinatic/aws-sso-cli/internal/logger" + testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" "github.com/synfinatic/aws-sso-cli/internal/url" ) @@ -227,9 +229,17 @@ func (suite *SettingsTestSuite) TestGetDefaultRegion() { assert.Equal(t, "eu-west-1", suite.settings.GetDefaultRegion(258234615182, "LimitedAccess", false)) assert.Equal(t, "us-east-1", suite.settings.GetDefaultRegion(833365043586, "AWSAdministratorAccess:", false)) - assert.Panics(t, func() { - suite.settings.GetDefaultRegion(-1, "foo", false) - }) + oldLogger := log.Copy() + tLogger := testlogger.NewTestLogger("DEBUG") + defer tLogger.Close() + log = tLogger + defer func() { log = oldLogger }() + + suite.settings.GetDefaultRegion(-1, "foo", false) + msg := testlogger.LogMessage{} + assert.NoError(t, tLogger.GetNext(&msg)) + assert.Contains(t, msg.Message, "Unable to GetDefaultRegion") + assert.Equal(t, logger.LevelFatal, msg.Level) } func (suite *SettingsTestSuite) TestOtherSSO() { @@ -328,7 +338,7 @@ func (suite *SettingsTestSuite) TestSetOverrides() { s.setOverrides(overrides) - assert.Equal(t, logrus.DebugLevel, log.Level) + assert.Equal(t, slog.LevelDebug, log.GetLevel()) // assert.True(t, log.ReportCaller) assert.Equal(t, "my-browser", s.Browser) assert.Equal(t, "hello", s.DefaultSSO) @@ -339,9 +349,18 @@ func TestCreatedAt(t *testing.T) { s := Settings{ configFile: "/dev/null/invalid", } - assert.Panics(t, func() { - s.CreatedAt() - }) + + oldLogger := log.Copy() + tLogger := testlogger.NewTestLogger("DEBUG") + defer tLogger.Close() + log = tLogger + defer func() { log = oldLogger }() + + assert.Panics(t, func() { s.CreatedAt() }) // will panic because log.Fatal() doesn't return + msg := testlogger.LogMessage{} + assert.NoError(t, tLogger.GetNext(&msg)) + assert.Contains(t, msg.Message, "Unable to open") + assert.Equal(t, logger.LevelFatal, msg.Level) } func TestApplyDeprecations(t *testing.T) { diff --git a/internal/storage/keyring_test.go b/internal/storage/keyring_test.go index 9c1196ef..350a6e4f 100644 --- a/internal/storage/keyring_test.go +++ b/internal/storage/keyring_test.go @@ -21,7 +21,6 @@ package storage import ( "encoding/json" "fmt" - "log/slog" "os" "path" "strings" @@ -31,7 +30,6 @@ import ( "github.com/99designs/keyring" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/synfinatic/aws-sso-cli/internal/logger" testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" ) @@ -496,8 +494,10 @@ func TestSplitCredentials(t *testing.T) { // setup logger for testing oldLogger := log.Copy() tLogger := testlogger.NewTestLogger("DEBUG") - logger.SetLogger(tLogger) - defer func() { logger.SetLogger(oldLogger) }() + defer tLogger.Close() + + log = tLogger + defer func() { log = oldLogger }() defer func() { os.RemoveAll(d) @@ -574,13 +574,7 @@ func TestSplitCredentials(t *testing.T) { _, err = store.joinAndGetKeyringData(RECORD_KEY) assert.Error(t, err) - // but OpenKeyring is fine, just returns a warning + // but OpenKeyring is fine _, err = OpenKeyring(c) assert.NoError(t, err) - - msg := testlogger.LogMessage{} - tLogger.RefreshBuffer() - assert.NoError(t, tLogger.GetLast(&msg)) - assert.Equal(t, slog.LevelWarn, msg.Level) - assert.Contains(t, "unable to fetch", msg.Message) } diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 6e704c18..4306b7fe 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -110,7 +110,7 @@ func (r *RoleCredentials) ExpireString() string { func (r *RoleCredentials) AccountIdStr() string { s, err := utils.AccountIdToString(r.AccountId) if err != nil { - panic(fmt.Sprintf("unable to parse accountId from AWS role credentials: %s", err.Error())) + log.Fatal("unable to parse accountId from AWS role credentials", "error", err.Error()) } return s } @@ -167,7 +167,7 @@ func (sc *StaticCredentials) UserArn() string { func (sc *StaticCredentials) AccountIdStr() string { s, err := utils.AccountIdToString(sc.AccountId) if err != nil { - panic(fmt.Sprintf("Invalid AccountId from AWS static credentials: %d", sc.AccountId)) + log.Fatal("Invalid AccountId from AWS static credentials", "accountId", sc.AccountId) } return s } diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 848bd12d..919cfb62 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -24,6 +24,8 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/synfinatic/aws-sso-cli/internal/logger" + testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" ) func TestCreateTokenResponseExpired(t *testing.T) { @@ -115,6 +117,14 @@ func TestGetArn(t *testing.T) { } func TestGetAccountIdStr(t *testing.T) { + // setup logger for testing + oldLogger := log.Copy() + tLogger := testlogger.NewTestLogger("DEBUG") + defer tLogger.Close() + + log = tLogger + defer func() { log = oldLogger }() + x := StaticCredentials{ UserName: "foobar", AccountId: 23456789012, @@ -125,7 +135,11 @@ func TestGetAccountIdStr(t *testing.T) { UserName: "foobar", AccountId: -1, } - assert.Panics(t, func() { x.AccountIdStr() }) + _ = x.AccountIdStr() + msg := testlogger.LogMessage{} + assert.NoError(t, tLogger.GetNext(&msg)) + assert.Contains(t, msg.Message, "Invalid AccountId") + assert.Equal(t, logger.LevelFatal, msg.Level) } func TestGetHeader(t *testing.T) { diff --git a/internal/tags/tags_list.go b/internal/tags/tags_list.go index 21cf150a..35694a6a 100644 --- a/internal/tags/tags_list.go +++ b/internal/tags/tags_list.go @@ -20,7 +20,6 @@ package tags import ( "fmt" - "os" "sort" "strconv" "strings" @@ -139,8 +138,7 @@ func ReformatHistory(value string) string { i, err := strconv.ParseInt(x[1], 10, 64) if err != nil { - log.Error("unable to parse epoch", "value", value, "epoch", x[1], "split", x, "error", err) - os.Exit(1) + log.Fatal("unable to parse epoch", "value", value, "epoch", x[1], "split", x, "error", err) } d := time.Since(time.Unix(i, 0)).Truncate(time.Second) diff --git a/internal/tags/tags_list_test.go b/internal/tags/tags_list_test.go index 514252e2..59acf6cd 100644 --- a/internal/tags/tags_list_test.go +++ b/internal/tags/tags_list_test.go @@ -9,6 +9,8 @@ import ( yaml "github.com/goccy/go-yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "github.com/synfinatic/aws-sso-cli/internal/logger" + testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" ) type TagsListTestSuite struct { @@ -150,6 +152,12 @@ func (suite *TagsListTestSuite) TestUniqueValues() { func (suite *TagsListTestSuite) TestReformatHistory() { t := suite.T() + oldLogger := log.Copy() + tLogger := testlogger.NewTestLogger("DEBUG") + defer tLogger.Close() + log = tLogger + defer func() { log = oldLogger }() + // special case, has no timestamp assert.Equal(t, "foo", ReformatHistory("foo")) @@ -158,8 +166,14 @@ func (suite *TagsListTestSuite) TestReformatHistory() { "foo,bar", } + msg := testlogger.LogMessage{} + for _, x := range invalidTS { - assert.Panics(t, func() { ReformatHistory(x) }) + ReformatHistory(x) + assert.NoError(t, tLogger.GetNext(&msg)) + assert.Contains(t, msg.Message, "unable to parse epoch") + assert.Equal(t, logger.LevelFatal, msg.Level) + tLogger.Reset() } // valid case From 9956c656dd39640c92e340d18a20e27a7cfd474a Mon Sep 17 00:00:00 2001 From: Aaron Turner Date: Mon, 19 Aug 2024 16:57:22 -0700 Subject: [PATCH 10/10] move logger to external library: synfinatic/flexlog --- CHANGELOG.md | 1 + cmd/aws-sso/logger.go | 6 +- cmd/aws-sso/select_args.go | 2 +- go.mod | 11 +- go.sum | 6 +- internal/ecs/client/client.go | 6 +- internal/ecs/http.go | 4 +- internal/ecs/server/server.go | 6 +- internal/helper/helper.go | 6 +- internal/logger/console.go | 106 -------------- internal/logger/custom_levels.go | 73 ---------- internal/logger/init.go | 58 -------- internal/logger/interface.go | 42 ------ internal/logger/json.go | 82 ----------- internal/logger/levels.go | 125 ----------------- internal/logger/logger.go | 171 ----------------------- internal/logger/test/logger.go | 228 ------------------------------- internal/logger/tint.go | 24 ---- internal/predictor/predictor.go | 6 +- internal/sso/cache_test.go | 2 +- internal/sso/logger.go | 6 +- internal/sso/settings_test.go | 8 +- internal/storage/keyring_test.go | 2 +- internal/storage/storage.go | 6 +- internal/storage/storage_test.go | 6 +- internal/tags/logger.go | 6 +- internal/tags/tags_list_test.go | 6 +- internal/url/url.go | 6 +- internal/utils/utils.go | 6 +- 29 files changed, 56 insertions(+), 961 deletions(-) delete mode 100644 internal/logger/console.go delete mode 100644 internal/logger/custom_levels.go delete mode 100644 internal/logger/init.go delete mode 100644 internal/logger/interface.go delete mode 100644 internal/logger/json.go delete mode 100644 internal/logger/levels.go delete mode 100644 internal/logger/logger.go delete mode 100644 internal/logger/test/logger.go delete mode 100644 internal/logger/tint.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 88d23725..d67ab48c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * `tags` command no longer supports the `--force-update` option * Change default log level from `warn` to `info` * Remove mention from docs that Firefox Multi-Account Containers plugin is supported #1021 + * Switch from logrus to log/slog #1001 ### New Features diff --git a/cmd/aws-sso/logger.go b/cmd/aws-sso/logger.go index 682b1866..ad18b64e 100644 --- a/cmd/aws-sso/logger.go +++ b/cmd/aws-sso/logger.go @@ -18,10 +18,10 @@ package main * along with this program. If not, see . */ -import "github.com/synfinatic/aws-sso-cli/internal/logger" +import "github.com/synfinatic/flexlog" -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } diff --git a/cmd/aws-sso/select_args.go b/cmd/aws-sso/select_args.go index eafcc1b6..a7ede1eb 100644 --- a/cmd/aws-sso/select_args.go +++ b/cmd/aws-sso/select_args.go @@ -40,7 +40,7 @@ func (e *InvalidArgsError) Error() string { if e.arg != "" { return fmt.Sprintf(e.msg, e.arg) } - return fmt.Sprintf(e.msg) + return e.msg } type NoRoleSelectedError struct{} diff --git a/go.mod b/go.mod index 60d32b6d..f91a7cd5 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/posener/complete v1.2.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/stretchr/testify v1.9.0 + github.com/synfinatic/flexlog v0.0.2 github.com/synfinatic/gotable v0.0.3 github.com/willabides/kongplete v0.2.0 golang.org/x/crypto v0.25.0 // indirect @@ -35,7 +36,7 @@ require ( github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/dvsekhvalnov/jose2go v1.6.0 // indirect - github.com/fatih/color v1.17.0 + github.com/fatih/color v1.17.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/google/uuid v1.6.0 // indirect @@ -45,7 +46,7 @@ require ( github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect @@ -76,9 +77,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 github.com/docker/docker v27.1.2+incompatible github.com/docker/go-connections v0.5.0 - github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b - github.com/lmittmann/tint v1.0.5 - github.com/veqryn/slog-json v0.3.0 golang.org/x/net v0.27.0 ) @@ -96,15 +94,18 @@ require ( github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-json-experiment/json v0.0.0-20240815174924-0599f16bf0e2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/lmittmann/tint v1.0.5 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/veqryn/slog-json v0.3.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect diff --git a/go.sum b/go.sum index 48b23117..322329d5 100644 --- a/go.sum +++ b/go.sum @@ -126,8 +126,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b h1:IM96IiRXFcd7l+mU8Sys9pcggoBLbH/dEgzOESrS8F8= -github.com/go-json-experiment/json v0.0.0-20240524174822-2d9f40f7385b/go.mod h1:uDEMZSTQMj7V6Lxdrx4ZwchmHEGdICbjuY+GQd7j9LM= +github.com/go-json-experiment/json v0.0.0-20240815174924-0599f16bf0e2 h1:T01ryfhob+ta3ADOh2B2FLA1cFLVOaQJH/iwh22rIFM= +github.com/go-json-experiment/json v0.0.0-20240815174924-0599f16bf0e2/go.mod h1:uDEMZSTQMj7V6Lxdrx4ZwchmHEGdICbjuY+GQd7j9LM= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -394,6 +394,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/synfinatic/flexlog v0.0.2 h1:rdybUZfIBKuARa8yU26luzNC4nJ34UKwzid4yZ7PHOg= +github.com/synfinatic/flexlog v0.0.2/go.mod h1:YQiokc3ujp5GJA04wAXo6ce/LsUK6SrUXsOnHqGKsas= github.com/synfinatic/gotable v0.0.3 h1:KI01OLECmOv7laXVNtw6T4kEHue09z9OuQwtNB8D5Mw= github.com/synfinatic/gotable v0.0.3/go.mod h1:kWXD1bxZY6tyu6tWK3CIbGAOrtF7teg0ZQotUMDvoMw= github.com/veqryn/slog-json v0.3.0 h1:jI2ORtKP1uQss4zmTR2uCpIDw/XnUvVdr5+0vDNl4Gk= diff --git a/internal/ecs/client/client.go b/internal/ecs/client/client.go index 2d191b54..e2dc5f1b 100644 --- a/internal/ecs/client/client.go +++ b/internal/ecs/client/client.go @@ -32,14 +32,14 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/synfinatic/aws-sso-cli/internal/ecs" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/storage" + "github.com/synfinatic/flexlog" ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } type ECSClient struct { diff --git a/internal/ecs/http.go b/internal/ecs/http.go index 91ea66f4..7fe2e23c 100644 --- a/internal/ecs/http.go +++ b/internal/ecs/http.go @@ -5,8 +5,8 @@ import ( "net/http" "strconv" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/storage" + "github.com/synfinatic/flexlog" ) // Use format as defined here: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials/endpointcreds @@ -35,7 +35,7 @@ func WriteCreds(w http.ResponseWriter, creds *storage.RoleCredentials) { // JSONResponse return a JSON blob as a result func JSONResponse(w http.ResponseWriter, jdata interface{}) { - log := logger.GetLogger() + log := flexlog.GetLogger() w.Header().Set("Content-Type", CHARSET_JSON) if err := json.NewEncoder(w).Encode(jdata); err != nil { log.Error(err.Error()) diff --git a/internal/ecs/server/server.go b/internal/ecs/server/server.go index 3dd96938..3438b526 100644 --- a/internal/ecs/server/server.go +++ b/internal/ecs/server/server.go @@ -28,14 +28,14 @@ import ( // "github.com/davecgh/go-spew/spew" "github.com/synfinatic/aws-sso-cli/internal/ecs" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/storage" + "github.com/synfinatic/flexlog" ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } type EcsServer struct { diff --git a/internal/helper/helper.go b/internal/helper/helper.go index 44d142d3..e2fe1651 100644 --- a/internal/helper/helper.go +++ b/internal/helper/helper.go @@ -27,14 +27,14 @@ import ( "path" "github.com/riywo/loginshell" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/utils" + "github.com/synfinatic/flexlog" ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } //go:embed bash_profile.sh zshrc.sh aws-sso.fish diff --git a/internal/logger/console.go b/internal/logger/console.go deleted file mode 100644 index c0b83d4e..00000000 --- a/internal/logger/console.go +++ /dev/null @@ -1,106 +0,0 @@ -package logger - -/* - * AWS SSO CLI - * Copyright (c) 2021-2024 Aaron Turner - * - * This program is free software: you can redistribute it - * and/or modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or with the authors permission any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import ( - "context" - "io" - "log/slog" - "runtime" - "time" - - "github.com/lmittmann/tint" -) - -const ( - FrameMarker = "__skip_frames" -) - -// NewConsole creates a new slog.Handler for the ConsoleHandler, which wraps tint.NewHandler -// with some customizations. -func NewConsole(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) { - lvl := new(slog.LevelVar) - lvl.Set(level.Level()) - - opts := tint.Options{ - Level: lvl, - AddSource: addSource, - ReplaceAttr: replaceAttrConsole, - TimeFormat: time.Kitchen, - // TimeFormat: "", - NoColor: true, // let the replaceAttr do the coloring - } - - return NewConsoleHandler(w, &opts), lvl -} - -// implement the slog.Handler interface via the tint.Handler -type ConsoleHandler struct { - slog.Handler -} - -// ConsoleHandler is a slog.Handler that wraps tint.Handler -func NewConsoleHandler(w io.Writer, opts *tint.Options) slog.Handler { - return &ConsoleHandler{ - tint.NewHandler(w, opts), - } -} - -// Handle is a custom wrapper around the tint.Handler.Handle method which fixes up -// the PC value to be the correct caller for the Fatal/Trace methods -func (h *ConsoleHandler) Handle(ctx context.Context, r slog.Record) error { - var fixStack int64 = 0 - r.Attrs(func(a slog.Attr) bool { - if a.Key == FrameMarker { - fixStack = a.Value.Int64() - return false - } - return true - }) - - if fixStack > 0 { - rn := r.Clone() - rn.PC, _, _, _ = runtime.Caller(int(fixStack)) - return h.Handler.Handle(ctx, rn) - } - return h.Handler.Handle(ctx, r) -} - -func replaceAttrConsole(groups []string, a slog.Attr) slog.Attr { - // Remove time from the output on the console - if a.Key == slog.TimeKey { - return slog.Attr{} - } - - // Remove the frame marker attribute flag if it's present - if a.Key == FrameMarker { - return slog.Attr{} - } - - // Colorize and rename the log level - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - levelColor, ok := LevelColorsMap[level] - if ok { - a.Value = slog.StringValue(levelColor.String(logger.Color())) - } - } - - return a -} diff --git a/internal/logger/custom_levels.go b/internal/logger/custom_levels.go deleted file mode 100644 index 890aa0c3..00000000 --- a/internal/logger/custom_levels.go +++ /dev/null @@ -1,73 +0,0 @@ -package logger - -import ( - "context" - "log/slog" - "os" - - "github.com/fatih/color" -) - -// Define our custom levels -const ( - LevelTrace = slog.Level(-8) - LevelFatal = slog.Level(12) - StackFrames = 5 // number of stack frames to skip in Handler.Handle -) - -var LevelNames = map[slog.Leveler]string{ - LevelTrace: "TRACE", - LevelFatal: "FATAL", -} - -var LevelStrings = map[string]slog.Level{ - "TRACE": LevelTrace, - "FATAL": LevelFatal, - "INFO": slog.LevelInfo, - "WARN": slog.LevelWarn, - "ERROR": slog.LevelError, - "DEBUG": slog.LevelDebug, -} - -var LevelColorsMap map[slog.Level]LevelColor = map[slog.Level]LevelColor{ - LevelTrace: {Name: "TRACE", Color: color.FgGreen}, - LevelFatal: {Name: "FATAL", Color: color.FgRed}, - slog.LevelInfo: {Name: "INFO ", Color: color.FgBlue}, - slog.LevelWarn: {Name: "WARN ", Color: color.FgYellow}, - slog.LevelError: {Name: "ERROR", Color: color.FgRed}, - slog.LevelDebug: {Name: "DEBUG", Color: color.FgMagenta}, -} - -// Log a message at the Trace level -func (l *Logger) Trace(msg string, args ...interface{}) { - ctx := context.Background() - l.LogWithSource(ctx, LevelTrace, StackFrames, msg, args...) -} - -func (l *Logger) TraceContext(ctx context.Context, msg string, args ...interface{}) { - l.LogWithSource(ctx, LevelTrace, StackFrames, msg, args...) -} - -// Log a message at the Fatal level and exit -func (l *Logger) Fatal(msg string, args ...interface{}) { - ctx := context.Background() - l.LogWithSource(ctx, LevelFatal, StackFrames, msg, args...) - os.Exit(1) -} - -func (l *Logger) FatalContext(ctx context.Context, msg string, args ...interface{}) { - l.LogWithSource(ctx, LevelFatal, StackFrames, msg, args...) - os.Exit(1) -} - -// logWithSource sets the __source attribute so that our Handler knows -// to modify the r.PC value to include the original caller. -func (l *Logger) LogWithSource(ctx context.Context, level slog.Level, frames int, msg string, args ...interface{}) { - var allArgs []interface{} - allArgs = append(allArgs, args...) - - if l.addSource { - allArgs = append(allArgs, slog.Int(FrameMarker, frames)) - } - l.logger.Log(ctx, level, msg, allArgs...) -} diff --git a/internal/logger/init.go b/internal/logger/init.go deleted file mode 100644 index 62e07410..00000000 --- a/internal/logger/init.go +++ /dev/null @@ -1,58 +0,0 @@ -package logger - -import ( - "io" - "log/slog" - "os" - - "github.com/mattn/go-isatty" -) - -var logger CustomLogger - -type NewLoggerFunc func(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) - -// default to the console logger -var CreateLogger NewLoggerFunc = NewJSON // NewConsole - -// initialize the default logger to log to stderr and log at the warn level -func init() { - w := os.Stderr - color := isatty.IsTerminal(w.Fd()) - addSource := false - level := slog.LevelWarn - - logger = NewLogger(CreateLogger, w, addSource, level, color) - - slog.SetDefault(logger.GetLogger()) -} - -func SetLogger(l CustomLogger) { - logger = l -} - -func GetLogger() CustomLogger { - return logger -} - -func SetDefaultLogger(l CustomLogger) { - slog.SetDefault(l.GetLogger()) -} - -// SwitchLogger changes the current logger to the specified type -func SwitchLogger(name string) { - var loggers = map[string]NewLoggerFunc{ - "console": NewConsole, - "json": NewJSON, - "tint": NewTint, - } - var ok bool - CreateLogger, ok = loggers[name] - if !ok { - logger.Fatal("Invalid logger", "name", name) - } - - // switch the logger - logger = NewLogger(CreateLogger, logger.Writer(), logger.AddSource(), logger.Level(), logger.Color()) - slog.SetDefault(logger.GetLogger()) -} diff --git a/internal/logger/interface.go b/internal/logger/interface.go deleted file mode 100644 index 6a4f28a7..00000000 --- a/internal/logger/interface.go +++ /dev/null @@ -1,42 +0,0 @@ -package logger - -import ( - "context" - "io" - "log/slog" -) - -type CustomLogger interface { - // slog.Logger methods - Debug(msg string, args ...any) - DebugContext(ctx context.Context, msg string, args ...any) - Enabled(ctx context.Context, level slog.Level) bool - Error(msg string, args ...any) - ErrorContext(ctx context.Context, msg string, args ...any) - Handler() slog.Handler - Info(msg string, args ...any) - InfoContext(ctx context.Context, msg string, args ...any) - Log(ctx context.Context, level slog.Level, msg string, args ...any) - LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) - Warn(msg string, args ...any) - WarnContext(ctx context.Context, msg string, args ...any) - With(args ...any) *slog.Logger - WithGroup(name string) *slog.Logger - // custom methods - Copy() CustomLogger - // Clone(f NewLoggerFunc, w io.Writer) *CustomLogger - GetLevel() slog.Level - GetLogger() *slog.Logger - SetLevel(level slog.Leveler) - SetLevelString(level string) error - SetLogger(logger *slog.Logger) - SetReportCaller(reportCaller bool) - Trace(msg string, args ...any) - TraceContext(ctx context.Context, msg string, args ...any) - Fatal(msg string, args ...any) - FatalContext(ctx context.Context, msg string, args ...any) - Writer() io.Writer - AddSource() bool - Level() *slog.LevelVar - Color() bool -} diff --git a/internal/logger/json.go b/internal/logger/json.go deleted file mode 100644 index f4f7fea3..00000000 --- a/internal/logger/json.go +++ /dev/null @@ -1,82 +0,0 @@ -package logger - -import ( - "context" - "io" - "log/slog" - "runtime" - - "github.com/go-json-experiment/json" - "github.com/go-json-experiment/json/jsontext" - slogjson "github.com/veqryn/slog-json" -) - -func NewJSON(w io.Writer, addSource bool, level slog.Leveler, _ bool) (slog.Handler, *slog.LevelVar) { - lvl := new(slog.LevelVar) - lvl.Set(level.Level()) - - opts := slogjson.HandlerOptions{ - Level: lvl, - AddSource: addSource, - ReplaceAttr: replaceAttrJson, - JSONOptions: json.JoinOptions( - // Options from the json v2 library (these are the defaults) - json.Deterministic(true), - jsontext.AllowDuplicateNames(true), - jsontext.AllowInvalidUTF8(true), - jsontext.EscapeForJS(true), - jsontext.SpaceAfterColon(false), - jsontext.SpaceAfterComma(true), - ), - } - - return NewJSONHandler(w, &opts), lvl -} - -type JsonHandler struct { - slog.Handler -} - -func NewJSONHandler(w io.Writer, opts *slogjson.HandlerOptions) slog.Handler { - return &JsonHandler{ - slogjson.NewHandler(w, opts), - } -} - -// Handle is a custom wrapper around the slogjson.Handler.Handle method which fixes up -// the PC value to be the correct caller for the Fatal/Trace methods -func (h *JsonHandler) Handle(ctx context.Context, r slog.Record) error { - var fixStack int64 = 0 - r.Attrs(func(a slog.Attr) bool { - if a.Key == FrameMarker { - fixStack = a.Value.Int64() - return false - } - return true - }) - - if fixStack > 0 { - rn := r.Clone() - rn.PC, _, _, _ = runtime.Caller(int(fixStack)) - return h.Handler.Handle(ctx, rn) - } - return h.Handler.Handle(ctx, r) -} - -func replaceAttrJson(groups []string, a slog.Attr) slog.Attr { - // Remove the frame marker attribute flag if it's present - if a.Key == FrameMarker { - return slog.Attr{} - } - - // Rename the log level - if a.Key == slog.LevelKey { - level := a.Value.Any().(slog.Level) - levelColor, ok := LevelColorsMap[level] - if ok { - a.Value = slog.StringValue(levelColor.String(false)) - } - } - - return a -} diff --git a/internal/logger/levels.go b/internal/logger/levels.go deleted file mode 100644 index 7bffeac0..00000000 --- a/internal/logger/levels.go +++ /dev/null @@ -1,125 +0,0 @@ -package logger - -import ( - "log/slog" - - "github.com/fatih/color" -) - -// LevelColors defines the name as displayed to the user and color of a log level. -type LevelColor struct { - // Name is the name of the log level - Name string - // Color is the color of the log level - Color color.Attribute - serialized string - colored bool -} - -// String returns the level name, optionally with color applied. -func (lc *LevelColor) String(colored bool) string { - if len(lc.serialized) == 0 || lc.colored != colored { - if colored { - lc.serialized = color.New(lc.Color).SprintFunc()(lc.Name) - } else { - lc.serialized = lc.Name - } - } - return lc.serialized -} - -// Copy returns a copy of the LevelColor. -func (lc *LevelColor) Copy() *LevelColor { - return &LevelColor{ - Name: lc.Name, - Color: lc.Color, - serialized: lc.serialized, - colored: lc.colored, - } -} - -// LevelColorsMapping is a map of log levels to their colors and is what -// the user defines in their configuration. -type LevelColorsMapping map[slog.Level]LevelColor - -// min returns the mapped minimum index -func (lm *LevelColorsMapping) min() int { - idx := 1000 - for check := range *lm { - if int(check) < idx { - idx = int(check) - } - } - return idx -} - -// size returns the size of the slice needed to store the LevelColors. -func (lm *LevelColorsMapping) size(offset int) int { - maxIdx := -1000 - for check := range *lm { - if int(check) > maxIdx { - maxIdx = int(check) - } - } - return offset + maxIdx + 1 -} - -// offset returns the index offset needed to map negative log levels. -func (lm *LevelColorsMapping) offset() int { - min := lm.min() - if min < 0 { - min = -min - } - return min -} - -// LevelColors returns the LevelColors for the LevelColorsMapping. -func (lm *LevelColorsMapping) LevelColors() *LevelColors { - lcList := make([]*LevelColor, lm.size(lm.offset())) - for idx, lc := range *lm { - lcList[int(idx)+lm.offset()] = lc.Copy() - } - lc := LevelColors{ - levels: lcList, - offset: lm.offset(), - } - return &lc -} - -// LevelColors is our internal representation of the user-defined LevelColorsMapping. -// We map the log levels via their slog.Level to their LevelColor using an offset -// to ensure we can map negative level values to our slice. -type LevelColors struct { - levels []*LevelColor - offset int -} - -// LevelColor returns the LevelColor for the given log level. -// Returns nil indicating if the log level was not found. -func (lc *LevelColors) LevelColor(level slog.Level) *LevelColor { - if len(lc.levels) == 0 { - return nil - } - - idx := int(level.Level()) + lc.offset - if len(lc.levels) < idx { - return &LevelColor{} - } - return lc.levels[idx] -} - -// Copy returns a copy of the LevelColors. -func (lc *LevelColors) Copy() *LevelColors { - if len(lc.levels) == 0 { - return &LevelColors{ - levels: []*LevelColor{}, - } - } - - lcCopy := LevelColors{ - levels: make([]*LevelColor, len(lc.levels)), - offset: lc.offset, - } - copy(lcCopy.levels, lc.levels) - return &lcCopy -} diff --git a/internal/logger/logger.go b/internal/logger/logger.go deleted file mode 100644 index 097a08f9..00000000 --- a/internal/logger/logger.go +++ /dev/null @@ -1,171 +0,0 @@ -package logger - -/* - * AWS SSO CLI - * Copyright (c) 2021-2024 Aaron Turner - * - * This program is free software: you can redistribute it - * and/or modify it under the terms of the GNU General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or with the authors permission any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -import ( - "context" - "fmt" - "io" - "log/slog" - "strings" -) - -// Our logger which wraps slog.Logger and implements CustomLogger -type Logger struct { - logger *slog.Logger - addSource bool - color bool - level *slog.LevelVar - writer io.Writer -} - -// NewLoggerFunc creates a new Logger -func NewLogger(f NewLoggerFunc, w io.Writer, addSource bool, level slog.Leveler, color bool) *Logger { - handle, lvl := f(w, addSource, level, color) - return &Logger{ - logger: slog.New(handle), - addSource: addSource, - color: color, - level: lvl, - writer: w, - } -} - -// Debug logs a message at the debug level -func (l *Logger) Debug(msg string, args ...any) { - l.logger.Debug(msg, args...) -} - -// DebugContext logs a message at the debug level with context -func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any) { - l.logger.DebugContext(ctx, msg, args...) -} - -func (l *Logger) Error(msg string, args ...any) { - l.logger.Error(msg, args...) -} - -func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) { - l.logger.ErrorContext(ctx, msg, args...) -} - -func (l *Logger) Info(msg string, args ...any) { - l.logger.Info(msg, args...) -} - -func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) { - l.logger.InfoContext(ctx, msg, args...) -} - -func (l *Logger) Log(ctx context.Context, level slog.Level, msg string, args ...any) { - l.logger.Log(ctx, level, msg, args...) -} - -func (l *Logger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { - l.logger.LogAttrs(ctx, level, msg, attrs...) -} - -func (l *Logger) Warn(msg string, args ...any) { - l.logger.Warn(msg, args...) -} - -func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any) { - l.logger.WarnContext(ctx, msg, args...) -} - -func (l *Logger) Handler() slog.Handler { - return l.logger.Handler() -} - -func (l *Logger) With(args ...any) *slog.Logger { - return l.logger.With(args...) -} - -func (l *Logger) WithGroup(name string) *slog.Logger { - return l.logger.WithGroup(name) -} - -// Copy returns a copy of the Logger current Logger -func (l *Logger) Copy() CustomLogger { - return NewLogger(CreateLogger, l.writer, l.addSource, l.level, l.color) -} - -func (l *Logger) Writer() io.Writer { - return l.writer -} - -func (l *Logger) AddSource() bool { - return l.addSource -} - -func (l *Logger) Level() *slog.LevelVar { - return l.level -} - -func (l *Logger) Color() bool { - return l.color -} - -func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool { - return l.logger.Enabled(ctx, level) -} - -/* -// Clone returns a clone of the current Logger with a new Logging function -func (l *Logger) Clone(f NewLoggerFunc, w io.Writer) *Logger { - return NewLogger(f, w, l.addSource, l.level, l.color) -} -*/ - -func (l *Logger) GetLogger() *slog.Logger { - return l.logger -} - -func (l *Logger) SetLogger(logger *slog.Logger) { - l.logger = logger -} - -// SetLevel sets the log level for the logger -func (l *Logger) SetLevel(level slog.Leveler) { - l.level.Set(level.Level()) -} - -func (l *Logger) SetLevelString(level string) error { - if _, ok := LevelStrings[strings.ToUpper(level)]; !ok { - return fmt.Errorf("invalid log level: %s", level) - } - l.level.Set(LevelStrings[strings.ToUpper(level)].Level()) - return nil -} - -// SetReportCaller sets whether to include the source file and line number in the log output -// Doing so will replace the current logger with a new one that has the new setting -func (l *Logger) SetReportCaller(reportCaller bool) { - if l.addSource == reportCaller { - return // do nothing - } - l.addSource = reportCaller - handler, _ := CreateLogger(l.writer, l.addSource, slog.LevelWarn, l.color) - logger.SetLogger(slog.New(handler)) -} - -// GetLevel returns the current log level -func (l *Logger) GetLevel() slog.Level { - return slog.Level(l.level.Level()) -} diff --git a/internal/logger/test/logger.go b/internal/logger/test/logger.go deleted file mode 100644 index 029dee76..00000000 --- a/internal/logger/test/logger.go +++ /dev/null @@ -1,228 +0,0 @@ -package test - -/* - // Example usage: - func TestMyFunction(t *testing.T) { - // setup the test logger - tLogger := testlogger.NewTestLogger("DEBUG") - defer tLogger.Close() - - oldLogger := log.Copy() - log = tLogger - defer func() { log = oldLogger }() - - // call the function(s) you want to test - - // check the log messages - msg := testlogger.LogMessage{} - assert.NoError(t, tLogger.GetNext(&msg)) - assert.Equal(t, "my message", msg.Message) - assert.Equal(t, logger.LevelDebug, msg.Level) - - // Clear any remaining log messages for the next test - tLogger.Reset() - } - -*/ - -import ( - "bufio" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "reflect" - "regexp" - "strings" - "time" - - "github.com/davecgh/go-spew/spew" - "github.com/synfinatic/aws-sso-cli/internal/logger" -) - -// TeslLogger implements logger.CustomLogger -type TestLogger struct { - *logger.Logger - r *io.PipeReader - w *io.PipeWriter - errors chan error - messages chan LogMessage - close bool -} - -type LogMessage struct { - LevelStr string `json:"level"` - Level slog.Level - Message string `json:"msg"` - Time string `json:"time"` - Source FileSource `json:"source"` - Error string `json:"error"` // i standardized on this field name - // XXX: Problem is that all the extra fields are not being decoded -} - -type FileSource struct { - File string `json:"file"` - Function string `json:"function"` - Line int `json:"line"` -} - -func NewTestLogger(level string) *TestLogger { - reader, writer := io.Pipe() - - l := logger.LevelStrings[strings.ToUpper(level)].Level() - - tl := TestLogger{ - Logger: logger.NewLogger(logger.NewJSON, writer, false, l, false), - w: writer, - r: reader, - errors: make(chan error, 10), - messages: make(chan LogMessage, 10), - close: false, - } - - // start a goroutine to read from the pipe and decode the log messages - go func() { - i := 0 - r := bufio.NewReader(tl.r) - - for !tl.close { - line, err := r.ReadString('\n') - i++ - if err != nil { - if err == io.EOF { - break - } else { - tl.errors <- err - break - } - } - msg := LogMessage{} - if err := json.Unmarshal([]byte(line), &msg); err != nil { - tl.errors <- fmt.Errorf("unable to decode log message: %s", err.Error()) - break - } - msg.Level = logger.LevelStrings[msg.LevelStr] - - tl.messages <- msg - } - }() - return &tl -} - -func (tl *TestLogger) Close() { - tl.close = true - tl.w.Close() - tl.r.Close() -} - -// Reset clears all the written log messages -func (tl *TestLogger) Reset() { - if tl.close { - return - } - r := bufio.NewReader(tl.r) - for r.Buffered() > 0 { - _, _ = r.ReadString('\n') - } -} - -func (tl *TestLogger) GetNext(msg *LogMessage) error { - if tl.close { - return errors.New("logger closed") - } - - t := time.NewTicker(100 * time.Millisecond) - select { - case err := <-tl.errors: - return err - case m := <-tl.messages: - *msg = m - return nil - case <-t.C: - return errors.New("no log messages found") - } -} - -// GetNextLevel returns the next logged message of the given level -func (tl *TestLogger) GetNextLevel(level slog.Level, msg *LogMessage) error { - for { - err := tl.GetNext(msg) - if err != nil { - return err - } - - if msg.Level == level { - return nil - } - } -} - -func (tl *TestLogger) CheckNextLevelEqual(level slog.Level, field, value string) (bool, error) { - msg := LogMessage{} - err := tl.GetNextLevel(level, &msg) - if err != nil { - return false, err - } - - fmt.Printf("CheckNextLevelEqual decoded msg: %s", spew.Sdump(msg)) - if reflect.ValueOf(msg).FieldByName(field).String() == value { - return true, nil - } - return false, nil -} - -func (tl *TestLogger) CheckNextLevelMatch(level slog.Level, field string, match *regexp.Regexp) (bool, error) { - msg := LogMessage{} - err := tl.GetNextLevel(level, &msg) - if err != nil { - return false, err - } - - fmt.Printf("CheckNextLevelMatch decoded msg: %s", spew.Sdump(msg)) - value := reflect.ValueOf(msg).FieldByName(field).String() - if match.MatchString(value) { - return true, nil - } - return false, nil -} - -func (tl *TestLogger) CheckNextEqual(field, value string) (bool, error) { - msg := LogMessage{} - err := tl.GetNext(&msg) - if err != nil { - return false, err - } - - fmt.Printf("CheckNext decoded msg: %s", spew.Sdump(msg)) - if reflect.ValueOf(msg).FieldByName(field).String() == value { - return true, nil - } - return false, nil -} - -func (tl *TestLogger) CheckNextMatch(field string, match *regexp.Regexp) (bool, error) { - msg := LogMessage{} - err := tl.GetNext(&msg) - if err != nil { - return false, err - } - - fmt.Printf("CheckNextMatch decoded msg: %s", spew.Sdump(msg)) - value := reflect.ValueOf(msg).FieldByName(field).String() - if match.MatchString(value) { - return true, nil - } - return false, nil -} - -// Log a message at the Fatal level and exit -func (tl *TestLogger) Fatal(msg string, args ...interface{}) { - ctx := context.Background() - tl.Logger.LogWithSource(ctx, logger.LevelFatal, logger.StackFrames, msg, args...) -} - -func (tl *TestLogger) FatalContext(ctx context.Context, msg string, args ...interface{}) { - tl.Logger.LogWithSource(ctx, logger.LevelFatal, logger.StackFrames, msg, args...) -} diff --git a/internal/logger/tint.go b/internal/logger/tint.go deleted file mode 100644 index c01fc63e..00000000 --- a/internal/logger/tint.go +++ /dev/null @@ -1,24 +0,0 @@ -package logger - -import ( - "io" - "log/slog" - "time" - - "github.com/lmittmann/tint" -) - -func NewTint(w io.Writer, addSource bool, level slog.Leveler, color bool) (slog.Handler, *slog.LevelVar) { - lvl := new(slog.LevelVar) - lvl.Set(level.Level()) - - opts := tint.Options{ - Level: lvl, - AddSource: addSource, - ReplaceAttr: replaceAttrConsole, - TimeFormat: time.Kitchen, - NoColor: !color, - } - - return tint.NewHandler(w, &opts), lvl -} diff --git a/internal/predictor/predictor.go b/internal/predictor/predictor.go index 477d5d1b..fdc5305c 100644 --- a/internal/predictor/predictor.go +++ b/internal/predictor/predictor.go @@ -25,15 +25,15 @@ import ( // "github.com/davecgh/go-spew/spew" "github.com/goccy/go-yaml" "github.com/posener/complete" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/sso" "github.com/synfinatic/aws-sso-cli/internal/utils" + "github.com/synfinatic/flexlog" ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } type Predictor struct { diff --git a/internal/sso/cache_test.go b/internal/sso/cache_test.go index 848ae294..2de67713 100644 --- a/internal/sso/cache_test.go +++ b/internal/sso/cache_test.go @@ -29,7 +29,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" + testlogger "github.com/synfinatic/flexlog/test" ) const ( diff --git a/internal/sso/logger.go b/internal/sso/logger.go index 13dbcbde..3de92b42 100644 --- a/internal/sso/logger.go +++ b/internal/sso/logger.go @@ -18,10 +18,10 @@ package sso * along with this program. If not, see . */ -import "github.com/synfinatic/aws-sso-cli/internal/logger" +import "github.com/synfinatic/flexlog" -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } diff --git a/internal/sso/settings_test.go b/internal/sso/settings_test.go index 6193451d..c1078337 100644 --- a/internal/sso/settings_test.go +++ b/internal/sso/settings_test.go @@ -27,9 +27,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/synfinatic/aws-sso-cli/internal/logger" - testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" "github.com/synfinatic/aws-sso-cli/internal/url" + "github.com/synfinatic/flexlog" + testlogger "github.com/synfinatic/flexlog/test" ) const ( @@ -239,7 +239,7 @@ func (suite *SettingsTestSuite) TestGetDefaultRegion() { msg := testlogger.LogMessage{} assert.NoError(t, tLogger.GetNext(&msg)) assert.Contains(t, msg.Message, "Unable to GetDefaultRegion") - assert.Equal(t, logger.LevelFatal, msg.Level) + assert.Equal(t, flexlog.LevelFatal, msg.Level) } func (suite *SettingsTestSuite) TestOtherSSO() { @@ -360,7 +360,7 @@ func TestCreatedAt(t *testing.T) { msg := testlogger.LogMessage{} assert.NoError(t, tLogger.GetNext(&msg)) assert.Contains(t, msg.Message, "Unable to open") - assert.Equal(t, logger.LevelFatal, msg.Level) + assert.Equal(t, flexlog.LevelFatal, msg.Level) } func TestApplyDeprecations(t *testing.T) { diff --git a/internal/storage/keyring_test.go b/internal/storage/keyring_test.go index 350a6e4f..73ed672d 100644 --- a/internal/storage/keyring_test.go +++ b/internal/storage/keyring_test.go @@ -30,7 +30,7 @@ import ( "github.com/99designs/keyring" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" + testlogger "github.com/synfinatic/flexlog/test" ) type KeyringSuite struct { diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 4306b7fe..d305bf01 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -25,15 +25,15 @@ import ( "reflect" "time" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/utils" + "github.com/synfinatic/flexlog" "github.com/synfinatic/gotable" ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } // this struct should be cached for long term if possible diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 919cfb62..ffef1a0d 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -24,8 +24,8 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/synfinatic/aws-sso-cli/internal/logger" - testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" + "github.com/synfinatic/flexlog" + testlogger "github.com/synfinatic/flexlog/test" ) func TestCreateTokenResponseExpired(t *testing.T) { @@ -139,7 +139,7 @@ func TestGetAccountIdStr(t *testing.T) { msg := testlogger.LogMessage{} assert.NoError(t, tLogger.GetNext(&msg)) assert.Contains(t, msg.Message, "Invalid AccountId") - assert.Equal(t, logger.LevelFatal, msg.Level) + assert.Equal(t, flexlog.LevelFatal, msg.Level) } func TestGetHeader(t *testing.T) { diff --git a/internal/tags/logger.go b/internal/tags/logger.go index e7fea36c..5494006e 100644 --- a/internal/tags/logger.go +++ b/internal/tags/logger.go @@ -18,10 +18,10 @@ package tags * along with this program. If not, see . */ -import "github.com/synfinatic/aws-sso-cli/internal/logger" +import "github.com/synfinatic/flexlog" -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } diff --git a/internal/tags/tags_list_test.go b/internal/tags/tags_list_test.go index 59acf6cd..fa9ba001 100644 --- a/internal/tags/tags_list_test.go +++ b/internal/tags/tags_list_test.go @@ -9,8 +9,8 @@ import ( yaml "github.com/goccy/go-yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" - "github.com/synfinatic/aws-sso-cli/internal/logger" - testlogger "github.com/synfinatic/aws-sso-cli/internal/logger/test" + "github.com/synfinatic/flexlog" + testlogger "github.com/synfinatic/flexlog/test" ) type TagsListTestSuite struct { @@ -172,7 +172,7 @@ func (suite *TagsListTestSuite) TestReformatHistory() { ReformatHistory(x) assert.NoError(t, tLogger.GetNext(&msg)) assert.Contains(t, msg.Message, "unable to parse epoch") - assert.Equal(t, logger.LevelFatal, msg.Level) + assert.Equal(t, flexlog.LevelFatal, msg.Level) tLogger.Reset() } diff --git a/internal/url/url.go b/internal/url/url.go index 1631a50e..68d6902f 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -28,15 +28,15 @@ import ( "github.com/atotto/clipboard" "github.com/skratchdot/open-golang/open" - "github.com/synfinatic/aws-sso-cli/internal/logger" "github.com/synfinatic/aws-sso-cli/internal/utils" + "github.com/synfinatic/flexlog" // default opener ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } // taken from https://github.com/honsiorovskyi/open-url-in-container/blob/1.0.3/launcher.sh diff --git a/internal/utils/utils.go b/internal/utils/utils.go index ae32acf1..6b65c994 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -28,13 +28,13 @@ import ( "strings" "time" - "github.com/synfinatic/aws-sso-cli/internal/logger" + "github.com/synfinatic/flexlog" ) -var log logger.CustomLogger +var log flexlog.FlexLogger func init() { - log = logger.GetLogger() + log = flexlog.GetLogger() } const MAX_AWS_ACCOUNTID = 999999999999