@@ 26,6 26,9 @@ import (
"net/url"
"os"
"strconv"
+ "strings"
+
+ "golang.org/x/exp/slices"
"github.com/spf13/viper"
"go.uber.org/zap"
@@ 36,37 39,72 @@ import (
type Notification struct {
messageBody string
imageAttachment string // Will be sent empty for now.
- device string
+ device string // Contains a comma delimited list of devices which will receive the message.
html int // Set to 1 for enabling HTML parsing, will be 0 for now.
- priority int // Can be set from -2 to 2. 0 is default.
- sound string // See the API docs for valid names.
+ priority int // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels.
+ sound string // Notification's sound. See the API docs for valid names.
timestamp int // This is a UNIX timestamp.
- title string
+ title string // Notification's title.
url string // You can attach a URL to a notification.
urlTitle string // You can add a title to the URL, if left blank, the URL will be shown.
}
// This struct contains the configuration information we are going to use during execution.
type RuntimeConfiguration struct {
- version string // Stores the version of the application.
- versionRequested bool // Set to true if version is requested.
- dryrun bool // Set to true if we're simulating, but not sending messages.
- configFilePath string // This variable stores our configuration file's path.
- apiKey string // This is the secret key we use for sending messages.
- userKey string // This is the secret key of the user, used as a recipient address.
- logfilePaths []string // This variable contains the absolute path for the logfile.
- verbosity int // This variable stores the requested log verbosity. Loggers will be setup according to this.
+ applicationName string // Stores the sanitized version of the application name.
+ version string // Stores the version of the application.
+ dryrun bool // Setting to true will make code switch to "simulation" mode.
+ configFilePath string // This variable stores our configuration file's path.
+ apiKey string // Secret key we use for sending messages.
+ userKey string // Secret key of the user, used as a recipient address.
+ logfilePaths []string // Contains the absolute path for the logfile.
+ logLevel string // Contains the logging level the application starts with.
+}
+
+// Following struct is for temporarily storing flag values passed during program invocation.
+// Storing these here allows us neat tricks like configuration file location overrides
+// without altering the initialization order of the application.
+type FlagStorage struct {
+ // Below block contains variables about the notification itself.
+ device string // Contains a comma delimited list of devices which will receive the message.
+ priority int // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels.
+ sound string // Notification's sound. See the API docs for valid names.
+ title string // Notification's title.
+ url string // You can attach a URL to a notification.
+ urlTitle string // You can add a title to the URL, if left blank, the URL will be shown.
+
+ // Configuration file related flags are stored below:
+ configFilePath string // Store the configuration file override here, if present.
+
+ // Next block contains options about program execution and behavior.
+ versionRequested bool // Setting to true will make nudge to write version and exit.
+ dryrun bool // Setting to true will make code switch to "simulation" mode.
+ logLevel string // Contains the logging level the application starts with.
+}
+
+// Following struct contains the set of acceptable configuration option values.
+// checkConfigurationSanity() function consulsts this structure while verifying the final configuration.
+type ValidConfigurationOptions struct {
+ possibleLogLevels []string
+}
+
+// This function initializes the ValidConfigurationOptions data structure, which will be
+// used by the configuration sanity checker down the road, to make sure that everything
+// is nice and shiny.
+func initializeValidConfigurationOptions(ValidConfigurationOptions *ValidConfigurationOptions) {
+ ValidConfigurationOptions.possibleLogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"}
}
// This function stores and applies the defaults of the application.
// It's called first to initialize the defaults, then config file and the lastly the flags
// Can change these defaults.
-func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, logger *zap.SugaredLogger) {
+func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification) {
// Get the notificationTitle first. We will use it for message titles, as default
notificationTitle, err := os.Hostname()
if err != nil {
- logger.Warnf("Encountered an error while obtaining hostname, will use \"Nudge\" instead.")
+ // Since logger is not up when we call this function, we'll use Println here.
+ fmt.Println("Encountered an error while obtaining hostname, will use \"Nudge\" instead.")
notificationTitle = "Nudge"
}
@@ 78,43 116,53 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif
notificationToSend.title = notificationTitle
// Let's set defaults for the application itself, again where it makes sense.
- runtimeConfiguration.version = "0.1.0"
+ applicationName := strings.Split(os.Args[0], "/")
+ runtimeConfiguration.applicationName = applicationName[len(applicationName)-1]
+ runtimeConfiguration.version = "0.2.0"
runtimeConfiguration.dryrun = false
runtimeConfiguration.logfilePaths = []string{"stdout"}
- runtimeConfiguration.verbosity = 0
+ runtimeConfiguration.logLevel = "warn"
}
// This function reads the configuration file and applies it to relevant data structures.
// It's not designed as a method, since it might touch multiple data structures at once.
-func readAndApplyConfiguration(configurationFilePath *string, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, logger *zap.SugaredLogger) {
- // Let's define the nature of our configuration file.
- viper.SetConfigName("nudge.conf")
- viper.SetConfigType("toml")
+func readAndApplyConfiguration(configurationFilePath *string, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
+ if flagStorage.configFilePath != "" {
+ logger.Debugf("Configuration file is set to %s, via command line.", flagStorage.configFilePath)
+ viper.SetConfigFile(flagStorage.configFilePath)
+ viper.SetConfigType("toml") // Because we use ".conf" extension, not ".toml".
- // If we have overriden the config path, add it. If not, add the defaults.
- // We need user's home directory to add some neat magic.
- userConfigDirectory, err := os.UserConfigDir()
+ } else {
+ // If no configuration file override is given:
+ // Let's define the nature of our configuration file.
+ viper.SetConfigName("nudge.conf")
+ viper.SetConfigType("toml")
- if err != nil {
- logger.Panicf("Error getting user's config directory, exiting (error is %s).", err.Error())
- }
+ // We need user's home directory to add some neat magic.
+ userConfigDirectory, err := os.UserConfigDir()
- workingDirectory, err := os.Getwd()
- logger.Debugf("Current directory is %s", workingDirectory)
+ if err != nil {
+ logger.Panicf("Error getting user's config directory, exiting (error is %s).", err.Error())
+ }
- if err != nil {
- logger.Panicf("Error getting working directory, exiting (error is %s).", err.Error())
- }
+ workingDirectory, err := os.Getwd()
+ logger.Debugf("Current directory is %s", workingDirectory)
+
+ if err != nil {
+ logger.Panicf("Error getting working directory, exiting (error is %s).", err.Error())
+ }
- // The logic is to allow user to override local config with a test config.
- // And to override system-wide config with a local config.
- viper.AddConfigPath(".") // User might create a test-config at the current directory.
- viper.AddConfigPath(userConfigDirectory) // User might create a user-wide config.
- viper.AddConfigPath("/etc") // A system-wide config can be present.
- viper.AddConfigPath(workingDirectory + "/conf") // The user may have installed the application in a portable way.
- viper.AddConfigPath(workingDirectory) // This is a last resort and test directory.
+ // The logic is to allow user to override local config with a test config.
+ // And to override system-wide config with a local config.
+ viper.AddConfigPath(".") // User might create a test-config at the current directory.
+ viper.AddConfigPath(userConfigDirectory) // User might create a user-wide config.
+ viper.AddConfigPath("/etc") // A system-wide config can be present.
+ viper.AddConfigPath(workingDirectory + "/conf") // The user may have installed the application in a portable way.
+ viper.AddConfigPath(workingDirectory) // This is a last resort and test directory.
+ }
- err = viper.ReadInConfig() // Find and read the config file
+ // Since this is common to both methods, this can live here.
+ err := viper.ReadInConfig() // Find and read the config file
// Handle errors reading the config file.
// Do not create warnings here, all config warnings are handled by checkConfigurationSanity() function.
@@ 125,60 173,143 @@ func readAndApplyConfiguration(configurationFilePath *string, runtimeConfigurati
// If we've reached here, we can take note of the file path now.
runtimeConfiguration.configFilePath = viper.ConfigFileUsed()
+ // Let the user know where the config file is.
+ logger.Debugf("Configuration file is found at %s.", runtimeConfiguration.configFilePath)
+
// Check options one by one whether they're set and apply present options.
// We do not panic if anything mandatory is missing, but we'll warn the user politely.
// Below is the pushover section.
if viper.IsSet("pushover.api_key") {
runtimeConfiguration.apiKey = viper.GetString("pushover.api_key")
+ logger.Debugf("Pushover API key found & set to %s.", runtimeConfiguration.apiKey)
}
if viper.IsSet("pushover.user_key") {
runtimeConfiguration.userKey = viper.GetString("pushover.user_key")
+ logger.Debugf("Pushover User key is found & set to %s.", runtimeConfiguration.userKey)
}
- // Options related to logging section handled next.
+ // Logging related configuration follows:
+ // Logging paths:
if viper.IsSet("logging.logging_paths") {
runtimeConfiguration.logfilePaths = viper.GetStringSlice("logging.logging_paths")
+ logger.Debugf("Logging paths are found & set to %s.", runtimeConfiguration.logfilePaths)
}
- // Logging related configuration follows:
- // TODO: Get logging level and process the data.
+ // Logging level:
+ if viper.IsSet("logging.log_level") {
+ runtimeConfiguration.logLevel = viper.GetString("logging.log_level")
+ logger.Debugf("Logging level is found and set to %s.", runtimeConfiguration.logLevel)
+ }
}
}
// This functions defines the flags, binds them and parses them.
// Note: Code defaults are also baked into this function as a start, they will be moved out later.
// Cue Lord of the Rings from Blind Guardian.
-func initFlags(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration) {
+func initFlags(notificationToSend *Notification, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
// In this initial version, we only handle some of the features. Rest will be handled later.
- flag.StringVar(¬ificationToSend.messageBody, "message", notificationToSend.messageBody, "Message to send in the notification.")
- flag.StringVar(¬ificationToSend.device, "devices", notificationToSend.device, "List of devices to be notified. Separate multiple devices with ','.")
- flag.IntVar(¬ificationToSend.priority, "priority", notificationToSend.priority, "Adjust notification priority. Between -2 and 2. (default "+strconv.Itoa(notificationToSend.priority)+")")
- flag.StringVar(¬ificationToSend.sound, "sound", notificationToSend.sound, "Set notification sound.")
- flag.StringVar(¬ificationToSend.title, "title", notificationToSend.title, "Notification title. Hostname is used if omitted.")
- flag.StringVar(¬ificationToSend.url, "url", notificationToSend.url, "An optional URL to attach to the notification.")
- flag.StringVar(¬ificationToSend.urlTitle, "url_title", notificationToSend.urlTitle, "An optional title for URL to send.")
- flag.BoolVar(&runtimeConfiguration.dryrun, "dryrun", runtimeConfiguration.dryrun, "Do not send the message, but pretend to do, for testing.")
-
- // Following flags are for controlling the output & verbosity.
- // TODO: Will be implemented in version 0.2
- // flag.IntVar(&runtimeConfiguration.verbosity, "verbosity", 1, "Set verbosity level. Between -1 and 5.")
- // flag.StringVar(&runtimeConfiguration.logfilePath, "logfile_path", "", "Define the path of the log file.")
+ // Values are sent to flagStorage, because we'll apply them after parsing the config file.
+ flag.StringVar(&flagStorage.device, "devices", notificationToSend.device, "List of devices to be notified. Separate multiple devices with ','.")
+ flag.IntVar(&flagStorage.priority, "priority", notificationToSend.priority, "Adjust notification priority. Between -2 and 2. (default "+strconv.Itoa(notificationToSend.priority)+")")
+ flag.StringVar(&flagStorage.sound, "sound", notificationToSend.sound, "Set notification sound.")
+ flag.StringVar(&flagStorage.title, "title", notificationToSend.title, "Notification title. Hostname is used if omitted.")
+ flag.StringVar(&flagStorage.url, "url", notificationToSend.url, "An optional URL to attach to the notification.")
+ flag.StringVar(&flagStorage.urlTitle, "url_title", notificationToSend.urlTitle, "An optional title for URL to send.")
+
+ // Below this point we handle the runtime configuration of Nudge.
+ flag.BoolVar(&flagStorage.dryrun, "dryrun", flagStorage.dryrun, "Simulate sending a notification.")
+
+ // Get logging level and save it.
+ flag.StringVar(&flagStorage.logLevel, "log_level", flagStorage.logLevel, "Change the logging level.")
// Following flag is for controlling the place of the configuration file.
- // FIXME: This feature is not working at the moment, so it's disabled.
- // flag.StringVar(&runtimeConfiguration.configFilePath, "config_path", runtimeConfiguration.configFilePath, "Define or override configuration file path.")
+ flag.StringVar(&flagStorage.configFilePath, "config_path", flagStorage.configFilePath, "Define or override configuration file path.")
// Always implement a version string.
- flag.BoolVar(&runtimeConfiguration.versionRequested, "version", false, "Print version and exit.")
+ flag.BoolVar(&flagStorage.versionRequested, "version", false, "Print version and exit.")
flag.Parse()
+
+ // We accept a single argument as the message. If we have the message, we set it.
+ // Otherwise, we leave the message empty. The check in the min() will
+ // handle the error condition.
+ if flag.NArg() == 1 {
+ notificationToSend.messageBody = flag.Arg(0)
+ }
+
+ // Print how many arguments we got, as a debug message. flag.Narg() returns the number
+ // of remaining arguments after parsing everything.
+ logger.Debugf("Received %d arguments, after parsing flags.", flag.NArg())
+}
+
+// This function simply copies relevant flags to runtime configuration and notification to send.
+func applyFlags(setFlags *[]string, flagStorage *FlagStorage, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, logger *zap.SugaredLogger) {
+ // TODO: Revise this function and add proper comments.
+ logger.Debugf("Starting to apply flags to runtime configuration.")
+ // This is left as a check, to see whether we can approach flag module globally.
+ logger.Debugf("Flag parse status is %t.", flag.Parsed())
+
+ logger.Debugf("The set flags are %s.", *setFlags)
+
+ for _, flagValue := range *setFlags {
+ logger.Debugf("Working on flag %s.", flagValue)
+
+ switch flagValue {
+ case "devices":
+ notificationToSend.device = flagStorage.device
+ logger.Debugf("Recipient devices are set to %s.", notificationToSend.device)
+ case "priority":
+ notificationToSend.priority = flagStorage.priority
+ logger.Debugf("Notification priority is changed to %d.", notificationToSend.priority)
+ case "sound":
+ notificationToSend.sound = flagStorage.sound
+ logger.Debugf("Notification sound is changed to %s.", notificationToSend.sound)
+ case "title":
+ notificationToSend.title = flagStorage.title
+ logger.Debugf("Notification title is changed to %s.", notificationToSend.title)
+ case "url":
+ notificationToSend.url = flagStorage.url
+ logger.Debugf("Notification attached URL is set to %s.", notificationToSend.url)
+ case "url_title":
+ notificationToSend.urlTitle = flagStorage.urlTitle
+ logger.Debugf("Notification attached URL will be shown as %s.", notificationToSend.urlTitle)
+ case "dryrun":
+ runtimeConfiguration.dryrun = flagStorage.dryrun
+ logger.Debugf("Dry run mode is set to %t.", runtimeConfiguration.dryrun)
+ case "log_level":
+ runtimeConfiguration.logLevel = flagStorage.logLevel
+ logger.Debugf("Logging level is set to %s.", runtimeConfiguration.logLevel)
+ case "config_path":
+ logger.Debugf("Ignoring flag %s. Handled elsewhere.", flagValue)
+ case "version":
+ logger.Debugf("Ignoring flag %s. Handled elsewhere.", flagValue)
+ default:
+ logger.Warnf("Unhandled flag %s. Ignoring.", flagValue)
+ }
+ }
+ // Skipped configPath, because it's handled in readAndApplyConfiguration.
+ // Skipped versionRequested, because it's handled in main() directly.
+
+ logger.Debugf("Application of flags is completed, returning.")
}
// This function checks configuration sanity when called, and creates warnings or errors depending on the situation.
// This function also called just before sending the notification, hence it handles showing help, creating warnings and other
// related user interfacing notifications.
-func checkConfigurationSanity(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, logger *zap.SugaredLogger) {
+func checkConfigurationSanity(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, validConfigurationOptions *ValidConfigurationOptions, logger *zap.SugaredLogger) {
+ // Check whether supplied logging level is acceptable.
+ // Pointer for the method is taken from https://stackoverflow.com/a/38654444
+ // Also see https://go.dev/blog/slices-intro & https://pkg.go.dev/golang.org/x/exp/slices
+ // Since slices.Index() returns -1 if the value is not found, comparing it with -1 and negating the result gives us the correct boolean value.
+ logLevelValid := !(slices.Index(validConfigurationOptions.possibleLogLevels, runtimeConfiguration.logLevel) == -1)
+
+ if !logLevelValid {
+ logger.Fatalf("Provided log level (%s) is not in the set of acceptable values. Please make sure you define or supply a valid log level.", runtimeConfiguration.logLevel)
+ }
+
+ logger.Debugf("Provided log level (%s) is valid.", runtimeConfiguration.logLevel)
+
// Check whether we have the API & User keys.
// Let's do some magic:
apiKeyPresent := len(runtimeConfiguration.apiKey) != 0 // This evaluates to bool, heh. :)
@@ 186,18 317,10 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura
// Print the warnings if required:
if !apiKeyPresent {
- logger.Warnf("API key is not found or defined. You can't send notifications until you set it.")
+ logger.Fatalf("API key is not found or defined. You can't send notifications until you set it.")
}
if !userKeyPresent {
- logger.Warnf("User key is not found or defined. You can't send notifications until you set it.")
- }
-
-
-
- // If no message is given, let's print help and exit.
- if notificationToSend.messageBody == "" {
- flag.PrintDefaults()
- os.Exit(1)
+ logger.Fatalf("User key is not found or defined. You can't send notifications until you set it.")
}
// If we arrive to this point, it means we can send a message, but we need to check
@@ 209,28 332,31 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura
// This function pretty prints application state.
// The output is limited to 80 chars to fit legacy terminals.
-func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notification) {
- fmt.Println("Current configuration")
- fmt.Println("---------------------")
- fmt.Printf("Configuration file path: %s\n", runtimeConfiguration.configFilePath)
- fmt.Printf("Pushover API key: %s\n", runtimeConfiguration.apiKey)
- fmt.Printf("Pushover user key: %s\n", runtimeConfiguration.userKey)
- fmt.Printf("Log file path: %s\n", runtimeConfiguration.logfilePaths)
- fmt.Printf("Log verbosity: %d\n\n", runtimeConfiguration.verbosity)
+func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notification, logger *zap.SugaredLogger) {
+ logger.Infof("Current configuration")
+ logger.Infof("---------------------")
+ logger.Infof("Configuration file path: %s", runtimeConfiguration.configFilePath)
+ logger.Infof("Pushover API key: %s", runtimeConfiguration.apiKey)
+ logger.Infof("Pushover user key: %s", runtimeConfiguration.userKey)
+ logger.Infof("Log file path: %s", runtimeConfiguration.logfilePaths)
+ logger.Infof("Current logging level is %s.", runtimeConfiguration.logLevel)
+ logger.Infof("") // Leave an empty line.
// Dump notification in a different block.
- fmt.Println("Notification details")
- fmt.Println("--------------------")
- fmt.Printf("Recipients: %s\n", notification.device)
- fmt.Printf("Priority: %d\n", notification.priority)
- fmt.Printf("Sound: %s\n", notification.sound)
- fmt.Printf("Timestamp: %d\n", notification.timestamp)
- fmt.Printf("HTML message: %d\n", notification.html)
- fmt.Printf("URL title: %s\n", notification.urlTitle)
- fmt.Printf("URL: %s\n", notification.url)
- fmt.Printf("Image attachment: %s\n", notification.imageAttachment)
- fmt.Printf("Message title: %s\n", notification.title)
- fmt.Printf("Message: %s\n", notification.messageBody)
+ logger.Infof("Notification details")
+ logger.Infof("--------------------")
+ logger.Infof("Recipients: %s", notification.device)
+ logger.Infof("Priority: %d", notification.priority)
+ logger.Infof("Sound: %s", notification.sound)
+ logger.Infof("Timestamp: %d", notification.timestamp)
+ logger.Infof("HTML message: %d", notification.html)
+ logger.Infof("URL title: %s", notification.urlTitle)
+ logger.Infof("URL: %s", notification.url)
+ logger.Infof("Image attachment: %s", notification.imageAttachment)
+ logger.Infof("Message title: %s", notification.title)
+ logger.Infof("Message: %s", notification.messageBody)
+
+ logger.Infof("") // Leave an empty line.
}
// This function sends the actual notification via PUSH request, via Pushover.
@@ 261,13 387,33 @@ func sendNotification(notificationToSend *Notification, runtimeConfiguration *Ru
func main() {
// Create the data structures we need.
var runtimeConfiguration RuntimeConfiguration
+ var flagStorage FlagStorage
var notificationToSend Notification
+ var validConfigurationOptions ValidConfigurationOptions
+
+ // Since there's no easy way to check whether a flag is set, I need a list of set flags.
+ var setFlags []string
+
+ // Start by applying default configuration before building things up.
+ // Start with initializing valid configuration space.
+ initializeValidConfigurationOptions(&validConfigurationOptions)
+
+ // Next, apply the defaults:
+ applyDefaultConfiguration(&runtimeConfiguration, ¬ificationToSend)
// TODO: Prettify logger with some stylistic configuration here.
// Relevant documentation is here: https://pkg.go.dev/go.uber.org/zap#hdr-Configuring_Zap
-
- rawJSON := []byte(`{
- "level": "warn",
+ /*
+ * To be able to ship 0.2 as quickly as possible, Zap will be configured as follows:
+ * 1- Create a good enough configuration with JSON based on system defaults.
+ * 2- Build Zap with that configuration.
+ * 3- Reconfigure Zap after parsing all the options and creating the final runtime
+ * configuration.
+ *
+ * XXX: This JSON config part will be replaced with a programmatic block later.
+ */
+ zapDefaultConfigJSON := []byte(`{
+ "level": "debug",
"encoding": "console",
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
@@ 280,11 426,24 @@ func main() {
}
}`)
- var cfg zap.Config
- if err := json.Unmarshal(rawJSON, &cfg); err != nil {
+ var zapRuntimeConfig zap.Config
+ if err := json.Unmarshal(zapDefaultConfigJSON, &zapRuntimeConfig); err != nil {
panic(err)
}
- logger := zap.Must(cfg.Build())
+
+ /*
+ * We build the logger with the default configuration before everything else.
+ * We know that the config is sane, because it's hardcoded. We're using built-in
+ * defaults at that point.
+ */
+ zapDefaultAtomicLevel, err := zap.ParseAtomicLevel(runtimeConfiguration.logLevel)
+
+ if err != nil {
+ panic (err)
+ }
+
+ zapRuntimeConfig.Level = zapDefaultAtomicLevel
+ logger := zap.Must(zapRuntimeConfig.Build())
defer logger.Sync() // Make sure that we sync when we exit.
@@ 292,33 451,65 @@ func main() {
sugaredLogger := logger.Sugar()
sugaredLogger.Debugf("Logger is up.")
- // First, apply the defaults:
- applyDefaultConfiguration(&runtimeConfiguration, ¬ificationToSend, sugaredLogger)
-
- // First, start with reading the config, if present.
- // If file is not present, fallback to defaults.
- readAndApplyConfiguration(&runtimeConfiguration.configFilePath, &runtimeConfiguration, ¬ificationToSend, sugaredLogger)
-
// Then let's see what we have at hand (options, parameters, flags).
- initFlags(¬ificationToSend, &runtimeConfiguration)
+ // Flags are parsed first, stored in a secondary config area.
+ // This allows us to override config file with flags more gracefully.
+ initFlags(¬ificationToSend, &flagStorage, sugaredLogger)
+ // Start with reading the config, if present.
+ // If file is not present, fallback to defaults.
+ readAndApplyConfiguration(&runtimeConfiguration.configFilePath, &runtimeConfiguration, ¬ificationToSend, &flagStorage, sugaredLogger)
+
+ // To be able to satisfy Defaults -> Configuration -> Flags chain,
+ // I need to apply the options got from flags to Runtime Configuration at this point.
+ // However I need to get the list of set flags from Flag. Since only visit() allows me
+ // to do it, I need to use this anonymous function to make this work.
+ flag.Visit(func(setFlag *flag.Flag) {
+ setFlags = append(setFlags, setFlag.Name)
+ })
+
+ // Then I can apply the set flags.
+ applyFlags(&setFlags, &flagStorage, &runtimeConfiguration, ¬ificationToSend, sugaredLogger)
+
+ // Run all the configuration checks and issue relevant warnings or errors.
+ // TODO: Is here the best place to call this? Take a look before releasing.
+ checkConfigurationSanity(¬ificationToSend, &runtimeConfiguration, &validConfigurationOptions, sugaredLogger)
+
+ // This is the demarcation line between config reading and starting doing things.
+
+ // Let's try to change the logging level of our current logger.
+ newLogLevelJSON := []byte(runtimeConfiguration.logLevel)
+
+ err = zapRuntimeConfig.Level.UnmarshalText(newLogLevelJSON)
+
+ if err != nil {
+ sugaredLogger.Panicf("Supplied log level %s is invalid, exiting.", runtimeConfiguration.logLevel)
+ }
+
+ sugaredLogger.Debug("Logging level is changed to %s.", runtimeConfiguration.logLevel)
+
// Check whether our version is asked or not.
// Version info shall always return clean.
- if runtimeConfiguration.versionRequested {
- fmt.Printf("%s version %s\n", os.Args[0], runtimeConfiguration.version)
+ if flagStorage.versionRequested {
+ fmt.Printf("%s version %s\n", runtimeConfiguration.applicationName, runtimeConfiguration.version)
os.Exit(0) // Exit gracefully.
}
- // Run all the configuration checks and issue relevant warnings or errors.
- checkConfigurationSanity(¬ificationToSend, &runtimeConfiguration, sugaredLogger)
+ // If no message is given, let's print help and exit.
+ if notificationToSend.messageBody == "" {
+ sugaredLogger.Debugf("No message body specified, exiting.")
+ fmt.Printf("Usage: %s [OPTIONS] message\n", runtimeConfiguration.applicationName)
+ flag.PrintDefaults()
+ os.Exit(1)
+ }
// Show the current state of the code.
- printState(&runtimeConfiguration, ¬ificationToSend)
+ printState(&runtimeConfiguration, ¬ificationToSend, sugaredLogger)
// Let's send the notification:
if runtimeConfiguration.dryrun == false {
sendNotification(¬ificationToSend, &runtimeConfiguration)
} else {
- sugaredLogger.Infow("Not sending the notification since in dry run mode.")
+ sugaredLogger.Warnf("Not sending the notification since in dry run mode.")
}
}