diff --git a/src/cmd/cmd.go b/src/cmd/cmd.go new file mode 100644 index 0000000000..169c0747dc --- /dev/null +++ b/src/cmd/cmd.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package cmd contains the CLI commands for Zarf. +package cmd + +// setBaseDirectory sets the base directory. This is a directory with a zarf.yaml. +func setBaseDirectory(args []string) string { + if len(args) > 0 { + return args[0] + } + return "." +} diff --git a/src/cmd/common/setup.go b/src/cmd/common/setup.go deleted file mode 100644 index ac47d0c95d..0000000000 --- a/src/cmd/common/setup.go +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package common handles command configuration across all commands -package common - -import ( - "errors" - "fmt" - "io" - "os" - "time" - - "github.com/pterm/pterm" - - "github.com/zarf-dev/zarf/src/pkg/message" -) - -// SetupCLI sets up the CLI logging -func SetupCLI(logLevel string, skipLogFile, noColor bool) error { - if noColor { - message.DisableColor() - } - - printViperConfigUsed() - - if logLevel != "" { - match := map[string]message.LogLevel{ - "warn": message.WarnLevel, - "info": message.InfoLevel, - "debug": message.DebugLevel, - "trace": message.TraceLevel, - } - lvl, ok := match[logLevel] - if !ok { - return errors.New("invalid log level, valid options are warn, info, debug, and trace") - } - message.SetLogLevel(lvl) - message.Debug("Log level set to " + logLevel) - } - - // Disable progress bars for CI envs - if os.Getenv("CI") == "true" { - message.Debug("CI environment detected, disabling progress bars") - message.NoProgress = true - } - - if !skipLogFile { - ts := time.Now().Format("2006-01-02-15-04-05") - f, err := os.CreateTemp("", fmt.Sprintf("zarf-%s-*.log", ts)) - if err != nil { - return fmt.Errorf("could not create a log file in a the temporary directory: %w", err) - } - logFile, err := message.UseLogFile(f) - if err != nil { - return fmt.Errorf("could not save a log file to the temporary directory: %w", err) - } - pterm.SetDefaultOutput(io.MultiWriter(os.Stderr, logFile)) - message.Notef("Saving log file to %s", f.Name()) - } - return nil -} diff --git a/src/cmd/common/utils.go b/src/cmd/common/utils.go deleted file mode 100644 index 1da7e456ee..0000000000 --- a/src/cmd/common/utils.go +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -// Package common handles command configuration across all commands -package common - -// SetBaseDirectory sets the base directory. This is a directory with a zarf.yaml. -func SetBaseDirectory(args []string) string { - if len(args) > 0 { - return args[0] - } - return "." -} diff --git a/src/cmd/common/viper.go b/src/cmd/common/viper.go index fa4f548582..cef9456562 100644 --- a/src/cmd/common/viper.go +++ b/src/cmd/common/viper.go @@ -5,14 +5,15 @@ package common import ( + "context" "errors" "os" "strings" + "github.com/zarf-dev/zarf/src/pkg/logger" + "github.com/spf13/viper" "github.com/zarf-dev/zarf/src/config" - "github.com/zarf-dev/zarf/src/config/lang" - "github.com/zarf-dev/zarf/src/pkg/message" ) // Constants for use when loading configurations from viper config files @@ -20,17 +21,21 @@ const ( // Root config keys - VLogLevel = "log_level" VArchitecture = "architecture" - VNoLogFile = "no_log_file" - VNoProgress = "no_progress" - VNoColor = "no_color" VZarfCache = "zarf_cache" VTmpDir = "tmp_dir" VInsecure = "insecure" VPlainHTTP = "plain_http" VInsecureSkipTLSVerify = "insecure_skip_tls_verify" + // Root config, Logging + + VLogLevel = "log_level" + VLogFormat = "log_format" + VNoLogFile = "no_log_file" + VNoProgress = "no_progress" + VNoColor = "no_color" + // Init config keys VInitComponents = "init.components" @@ -162,7 +167,10 @@ func isVersionCmd() bool { return len(args) > 1 && (args[1] == "version" || args[1] == "v") } -func printViperConfigUsed() { +// PrintViperConfigUsed informs users when Zarf has detected a config file. +func PrintViperConfigUsed(ctx context.Context) { + l := logger.From(ctx) + // Only print config info if viper is initialized. vInitialized := v != nil if !vInitialized { @@ -173,11 +181,11 @@ func printViperConfigUsed() { return } if vConfigError != nil { - message.WarnErrf(vConfigError, lang.CmdViperErrLoadingConfigFile, vConfigError.Error()) + l.Error("unable to load config file", "error", vConfigError) return } if cfgFile := v.ConfigFileUsed(); cfgFile != "" { - message.Notef(lang.CmdViperInfoUsingConfigFile, cfgFile) + l.Info("using config file", "location", cfgFile) } } diff --git a/src/cmd/dev.go b/src/cmd/dev.go index 34e0f76776..09cb67e53e 100644 --- a/src/cmd/dev.go +++ b/src/cmd/dev.go @@ -44,7 +44,7 @@ var devDeployCmd = &cobra.Command{ Short: lang.CmdDevDeployShort, Long: lang.CmdDevDeployLong, RunE: func(cmd *cobra.Command, args []string) error { - pkgConfig.CreateOpts.BaseDir = common.SetBaseDirectory(args) + pkgConfig.CreateOpts.BaseDir = setBaseDirectory(args) v := common.GetViper() pkgConfig.CreateOpts.SetVariables = helpers.TransformAndMergeMap( @@ -234,7 +234,7 @@ var devFindImagesCmd = &cobra.Command{ Short: lang.CmdDevFindImagesShort, Long: lang.CmdDevFindImagesLong, RunE: func(cmd *cobra.Command, args []string) error { - pkgConfig.CreateOpts.BaseDir = common.SetBaseDirectory(args) + pkgConfig.CreateOpts.BaseDir = setBaseDirectory(args) v := common.GetViper() @@ -289,7 +289,7 @@ var devLintCmd = &cobra.Command{ Long: lang.CmdDevLintLong, RunE: func(cmd *cobra.Command, args []string) error { config.CommonOptions.Confirm = true - pkgConfig.CreateOpts.BaseDir = common.SetBaseDirectory(args) + pkgConfig.CreateOpts.BaseDir = setBaseDirectory(args) v := common.GetViper() pkgConfig.CreateOpts.SetVariables = helpers.TransformAndMergeMap( v.GetStringMapString(common.VPkgCreateSet), pkgConfig.CreateOpts.SetVariables, strings.ToUpper) diff --git a/src/cmd/initialize.go b/src/cmd/initialize.go index 01470e817d..70e73c1223 100644 --- a/src/cmd/initialize.go +++ b/src/cmd/initialize.go @@ -8,7 +8,6 @@ import ( "context" "errors" "fmt" - "os" "path" "path/filepath" "strings" @@ -37,9 +36,6 @@ var initCmd = &cobra.Command{ Long: lang.CmdInitLong, Example: lang.CmdInitExample, RunE: func(cmd *cobra.Command, _ []string) error { - zarfLogo := message.GetLogo() - _, _ = fmt.Fprintln(os.Stderr, zarfLogo) - if err := validateInitFlags(); err != nil { return fmt.Errorf("invalid command flags were provided: %w", err) } diff --git a/src/cmd/package.go b/src/cmd/package.go index 30b80c612a..58e714a543 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -47,7 +47,7 @@ var packageCreateCmd = &cobra.Command{ Short: lang.CmdPackageCreateShort, Long: lang.CmdPackageCreateLong, RunE: func(cmd *cobra.Command, args []string) error { - pkgConfig.CreateOpts.BaseDir = common.SetBaseDirectory(args) + pkgConfig.CreateOpts.BaseDir = setBaseDirectory(args) var isCleanPathRegex = regexp.MustCompile(`^[a-zA-Z0-9\_\-\/\.\~\\:]+$`) if !isCleanPathRegex.MatchString(config.CommonOptions.CachePath) { diff --git a/src/cmd/root.go b/src/cmd/root.go index bf695e3760..f72358e288 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -6,10 +6,17 @@ package cmd import ( "context" + "errors" "fmt" + "io" + "log/slog" "os" "slices" "strings" + "time" + + "github.com/zarf-dev/zarf/src/cmd/say" + "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -18,7 +25,6 @@ import ( "github.com/zarf-dev/zarf/src/cmd/tools" "github.com/zarf-dev/zarf/src/config" "github.com/zarf-dev/zarf/src/config/lang" - "github.com/zarf-dev/zarf/src/pkg/layout" "github.com/zarf-dev/zarf/src/pkg/message" "github.com/zarf-dev/zarf/src/types" ) @@ -28,6 +34,8 @@ var ( pkgConfig = types.PackagerConfig{} // LogLevelCLI holds the log level as input from a command LogLevelCLI string + // LogFormat holds the log format as input from a command + LogFormat string // SkipLogFile is a flag to skip logging to a file SkipLogFile bool // NoColor is a flag to disable colors in output @@ -35,67 +43,75 @@ var ( ) var rootCmd = &cobra.Command{ - Use: "zarf COMMAND", - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - // If --insecure was provided, set --insecure-skip-tls-verify and --plain-http to match - if config.CommonOptions.Insecure { - config.CommonOptions.InsecureSkipTLSVerify = true - config.CommonOptions.PlainHTTP = true - } + Use: "zarf COMMAND", + Short: lang.RootCmdShort, + Long: lang.RootCmdLong, + Args: cobra.MaximumNArgs(1), + SilenceUsage: true, + // TODO(mkcp): Do we actually want to silence errors here? + SilenceErrors: true, + PersistentPreRunE: preRun, + Run: run, +} - // Skip for vendor only commands - if common.CheckVendorOnlyFromPath(cmd) { - return nil - } +func preRun(cmd *cobra.Command, _ []string) error { + // If --insecure was provided, set --insecure-skip-tls-verify and --plain-http to match + if config.CommonOptions.Insecure { + config.CommonOptions.InsecureSkipTLSVerify = true + config.CommonOptions.PlainHTTP = true + } - skipLogFile := SkipLogFile + // Skip for vendor only commands + if common.CheckVendorOnlyFromPath(cmd) { + return nil + } - // Dont write tool commands to file. - comps := strings.Split(cmd.CommandPath(), " ") - if len(comps) > 1 && comps[1] == "tools" { - skipLogFile = true - } - if len(comps) > 1 && comps[1] == "version" { - skipLogFile = true - } + // Setup message + skipLogFile := SkipLogFile - // Dont write help command to file. - if cmd.Parent() == nil { - skipLogFile = true - } + // Don't write tool commands to file. + comps := strings.Split(cmd.CommandPath(), " ") + if len(comps) > 1 && comps[1] == "tools" { + skipLogFile = true + } + if len(comps) > 1 && comps[1] == "version" { + skipLogFile = true + } - err := common.SetupCLI(LogLevelCLI, skipLogFile, NoColor) - if err != nil { - return err - } - return nil - }, - Short: lang.RootCmdShort, - Long: lang.RootCmdLong, - Args: cobra.MaximumNArgs(1), - SilenceUsage: true, - SilenceErrors: true, - Run: func(cmd *cobra.Command, args []string) { - zarfLogo := message.GetLogo() - _, _ = fmt.Fprintln(os.Stderr, zarfLogo) - err := cmd.Help() - if err != nil { - _, _ = fmt.Fprintln(os.Stderr, err) - } + // Dont write help command to file. + if cmd.Parent() == nil { + skipLogFile = true + } + err := setupMessage(LogLevelCLI, skipLogFile, NoColor) + if err != nil { + return err + } - if len(args) > 0 { - if strings.Contains(args[0], config.ZarfPackagePrefix) || strings.Contains(args[0], "zarf-init") { - message.Warnf(lang.RootCmdDeprecatedDeploy, args[0]) - } - if args[0] == layout.ZarfYAML { - message.Warn(lang.RootCmdDeprecatedCreate) - } - } - }, + // Configure logger and add it to cmd context. + l, err := setupLogger(LogLevelCLI, LogFormat) + if err != nil { + return err + } + ctx := logger.WithContext(cmd.Context(), l) + cmd.SetContext(ctx) + + // Print out config location + common.PrintViperConfigUsed(cmd.Context()) + return nil +} + +func run(cmd *cobra.Command, _ []string) { + err := cmd.Help() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + } } // Execute is the entrypoint for the CLI. func Execute(ctx context.Context) { + // Add `zarf say` + rootCmd.AddCommand(say.Command()) + cmd, err := rootCmd.ExecuteContextC(ctx) if err == nil { return @@ -122,15 +138,94 @@ func init() { v := common.InitViper() + // Logs rootCmd.PersistentFlags().StringVarP(&LogLevelCLI, "log-level", "l", v.GetString(common.VLogLevel), lang.RootCmdFlagLogLevel) - rootCmd.PersistentFlags().StringVarP(&config.CLIArch, "architecture", "a", v.GetString(common.VArchitecture), lang.RootCmdFlagArch) + rootCmd.PersistentFlags().StringVar(&LogFormat, "log-format", v.GetString(common.VLogFormat), "Select a logging format. Defaults to 'text'. Valid options are: 'text', 'json'") rootCmd.PersistentFlags().BoolVar(&SkipLogFile, "no-log-file", v.GetBool(common.VNoLogFile), lang.RootCmdFlagSkipLogFile) rootCmd.PersistentFlags().BoolVar(&message.NoProgress, "no-progress", v.GetBool(common.VNoProgress), lang.RootCmdFlagNoProgress) rootCmd.PersistentFlags().BoolVar(&NoColor, "no-color", v.GetBool(common.VNoColor), lang.RootCmdFlagNoColor) + + rootCmd.PersistentFlags().StringVarP(&config.CLIArch, "architecture", "a", v.GetString(common.VArchitecture), lang.RootCmdFlagArch) rootCmd.PersistentFlags().StringVar(&config.CommonOptions.CachePath, "zarf-cache", v.GetString(common.VZarfCache), lang.RootCmdFlagCachePath) rootCmd.PersistentFlags().StringVar(&config.CommonOptions.TempDirectory, "tmpdir", v.GetString(common.VTmpDir), lang.RootCmdFlagTempDir) + + // Security rootCmd.PersistentFlags().BoolVar(&config.CommonOptions.Insecure, "insecure", v.GetBool(common.VInsecure), lang.RootCmdFlagInsecure) rootCmd.PersistentFlags().MarkDeprecated("insecure", "please use --plain-http, --insecure-skip-tls-verify, or --skip-signature-validation instead.") rootCmd.PersistentFlags().BoolVar(&config.CommonOptions.PlainHTTP, "plain-http", v.GetBool(common.VPlainHTTP), lang.RootCmdFlagPlainHTTP) rootCmd.PersistentFlags().BoolVar(&config.CommonOptions.InsecureSkipTLSVerify, "insecure-skip-tls-verify", v.GetBool(common.VInsecureSkipTLSVerify), lang.RootCmdFlagInsecureSkipTLSVerify) + + // HACK(mkcp): This is a workaround for us testing that help output matches to the byte. Undo this and update tests + // before release. + rootCmd.PersistentFlags().MarkHidden("log-format") +} + +// setup Logger handles creating a logger and setting it as the global default. +func setupLogger(level, format string) (*slog.Logger, error) { + // If we didn't get a level from config, fallback to "info" + if level == "" { + level = "info" + } + sLevel, err := logger.ParseLevel(level) + if err != nil { + return nil, err + } + cfg := logger.Config{ + Level: sLevel, + Format: logger.Format(format), + Destination: logger.DestinationDefault, + } + l, err := logger.New(cfg) + if err != nil { + return nil, err + } + logger.SetDefault(l) + l.Debug("logger successfully initialized", "cfg", cfg) + return l, nil +} + +// setupMessage configures message while we migrate over to logger. +func setupMessage(logLevel string, skipLogFile, noColor bool) error { + // TODO(mkcp): Delete no-color + if noColor { + message.DisableColor() + } + + if logLevel != "" { + match := map[string]message.LogLevel{ + // NOTE(mkcp): Add error for forwards compatibility with logger + "error": message.WarnLevel, + "warn": message.WarnLevel, + "info": message.InfoLevel, + "debug": message.DebugLevel, + "trace": message.TraceLevel, + } + lvl, ok := match[logLevel] + if !ok { + return errors.New("invalid log level, valid options are warn, info, debug, error, and trace") + } + message.SetLogLevel(lvl) + message.Debug("Log level set to " + logLevel) + } + + // Disable progress bars for CI envs + if os.Getenv("CI") == "true" { + message.Debug("CI environment detected, disabling progress bars") + message.NoProgress = true + } + + if !skipLogFile { + ts := time.Now().Format("2006-01-02-15-04-05") + f, err := os.CreateTemp("", fmt.Sprintf("zarf-%s-*.log", ts)) + if err != nil { + return fmt.Errorf("could not create a log file in a the temporary directory: %w", err) + } + logFile, err := message.UseLogFile(f) + if err != nil { + return fmt.Errorf("could not save a log file to the temporary directory: %w", err) + } + pterm.SetDefaultOutput(io.MultiWriter(os.Stderr, logFile)) + message.Notef("Saving log file to %s", f.Name()) + } + return nil } diff --git a/src/cmd/say/say.go b/src/cmd/say/say.go new file mode 100644 index 0000000000..94daed409f --- /dev/null +++ b/src/cmd/say/say.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package say prints out the adorable creature we all know and love. +package say + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// Command prints out the Zarf logo. +func Command() *cobra.Command { + return &cobra.Command{ + Use: "say", + Short: "Print Zarf logo", + Long: "Print out the adorable Zarf logo", + // HACK(mkcp): Hidden is a workaround until we update `test-docs-and-schema` for the new command and flags. + Hidden: true, + RunE: func(_ *cobra.Command, _ []string) error { + _, err := fmt.Fprintln(os.Stderr, logo()) + return err + }, + } +} + +func logo() string { + return ` +         *,                                                                               +         *(((&&&&&/*.                                                                     +          *(((((%&&&&&&&*,                                                                +           *(((((((&&&&&&&&&*              ,,*****,.                      **%&&&&&((((((  +            *(((((((((&&&&&&&@*    **@@@@@@&&&&&&&&&&@@@@@**         */&&&&&&((((((((((   +              *((((((///(&&&&&&@@@@&&&&@@@@@@@@@&&&&&&&&&&&&&&@/* *%&&&&&&/////((((((*    +                *(((///////&&&&&&&&&&&&&@@@@@@@@@&&&&&&&&&&&&&&&&&(%&&&/**///////(/*      +       */&&&&&&&&&&&&&&&&*/***&%&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&/*******///*          +   *&%&&&&&&&&&&&&&&&&&&&&&&&***&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&****/&&&&&&&&&&&&(/* + */((((((((((((((///////******%%&&&&&&&&//&@@*&&&&&&&&&&&&&&&&&&&#%&&&&*/####(/////((((/. +     */((((((((((///////******%%%%%%%%%(##@@@//%&%%%%%%%&%&%%&&/(@@(/&&(***///////(((*    +          ***(((((/////********%%%%%%%/&%***/((%%%%%%%%%%%%%%%%(#&@*%/%%***/////**        +            *&&%%%%%%//*******%%%%%%%%@@****%%/%%%%%%%%%%%%%%%%%%***@@%%**(%%%&&*         +          *&&%%%%%(////******/(%%%%%%%@@@**@@&%%%%%%%%%%%%%%%%%(@@*%@@%%*****////%&*      +        *&%%%%%#////////***/////%%%%%%*@@@@@/%%%%%%%%%%%%%%%%%%%%@@@@%%*****///////((*    +       *%%%%((((///////*    *////(%%%%%%##%%%%%%%%%%%(%%%%*%%%%%%%%%%%*                   +      *(((((((/***            */////#%%%%%%%%%%#%%%%%%%%%%%%%%%%%%%%#*                    +                   %%(           ,*///((%%%%%%%%(**/#%%%##**/%%%%%*                       +                 %%%&&&&           *///*/(((((########//######**                          +                 %&&&&&*          *#######(((((((//////((((*                              +                                  ###%##############(((#####*                             +                   %@&&          *&#(%######*#########(#####/                             +                   /&&* ..       ,&#(/%####(*#########/#####/             #%@%&&&         +             **         &&     ./%##((*&####/(#######(#####*(*            %&&&&&&         +           *@%%@*             *&#####((((####*(#####(*###(*(##*              ,  %@&       +          *@%%%%*            *%######((((*%####/*((*%####/*(###*  *                       +         *@%%%%%%*      *##* **#(###((((///#*#*(((((/#**#((*(##**#,*/##*,    %@&&         +         *@%%%*%%%*  ****,*##/*#*##(((((((/(((((((((/(((*(((((###########*,  #&&#         +         *@%%%*(%%%/*   **######(#((..((((((((((((((((((*  ,*(#####(((((*,                +         *@%%%#(*%%%%*   ,**/####(* */(((((((((((((((((*     ,**,                         +          *@%%%*(/(%%%%/*     ******(((((((((((*(((((*                                    +           *@%%%#(((*/%%%%%%##%%*((((((((((((**((((*                                      +            *@%%%%*(((((((((((((((((((((((*/%*((*.             (&&&(                      +             ,*%%%%%%*((((((((((((((((**%%%**,                (&                          +                *%%%%%%%%%(/*****(#%%%%%**                      &%                        +                   ,**%%%%%%%%%%%%%***                                                    +                                                                                          +             ,((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((,           +                         .....(((((((/////////////////((((((((.....                       +                                                                                          +            ///////////////      ///////      *****************  ***************,         +                    ////.       ///  ////     *///          ***  ****                     +                 ////,         ///    ////    *///////////////.  /////**/******           +              /////          //////////////   *///      *///     ///*                     +           ./////////////// ////         ///  *///        ////   ///*                     +                                                                                          + +` +} diff --git a/src/config/lang/english.go b/src/config/lang/english.go index c78e57a443..18889ab0a1 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -589,10 +589,6 @@ $ zarf tools update-creds artifact --artifact-push-username={USERNAME} --artifac // tools version CmdToolsVersionShort = "Print the version" - - // cmd viper setup - CmdViperErrLoadingConfigFile = "failed to load config file: %s" - CmdViperInfoUsingConfigFile = "Using config file %s" ) // Zarf Agent messages diff --git a/src/pkg/logger/logger.go b/src/pkg/logger/logger.go index 41400666d8..aa0d4b7f07 100644 --- a/src/pkg/logger/logger.go +++ b/src/pkg/logger/logger.go @@ -5,6 +5,7 @@ package logger import ( + "context" "fmt" "io" "log/slog" @@ -42,6 +43,8 @@ var validLevels = map[Level]bool{ // strLevels maps a string to its Level. var strLevels = map[string]Level{ + // NOTE(mkcp): Map trace to debug for backwards compatibility. + "trace": Debug, "debug": Debug, "info": Info, "warn": Warn, @@ -145,6 +148,34 @@ func New(cfg Config) (*slog.Logger, error) { return log, nil } +// ctxKey provides a location to store a logger in a context. +type ctxKey struct{} + +// defaultCtxKey provides a default key if one is not passed into From. +var defaultCtxKey = ctxKey{} + +// WithContext takes a context.Context and a *slog.Logger, storing it on the key +func WithContext(ctx context.Context, logger *slog.Logger) context.Context { + return context.WithValue(ctx, defaultCtxKey, logger) +} + +// From takes a context and reads out a *slog.Logger. If From does not find a value it will return a discarding logger +// similar to log-format "none". +func From(ctx context.Context) *slog.Logger { + // Grab value from key + log := ctx.Value(defaultCtxKey) + + // Ensure our value is a *slog.Logger before we cast. + switch l := log.(type) { + case *slog.Logger: + return l + default: + // Value is empty or not a *slog.Logger, pass back a Discard logger. + h := slog.NewTextHandler(DestinationNone, &slog.HandlerOptions{}) + return slog.New(h) + } +} + // Default retrieves a logger from the package default. This is intended as a fallback when a logger cannot easily be // passed in as a dependency, like when developing a new function. Use it like you would use context.TODO(). func Default() *slog.Logger { diff --git a/src/pkg/logger/logger_test.go b/src/pkg/logger/logger_test.go index db8851e25c..409922c7b2 100644 --- a/src/pkg/logger/logger_test.go +++ b/src/pkg/logger/logger_test.go @@ -5,6 +5,7 @@ package logger import ( + "context" "os" "testing" @@ -214,3 +215,13 @@ func Test_ParseLevelErrors(t *testing.T) { }) } } + +func TestContext(t *testing.T) { + t.Parallel() + + t.Run("can load a logger from the default key", func(t *testing.T) { + ctx := WithContext(context.Background(), Default()) + res := From(ctx) + require.NotNil(t, res) + }) +}