Repository: code-ready/crc
Pull request #1794 (2020-12-22 20:38:03)

Intercept errors and send to segment for all commands
* Remove most of os.Exit in the middle of the code. This is needed to send errors to segment.
* Use RunE instead of Run. postRun() is now correctly called after each method. Even in case of an error.
* Introduce a segment middleware for all commands (except config because it lives in a separate pkg - to be changed)
* Log the error and the command used in segment.
* Refactor segment tests.

openshift-ci-robot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/code-ready/crc/pull/1794#" title="Author self-approved">guillaumerose</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=code-ready%2Fcrc).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/code-ready/crc/blob/master/OWNERS)~~ [guillaumerose]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
openshift-ci[bot]: @guillaumerose: The following test **failed**, say `/retest` to rerun all failed tests:

Test name | Commit | Details | Rerun command
--- | --- | --- | ---
ci/prow/e2e-crc | b5323506470224e3ebf5912f09daeeec16421f99 | [link](https://prow.ci.openshift.org/view/gs/origin-ci-test/pr-logs/pull/code-ready_crc/1794/pull-ci-code-ready-crc-master-e2e-crc/1341483163070763008) | `/test e2e-crc`

[Full PR test history](https://prow.ci.openshift.org/pr-history?org=code-ready&repo=crc&pr=1794). [Your PR dashboard](https://prow.ci.openshift.org/pr?query=is:pr+state:open+author:guillaumerose).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
gbraad: haven't looked at the PR yet, but from the discussions with management we shouldn't send stacktraces (or logs). so we need to be smarter about the error messages first and prevent being ambiguous before we can send an error message,
gbraad: I see the change from the wrapper to an attachMiddleware function. somehow the clarity is lost because of this; has become very indirect like a generic exceptionhandler. this does not mean it is wrong, just 'afraid' it is harder to find ... perhaps good to understand what other use `attachMiddleware` would have. as it is now a single use function.
guillaumerose: rebased!
guillaumerose: All comments seem to be resolved. @praveenkumar @gbraad can you PTAL? Thanks.
praveenkumar: /lgtm

Patch:
diff --git a/cmd/crc/cmd/cleanup.go b/cmd/crc/cmd/cleanup.go
index 854504fbd..c443917b8 100644
--- a/cmd/crc/cmd/cleanup.go
+++ b/cmd/crc/cmd/cleanup.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"os"
 
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/preflight"
 	"github.com/spf13/cobra"
 )
@@ -20,10 +19,8 @@ var cleanupCmd = &cobra.Command{
 	Use:   "cleanup",
 	Short: "Undo config changes",
 	Long:  "Undo all the configuration changes done by 'crc setup' command",
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runCleanup(); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runCleanup()
 	},
 }
 
diff --git a/cmd/crc/cmd/config/get.go b/cmd/crc/cmd/config/get.go
index 1acb8cdc2..2a6cb92f7 100644
--- a/cmd/crc/cmd/config/get.go
+++ b/cmd/crc/cmd/config/get.go
@@ -1,10 +1,10 @@
 package config
 
 import (
+	"errors"
 	"fmt"
 
 	"github.com/code-ready/crc/pkg/crc/config"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/spf13/cobra"
 )
@@ -14,20 +14,21 @@ func configGetCmd(config config.Storage) *cobra.Command {
 		Use:   "get CONFIG-KEY",
 		Short: "Get a crc configuration property",
 		Long:  `Gets a crc configuration property.`,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) < 1 {
-				exit.WithMessage(1, "Please provide a configuration property to get")
+				return errors.New("Please provide a configuration property to get")
 			}
 			key := args[0]
 			v := config.Get(key)
 			switch {
 			case v.Invalid:
-				exit.WithMessage(1, fmt.Sprintf("Configuration property '%s' does not exist", key))
+				return fmt.Errorf("Configuration property '%s' does not exist", key)
 			case v.IsDefault:
-				exit.WithMessage(1, fmt.Sprintf("Configuration property '%s' is not set. Default value is '%s'", key, v.AsString()))
+				return fmt.Errorf("Configuration property '%s' is not set. Default value is '%s'", key, v.AsString())
 			default:
 				output.Outln(key, ":", v.AsString())
 			}
+			return nil
 		},
 	}
 }
diff --git a/cmd/crc/cmd/config/set.go b/cmd/crc/cmd/config/set.go
index 3325855a2..f6b356b20 100644
--- a/cmd/crc/cmd/config/set.go
+++ b/cmd/crc/cmd/config/set.go
@@ -1,8 +1,9 @@
 package config
 
 import (
+	"errors"
+
 	"github.com/code-ready/crc/pkg/crc/config"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/spf13/cobra"
 )
@@ -12,18 +13,19 @@ func configSetCmd(config config.Storage) *cobra.Command {
 		Use:   "set CONFIG-KEY VALUE",
 		Short: "Set a crc configuration property",
 		Long:  `Sets a crc configuration property.`,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) < 2 {
-				exit.WithMessage(1, "Please provide a configuration property and its value as in 'crc config set KEY VALUE'")
+				return errors.New("Please provide a configuration property and its value as in 'crc config set KEY VALUE'")
 			}
 			setMessage, err := config.Set(args[0], args[1])
 			if err != nil {
-				exit.WithMessage(1, err.Error())
+				return err
 			}
 
 			if setMessage != "" {
 				output.Outln(setMessage)
 			}
+			return nil
 		},
 	}
 }
diff --git a/cmd/crc/cmd/config/unset.go b/cmd/crc/cmd/config/unset.go
index 16a0bf9c7..cfbe6484b 100644
--- a/cmd/crc/cmd/config/unset.go
+++ b/cmd/crc/cmd/config/unset.go
@@ -1,8 +1,9 @@
 package config
 
 import (
+	"errors"
+
 	"github.com/code-ready/crc/pkg/crc/config"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/spf13/cobra"
 )
@@ -12,17 +13,18 @@ func configUnsetCmd(config config.Storage) *cobra.Command {
 		Use:   "unset CONFIG-KEY",
 		Short: "Unset a crc configuration property",
 		Long:  `Unsets a crc configuration property.`,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) != 1 {
-				exit.WithMessage(1, "Please provide a configuration property to unset")
+				return errors.New("Please provide a configuration property to unset")
 			}
 			unsetMessage, err := config.Unset(args[0])
 			if err != nil {
-				exit.WithMessage(1, err.Error())
+				return err
 			}
 			if unsetMessage != "" {
 				output.Outln(unsetMessage)
 			}
+			return nil
 		},
 	}
 }
diff --git a/cmd/crc/cmd/config/view.go b/cmd/crc/cmd/config/view.go
index 3905d73f6..f574e3f1a 100644
--- a/cmd/crc/cmd/config/view.go
+++ b/cmd/crc/cmd/config/view.go
@@ -9,7 +9,6 @@ import (
 	"text/template"
 
 	"github.com/code-ready/crc/pkg/crc/config"
-	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/spf13/cobra"
 )
 
@@ -29,14 +28,12 @@ func configViewCmd(config config.Storage) *cobra.Command {
 		Use:   "view",
 		Short: "Display all assigned crc configuration properties",
 		Long:  `Displays all assigned crc configuration properties and their values.`,
-		Run: func(cmd *cobra.Command, args []string) {
+		RunE: func(cmd *cobra.Command, args []string) error {
 			tmpl, err := determineTemplate(configViewFormat)
 			if err != nil {
-				logging.Fatal(err)
-			}
-			if err := runConfigView(config.AllConfigs(), tmpl, os.Stdout); err != nil {
-				logging.Fatal(err)
+				return err
 			}
+			return runConfigView(config.AllConfigs(), tmpl, os.Stdout)
 		},
 	}
 	configViewCmd.Flags().StringVar(&configViewFormat, "format", DefaultConfigViewFormat,
diff --git a/cmd/crc/cmd/console.go b/cmd/crc/cmd/console.go
index 7a28e002b..ef5831c7c 100644
--- a/cmd/crc/cmd/console.go
+++ b/cmd/crc/cmd/console.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"os"
 
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/code-ready/machine/libmachine/state"
 	"github.com/pkg/browser"
@@ -31,10 +30,8 @@ var consoleCmd = &cobra.Command{
 	Aliases: []string{"dashboard"},
 	Short:   "Open the OpenShift Web Console in the default browser",
 	Long:    `Open the OpenShift Web Console in the default browser or print its URL or credentials`,
-	Run: func(cmd *cobra.Command, args []string) {
-		if renderErr := runConsole(os.Stdout, newMachine(), consolePrintURL, consolePrintCredentials, outputFormat); renderErr != nil {
-			exit.WithMessage(1, renderErr.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runConsole(os.Stdout, newMachine(), consolePrintURL, consolePrintCredentials, outputFormat)
 	},
 }
 
diff --git a/cmd/crc/cmd/daemon.go b/cmd/crc/cmd/daemon.go
index ab33bea91..f20530cbd 100644
--- a/cmd/crc/cmd/daemon.go
+++ b/cmd/crc/cmd/daemon.go
@@ -12,7 +12,6 @@ import (
 
 	crcConfig "github.com/code-ready/crc/pkg/crc/config"
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/gvisor-tap-vsock/pkg/transport"
 	"github.com/code-ready/gvisor-tap-vsock/pkg/types"
@@ -32,7 +31,7 @@ var daemonCmd = &cobra.Command{
 	Short:  "Run the crc daemon",
 	Long:   "Run the crc daemon",
 	Hidden: true,
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 		// setup separate logging for daemon
 		logging.CloseLogging()
 		logging.InitLogrus(logging.LogLevel, constants.DaemonLogFilePath)
@@ -50,7 +49,7 @@ var daemonCmd = &cobra.Command{
 			}
 		}
 
-		if err := run(&types.Configuration{
+		err := run(&types.Configuration{
 			Debug:             false, // never log packets
 			CaptureFile:       captureFile(),
 			MTU:               4000, // Large packets slightly improve the performance. Less small packets.
@@ -89,9 +88,8 @@ var daemonCmd = &cobra.Command{
 				":6443": "192.168.127.2:6443",
 				":443":  "192.168.127.2:443",
 			},
-		}, endpoints); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+		}, endpoints)
+		return err
 	},
 }
 
diff --git a/cmd/crc/cmd/delete.go b/cmd/crc/cmd/delete.go
index 0abb8d699..48ac6f6ec 100644
--- a/cmd/crc/cmd/delete.go
+++ b/cmd/crc/cmd/delete.go
@@ -7,7 +7,6 @@ import (
 	"os"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/input"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/spf13/cobra"
@@ -27,10 +26,8 @@ var deleteCmd = &cobra.Command{
 	Use:   "delete",
 	Short: "Delete the OpenShift cluster",
 	Long:  "Delete the OpenShift cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runDelete(os.Stdout, newMachine(), clearCache, constants.MachineCacheDir, outputFormat != jsonFormat, globalForce, outputFormat); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runDelete(os.Stdout, newMachine(), clearCache, constants.MachineCacheDir, outputFormat != jsonFormat, globalForce, outputFormat)
 	},
 }
 
diff --git a/cmd/crc/cmd/ip.go b/cmd/crc/cmd/ip.go
index e7db5a68f..576178ac0 100644
--- a/cmd/crc/cmd/ip.go
+++ b/cmd/crc/cmd/ip.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/spf13/cobra"
 )
@@ -14,10 +13,8 @@ var ipCmd = &cobra.Command{
 	Use:   "ip",
 	Short: "Get IP address of the running OpenShift cluster",
 	Long:  "Get IP address of the running OpenShift cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runIP(args); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runIP(args)
 	},
 }
 
diff --git a/cmd/crc/cmd/oc_env.go b/cmd/crc/cmd/oc_env.go
index aea0177bc..09495bf92 100644
--- a/cmd/crc/cmd/oc_env.go
+++ b/cmd/crc/cmd/oc_env.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/code-ready/crc/pkg/os/shell"
 	"github.com/spf13/cobra"
@@ -18,10 +17,8 @@ var ocEnvCmd = &cobra.Command{
 	Use:   "oc-env",
 	Short: "Add the 'oc' executable to PATH",
 	Long:  `Add the OpenShift client executable 'oc' to PATH`,
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runOcEnv(args); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runOcEnv(args)
 	},
 }
 
diff --git a/cmd/crc/cmd/podman_env.go b/cmd/crc/cmd/podman_env.go
index e422ab619..ad37ea0f4 100644
--- a/cmd/crc/cmd/podman_env.go
+++ b/cmd/crc/cmd/podman_env.go
@@ -1,10 +1,10 @@
 package cmd
 
 import (
+	"errors"
 	"fmt"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/output"
 	"github.com/code-ready/crc/pkg/os/shell"
 	"github.com/spf13/cobra"
@@ -14,17 +14,13 @@ var podmanEnvCmd = &cobra.Command{
 	Use:   "podman-env",
 	Short: "Setup podman environment",
 	Long:  `Setup environment for 'podman' executable to access podman on CRC VM`,
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runPodmanEnv(args); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		// See issue #961; Currently does not work on Windows in combination with the CRC vm.
+		return errors.New("currently not supported")
 	},
 }
 
-func runPodmanEnv(args []string) error {
-	// See issue #961; Currently does not work on Windows in combination with the CRC vm.
-	exit.WithMessage(1, "Currently not supported.")
-
+func RunPodmanEnv(args []string) error {
 	userShell, err := shell.GetShell(forceShell)
 	if err != nil {
 		return fmt.Errorf("Error running the podman-env command: %s", err.Error())
diff --git a/cmd/crc/cmd/root.go b/cmd/crc/cmd/root.go
index b300b358b..f9ba2491e 100644
--- a/cmd/crc/cmd/root.go
+++ b/cmd/crc/cmd/root.go
@@ -3,12 +3,12 @@ package cmd
 import (
 	"fmt"
 	"io/ioutil"
+	"os"
 	"strings"
 
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	crcConfig "github.com/code-ready/crc/pkg/crc/config"
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/code-ready/crc/pkg/crc/network"
@@ -22,16 +22,15 @@ var rootCmd = &cobra.Command{
 	Use:   commandName,
 	Short: descriptionShort,
 	Long:  descriptionLong,
-	PersistentPreRun: func(cmd *cobra.Command, args []string) {
-		runPrerun()
+	PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+		return runPrerun()
 	},
 	Run: func(cmd *cobra.Command, args []string) {
 		runRoot()
 		_ = cmd.Help()
 	},
-	PersistentPostRun: func(cmd *cobra.Command, args []string) {
-		runPostrun()
-	},
+	SilenceUsage:  true,
+	SilenceErrors: true,
 }
 
 var (
@@ -61,18 +60,22 @@ func init() {
 	rootCmd.PersistentFlags().StringVar(&logging.LogLevel, "log-level", constants.DefaultLogLevel, "log level (e.g. \"debug | info | warn | error\")")
 }
 
-func runPrerun() {
+func runPrerun() error {
 	// Setting up logrus
 	logging.InitLogrus(logging.LogLevel, constants.LogFilePath)
-	setProxyDefaults()
+	if err := setProxyDefaults(); err != nil {
+		return err
+	}
 
 	for _, str := range defaultVersion().lines() {
 		logging.Debugf(str)
 	}
+	return nil
 }
 
 func runPostrun() {
 	logging.CloseLogging()
+	segmentClient.Close()
 }
 
 func runRoot() {
@@ -80,9 +83,14 @@ func runRoot() {
 }
 
 func Execute() {
+	attachMiddleware([]string{}, rootCmd)
+
 	if err := rootCmd.Execute(); err != nil {
-		logging.Fatal(err)
+		runPostrun()
+		_, _ = fmt.Fprintln(os.Stderr, err.Error())
+		os.Exit(1)
 	}
+	runPostrun()
 }
 
 func checkIfMachineMissing(client machine.Client) error {
@@ -96,7 +104,7 @@ func checkIfMachineMissing(client machine.Client) error {
 	return nil
 }
 
-func setProxyDefaults() {
+func setProxyDefaults() error {
 	httpProxy := config.Get(cmdConfig.HTTPProxy).AsString()
 	httpsProxy := config.Get(cmdConfig.HTTPSProxy).AsString()
 	noProxy := config.Get(cmdConfig.NoProxy).AsString()
@@ -104,12 +112,12 @@ func setProxyDefaults() {
 
 	proxyCAData, err := getProxyCAData(proxyCAFile)
 	if err != nil {
-		exit.WithMessage(1, fmt.Sprintf("not able to read proxyCAFile %s: %v", proxyCAFile, err.Error()))
+		return fmt.Errorf("not able to read proxyCAFile %s: %v", proxyCAFile, err.Error())
 	}
 
 	proxyConfig, err := network.NewProxyDefaults(httpProxy, httpsProxy, noProxy, proxyCAData)
 	if err != nil {
-		exit.WithMessage(1, err.Error())
+		return err
 	}
 
 	if proxyConfig.IsEnabled() {
@@ -117,6 +125,7 @@ func setProxyDefaults() {
 			proxyConfig.HTTPSProxyForDisplay(), proxyConfig.GetNoProxyString(), proxyCAFile)
 		proxyConfig.ApplyToEnvironment()
 	}
+	return nil
 }
 
 func getProxyCAData(proxyCAFile string) (string, error) {
@@ -157,3 +166,27 @@ func newMachineWithConfig(config crcConfig.Storage) machine.Client {
 func addForceFlag(cmd *cobra.Command) {
 	cmd.PersistentFlags().BoolVarP(&globalForce, "force", "f", false, "Forcefully perform this action")
 }
+
+func executeWithLogging(fullCmd string, input func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) error {
+	return func(cmd *cobra.Command, args []string) error {
+		if err := input(cmd, args); err != nil {
+			if serr := segmentClient.Upload(fullCmd, err); serr != nil {
+				fmt.Println(serr.Error())
+			}
+			return err
+		}
+		return nil
+	}
+}
+
+func attachMiddleware(names []string, cmd *cobra.Command) {
+	if cmd.HasSubCommands() {
+		for _, command := range cmd.Commands() {
+			attachMiddleware(append(names, cmd.Name()), command)
+		}
+	} else if cmd.RunE != nil {
+		fullCmd := strings.Join(append(names, cmd.Name()), " ")
+		src := cmd.RunE
+		cmd.RunE = executeWithLogging(fullCmd, src)
+	}
+}
diff --git a/cmd/crc/cmd/setup.go b/cmd/crc/cmd/setup.go
index f6b356ce5..d8b5ace93 100644
--- a/cmd/crc/cmd/setup.go
+++ b/cmd/crc/cmd/setup.go
@@ -8,7 +8,6 @@ import (
 
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/preflight"
 	"github.com/spf13/cobra"
 )
@@ -23,13 +22,11 @@ var setupCmd = &cobra.Command{
 	Use:   "setup",
 	Short: "Set up prerequisites for the OpenShift cluster",
 	Long:  "Set up local virtualization and networking infrastructure for the OpenShift cluster",
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 		if err := viper.BindFlagSet(cmd.Flags()); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
-		if err := runSetup(args); err != nil {
-			exit.WithMessage(1, err.Error())
+			return err
 		}
+		return runSetup(args)
 	},
 }
 
diff --git a/cmd/crc/cmd/start.go b/cmd/crc/cmd/start.go
index 90c95d202..565dcc54f 100644
--- a/cmd/crc/cmd/start.go
+++ b/cmd/crc/cmd/start.go
@@ -11,7 +11,6 @@ import (
 	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	"github.com/code-ready/crc/pkg/crc/cluster"
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/input"
 	"github.com/code-ready/crc/pkg/crc/logging"
 	"github.com/code-ready/crc/pkg/crc/machine"
@@ -43,16 +42,14 @@ var startCmd = &cobra.Command{
 	Use:   "start",
 	Short: "Start the OpenShift cluster",
 	Long:  "Start the OpenShift cluster",
-	Run: func(cmd *cobra.Command, args []string) {
+	RunE: func(cmd *cobra.Command, args []string) error {
 		if err := viper.BindFlagSet(cmd.Flags()); err != nil {
-			exit.WithMessage(1, err.Error())
+			return err
 		}
 		if err := renderStartResult(runStart(args)); err != nil {
-			if serr := segmentClient.Upload(err); serr != nil {
-				fmt.Println(serr.Error())
-			}
-			exit.WithMessage(1, err.Error())
+			return err
 		}
+		return nil
 	},
 }
 
diff --git a/cmd/crc/cmd/status.go b/cmd/crc/cmd/status.go
index e9f8c60ef..10b05b555 100644
--- a/cmd/crc/cmd/status.go
+++ b/cmd/crc/cmd/status.go
@@ -8,7 +8,6 @@ import (
 	"text/tabwriter"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/docker/go-units"
 	"github.com/spf13/cobra"
@@ -23,10 +22,8 @@ var statusCmd = &cobra.Command{
 	Use:   "status",
 	Short: "Display status of the OpenShift cluster",
 	Long:  "Show details about the OpenShift cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runStatus(os.Stdout, newMachine(), constants.MachineCacheDir, outputFormat); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runStatus(os.Stdout, newMachine(), constants.MachineCacheDir, outputFormat)
 	},
 }
 
diff --git a/cmd/crc/cmd/stop.go b/cmd/crc/cmd/stop.go
index cf1cce9f4..61d533123 100644
--- a/cmd/crc/cmd/stop.go
+++ b/cmd/crc/cmd/stop.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"os"
 
-	"github.com/code-ready/crc/pkg/crc/exit"
 	"github.com/code-ready/crc/pkg/crc/input"
 	"github.com/code-ready/crc/pkg/crc/machine"
 	"github.com/code-ready/machine/libmachine/state"
@@ -23,10 +22,8 @@ var stopCmd = &cobra.Command{
 	Use:   "stop",
 	Short: "Stop the OpenShift cluster",
 	Long:  "Stop the OpenShift cluster",
-	Run: func(cmd *cobra.Command, args []string) {
-		if renderErr := runStop(os.Stdout, newMachine(), outputFormat != jsonFormat, globalForce, outputFormat); renderErr != nil {
-			exit.WithMessage(1, renderErr.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runStop(os.Stdout, newMachine(), outputFormat != jsonFormat, globalForce, outputFormat)
 	},
 }
 
diff --git a/cmd/crc/cmd/version.go b/cmd/crc/cmd/version.go
index bb770709b..2d8a414ce 100644
--- a/cmd/crc/cmd/version.go
+++ b/cmd/crc/cmd/version.go
@@ -6,7 +6,6 @@ import (
 	"os"
 
 	"github.com/code-ready/crc/pkg/crc/constants"
-	"github.com/code-ready/crc/pkg/crc/exit"
 	crcversion "github.com/code-ready/crc/pkg/crc/version"
 	"github.com/spf13/cobra"
 )
@@ -20,10 +19,8 @@ var versionCmd = &cobra.Command{
 	Use:   "version",
 	Short: "Print version information",
 	Long:  "Print version information",
-	Run: func(cmd *cobra.Command, args []string) {
-		if err := runPrintVersion(os.Stdout, defaultVersion(), outputFormat); err != nil {
-			exit.WithMessage(1, err.Error())
-		}
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return runPrintVersion(os.Stdout, defaultVersion(), outputFormat)
 	},
 }
 
diff --git a/pkg/crc/exit/atexit.go b/pkg/crc/exit/atexit.go
deleted file mode 100644
index 0c657ec34..000000000
--- a/pkg/crc/exit/atexit.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package exit
-
-import (
-	"fmt"
-	"os"
-
-	"github.com/code-ready/crc/pkg/crc/output"
-)
-
-// WithMessage prints the specified message and then exits the program with the specified exit code.
-// If the exit code is 0, the message is prints to stdout, otherwise to stderr.
-func WithMessage(code int, text string, args ...interface{}) {
-	if code == 0 {
-		_, _ = output.Fout(os.Stdout, fmt.Sprintf(text, args...))
-	} else {
-		_, _ = output.Fout(os.Stderr, fmt.Sprintf(text, args...))
-	}
-	os.Exit(code)
-}
diff --git a/pkg/crc/segment/segment.go b/pkg/crc/segment/segment.go
index 25c612184..254dfb386 100644
--- a/pkg/crc/segment/segment.go
+++ b/pkg/crc/segment/segment.go
@@ -22,8 +22,12 @@ type Client struct {
 }
 
 func NewClient(config *crcConfig.Config) (*Client, error) {
-	telemetryFilePath := filepath.Join(constants.GetHomeDir(), ".redhat", "anonymousId")
+	return newCustomClient(config,
+		filepath.Join(constants.GetHomeDir(), ".redhat", "anonymousId"),
+		analytics.DefaultEndpoint)
+}
 
+func newCustomClient(config *crcConfig.Config, telemetryFilePath, segmentEndpoint string) (*Client, error) {
 	client, err := analytics.NewWithConfig("cvpHsNcmGCJqVzf6YxrSnVlwFSAZaYtp", analytics.Config{
 		DefaultContext: &analytics.Context{
 			App: analytics.AppInfo{
@@ -31,28 +35,38 @@ func NewClient(config *crcConfig.Config) (*Client, error) {
 				Version: version.GetCRCVersion(),
 			},
 		},
+		Endpoint: segmentEndpoint,
+		Logger:   &loggingAdapter{},
 	})
 	if err != nil {
-		return &Client{}, err
+		return nil, err
 	}
 
-	return &Client{segmentClient: client, config: config, telemetryFilePath: telemetryFilePath}, nil
+	return &Client{
+		segmentClient:     client,
+		config:            config,
+		telemetryFilePath: telemetryFilePath,
+	}, nil
+}
+
+func (c *Client) Close() error {
+	return c.segmentClient.Close()
 }
 
-func (c *Client) Upload(err error) error {
-	defer c.segmentClient.Close()
+func (c *Client) Upload(action string, err error) error {
 	if !c.config.Get(config.ConsentTelemetry).AsBool() {
 		return nil
 	}
-	logging.Info("Uploading the error to segment")
+	logging.Debug("Uploading the error to segment")
 
 	anonymousID, uerr := getUserIdentity(c.telemetryFilePath)
 	if uerr != nil {
 		return uerr
 	}
 
-	t := analytics.NewTraits()
-	t = setError(t, err)
+	t := analytics.NewTraits().
+		Set("action", action).
+		Set("error", err.Error())
 
 	return c.segmentClient.Enqueue(analytics.Identify{
 		AnonymousId: anonymousID,
@@ -80,6 +94,12 @@ func getUserIdentity(telemetryFilePath string) (string, error) {
 	return strings.TrimSpace(string(id)), nil
 }
 
-func setError(t analytics.Traits, err error) analytics.Traits {
-	return t.Set("error", err.Error())
+type loggingAdapter struct{}
+
+func (l *loggingAdapter) Logf(format string, args ...interface{}) {
+	logging.Infof(format, args...)
+}
+
+func (l *loggingAdapter) Errorf(format string, args ...interface{}) {
+	logging.Errorf(format, args...)
 }


Test patch:
diff --git a/pkg/crc/segment/segment_test.go b/pkg/crc/segment/segment_test.go
index 2f7acce08..cdf4faa22 100644
--- a/pkg/crc/segment/segment_test.go
+++ b/pkg/crc/segment/segment_test.go
@@ -1,10 +1,8 @@
 package segment
 
 import (
-	"bytes"
 	"encoding/json"
 	"errors"
-	"io"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -12,15 +10,13 @@ import (
 	"path/filepath"
 	"testing"
 
+	cmdConfig "github.com/code-ready/crc/cmd/crc/cmd/config"
 	crcConfig "github.com/code-ready/crc/pkg/crc/config"
-	"github.com/segmentio/analytics-go"
+	"github.com/code-ready/crc/pkg/crc/logging"
+	"github.com/code-ready/crc/pkg/crc/version"
 	"github.com/stretchr/testify/require"
 )
 
-const (
-	ConsentTelemetry = "consent-telemetry"
-)
-
 type segmentResponse struct {
 	Batch []struct {
 		AnonymousID string `json:"anonymousId"`
@@ -47,21 +43,13 @@ func mockServer() (chan []byte, *httptest.Server) {
 	done := make(chan []byte, 1)
 
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		buf := bytes.NewBuffer(nil)
-		io.Copy(buf, r.Body) // nolint
-
-		var v interface{}
-		err := json.Unmarshal(buf.Bytes(), &v)
+		defer r.Body.Close()
+		bin, err := ioutil.ReadAll(r.Body)
 		if err != nil {
-			panic(err)
+			logging.Error(err)
+			return
 		}
-
-		b, err := json.MarshalIndent(v, "", "  ")
-		if err != nil {
-			panic(err)
-		}
-
-		done <- b
+		done <- bin
 	}))
 
 	return done, server
@@ -70,7 +58,11 @@ func mockServer() (chan []byte, *httptest.Server) {
 func newTestConfig(value bool) (*crcConfig.Config, error) {
 	storage := crcConfig.NewEmptyInMemoryStorage()
 	config := crcConfig.New(storage)
-	config.AddSetting(ConsentTelemetry, value, crcConfig.ValidateBool, crcConfig.SuccessfullyApplied)
+	cmdConfig.RegisterSettings(config)
+
+	if _, err := config.Set(cmdConfig.ConsentTelemetry, value); err != nil {
+		return nil, err
+	}
 	return config, nil
 }
 
@@ -79,17 +71,6 @@ func TestClientUploadWithConsent(t *testing.T) {
 	defer server.Close()
 	defer close(body)
 
-	client, err := analytics.NewWithConfig("dummykey", analytics.Config{
-		DefaultContext: &analytics.Context{
-			App: analytics.AppInfo{
-				Name:    "crc",
-				Version: "1.20.0",
-			},
-		},
-		Endpoint: server.URL,
-	})
-	require.NoError(t, err)
-
 	dir, err := ioutil.TempDir("", "cfg")
 	require.NoError(t, err)
 	defer os.RemoveAll(dir)
@@ -97,22 +78,22 @@ func TestClientUploadWithConsent(t *testing.T) {
 	config, err := newTestConfig(true)
 	require.NoError(t, err)
 
-	c := &Client{segmentClient: client, config: config, telemetryFilePath: filepath.Join(dir, "telemetry")}
+	c, err := newCustomClient(config, filepath.Join(dir, "telemetry"), server.URL)
+	require.NoError(t, err)
 
-	require.NoError(t, c.Upload(errors.New("an error occurred")))
+	require.NoError(t, c.Upload("start", errors.New("an error occurred")))
+	require.NoError(t, c.Close())
 
-	s := segmentResponse{}
 	select {
-	case x, ok := <-body:
-		if ok {
-			err = json.Unmarshal(x, &s)
-			require.NoError(t, err)
-		}
+	case x := <-body:
+		s := segmentResponse{}
+		require.NoError(t, json.Unmarshal(x, &s))
+		require.Equal(t, s.Batch[0].Traits.Error, "an error occurred")
+		require.Equal(t, s.Context.App.Name, "crc")
+		require.Equal(t, s.Context.App.Version, version.GetCRCVersion())
 	default:
+		require.Fail(t, "server should receive data")
 	}
-	require.Equal(t, s.Batch[0].Traits.Error, "an error occurred")
-	require.Equal(t, s.Context.App.Name, "crc")
-	require.Equal(t, s.Context.App.Version, "1.20.0")
 }
 
 func TestClientUploadWithOutConsent(t *testing.T) {
@@ -120,17 +101,6 @@ func TestClientUploadWithOutConsent(t *testing.T) {
 	defer server.Close()
 	defer close(body)
 
-	client, err := analytics.NewWithConfig("dummykey", analytics.Config{
-		DefaultContext: &analytics.Context{
-			App: analytics.AppInfo{
-				Name:    "crc",
-				Version: "1.20.0",
-			},
-		},
-		Endpoint: server.URL,
-	})
-	require.NoError(t, err)
-
 	dir, err := ioutil.TempDir("", "cfg")
 	require.NoError(t, err)
 	defer os.RemoveAll(dir)
@@ -138,19 +108,15 @@ func TestClientUploadWithOutConsent(t *testing.T) {
 	config, err := newTestConfig(false)
 	require.NoError(t, err)
 
-	c := &Client{segmentClient: client, config: config, telemetryFilePath: filepath.Join(dir, "telemetry")}
+	c, err := newCustomClient(config, filepath.Join(dir, "telemetry"), server.URL)
+	require.NoError(t, err)
 
-	require.NoError(t, c.Upload(errors.New("an error occurred")))
+	require.NoError(t, c.Upload("start", errors.New("an error occurred")))
+	require.NoError(t, c.Close())
 
-	s := segmentResponse{}
 	select {
-	case x, ok := <-body:
-		if ok {
-			err = json.Unmarshal(x, &s)
-			require.NoError(t, err)
-		}
+	case <-body:
+		require.Fail(t, "server should not receive data")
 	default:
 	}
-
-	require.Len(t, s.Batch, 0)
 }