diff --git a/.gitignore b/.gitignore index 079f96f8..80a2b099 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ /release_assets # Ignore binaries generated by pre-v0.1.0 builds -check-mail +/check-mail -# Ignore plugin name -check_imap_mailbox +# Ignore plugin binary at root of repo (often generated here when testing) +/check_imap_mailbox diff --git a/cmd/check_imap_mailbox/main.go b/cmd/check_imap_mailbox/main.go index 87bfd2d3..5ddc5d67 100644 --- a/cmd/check_imap_mailbox/main.go +++ b/cmd/check_imap_mailbox/main.go @@ -13,7 +13,6 @@ import ( "strings" "github.com/emersion/go-imap" - "github.com/emersion/go-imap/client" zlog "github.com/rs/zerolog/log" @@ -39,8 +38,12 @@ func main() { // defer this from the start so it is the last deferred function to run defer nagiosExitState.ReturnCheckResults() - // Setup configuration by parsing user-provided flags - cfg, cfgErr := config.New() + // Setup configuration by parsing user-provided flags. This plugin does + // not currently support retrieving settings from a user-provided config + // file. Because this may change in the near future, we are structuring + // this plugin in a way to support that direction. + useConfigFile := false + cfg, cfgErr := config.New(useConfigFile) switch { case errors.Is(cfgErr, config.ErrVersionRequested): fmt.Println(config.Version()) @@ -66,123 +69,35 @@ func main() { nagiosExitState.BrandingCallback = config.Branding("Notification generated by ") } - server := fmt.Sprintf("%s:%d", cfg.Server, cfg.Port) + // loop over accounts + for _, account := range cfg.Accounts { - cfg.Log.Debug().Msg("connecting to remote server") - c, err := client.DialTLS(server, nil) - if err != nil { - cfg.Log.Error().Err(err).Msgf("error connecting to server") - nagiosExitState.LastError = err - nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: Error connecting to %s", - nagios.StateCRITICALLabel, - server, - ) - nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode - return - } - cfg.Log.Debug().Msg("Connected") + logger := cfg.Log.With(). + Str("username", account.Username). + Str("server", account.Server). + Int("port", account.Port). + Str("folders_to_check", account.Folders.String()). + Logger() - cfg.Log.Debug().Msg("Logging in") - if err := c.Login(cfg.Username, cfg.Password); err != nil { - cfg.Log.Error().Err(err).Msg("Login error occurred") - nagiosExitState.LastError = err - nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: Login error occurred", - nagios.StateCRITICALLabel, - ) - nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode - return - } - cfg.Log.Debug().Msg("Logged in") - - cfg.Log.Debug().Msg("Defer logout") - // At this point in the code we are connected to the remote server and are - // also logged in with a valid account. Calling os.Exit(X) at this point - // will cause any deferred functions to be skipped, so we instead use - // nagiosExitState.ReturnCheckResults() anywhere we would have called - // os.Exit() with the intended status code. This allows us to safely defer - // a Logout call here and have a reasonable expectation that it will both - // run AND that we'll have an opportunity to report those logout issues as - // this application exits. - defer func(accountName string) { - cfg.Log.Debug().Msgf("%s: Logging out", accountName) - if err := c.Logout(); err != nil { - cfg.Log.Error().Err(err).Msgf("%s: Failed to log out", accountName) - nagiosExitState.LastError = err + c, connectErr := mbxs.Connect(account.Server, account.Port, logger) + if connectErr != nil { + logger.Error().Err(connectErr).Msgf("error connecting to server") + nagiosExitState.LastError = connectErr nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: Error logging out", - nagios.StateWARNINGLabel, + "%s: Error connecting to %s", + nagios.StateCRITICALLabel, + account.Server, ) - nagiosExitState.ExitStatusCode = nagios.StateWARNINGExitCode - return - } - cfg.Log.Debug().Msgf("%s: Logged out", accountName) - }(cfg.Username) - - // Generate background job to list mailboxes, send down channel until done - mailboxes := make(chan *imap.MailboxInfo, 10) - done := make(chan error, 1) - // NOTE: This goroutine shuts down once c.List() finishes its work - go func() { - cfg.Log.Debug().Msg("Running c.List() to fetch a list of available mailboxes") - done <- c.List("", "*", mailboxes) - }() - - var mailboxesList = make([]string, 0, mailboxCountGuesstimate) - for m := range mailboxes { - cfg.Log.Debug().Msg("collected mailbox from channel") - mailboxesList = append(mailboxesList, m.Name) - } - - if err := <-done; err != nil { - cfg.Log.Error().Err(err).Msg("Error occurred listing mailboxes") - nagiosExitState.LastError = err - nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: Error occurred listing mailboxes", - nagios.StateCRITICALLabel, - ) - nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode - return - } - - cfg.Log.Debug().Msg("no errors encountered listing mailboxes") - - // Prove that our slice is intact - for _, m := range mailboxesList { - cfg.Log.Debug().Str("mailbox", m).Msg("") - } - - // Confirm that requested folders are present on server - var validatedMailboxesList = make([]string, 0, mailboxCountGuesstimate) - for _, folder := range cfg.Folders { - cfg.Log.Debug().Str("mailbox", folder).Msg("Processing requested folder") - - // At this point we are looping over the requested folders, but - // haven't yet confirmed that they exist as mailboxes on the remote - // server. - - if strings.ToLower(folder) == "inbox" { - - // NOTE: The "inbox" mailbox/folder name is NOT case-sensitive, - // but *all* others should be considered case-sensitive. We should - // be able to safely skip validating "inbox" here since it is a - // required mailbox/folder name, but all the same we will play it - // safe and perform a case-insensitive check for a match. - cfg.Log.Debug().Str("mailbox", folder).Msg("Performing case-insensitive validation") - if textutils.InList(folder, mailboxesList, true) { - validatedMailboxesList = append(validatedMailboxesList, folder) - } + nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode - continue + return } - cfg.Log.Debug().Str("mailbox", folder).Msg("Performing case-sensitive validation") - if !textutils.InList(folder, mailboxesList, false) { - cfg.Log.Error().Str("mailbox", folder).Bool("found", false).Msg("") - nagiosExitState.LastError = fmt.Errorf("mailbox not found: %q", folder) + if loginErr := mbxs.Login(c, account.Username, account.Password, logger); loginErr != nil { + logger.Error().Err(loginErr).Msg("Login error occurred") + nagiosExitState.LastError = loginErr nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: Mailbox not found", + "%s: Login error occurred", nagios.StateCRITICALLabel, ) nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode @@ -190,76 +105,173 @@ func main() { return } - // At this point we have confirmed that the requested folder to - // monitor is in the list of folders found on the server - cfg.Log.Debug().Str("mailbox", folder).Bool("found", true).Msg("") - validatedMailboxesList = append(validatedMailboxesList, folder) - - } + logger.Debug().Msg("Defer logout") + // At this point in the code we are connected to the remote server and are + // also logged in with a valid account. Calling os.Exit(X) at this point + // will cause any deferred functions to be skipped, so we instead use + // nagiosExitState.ReturnCheckResults() anywhere we would have called + // os.Exit() with the intended status code. This allows us to safely defer + // a Logout call here and have a reasonable expectation that it will both + // run AND that we'll have an opportunity to report those logout issues as + // this application exits. + defer func(accountName string) { + logger.Debug().Msgf("%s: Logging out", accountName) + if err := c.Logout(); err != nil { + logger.Error().Err(err).Msgf("%s: Failed to log out", accountName) + nagiosExitState.LastError = err + nagiosExitState.ServiceOutput = fmt.Sprintf( + "%s: Error logging out", + nagios.StateWARNINGLabel, + ) + nagiosExitState.ExitStatusCode = nagios.StateWARNINGExitCode + return + } + logger.Debug().Msgf("%s: Logged out", accountName) + }(account.Username) + + // Generate background job to list mailboxes, send down channel until done + mailboxes := make(chan *imap.MailboxInfo, 10) + done := make(chan error, 1) + // NOTE: This goroutine shuts down once c.List() finishes its work + go func() { + logger.Debug().Msg("Running c.List() to fetch a list of available mailboxes") + done <- c.List("", "*", mailboxes) + }() + + var mailboxesList = make([]string, 0, mailboxCountGuesstimate) + for m := range mailboxes { + logger.Debug().Msg("collected mailbox from channel") + mailboxesList = append(mailboxesList, m.Name) + } - // At this point we have created a list of validated mailboxes. Process - // them to determine number of emails within each of them. Based on our - // existing check and manual processing schedule, we normally see - // somewhere between 1 and 5 mail items for normal accounts and under 30 - // for heavily spammed accounts. Preallocating the results slice with a - // midrange starting value for now, but keeping the initial length at 0 - // to allow append() to work as expected. - var results = make(mbxs.MailboxCheckResults, 0, 10) - for _, folder := range validatedMailboxesList { - - cfg.Log.Debug().Msg("Selecting mailbox") - mailbox, err := c.Select(folder, false) - if err != nil { - cfg.Log.Error().Err(err).Str("mailbox", folder).Msg("Error occurred selecting mailbox") + if err := <-done; err != nil { + logger.Error().Err(err).Msg("Error occurred listing mailboxes") nagiosExitState.LastError = err nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: Error occurred selecting mailbox %s", + "%s: Error occurred listing mailboxes", nagios.StateCRITICALLabel, - folder, ) nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode return } - cfg.Log.Debug().Str("mailbox", folder).Msgf("Mailbox flags: %v", mailbox.Flags) + logger.Debug().Msg("no errors encountered listing mailboxes") - cfg.Log.Debug().Msgf("%d mail items found in %s for %s", - mailbox.Messages, folder, cfg.Username) + // Prove that our slice is intact + for _, m := range mailboxesList { + logger.Debug().Str("mailbox", m).Msg("") + } - results = append(results, mbxs.MailboxCheckResult{ - MailboxName: folder, - ItemsFound: int(mailbox.Messages), - }) - } + // Confirm that requested folders are present on server + var validatedMailboxesList = make([]string, 0, mailboxCountGuesstimate) + for _, folder := range account.Folders { + logger.Debug().Str("mailbox", folder).Msg("Processing requested folder") - // Evaluate whether anything was found and sound an alert if so - if results.GotMail() { - cfg.Log.Debug().Msgf("%d messages found: %s", - results.TotalMessagesFound(), - results.MessagesFoundSummary(), - ) - nagiosExitState.LastError = nil - nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: %s: %d messages found: %s", - nagios.StateWARNINGLabel, - cfg.Username, - results.TotalMessagesFound(), - results.MessagesFoundSummary(), - ) - nagiosExitState.ExitStatusCode = nagios.StateWARNINGExitCode - return + // At this point we are looping over the requested folders, but + // haven't yet confirmed that they exist as mailboxes on the remote + // server. + + if strings.ToLower(folder) == "inbox" { + + // NOTE: The "inbox" mailbox/folder name is NOT case-sensitive, + // but *all* others should be considered case-sensitive. We should + // be able to safely skip validating "inbox" here since it is a + // required mailbox/folder name, but all the same we will play it + // safe and perform a case-insensitive check for a match. + logger.Debug().Str("mailbox", folder).Msg("Performing case-insensitive validation") + if textutils.InList(folder, mailboxesList, true) { + validatedMailboxesList = append(validatedMailboxesList, folder) + } + + continue + } + + logger.Debug().Str("mailbox", folder).Msg("Performing case-sensitive validation") + if !textutils.InList(folder, mailboxesList, false) { + logger.Error().Str("mailbox", folder).Bool("found", false).Msg("") + nagiosExitState.LastError = fmt.Errorf("mailbox not found: %q", folder) + nagiosExitState.ServiceOutput = fmt.Sprintf( + "%s: Mailbox not found", + nagios.StateCRITICALLabel, + ) + nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode + + return + } + + // At this point we have confirmed that the requested folder to + // monitor is in the list of folders found on the server + logger.Debug().Str("mailbox", folder).Bool("found", true).Msg("") + validatedMailboxesList = append(validatedMailboxesList, folder) + + } + + // At this point we have created a list of validated mailboxes. Process + // them to determine number of emails within each of them. Based on our + // existing check and manual processing schedule, we normally see + // somewhere between 1 and 5 mail items for normal accounts and under 30 + // for heavily spammed accounts. Preallocating the results slice with a + // midrange starting value for now, but keeping the initial length at 0 + // to allow append() to work as expected. + var results = make(mbxs.MailboxCheckResults, 0, 10) + for _, folder := range validatedMailboxesList { + + logger.Debug().Msg("Selecting mailbox") + mailbox, err := c.Select(folder, false) + if err != nil { + logger.Error().Err(err).Str("mailbox", folder).Msg("Error occurred selecting mailbox") + nagiosExitState.LastError = err + nagiosExitState.ServiceOutput = fmt.Sprintf( + "%s: Error occurred selecting mailbox %s", + nagios.StateCRITICALLabel, + folder, + ) + nagiosExitState.ExitStatusCode = nagios.StateCRITICALExitCode + return + } + + logger.Debug().Str("mailbox", folder).Msgf("Mailbox flags: %v", mailbox.Flags) + + logger.Debug().Msgf("%d mail items found in %s for %s", + mailbox.Messages, folder, account.Username) + + results = append(results, mbxs.MailboxCheckResult{ + MailboxName: folder, + ItemsFound: int(mailbox.Messages), + }) + } + + // Evaluate whether anything was found and sound an alert if so + if results.GotMail() { + logger.Debug().Msgf("%d messages found: %s", + results.TotalMessagesFound(), + results.MessagesFoundSummary(), + ) + nagiosExitState.LastError = nil + nagiosExitState.ServiceOutput = fmt.Sprintf( + "%s: %s: %d messages found: %s", + nagios.StateWARNINGLabel, + account.Username, + results.TotalMessagesFound(), + results.MessagesFoundSummary(), + ) + nagiosExitState.ExitStatusCode = nagios.StateWARNINGExitCode + return + } } // Give the all clear: no mail was found cfg.Log.Debug().Msg("No messages found to report") + + // these values are known, consistent regardless of checking one or many + // accounts nagiosExitState.LastError = nil - nagiosExitState.ServiceOutput = fmt.Sprintf( - "%s: %s: No messages found in folders: %s", - nagios.StateOKLabel, - cfg.Username, - cfg.Folders.String(), - ) nagiosExitState.ExitStatusCode = nagios.StateOKExitCode + + // customize ServiceOutput and LongServiceOutput based on number of + // specified accounts + setSummary(cfg.Accounts, &nagiosExitState) + // implied return here :) } diff --git a/cmd/check_imap_mailbox/summary.go b/cmd/check_imap_mailbox/summary.go new file mode 100644 index 00000000..b58b21a3 --- /dev/null +++ b/cmd/check_imap_mailbox/summary.go @@ -0,0 +1,52 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/check-mail +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package main + +import ( + "fmt" + + "github.com/atc0005/check-mail/internal/config" + "github.com/atc0005/go-nagios" +) + +// setSummary customizes nagios.ExitState's ServiceOutput and +// LongServiceOutput based on number of user-specified accounts. +func setSummary(accounts []config.MailAccount, nes *nagios.ExitState) { + + if len(accounts) == 1 { + nes.ServiceOutput = fmt.Sprintf( + "%s: %s: No messages found in folders: %s", + nagios.StateOKLabel, + accounts[0].Username, + accounts[0].Folders.String(), + ) + + // We're done here. Not much to say if only checking one account. + return + } + + nes.ServiceOutput = fmt.Sprintf( + "%s: %s: No messages found in specified folders for accounts: %v", + nagios.StateOKLabel, + accounts[0].Username, + accounts, + ) + + for _, account := range accounts { + accountSummary := fmt.Sprintf( + "* Account: %s%s** Folders: %s%s%s", + account.Username, + nagios.CheckOutputEOL, + account.Folders.String(), + nagios.CheckOutputEOL, + nagios.CheckOutputEOL, + ) + nes.LongServiceOutput += accountSummary + } + +} diff --git a/internal/config/config.go b/internal/config/config.go index c1a7d9c7..18bd089a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -38,6 +38,19 @@ var Usage = func() { // information var ErrVersionRequested = errors.New("version information requested") +// MailAccount represents an email account listed within a configuration file. +type MailAccount struct { + Server string + Port int + Username string + Password string + Folders multiValueFlag + + // Name is often the bare username for the email account, but may not be. + // This is used as the section header within the configuration file. + Name string +} + // multiValueFlag is a custom type that satisfies the flag.Value interface in // order to accept multiple values for some of our flags. type multiValueFlag []string @@ -74,27 +87,6 @@ func (i *multiValueFlag) Set(value string) error { // command-line flags. type Config struct { - // Folders to check for mail. This value is provided a comma-separated - // list. - Folders multiValueFlag - - // Username represents the account used to login to the remote mail - // server. This is often in the form of an email address. - Username string - - // Password is the remote mail server account password. - Password string - - // Server is the fully-qualified domain name of the remote mail server. - Server string - - // Port is the TCP port used to connect to the remote server. This is - // commonly 993. - Port int - - // LoggingLevel is the supported logging level for this application. - LoggingLevel string - // EmitBranding controls whether "generated by" text is included at the // bottom of application output. This output is included in the Nagios // dashboard and notifications. This output may not mix well with branding @@ -106,6 +98,13 @@ type Config struct { // the version string and then immediately exit the application. ShowVersion bool + // LoggingLevel is the supported logging level for this application. + LoggingLevel string + + // Accounts is the collection of IMAP mail accounts checked by + // applications provided by this project. + Accounts []MailAccount + // Log is an embedded zerolog Logger initialized via config.New(). Log zerolog.Logger } @@ -129,16 +128,16 @@ func Branding(msg string) func() string { // provided flag and config file values. It is responsible for validating // user-provided values and initializing the logging settings used by this // application. -func New() (*Config, error) { +func New(useConfigFile bool) (*Config, error) { var config Config - config.handleFlagsConfig() + config.handleFlagsConfig(useConfigFile) if config.ShowVersion { return nil, ErrVersionRequested } - if err := config.validate(); err != nil { + if err := config.validate(useConfigFile); err != nil { return nil, fmt.Errorf("configuration validation failed: %w", err) } @@ -150,16 +149,14 @@ func New() (*Config, error) { // package output functions for Nagios via stdout and logging package for // troubleshooting via stderr. // - // Also, set common fields here so that we don't have to repeat them - // explicitly later. This will hopefully help to standardize the log + // We set some common fields here so that we don't have to repeat them + // explicitly later and then set additional fields while processing each + // email account. This approach is intended to help standardize the log // messages to make them easier to search through later when // troubleshooting. config.Log = zerolog.New(os.Stderr).With().Caller(). Str("version", Version()). - Str("username", config.Username). - Str("server", config.Server). - Int("port", config.Port). - Str("folders_to_check", config.Folders.String()).Logger() + Logger() if err := logging.SetLoggingLevel(config.LoggingLevel); err != nil { return nil, err @@ -169,28 +166,43 @@ func New() (*Config, error) { } -// Validate verifies all Config struct fields have been provided acceptable +// validate verifies all Config struct fields have been provided acceptable // values. -func (c Config) validate() error { - - if c.Folders == nil { - return fmt.Errorf("one or more folders not provided") - } - - if c.Port < 0 { - return fmt.Errorf("invalid TCP port number %d", c.Port) - } - - if c.Username == "" { - return fmt.Errorf("username not provided") - } - - if c.Password == "" { - return fmt.Errorf("password not provided") - } - - if c.Server == "" { - return fmt.Errorf("server FQDN not provided") +func (c Config) validate(useConfigFile bool) error { + + for _, account := range c.Accounts { + if account.Folders == nil { + return fmt.Errorf( + "one or more folders not provided for account %s", + account.Name, + ) + } + + if account.Port < 0 { + return fmt.Errorf( + "invalid TCP port number %d provided for account %s", + account.Port, + account.Name, + ) + } + + if account.Username == "" { + return fmt.Errorf("username not provided for account %s", + account.Name, + ) + } + + if account.Password == "" { + return fmt.Errorf("password not provided for account %s", + account.Name, + ) + } + + if account.Server == "" { + return fmt.Errorf("server FQDN not provided for account %s", + account.Name, + ) + } } requestedLoggingLevel := strings.ToLower(c.LoggingLevel) diff --git a/internal/config/flags.go b/internal/config/flags.go index 6b183cde..12c619b0 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -9,16 +9,23 @@ package config import "flag" -func (c *Config) handleFlagsConfig() { +func (c *Config) handleFlagsConfig(acceptConfigFile bool) { - flag.Var(&c.Folders, "folders", foldersFlagHelp) - flag.StringVar(&c.Username, "username", defaultUsername, usernameFlagHelp) - flag.StringVar(&c.Password, "password", defaultPassword, passwordFlagHelp) - flag.StringVar(&c.Server, "server", defaultServer, serverFlagHelp) - flag.IntVar(&c.Port, "port", defaultPort, portFlagHelp) - flag.StringVar(&c.LoggingLevel, "log-level", defaultLoggingLevel, loggingLevelFlagHelp) - flag.BoolVar(&c.EmitBranding, "branding", defaultEmitBranding, emitBrandingFlagHelp) + var account MailAccount + + // shared flags flag.BoolVar(&c.ShowVersion, "version", defaultDisplayVersionAndExit, versionFlagHelp) + flag.StringVar(&c.LoggingLevel, "log-level", defaultLoggingLevel, loggingLevelFlagHelp) + + // currently only applies to Nagios plugin + if !acceptConfigFile { + flag.Var(&account.Folders, "folders", foldersFlagHelp) + flag.StringVar(&account.Username, "username", defaultUsername, usernameFlagHelp) + flag.StringVar(&account.Password, "password", defaultPassword, passwordFlagHelp) + flag.StringVar(&account.Server, "server", defaultServer, serverFlagHelp) + flag.IntVar(&account.Port, "port", defaultPort, portFlagHelp) + flag.BoolVar(&c.EmitBranding, "branding", defaultEmitBranding, emitBrandingFlagHelp) + } // Allow our function to override the default Help output flag.Usage = Usage @@ -26,4 +33,10 @@ func (c *Config) handleFlagsConfig() { // parse flag definitions from the argument list flag.Parse() + // if CLI-provided values were given then record those as an entry in the + // list + if !acceptConfigFile { + c.Accounts = append(c.Accounts, account) + } + } diff --git a/internal/mbxs/connect.go b/internal/mbxs/connect.go new file mode 100644 index 00000000..838ae568 --- /dev/null +++ b/internal/mbxs/connect.go @@ -0,0 +1,52 @@ +// Copyright 2020 Adam Chalkley +// +// https://github.com/atc0005/check-mail +// +// Licensed under the MIT License. See LICENSE file in the project root for +// full license information. + +package mbxs + +import ( + "fmt" + + "github.com/emersion/go-imap/client" + "github.com/rs/zerolog" +) + +// Connect opens a connection to the specified IMAP server, returns a client +// connection. +func Connect(server string, port int, logger zerolog.Logger) (*client.Client, error) { + + s := fmt.Sprintf("%s:%d", server, port) + + logger.Debug().Msg("connecting to remote server") + c, err := client.DialTLS(s, nil) + if err != nil { + errMsg := "error connecting to server" + logger.Error().Err(err).Msgf(errMsg) + + return nil, fmt.Errorf("%s: %w", errMsg, err) + } + logger.Info().Msg("Connected") + + return c, nil + +} + +// Login uses the provided client connection and credentials to login to the +// remote server. +func Login(client *client.Client, username string, password string, logger zerolog.Logger) error { + + logger.Debug().Msg("Logging in") + if err := client.Login(username, password); err != nil { + errMsg := "login error occurred" + logger.Error().Err(err).Msg(errMsg) + + return fmt.Errorf("%s: %w", errMsg, err) + } + logger.Info().Msg("Logged in") + + return nil + +}