From b6678c2e185523b5bd604c1736c6ef18a9122b88 Mon Sep 17 00:00:00 2001
From: Aldo Lacuku <aldo@lacuku.eu>
Date: Wed, 25 Oct 2023 11:26:26 +0200
Subject: [PATCH] update(output): complete rework of the output system

Old flags "--verbose" and "--disable-styling" have been deprecated.

Two new flags configure the output system:

* --log-level can be one of info, warn debug or trace.
* --log-format can be one of color, text, json.

The output is done using a logger that is used across all commands.
Having a unique logger guarantees a consistent format of the output.

Signed-off-by: Aldo Lacuku <aldo@lacuku.eu>
---
 Makefile                                    |   2 +-
 cmd/artifact/follow/follow.go               |  26 +-
 cmd/artifact/info/info.go                   |   5 +-
 cmd/artifact/install/install.go             |  27 +-
 cmd/artifact/install/install_suite_test.go  |   3 +-
 cmd/artifact/install/install_test.go        |  24 +-
 cmd/cli_test.go                             |   2 +-
 cmd/index/add/add.go                        |  11 +-
 cmd/index/add/add_suite_test.go             |   3 +-
 cmd/index/add/add_test.go                   |  18 +-
 cmd/index/remove/remove.go                  |  12 +-
 cmd/index/update/update.go                  |  10 +-
 cmd/registry/auth/basic/basic.go            |   6 +-
 cmd/registry/auth/basic/basic_suite_test.go |   3 +-
 cmd/registry/auth/basic/basic_test.go       |  26 +-
 cmd/registry/auth/gcp/gcp.go                |   7 +-
 cmd/registry/auth/oauth/oauth.go            |  12 +-
 cmd/registry/auth/oauth/oauth_suite_test.go |   3 +-
 cmd/registry/auth/oauth/oauth_test.go       |  13 +-
 cmd/registry/pull/pull.go                   |  10 +-
 cmd/registry/pull/pull_suite_test.go        |   3 +-
 cmd/registry/pull/pull_test.go              |  20 +-
 cmd/registry/push/push.go                   |   7 +-
 cmd/registry/push/push_suite_test.go        |   3 +-
 cmd/registry/push/push_test.go              |  45 ++-
 cmd/registry/registry.go                    |   2 +
 cmd/root.go                                 |  12 +-
 cmd/testdata/help.txt                       |   8 +-
 cmd/testdata/noargsnoflags.txt              |   8 +-
 cmd/testdata/wrongflag.txt                  |   9 +-
 cmd/version/version.go                      |   3 -
 cmd/version/version_test.go                 |   1 -
 go.mod                                      |   2 +-
 internal/follower/follower.go               |  74 ++---
 internal/utils/credentials.go               |   6 +-
 main.go                                     |   8 +-
 pkg/install/tls/generator.go                |   8 +-
 pkg/install/tls/handler.go                  |   5 +-
 pkg/options/common.go                       |  52 +--
 pkg/options/enum.go                         |  58 ++++
 pkg/options/enum_test.go                    |  96 ++++++
 pkg/options/logFormat.go                    |  59 ++++
 pkg/options/logFormat_test.go               |  88 ++++++
 pkg/options/logLevel.go                     |  61 ++++
 pkg/options/logLevel_test.go                |  87 +++++
 pkg/options/options_suite_test.go           |  28 ++
 pkg/output/output.go                        | 161 ++++------
 pkg/output/output_test.go                   | 333 +++++++++++++++++---
 pkg/output/tracker.go                       |  26 +-
 49 files changed, 1082 insertions(+), 414 deletions(-)
 create mode 100644 pkg/options/enum.go
 create mode 100644 pkg/options/enum_test.go
 create mode 100644 pkg/options/logFormat.go
 create mode 100644 pkg/options/logFormat_test.go
 create mode 100644 pkg/options/logLevel.go
 create mode 100644 pkg/options/logLevel_test.go
 create mode 100644 pkg/options/options_suite_test.go

diff --git a/Makefile b/Makefile
index 5259d529..85d0b8df 100644
--- a/Makefile
+++ b/Makefile
@@ -34,7 +34,7 @@ test:
 .PHONY: gci
 gci:
 ifeq (, $(shell which gci))
-	@go install github.com/daixiang0/gci@v0.9.0
+	@go install github.com/daixiang0/gci@v0.11.1
 GCI=$(GOBIN)/gci
 else
 GCI=$(shell which gci)
diff --git a/cmd/artifact/follow/follow.go b/cmd/artifact/follow/follow.go
index f9c4cff5..92e7a266 100644
--- a/cmd/artifact/follow/follow.go
+++ b/cmd/artifact/follow/follow.go
@@ -253,6 +253,7 @@ Examples:
 
 // RunArtifactFollow executes the business logic for the artifact follow command.
 func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []string) error {
+	logger := o.Printer.Logger
 	// Retrieve configuration for follower
 	configuredFollower, err := config.Follower()
 	if err != nil {
@@ -278,15 +279,13 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
 	}
 
 	var wg sync.WaitGroup
-	// Disable styling
-	o.Printer.DisableStylingf()
 	// For each artifact create a follower.
 	var followers = make(map[string]*follower.Follower, 0)
 	for _, a := range args {
 		if o.cron != "" {
-			o.Printer.Info.Printfln("Creating follower for %q, with check using cron %s", a, o.cron)
+			logger.Info("Creating follower", logger.Args("artifact", a, "cron", o.cron))
 		} else {
-			o.Printer.Info.Printfln("Creating follower for %q, with check every %s", a, o.every.String())
+			logger.Info("Creating follower", logger.Args("artifact", a, "check every", o.every.String()))
 		}
 		ref, err := o.IndexCache.ResolveReference(a)
 		if err != nil {
@@ -305,7 +304,6 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
 			PluginsDir:        o.pluginsDir,
 			ArtifactReference: ref,
 			PlainHTTP:         o.PlainHTTP,
-			Verbose:           o.IsVerbose(),
 			CloseChan:         o.closeChan,
 			TmpDir:            o.tmpDir,
 			FalcoVersions:     o.versions,
@@ -319,11 +317,9 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
 		wg.Add(1)
 		followers[ref] = fol
 	}
-	// Enable styling
-	o.Printer.EnableStyling()
 
 	for k, f := range followers {
-		o.Printer.Info.Printfln("Starting follower for %q", k)
+		logger.Info("Starting follower", logger.Args("artifact", k))
 		go f.Follow(ctx)
 	}
 
@@ -331,7 +327,7 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
 	<-ctx.Done()
 
 	// We are done, shutdown the followers.
-	o.Printer.DefaultText.Printfln("closing followers...")
+	logger.Info("Closing followers...")
 	close(o.closeChan)
 
 	// Wait for the followers to shutdown or that the timer expires.
@@ -344,9 +340,9 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st
 
 	select {
 	case <-doneChan:
-		o.Printer.DefaultText.Printfln("followers correctly stopped.")
+		logger.Info("Followers correctly stopped.")
 	case <-time.After(timeout):
-		o.Printer.DefaultText.Printfln("Timed out waiting for followers to exit")
+		logger.Info("Timed out waiting for followers to exit")
 	}
 
 	return nil
@@ -433,11 +429,11 @@ type backoffTransport struct {
 func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error) {
 	var err error
 	var resp *http.Response
-
+	logger := bt.Printer.Logger
 	bt.startTime = time.Now()
 	bt.attempts = 0
 
-	bt.Printer.Verbosef("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay)
+	logger.Debug(fmt.Sprintf("Retrieving versions from Falco (timeout %s) ...", bt.Config.MaxDelay))
 
 	for {
 		resp, err = bt.Base.RoundTrip(req)
@@ -452,10 +448,10 @@ func (bt *backoffTransport) RoundTrip(req *http.Request) (*http.Response, error)
 				return resp, fmt.Errorf("timeout occurred while retrieving versions from Falco")
 			}
 
-			bt.Printer.Verbosef("error: %s. Trying again in %s", err.Error(), sleep.String())
+			logger.Debug(fmt.Sprintf("error: %s. Trying again in %s", err.Error(), sleep.String()))
 			time.Sleep(sleep)
 		} else {
-			bt.Printer.Verbosef("Successfully retrieved versions from Falco ...")
+			logger.Debug("Successfully retrieved versions from Falco")
 			return resp, err
 		}
 
diff --git a/cmd/artifact/info/info.go b/cmd/artifact/info/info.go
index 91b1a235..4a3c9a16 100644
--- a/cmd/artifact/info/info.go
+++ b/cmd/artifact/info/info.go
@@ -62,6 +62,7 @@ func NewArtifactInfoCmd(ctx context.Context, opt *options.Common) *cobra.Command
 
 func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string) error {
 	var data [][]string
+	logger := o.Printer.Logger
 
 	client, err := ociutils.Client(true)
 	if err != nil {
@@ -75,7 +76,7 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string
 		if err != nil {
 			entry, ok := o.IndexCache.MergedIndexes.EntryByName(name)
 			if !ok {
-				o.Printer.Warning.Printfln("cannot find %q, skipping", name)
+				logger.Warn("Cannot find artifact, skipping", logger.Args("name", name))
 				continue
 			}
 			ref = fmt.Sprintf("%s/%s", entry.Registry, entry.Repository)
@@ -93,7 +94,7 @@ func (o *artifactInfoOptions) RunArtifactInfo(ctx context.Context, args []string
 
 		tags, err := repo.Tags(ctx)
 		if err != nil && !errors.Is(err, context.Canceled) {
-			o.Printer.Warning.Printfln("cannot retrieve tags from t %q, %v", ref, err)
+			logger.Warn("Cannot retrieve tags from", logger.Args("ref", ref, "reason", err.Error()))
 			continue
 		} else if errors.Is(err, context.Canceled) {
 			// When the context is canceled we exit, since we receive a termination signal.
diff --git a/cmd/artifact/install/install.go b/cmd/artifact/install/install.go
index 46144c17..ff263ac4 100644
--- a/cmd/artifact/install/install.go
+++ b/cmd/artifact/install/install.go
@@ -22,6 +22,7 @@ import (
 	"path/filepath"
 	"runtime"
 
+	"github.com/pterm/pterm"
 	"github.com/spf13/cobra"
 	"github.com/spf13/viper"
 
@@ -183,6 +184,9 @@ Examples:
 
 // RunArtifactInstall executes the business logic for the artifact install command.
 func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []string) error {
+	var sp *pterm.SpinnerPrinter
+
+	logger := o.Printer.Logger
 	// Retrieve configuration for installer
 	configuredInstaller, err := config.Installer()
 	if err != nil {
@@ -244,7 +248,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
 	var refs []string
 	if o.resolveDeps {
 		// Solve dependencies
-		o.Printer.Info.Println("Resolving dependencies ...")
+		logger.Info("Resolving dependencies ...")
 		refs, err = ResolveDeps(resolver, args...)
 		if err != nil {
 			return err
@@ -253,7 +257,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
 		refs = args
 	}
 
-	o.Printer.Info.Printfln("Installing the following artifacts: %v", refs)
+	logger.Info("Installing artifacts", logger.Args("refs", refs))
 
 	for _, ref := range refs {
 		ref, err = o.IndexCache.ResolveReference(ref)
@@ -261,7 +265,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
 			return err
 		}
 
-		o.Printer.Info.Printfln("Preparing to pull %q", ref)
+		logger.Info("Preparing to pull artifact", logger.Args("ref", ref))
 
 		if err := puller.CheckAllowedType(ctx, ref, o.allowedTypes.Types); err != nil {
 			return err
@@ -290,12 +294,12 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
 			// the exact digest that we just pulled, even if the tag gets overwritten in the meantime.
 			digestRef := fmt.Sprintf("%s@%s", repo, result.RootDigest)
 
-			o.Printer.Info.Printfln("Verifying signature for %s", digestRef)
+			logger.Info("Verifying signature for artifact", logger.Args("digest", digestRef))
 			err = signature.Verify(ctx, digestRef, sig)
 			if err != nil {
 				return fmt.Errorf("error while verifying signature for %s: %w", digestRef, err)
 			}
-			o.Printer.Info.Printfln("Signature successfully verified!")
+			logger.Info("Signature successfully verified!")
 		}
 
 		var destDir string
@@ -312,14 +316,18 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
 			return fmt.Errorf("cannot use directory %q as install destination: %w", destDir, err)
 		}
 
-		sp, _ := o.Printer.Spinner.Start(fmt.Sprintf("INFO: Extracting and installing %q %q", result.Type, result.Filename))
+		logger.Info("Extracting and installing artifact", logger.Args("type", result.Type, "file", result.Filename))
+
+		if !o.Printer.DisableStyling {
+			sp, _ = o.Printer.Spinner.Start("Extracting and installing")
+		}
+
 		result.Filename = filepath.Join(tmpDir, result.Filename)
 
 		f, err := os.Open(result.Filename)
 		if err != nil {
 			return err
 		}
-
 		// Extract artifact and move it to its destination directory
 		_, err = utils.ExtractTarGz(f, destDir)
 		if err != nil {
@@ -331,7 +339,10 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args []
 			return err
 		}
 
-		sp.Success(fmt.Sprintf("Artifact successfully installed in %q", destDir))
+		if sp != nil {
+			_ = sp.Stop()
+		}
+		logger.Info("Artifact successfully installed", logger.Args("name", ref, "type", result.Type, "digest", result.Digest, "directory", destDir))
 	}
 
 	return nil
diff --git a/cmd/artifact/install/install_suite_test.go b/cmd/artifact/install/install_suite_test.go
index d3ecb5c4..f1ba2dbf 100644
--- a/cmd/artifact/install/install_suite_test.go
+++ b/cmd/artifact/install/install_suite_test.go
@@ -71,7 +71,6 @@ var _ = BeforeSuite(func() {
 	// Create and configure the common options.
 	opt = commonoptions.NewOptions()
 	opt.Initialize(commonoptions.WithWriter(output))
-	opt.Printer.DisableStylingf()
 
 	// Create the oras registry.
 	orasRegistry, err = testutils.NewOrasRegistry(registry, true)
@@ -98,5 +97,5 @@ var _ = AfterSuite(func() {
 func executeRoot(args []string) error {
 	rootCmd.SetArgs(args)
 	rootCmd.SetOut(output)
-	return cmd.Execute(rootCmd, opt.Printer)
+	return cmd.Execute(rootCmd, opt)
 }
diff --git a/cmd/artifact/install/install_test.go b/cmd/artifact/install/install_test.go
index bd726f35..f37de8ca 100644
--- a/cmd/artifact/install/install_test.go
+++ b/cmd/artifact/install/install_test.go
@@ -51,8 +51,8 @@ Flags:
 
 Global Flags:
       --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -v, --verbose           Enable verbose logs (default false)
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 
 `
 
@@ -170,7 +170,7 @@ var artifactInstallTests = Describe("install", func() {
 				args = []string{artifactCmd, installCmd, "--config", configFile}
 			})
 			installAssertFailedBehavior(artifactInstallUsage,
-				"ERRO: no artifacts to install, please configure artifacts or pass them as arguments to this command")
+				"ERROR no artifacts to install, please configure artifacts or pass them as arguments to this command")
 		})
 
 		When("unreachable registry", func() {
@@ -181,7 +181,7 @@ var artifactInstallTests = Describe("install", func() {
 				Expect(err).To(BeNil())
 				args = []string{artifactCmd, installCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
 			})
-			installAssertFailedBehavior(artifactInstallUsage, `ERRO: unable to fetch reference`)
+			installAssertFailedBehavior(artifactInstallUsage, `ERROR unable to fetch reference`)
 		})
 
 		When("invalid repository", func() {
@@ -193,7 +193,7 @@ var artifactInstallTests = Describe("install", func() {
 				Expect(err).To(BeNil())
 				args = []string{artifactCmd, installCmd, newReg, "--plain-http", "--config", configFile}
 			})
-			installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERRO: unable to fetch reference %q", newReg))
+			installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR unable to fetch reference %q", newReg))
 		})
 
 		When("with disallowed types (rulesfile)", func() {
@@ -222,7 +222,7 @@ var artifactInstallTests = Describe("install", func() {
 					"--config", configFilePath, "--allowed-types", "rulesfile"}
 			})
 
-			installAssertFailedBehavior(artifactInstallUsage, "ERRO: cannot download artifact of type \"plugin\": type not permitted")
+			installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"plugin\": type not permitted")
 		})
 
 		When("with disallowed types (plugin)", func() {
@@ -251,7 +251,7 @@ var artifactInstallTests = Describe("install", func() {
 					"--config", configFilePath, "--allowed-types", "plugin"}
 			})
 
-			installAssertFailedBehavior(artifactInstallUsage, "ERRO: cannot download artifact of type \"rulesfile\": type not permitted")
+			installAssertFailedBehavior(artifactInstallUsage, "ERROR cannot download artifact of type \"rulesfile\": type not permitted")
 		})
 
 		When("an unknown type is used", func() {
@@ -281,7 +281,7 @@ var artifactInstallTests = Describe("install", func() {
 					"--config", configFilePath, "--allowed-types", "plugin," + wrongType}
 			})
 
-			installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERRO: invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+
+			installAssertFailedBehavior(artifactInstallUsage, fmt.Sprintf("ERROR invalid argument \"plugin,%s\" for \"--allowed-types\" flag: "+
 				"not valid token %q: must be one of \"rulesfile\", \"plugin\"", wrongType, wrongType))
 		})
 
@@ -315,7 +315,7 @@ var artifactInstallTests = Describe("install", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+
+				expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
 					"as install destination: %s is not writable", destDir, destDir)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
@@ -353,7 +353,7 @@ var artifactInstallTests = Describe("install", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+
+				expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
 					"as install destination: %s doesn't exists", destDir, destDir)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
@@ -391,7 +391,7 @@ var artifactInstallTests = Describe("install", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+
+				expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
 					"as install destination: %s is not writable", destDir, destDir)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
@@ -429,7 +429,7 @@ var artifactInstallTests = Describe("install", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf("ERRO: cannot use directory %q "+
+				expectedError := fmt.Sprintf("ERROR cannot use directory %q "+
 					"as install destination: %s doesn't exists", destDir, destDir)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(artifactInstallUsage)))
diff --git a/cmd/cli_test.go b/cmd/cli_test.go
index bc38d3e8..280b2f46 100644
--- a/cmd/cli_test.go
+++ b/cmd/cli_test.go
@@ -75,7 +75,7 @@ var tests = []testCase{
 
 func run(t *testing.T, test *testCase) {
 	// Setup
-	c := New(context.Background(), &options.Common{})
+	c := New(context.Background(), options.NewOptions())
 	o := bytes.NewBufferString("")
 	c.SetOut(o)
 	c.SetErr(o)
diff --git a/cmd/index/add/add.go b/cmd/index/add/add.go
index f820a645..228e22ef 100644
--- a/cmd/index/add/add.go
+++ b/cmd/index/add/add.go
@@ -56,6 +56,7 @@ func NewIndexAddCmd(ctx context.Context, opt *options.Common) *cobra.Command {
 // RunIndexAdd implements the index add command.
 func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error {
 	var err error
+	logger := o.Printer.Logger
 
 	name := args[0]
 	url := args[1]
@@ -64,24 +65,24 @@ func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error
 		backend = args[2]
 	}
 
-	o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, config.IndexesDir)
+	logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
 	indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
 	if err != nil {
 		return fmt.Errorf("unable to create index cache: %w", err)
 	}
 
-	o.Printer.Info.Printfln("Adding index")
+	logger.Info("Adding index", logger.Args("name", name, "path", url))
 
 	if err = indexCache.Add(ctx, name, backend, url); err != nil {
 		return fmt.Errorf("unable to add index: %w", err)
 	}
 
-	o.Printer.Verbosef("Writing cache to disk")
+	logger.Debug("Writing cache to disk")
 	if _, err = indexCache.Write(); err != nil {
 		return fmt.Errorf("unable to write cache to disk: %w", err)
 	}
 
-	o.Printer.Verbosef("Adding new index entry to configuration file %q", o.ConfigFile)
+	logger.Debug("Adding new index entry to configuration", logger.Args("file", o.ConfigFile))
 	if err = config.AddIndexes([]config.Index{{
 		Name:    name,
 		URL:     url,
@@ -90,7 +91,7 @@ func (o *IndexAddOptions) RunIndexAdd(ctx context.Context, args []string) error
 		return fmt.Errorf("index entry %q: %w", name, err)
 	}
 
-	o.Printer.Success.Printfln("Index %q successfully added", name)
+	logger.Info("Index successfully added")
 
 	return nil
 }
diff --git a/cmd/index/add/add_suite_test.go b/cmd/index/add/add_suite_test.go
index 8b41d3e1..52eb7655 100644
--- a/cmd/index/add/add_suite_test.go
+++ b/cmd/index/add/add_suite_test.go
@@ -67,7 +67,6 @@ var _ = BeforeSuite(func() {
 	// Create and configure the common options.
 	opt = commonoptions.NewOptions()
 	opt.Initialize(commonoptions.WithWriter(output))
-	opt.Printer.DisableStylingf()
 
 	// Create temporary directory used to save the configuration file.
 	configFile, err = testutils.CreateEmptyFile("falcoctl.yaml")
@@ -84,5 +83,5 @@ var _ = AfterSuite(func() {
 func executeRoot(args []string) error {
 	rootCmd.SetArgs(args)
 	rootCmd.SetOut(output)
-	return cmd.Execute(rootCmd, opt.Printer)
+	return cmd.Execute(rootCmd, opt)
 }
diff --git a/cmd/index/add/add_test.go b/cmd/index/add/add_test.go
index f17f9542..6aef43d4 100644
--- a/cmd/index/add/add_test.go
+++ b/cmd/index/add/add_test.go
@@ -33,9 +33,9 @@ Flags:
 -h, --help   help for add
 
 Global Flags:
-	--config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-	--disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
--v, --verbose           Enable verbose logs (default false)
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 `
 
 //nolint:lll // no need to check for line length.
@@ -48,9 +48,9 @@ Flags:
   -h, --help   help for add
 
 Global Flags:
-      --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -v, --verbose           Enable verbose logs (default false)
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 `
 
 var addAssertFailedBehavior = func(usage, specificError string) {
@@ -97,14 +97,14 @@ var indexAddTests = Describe("add", func() {
 			BeforeEach(func() {
 				args = []string{indexCmd, addCmd, "--config", configFile, indexName}
 			})
-			addAssertFailedBehavior(indexAddUsage, "ERRO: accepts between 2 and 3 arg(s), received 1")
+			addAssertFailedBehavior(indexAddUsage, "ERROR accepts between 2 and 3 arg(s), received 1")
 		})
 
 		When("with invalid URL", func() {
 			BeforeEach(func() {
 				args = []string{indexCmd, addCmd, "--config", configFile, indexName, "NOTAPROTOCAL://something"}
 			})
-			addAssertFailedBehavior(indexAddUsage, "ERRO: unable to add index: unable to fetch index \"testName\""+
+			addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\""+
 				" with URL \"NOTAPROTOCAL://something\": unable to fetch index: cannot fetch index: Get "+
 				"\"notaprotocal://something\": unsupported protocol scheme \"notaprotocal\"")
 		})
@@ -113,7 +113,7 @@ var indexAddTests = Describe("add", func() {
 			BeforeEach(func() {
 				args = []string{indexCmd, addCmd, "--config", configFile, indexName, "http://noindex", "notabackend"}
 			})
-			addAssertFailedBehavior(indexAddUsage, "ERRO: unable to add index: unable to fetch index \"testName\" "+
+			addAssertFailedBehavior(indexAddUsage, "ERROR unable to add index: unable to fetch index \"testName\" "+
 				"with URL \"http://noindex\": unsupported index backend type: notabackend")
 		})
 	})
diff --git a/cmd/index/remove/remove.go b/cmd/index/remove/remove.go
index 88ebce05..ce1f9c29 100644
--- a/cmd/index/remove/remove.go
+++ b/cmd/index/remove/remove.go
@@ -54,30 +54,32 @@ func NewIndexRemoveCmd(ctx context.Context, opt *options.Common) *cobra.Command
 }
 
 func (o *indexRemoveOptions) RunIndexRemove(ctx context.Context, args []string) error {
-	o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, config.IndexesDir)
+	logger := o.Printer.Logger
+
+	logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
 	indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
 	if err != nil {
 		return fmt.Errorf("unable to create index cache: %w", err)
 	}
 
 	for _, name := range args {
-		o.Printer.Info.Printfln("Removing index %q", name)
+		logger.Info("Removing index", logger.Args("name", name))
 		if err = indexCache.Remove(name); err != nil {
 			return fmt.Errorf("unable to remove index: %w", err)
 		}
 	}
 
-	o.Printer.Verbosef("Writing cache to disk")
+	logger.Debug("Writing cache to disk")
 	if _, err = indexCache.Write(); err != nil {
 		return fmt.Errorf("unable to write cache to disk: %w", err)
 	}
 
-	o.Printer.Verbosef("Removing indexes entries from configuration file %q", o.ConfigFile)
+	logger.Debug("Removing indexes entries from configuration", logger.Args("file", o.ConfigFile))
 	if err = config.RemoveIndexes(args, o.ConfigFile); err != nil {
 		return err
 	}
 
-	o.Printer.Success.Printfln("Indexes successfully removed")
+	logger.Info("Indexes successfully removed")
 
 	return nil
 }
diff --git a/cmd/index/update/update.go b/cmd/index/update/update.go
index af63826a..dd570691 100644
--- a/cmd/index/update/update.go
+++ b/cmd/index/update/update.go
@@ -52,25 +52,27 @@ func NewIndexUpdateCmd(ctx context.Context, opt *options.Common) *cobra.Command
 }
 
 func (o *indexUpdateOptions) RunIndexUpdate(ctx context.Context, args []string) error {
-	o.Printer.Verbosef("Creating in-memory cache using indexes file %q and indexes directory %q", config.IndexesFile, config.IndexesDir)
+	logger := o.Printer.Logger
+
+	logger.Debug("Creating in-memory cache using", logger.Args("indexes file", config.IndexesFile, "indexes directory", config.IndexesDir))
 	indexCache, err := cache.New(ctx, config.IndexesFile, config.IndexesDir)
 	if err != nil {
 		return fmt.Errorf("unable to create index cache: %w", err)
 	}
 
 	for _, arg := range args {
-		o.Printer.Info.Printfln("Updating index %q", arg)
+		logger.Info("Updating index file", logger.Args("name", arg))
 		if err := indexCache.Update(ctx, arg); err != nil {
 			return fmt.Errorf("an error occurred while updating index %q: %w", arg, err)
 		}
 	}
 
-	o.Printer.Verbosef("Writing cache to disk")
+	logger.Debug("Writing cache to disk")
 	if _, err = indexCache.Write(); err != nil {
 		return fmt.Errorf("unable to write cache to disk: %w", err)
 	}
 
-	o.Printer.Success.Printfln("Indexes successfully updated")
+	logger.Info("Indexes successfully updated")
 
 	return nil
 }
diff --git a/cmd/registry/auth/basic/basic.go b/cmd/registry/auth/basic/basic.go
index fbc8d0e0..86e0c039 100644
--- a/cmd/registry/auth/basic/basic.go
+++ b/cmd/registry/auth/basic/basic.go
@@ -58,7 +58,7 @@ func NewBasicCmd(ctx context.Context, opt *options.Common) *cobra.Command {
 // RunBasic executes the business logic for the basic command.
 func (o *loginOptions) RunBasic(ctx context.Context, args []string) error {
 	reg := args[0]
-
+	logger := o.Printer.Logger
 	user, token, err := utils.GetCredentials(o.Printer)
 	if err != nil {
 		return err
@@ -78,8 +78,8 @@ func (o *loginOptions) RunBasic(ctx context.Context, args []string) error {
 	if err := basic.Login(ctx, client, credentialStore, reg, user, token); err != nil {
 		return err
 	}
-	o.Printer.Verbosef("credentials added to credential store")
-	o.Printer.Success.Println("Login succeeded")
+	logger.Debug("Credentials added", logger.Args("credential store", config.RegistryCredentialConfPath()))
+	logger.Info("Login succeeded", logger.Args("registry", reg, "user", user))
 
 	return nil
 }
diff --git a/cmd/registry/auth/basic/basic_suite_test.go b/cmd/registry/auth/basic/basic_suite_test.go
index f6d60693..8d0dadec 100644
--- a/cmd/registry/auth/basic/basic_suite_test.go
+++ b/cmd/registry/auth/basic/basic_suite_test.go
@@ -102,7 +102,6 @@ var _ = BeforeSuite(func() {
 	// Create and configure the common options.
 	opt = commonoptions.NewOptions()
 	opt.Initialize(commonoptions.WithWriter(output))
-	opt.Printer.DisableStylingf()
 
 	// Start the local registry.
 	go func() {
@@ -131,5 +130,5 @@ var _ = AfterSuite(func() {
 func executeRoot(args []string) error {
 	rootCmd.SetArgs(args)
 	rootCmd.SetOut(output)
-	return cmd.Execute(rootCmd, opt.Printer)
+	return cmd.Execute(rootCmd, opt)
 }
diff --git a/cmd/registry/auth/basic/basic_test.go b/cmd/registry/auth/basic/basic_test.go
index 7f42118d..2519ca50 100644
--- a/cmd/registry/auth/basic/basic_test.go
+++ b/cmd/registry/auth/basic/basic_test.go
@@ -107,32 +107,8 @@ var registryAuthBasicTests = Describe("auth", func() {
 				args = []string{registryCmd, authCmd, basicCmd}
 			})
 			registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage,
-				"ERRO: accepts 1 arg(s), received 0")
+				"ERROR accepts 1 arg(s), received 0")
 		})
-
-		/*
-					When("wrong credentials", func() {
-						BeforeEach(func() {
-
-							ptyFile, ttyFile, err := pty.Open()
-							Expect(err).To(BeNil())
-
-							os.Stdin = ttyFile
-							input := `username1
-			password1
-			`
-							_, err = ptyFile.Write([]byte(input))
-							Expect(err).To(BeNil())
-
-							http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
-
-							args = []string{registryCmd, authCmd, basicCmd, "--config", configFile, registryBasic}
-						})
-
-						registryAuthBasicAssertFailedBehavior(registryAuthBasicUsage,
-							"ERRO: accepts 0 arg(s), received 0")
-					})
-		*/
 	})
 
 })
diff --git a/cmd/registry/auth/gcp/gcp.go b/cmd/registry/auth/gcp/gcp.go
index 8d9e56f7..0f2d332b 100644
--- a/cmd/registry/auth/gcp/gcp.go
+++ b/cmd/registry/auth/gcp/gcp.go
@@ -66,20 +66,21 @@ func NewGcpCmd(ctx context.Context, opt *options.Common) *cobra.Command {
 // RunGcp executes the business logic for the gcp command.
 func (o *RegistryGcpOptions) RunGcp(ctx context.Context, args []string) error {
 	var err error
+	logger := o.Printer.Logger
 	reg := args[0]
 	if err = gcp.Login(ctx, reg); err != nil {
 		return err
 	}
-	o.Printer.Success.Printfln("GCP authentication successful for %q", reg)
+	logger.Info("GCP authentication successful", logger.Args("registry", reg))
 
-	o.Printer.Verbosef("Adding new gcp entry to configuration file %q", o.ConfigFile)
+	logger.Debug("Adding new gcp entry to configuration", logger.Args("file", o.ConfigFile))
 	if err = config.AddGcp([]config.GcpAuth{{
 		Registry: reg,
 	}}, o.ConfigFile); err != nil {
 		return fmt.Errorf("index entry %q: %w", reg, err)
 	}
 
-	o.Printer.Success.Printfln("GCP authentication entry for %q successfully added in configuration file", reg)
+	logger.Info("GCG authentication entry successfully added", logger.Args("registry", reg, "confgi file", o.ConfigFile))
 
 	return nil
 }
diff --git a/cmd/registry/auth/oauth/oauth.go b/cmd/registry/auth/oauth/oauth.go
index c9824a69..a36889ab 100644
--- a/cmd/registry/auth/oauth/oauth.go
+++ b/cmd/registry/auth/oauth/oauth.go
@@ -17,6 +17,7 @@ package oauth
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/spf13/cobra"
 	"golang.org/x/oauth2/clientcredentials"
@@ -24,6 +25,7 @@ import (
 	"github.com/falcosecurity/falcoctl/internal/config"
 	"github.com/falcosecurity/falcoctl/internal/login/oauth"
 	"github.com/falcosecurity/falcoctl/pkg/options"
+	"github.com/falcosecurity/falcoctl/pkg/output"
 )
 
 const (
@@ -67,17 +69,15 @@ func NewOauthCmd(ctx context.Context, opt *options.Common) *cobra.Command {
 
 	cmd.Flags().StringVar(&o.Conf.TokenURL, "token-url", "", "token URL used to get access and refresh tokens")
 	if err := cmd.MarkFlagRequired("token-url"); err != nil {
-		o.Printer.Error.Println("unable to mark flag \"token-url\" as required")
-		return nil
+		output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"token-url\" as required"))
 	}
 	cmd.Flags().StringVar(&o.Conf.ClientID, "client-id", "", "client ID of the OAuth2.0 app")
 	if err := cmd.MarkFlagRequired("client-id"); err != nil {
-		o.Printer.Error.Println("unable to mark flag \"client-id\" as required")
-		return nil
+		output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-id\" as required"))
 	}
 	cmd.Flags().StringVar(&o.Conf.ClientSecret, "client-secret", "", "client secret of the OAuth2.0 app")
 	if err := cmd.MarkFlagRequired("client-secret"); err != nil {
-		o.Printer.Error.Println("unable to mark flag \"client-secret\" as required")
+		output.ExitOnErr(o.Printer, fmt.Errorf("unable to mark flag \"client-secret\" as required"))
 		return nil
 	}
 	cmd.Flags().StringSliceVar(&o.Conf.Scopes, "scopes", nil, "comma separeted list of scopes for which requesting access")
@@ -91,6 +91,6 @@ func (o *RegistryOauthOptions) RunOAuth(ctx context.Context, args []string) erro
 	if err := oauth.Login(ctx, reg, &o.Conf); err != nil {
 		return err
 	}
-	o.Printer.Success.Printfln("client credentials correctly saved in %q", config.ClientCredentialsFile)
+	o.Printer.Logger.Info("Client credentials correctly saved", o.Printer.Logger.Args("file", config.ClientCredentialsFile))
 	return nil
 }
diff --git a/cmd/registry/auth/oauth/oauth_suite_test.go b/cmd/registry/auth/oauth/oauth_suite_test.go
index c7a09698..e7977654 100644
--- a/cmd/registry/auth/oauth/oauth_suite_test.go
+++ b/cmd/registry/auth/oauth/oauth_suite_test.go
@@ -84,7 +84,6 @@ var _ = BeforeSuite(func() {
 	// Create and configure the common options.
 	opt = commonoptions.NewOptions()
 	opt.Initialize(commonoptions.WithWriter(output))
-	opt.Printer.DisableStylingf()
 
 	// Create the oras registry.
 	orasRegistry, err = testutils.NewOrasRegistry(registry, true)
@@ -115,5 +114,5 @@ var _ = AfterSuite(func() {
 func executeRoot(args []string) error {
 	rootCmd.SetArgs(args)
 	rootCmd.SetOut(output)
-	return cmd.Execute(rootCmd, opt.Printer)
+	return cmd.Execute(rootCmd, opt)
 }
diff --git a/cmd/registry/auth/oauth/oauth_test.go b/cmd/registry/auth/oauth/oauth_test.go
index 0ff6534d..9ae37eae 100644
--- a/cmd/registry/auth/oauth/oauth_test.go
+++ b/cmd/registry/auth/oauth/oauth_test.go
@@ -65,8 +65,9 @@ Flags:
 
 Global Flags:
       --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -v, --verbose           Enable verbose logs (default false)
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
+
 `
 
 //nolint:unused // false positive
@@ -132,7 +133,7 @@ var registryAuthOAuthTests = Describe("auth", func() {
 				args = []string{registryCmd, authCmd, oauthCmd}
 			})
 			registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
-				"ERRO: accepts 1 arg(s), received 0")
+				"ERROR accepts 1 arg(s), received 0")
 		})
 
 		When("wrong client id", func() {
@@ -152,7 +153,7 @@ var registryAuthOAuthTests = Describe("auth", func() {
 				}
 			})
 			registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
-				`ERRO: wrong client credentials, unable to retrieve token`)
+				`ERROR wrong client credentials, unable to retrieve token`)
 		})
 
 		When("wrong client secret", func() {
@@ -172,7 +173,7 @@ var registryAuthOAuthTests = Describe("auth", func() {
 				}
 			})
 			registryAuthOAuthAssertFailedBehavior(registryAuthOAuthUsage,
-				`ERRO: wrong client credentials, unable to retrieve token`)
+				`ERROR wrong client credentials, unable to retrieve token`)
 		})
 	})
 
@@ -199,7 +200,7 @@ var registryAuthOAuthTests = Describe("auth", func() {
 
 			It("should successed", func() {
 				Expect(output).Should(gbytes.Say(regexp.QuoteMeta(
-					`INFO: client credentials correctly saved in`)))
+					`INFO  Client credentials correctly saved`)))
 			})
 		})
 
diff --git a/cmd/registry/pull/pull.go b/cmd/registry/pull/pull.go
index 7f713fe6..4fbd1e03 100644
--- a/cmd/registry/pull/pull.go
+++ b/cmd/registry/pull/pull.go
@@ -88,6 +88,7 @@ func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
 			if err != nil {
 				return err
 			}
+			o.Common.Initialize()
 			return nil
 		},
 		RunE: func(cmd *cobra.Command, args []string) error {
@@ -103,6 +104,7 @@ func NewPullCmd(ctx context.Context, opt *options.Common) *cobra.Command {
 
 // RunPull executes the business logic for the pull command.
 func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
+	logger := o.Printer.Logger
 	ref := args[0]
 
 	registry, err := utils.GetRegistryFromRef(ref)
@@ -120,12 +122,12 @@ func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
 		return err
 	}
 
-	o.Printer.Info.Printfln("Preparing to pull artifact %q", args[0])
+	logger.Info("Preparing to pull artifact", logger.Args("name", args[0]))
 
 	if o.destDir == "" {
-		o.Printer.Info.Printfln("Pulling artifact in the current directory")
+		logger.Info("Pulling artifact in the current directory")
 	} else {
-		o.Printer.Info.Printfln("Pulling artifact in %q directory", o.destDir)
+		logger.Info("Pulling artifact in", logger.Args("directory", o.destDir))
 	}
 
 	os, arch := runtime.GOOS, runtime.GOARCH
@@ -138,7 +140,7 @@ func (o *pullOptions) RunPull(ctx context.Context, args []string) error {
 		return err
 	}
 
-	o.Printer.Success.Printfln("Artifact of type %q pulled. Digest: %q", res.Type, res.Digest)
+	logger.Info("Artifact pulled", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest))
 
 	return nil
 }
diff --git a/cmd/registry/pull/pull_suite_test.go b/cmd/registry/pull/pull_suite_test.go
index 89b521f9..d057e488 100644
--- a/cmd/registry/pull/pull_suite_test.go
+++ b/cmd/registry/pull/pull_suite_test.go
@@ -70,7 +70,6 @@ var _ = BeforeSuite(func() {
 	// Create and configure the common options.
 	opt = commonoptions.NewOptions()
 	opt.Initialize(commonoptions.WithWriter(output))
-	opt.Printer.DisableStylingf()
 
 	// Create the oras registry.
 	orasRegistry, err = testutils.NewOrasRegistry(registry, true)
@@ -97,5 +96,5 @@ var _ = AfterSuite(func() {
 func executeRoot(args []string) error {
 	rootCmd.SetArgs(args)
 	rootCmd.SetOut(output)
-	return cmd.Execute(rootCmd, opt.Printer)
+	return cmd.Execute(rootCmd, opt)
 }
diff --git a/cmd/registry/pull/pull_test.go b/cmd/registry/pull/pull_test.go
index 854a0a78..ad29fd01 100644
--- a/cmd/registry/pull/pull_test.go
+++ b/cmd/registry/pull/pull_test.go
@@ -144,7 +144,7 @@ var registryPullTests = Describe("pull", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pullCmd}
 			})
-			pullAssertFailedBehavior(registryPullUsage, "ERRO: accepts 1 arg(s), received 0")
+			pullAssertFailedBehavior(registryPullUsage, "ERROR accepts 1 arg(s), received 0")
 		})
 
 		When("unreachable registry", func() {
@@ -155,7 +155,7 @@ var registryPullTests = Describe("pull", func() {
 				Expect(err).To(BeNil())
 				args = []string{registryCmd, pullCmd, "noregistry/testrules", "--plain-http", "--config", configFile}
 			})
-			pullAssertFailedBehavior(registryPullUsage, "ERRO: unable to connect to remote registry")
+			pullAssertFailedBehavior(registryPullUsage, "ERROR unable to connect to remote registry")
 		})
 
 		When("invalid repository", func() {
@@ -167,7 +167,7 @@ var registryPullTests = Describe("pull", func() {
 				Expect(err).To(BeNil())
 				args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
 			})
-			pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERRO: %s: not found", newReg))
+			pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR %s: not found", newReg))
 		})
 
 		When("unwritable --dest-dir", func() {
@@ -200,7 +200,7 @@ var registryPullTests = Describe("pull", func() {
 				artName := tmp[0]
 				tag := tmp[1]
 				expectedError := fmt.Sprintf(
-					"ERRO: unable to pull artifact generic-repo with %s tag from repo %s: failed to create file",
+					"ERROR unable to pull artifact generic-repo with %s tag from repo %s: failed to create file",
 					tag, artName)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
@@ -233,10 +233,8 @@ var registryPullTests = Describe("pull", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf(
-					"ERRO: unable to push artifact failed to ensure directories of the target path: mkdir %s: permission denied\n"+
-						"ERRO: unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: mkdir %s: permission denied",
-					destDir, artifact, tag, artifact, destDir)
+				expectedError := fmt.Sprintf("ERROR unable to pull artifact %s with tag %s from repo %s: failed to ensure directories of the target path: "+
+					"mkdir %s: permission denied", artifact, tag, artifact, destDir)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
 				Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
@@ -264,7 +262,7 @@ var registryPullTests = Describe("pull", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf("ERRO: %s: not found", registry+repo+"@"+wrongDigest)
+				expectedError := fmt.Sprintf("ERROR %s: not found", registry+repo+"@"+wrongDigest)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
 				Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
@@ -282,7 +280,7 @@ var registryPullTests = Describe("pull", func() {
 			})
 
 			It("check that fails and the usage is not printed", func() {
-				expectedError := fmt.Sprintf("ERRO: cannot extract registry name from ref %q", ref)
+				expectedError := fmt.Sprintf("ERROR cannot extract registry name from ref %q", ref)
 				Expect(err).To(HaveOccurred())
 				Expect(output).ShouldNot(gbytes.Say(regexp.QuoteMeta(registryPullUsage)))
 				Expect(output).Should(gbytes.Say(regexp.QuoteMeta(expectedError)))
@@ -298,7 +296,7 @@ var registryPullTests = Describe("pull", func() {
 				Expect(err).To(BeNil())
 				args = []string{registryCmd, pullCmd, newReg, "--plain-http", "--config", configFile}
 			})
-			pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERRO: unable to create new repository with ref %s: "+
+			pullAssertFailedBehavior(registryPullUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+
 				"invalid reference: invalid digest; invalid checksum digest format\n", newReg))
 		})
 
diff --git a/cmd/registry/push/push.go b/cmd/registry/push/push.go
index 15fe0e0c..ce785d33 100644
--- a/cmd/registry/push/push.go
+++ b/cmd/registry/push/push.go
@@ -124,6 +124,7 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error {
 	// When creating the tar.gz archives we need to remove them after we are done.
 	// We save the temporary dir where they live here.
 	var toBeDeleted string
+	logger := o.Printer.Logger
 
 	registry, err := utils.GetRegistryFromRef(ref)
 	if err != nil {
@@ -140,12 +141,12 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error {
 		return err
 	}
 
-	o.Printer.Info.Printfln("Preparing to push artifact %q of type %q", args[0], o.ArtifactType)
+	logger.Info("Preparing to push artifact", o.Printer.Logger.Args("name", args[0], "type", o.ArtifactType))
 
 	// Make sure to remove temporary working dir.
 	defer func() {
 		if err := os.RemoveAll(toBeDeleted); err != nil {
-			o.Printer.Warning.Printfln("Unable to remove temporary dir %q: %s", toBeDeleted, err.Error())
+			logger.Warn("Unable to remove temporary dir", logger.Args("name", toBeDeleted, "error", err.Error()))
 		}
 	}()
 
@@ -202,7 +203,7 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error {
 		return err
 	}
 
-	o.Printer.Success.Printfln("Artifact pushed. Digest: %q", res.Digest)
+	logger.Info("Artifact pushed", logger.Args("name", args[0], "type", res.Type, "digest", res.Digest))
 
 	return nil
 }
diff --git a/cmd/registry/push/push_suite_test.go b/cmd/registry/push/push_suite_test.go
index 08900a41..761973b7 100644
--- a/cmd/registry/push/push_suite_test.go
+++ b/cmd/registry/push/push_suite_test.go
@@ -71,7 +71,6 @@ var _ = BeforeSuite(func() {
 	// Create and configure the common options.
 	opt = commonoptions.NewOptions()
 	opt.Initialize(commonoptions.WithWriter(output))
-	opt.Printer.DisableStylingf()
 
 	// Create the oras registry.
 	orasRegistry, err = testutils.NewOrasRegistry(registry, true)
@@ -97,5 +96,5 @@ var _ = AfterSuite(func() {
 func executeRoot(args []string) error {
 	rootCmd.SetArgs(args)
 	rootCmd.SetOut(output)
-	return cmd.Execute(rootCmd, opt.Printer)
+	return cmd.Execute(rootCmd, opt)
 }
diff --git a/cmd/registry/push/push_test.go b/cmd/registry/push/push_test.go
index bcb9bf80..4afc4fba 100644
--- a/cmd/registry/push/push_test.go
+++ b/cmd/registry/push/push_test.go
@@ -49,10 +49,9 @@ Flags:
       --version string             set the version of the artifact
 
 Global Flags:
-      --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -v, --verbose           Enable verbose logs (default false)
-
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 `
 
 //nolint:lll,unused // no need to check for line length.
@@ -104,9 +103,9 @@ Flags:
       --version string             set the version of the artifact
 
 Global Flags:
-      --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -v, --verbose           Enable verbose logs (default false)
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 `
 
 //nolint:unused // false positive
@@ -156,8 +155,8 @@ var registryPushTests = Describe("push", func() {
 		})
 
 		It("should match the saved one", func() {
-
-			Expect(output).Should(gbytes.Say(regexp.QuoteMeta(registryPushHelp)))
+			outputMsg := string(output.Contents())
+			Expect(outputMsg).Should(Equal(registryPushHelp))
 		})
 	})
 
@@ -174,21 +173,21 @@ var registryPushTests = Describe("push", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, rulesfiletgz, "--type", "rulesfile"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: required flag(s) \"version\" not set\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR required flag(s) \"version\" not set")
 		})
 
 		When("without rulesfile", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pushCmd, "--config", configFile, rulesRepo, "--type", "rulesfile"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: requires at least 2 arg(s), only received 1\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR requires at least 2 arg(s), only received 1")
 		})
 
 		When("without registry", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pushCmd, "--config", configFile, rulesfiletgz, "--type", "rulesfile"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: requires at least 2 arg(s), only received 1\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR requires at least 2 arg(s), only received 1")
 		})
 
 		When("multiple rulesfiles", func() {
@@ -196,7 +195,7 @@ var registryPushTests = Describe("push", func() {
 				args = []string{registryCmd, pushCmd, rulesRepo, "--config", configFile, rulesfiletgz, rulesfiletgz,
 					"--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: expecting 1 rulesfile object received 2: invalid number of rulesfiles\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR expecting 1 rulesfile object received 2: invalid number of rulesfiles")
 		})
 
 		When("unreachable registry", func() {
@@ -204,7 +203,7 @@ var registryPushTests = Describe("push", func() {
 				args = []string{registryCmd, pushCmd, "noregistry/testrules", "--config", configFile, rulesfiletgz,
 					"--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: unable to connect to remote "+
+			pushAssertFailedBehavior(registryPushUsage, "ERROR unable to connect to remote "+
 				"registry \"noregistry\": Get \"http://noregistry/v2/\": dial tcp: lookup noregistry")
 		})
 
@@ -212,7 +211,7 @@ var registryPushTests = Describe("push", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pushCmd, registry, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERRO: cannot extract registry name from ref %q", registry))
+			pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERROR cannot extract registry name from ref %q", registry))
 		})
 
 		When("invalid repository", func() {
@@ -220,7 +219,7 @@ var registryPushTests = Describe("push", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pushCmd, newReg, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1", "--plain-http"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERRO: unable to create new repository with ref %s: "+
+			pushAssertFailedBehavior(registryPushUsage, fmt.Sprintf("ERROR unable to create new repository with ref %s: "+
 				"invalid reference: invalid digest; invalid checksum digest format\n", newReg))
 		})
 
@@ -229,7 +228,7 @@ var registryPushTests = Describe("push", func() {
 				args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile", "--version", "1.1.1",
 					"--plain-http", "--requires", "wrongreq"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: cannot parse \"wrongreq\"\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR cannot parse \"wrongreq\"")
 		})
 
 		When("invalid dependency", func() {
@@ -237,7 +236,7 @@ var registryPushTests = Describe("push", func() {
 				args = []string{registryCmd, pushCmd, rulesRepo, rulesfiletgz, "--config", configFile, "--type", "rulesfile",
 					"--version", "1.1.1", "--plain-http", "--depends-on", "wrongdep"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: cannot parse \"wrongdep\": invalid artifact reference "+
+			pushAssertFailedBehavior(registryPushUsage, "ERROR cannot parse \"wrongdep\": invalid artifact reference "+
 				"(must be in the format \"name:version\")\n")
 		})
 
@@ -245,8 +244,8 @@ var registryPushTests = Describe("push", func() {
 			BeforeEach(func() {
 				args = []string{registryCmd, pushCmd, pluginsRepo, plugintgz, "--config", configFile, "--type", "plugin", "--version", "1.1.1", "--plain-http"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: \"filepaths\" length (1) must match \"platforms\" "+
-				"length (0): number of filepaths and platform should be the same\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR \"filepaths\" length (1) must match \"platforms\" "+
+				"length (0): number of filepaths and platform should be the same")
 		})
 
 		When("wrong plugin type", func() {
@@ -254,7 +253,7 @@ var registryPushTests = Describe("push", func() {
 				args = []string{registryCmd, pushCmd, pluginsRepo, pluginsRepo, "--config", configFile,
 					"--type", "wrongType", "--version", "1.1.1", "--plain-http"}
 			})
-			pushAssertFailedBehavior(registryPushUsage, "ERRO: invalid argument \"wrongType\" for \"--type\" flag: must be one of \"rulesfile\", \"plugin\"\n")
+			pushAssertFailedBehavior(registryPushUsage, "ERROR invalid argument \"wrongType\" for \"--type\" flag: must be one of \"rulesfile\", \"plugin\"")
 		})
 	})
 
@@ -295,7 +294,7 @@ var registryPushTests = Describe("push", func() {
 				// We do not check the error here since we are checking it before
 				// pulling the artifact.
 				By("checking no error in output")
-				Expect(output).ShouldNot(gbytes.Say("ERRO:"))
+				Expect(output).ShouldNot(gbytes.Say("ERROR"))
 				Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir"))
 
 				By("checking descriptor")
@@ -471,7 +470,7 @@ var registryPushTests = Describe("push", func() {
 				// We do not check the error here since we are checking it before
 				// pulling the artifact.
 				By("checking no error in output")
-				Expect(output).ShouldNot(gbytes.Say("ERRO:"))
+				Expect(output).ShouldNot(gbytes.Say("ERROR"))
 				Expect(output).ShouldNot(gbytes.Say("Unable to remove temporary dir"))
 
 				By("checking descriptor")
diff --git a/cmd/registry/registry.go b/cmd/registry/registry.go
index a35fcd9d..30cccc2a 100644
--- a/cmd/registry/registry.go
+++ b/cmd/registry/registry.go
@@ -36,6 +36,8 @@ func NewRegistryCmd(ctx context.Context, opt *commonoptions.Common) *cobra.Comma
 		Long:                  "Interact with OCI registries",
 		SilenceErrors:         true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
+			// Initialize the options.
+			opt.Initialize()
 			// Load configuration from ENV variables and/or config file.
 			return config.Load(opt.ConfigFile)
 		},
diff --git a/cmd/root.go b/cmd/root.go
index d1d105de..186f91ca 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -26,7 +26,6 @@ import (
 	"github.com/falcosecurity/falcoctl/cmd/tls"
 	"github.com/falcosecurity/falcoctl/cmd/version"
 	"github.com/falcosecurity/falcoctl/pkg/options"
-	"github.com/falcosecurity/falcoctl/pkg/output"
 )
 
 const (
@@ -48,7 +47,14 @@ func New(ctx context.Context, opt *options.Common) *cobra.Command {
 		Use:               "falcoctl",
 		Short:             "The official CLI tool for working with Falco and its ecosystem components",
 		Long:              longRootCmd,
+		SilenceErrors:     true,
 		DisableAutoGenTag: true,
+		PersistentPreRun: func(cmd *cobra.Command, args []string) {
+			// Initialize the common options for all subcommands.
+			// Subcommands con overwrite the default settings by calling initialize with
+			// different options.
+			opt.Initialize()
+		},
 	}
 
 	// Global flags
@@ -65,10 +71,10 @@ func New(ctx context.Context, opt *options.Common) *cobra.Command {
 }
 
 // Execute configures the signal handlers and runs the command.
-func Execute(cmd *cobra.Command, printer *output.Printer) error {
+func Execute(cmd *cobra.Command, opt *options.Common) error {
 	// we do not log the error here since we expect that each subcommand
 	// handles the errors by itself.
 	err := cmd.Execute()
-	printer.CheckErr(err)
+	opt.Printer.CheckErr(err)
 	return err
 }
diff --git a/cmd/testdata/help.txt b/cmd/testdata/help.txt
index c94979f7..5fdd1c4f 100644
--- a/cmd/testdata/help.txt
+++ b/cmd/testdata/help.txt
@@ -21,9 +21,9 @@ Available Commands:
   version     Print the falcoctl version information
 
 Flags:
-      --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -h, --help              help for falcoctl
-  -v, --verbose           Enable verbose logs (default false)
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+  -h, --help                help for falcoctl
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 
 Use "falcoctl [command] --help" for more information about a command.
diff --git a/cmd/testdata/noargsnoflags.txt b/cmd/testdata/noargsnoflags.txt
index c94979f7..5fdd1c4f 100644
--- a/cmd/testdata/noargsnoflags.txt
+++ b/cmd/testdata/noargsnoflags.txt
@@ -21,9 +21,9 @@ Available Commands:
   version     Print the falcoctl version information
 
 Flags:
-      --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -h, --help              help for falcoctl
-  -v, --verbose           Enable verbose logs (default false)
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+  -h, --help                help for falcoctl
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 
 Use "falcoctl [command] --help" for more information about a command.
diff --git a/cmd/testdata/wrongflag.txt b/cmd/testdata/wrongflag.txt
index 429693f0..84363da1 100644
--- a/cmd/testdata/wrongflag.txt
+++ b/cmd/testdata/wrongflag.txt
@@ -1,4 +1,3 @@
-Error: unknown flag: --wrong
 Usage:
   falcoctl [command]
 
@@ -12,10 +11,10 @@ Available Commands:
   version     Print the falcoctl version information
 
 Flags:
-      --config string     config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
-      --disable-styling   Disable output styling such as spinners, progress bars and colors. Styling is automatically disabled if not attacched to a tty (default false)
-  -h, --help              help for falcoctl
-  -v, --verbose           Enable verbose logs (default false)
+      --config string       config file to be used for falcoctl (default "/etc/falcoctl/falcoctl.yaml")
+  -h, --help                help for falcoctl
+      --log-format string   Set formatting for logs (color, text, json) (default "color")
+      --log-level string    Set level for logs (info, warn, debug, trace) (default "info")
 
 Use "falcoctl [command] --help" for more information about a command.
 
diff --git a/cmd/version/version.go b/cmd/version/version.go
index 7934fa1f..ef97ff2b 100644
--- a/cmd/version/version.go
+++ b/cmd/version/version.go
@@ -119,20 +119,17 @@ func (o *options) Run(v *version) error {
 	case yamlFormat:
 		marshaled, err := yaml.Marshal(v)
 		if err != nil {
-			o.Printer.Error.Println(err.Error())
 			return err
 		}
 		o.Printer.DefaultText.Printf("%s:\n%s\n", "Client Version", string(marshaled))
 	case jsonFormat:
 		marshaled, err := json.MarshalIndent(v, "", "   ")
 		if err != nil {
-			o.Printer.Error.Println(err.Error())
 			return err
 		}
 		o.Printer.DefaultText.Printf("%s:\n%s \n", "Client Version", string(marshaled))
 	default:
 		// We should never hit this case.
-		o.Printer.Error.Printf("options of the version command were not validated: --output=%q should have been rejected", o.Output)
 		return fmt.Errorf("options of the version command were not validated: --output=%q should have been rejected", o.Output)
 	}
 
diff --git a/cmd/version/version_test.go b/cmd/version/version_test.go
index 5a240810..de39ea6d 100644
--- a/cmd/version/version_test.go
+++ b/cmd/version/version_test.go
@@ -150,7 +150,6 @@ var _ = Describe("Version", func() {
 			Context("run method", func() {
 				It("should print the error message", func() {
 					Expect(opt.Run(version)).Error().Should(HaveOccurred())
-					Expect(writer).Should(gbytes.Say("options of the version command were not validated"))
 				})
 			})
 		})
diff --git a/go.mod b/go.mod
index c3f02854..2b133809 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
 	github.com/go-oauth2/oauth2/v4 v4.5.2
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/google/go-containerregistry v0.16.1
+	github.com/gookit/color v1.5.4
 	github.com/mitchellh/mapstructure v1.5.0
 	github.com/onsi/ginkgo/v2 v2.10.0
 	github.com/onsi/gomega v1.27.8
@@ -163,7 +164,6 @@ require (
 	github.com/google/uuid v1.3.1 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
-	github.com/gookit/color v1.5.4 // indirect
 	github.com/gorilla/handlers v1.5.1 // indirect
 	github.com/gorilla/mux v1.8.0 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
diff --git a/internal/follower/follower.go b/internal/follower/follower.go
index ff2f3031..4f6526be 100644
--- a/internal/follower/follower.go
+++ b/internal/follower/follower.go
@@ -29,6 +29,7 @@ import (
 	"time"
 
 	"github.com/blang/semver"
+	"github.com/pterm/pterm"
 	"github.com/robfig/cron/v3"
 	"oras.land/oras-go/v2/registry"
 
@@ -52,8 +53,9 @@ type Follower struct {
 	currentDigest string
 	*ocipuller.Puller
 	*Config
-	*output.Printer
+	logger *pterm.Logger
 	config.FalcoVersions
+	*output.Printer
 }
 
 // Config configuration options for the Follower.
@@ -71,8 +73,6 @@ type Config struct {
 	ArtifactReference string
 	// PlainHTTP is set to true if all registry interaction must be in plain http.
 	PlainHTTP bool
-	// Verbose enables the verbose logs.
-	Verbose bool
 	// TmpDir directory where to save temporary files.
 	TmpDir string
 	// FalcoVersions is a struct containing all the required Falco versions that this follower
@@ -118,15 +118,13 @@ func New(ref string, printer *output.Printer, conf *Config) (*Follower, error) {
 		return nil, fmt.Errorf("unable to create temporary directory: %w", err)
 	}
 
-	customPrinter := printer.WithScope(ref)
-
 	return &Follower{
 		ref:           ref,
 		tag:           tag,
 		tmpDir:        tmpDir,
 		Puller:        puller,
 		Config:        conf,
-		Printer:       customPrinter,
+		logger:        printer.Logger,
 		FalcoVersions: conf.FalcoVersions,
 	}, nil
 }
@@ -142,7 +140,7 @@ func (f *Follower) Follow(ctx context.Context) {
 		select {
 		case <-f.CloseChan:
 			f.cleanUp()
-			fmt.Printf("follower for %q stopped\n", f.ref)
+			f.logger.Info("Follower stopped", f.logger.Args("followerName", f.ref))
 			// Notify that the follower is done.
 			f.WaitGroup.Done()
 			return
@@ -155,109 +153,111 @@ func (f *Follower) Follow(ctx context.Context) {
 
 func (f *Follower) follow(ctx context.Context) {
 	// First thing get the descriptor from remote repo.
-	f.Verbosef("fetching descriptor from remote repository...")
+	f.logger.Debug("Fetching descriptor from remote repository...", f.logger.Args("followerName", f.ref))
 	desc, err := f.Descriptor(ctx, f.ref)
 	if err != nil {
-		f.Error.Printfln("an error occurred while fetching descriptor from remote repository: %v", err)
+		f.logger.Debug(fmt.Sprintf("an error occurred while fetching descriptor from remote repository: %v", err))
 		return
 	}
-	f.Verbosef("descriptor correctly fetched")
+	f.logger.Debug("Descriptor correctly fetched", f.logger.Args("followerName", f.ref))
 
 	// If we have already processed then do nothing.
 	// TODO(alacuku): check that the file also exists to cover the case when someone has removed the file.
 	if desc.Digest.String() == f.currentDigest {
-		f.Verbosef("nothing to do, artifact already up to date.")
+		f.logger.Debug("Nothing to do, artifact already up to date.", f.logger.Args("followerName", f.ref))
 		return
 	}
 
-	f.Info.Printfln("found new version under tag %q", f.tag)
+	f.logger.Info("Found new artifact version", f.logger.Args("followerName", f.ref, "tag", f.tag))
 
 	// Pull config layer to check falco versions
 	artifactConfig, err := f.PullConfigLayer(ctx, f.ref)
 	if err != nil {
-		f.Error.Printfln("unable to pull config layer for ref %q: %v", f.ref, err)
+		f.logger.Error("Unable to pull config layer", f.logger.Args("followerName", f.ref, "reason", err.Error()))
 		return
 	}
 
 	err = f.checkRequirements(artifactConfig)
 	if err != nil {
-		f.Error.Printfln("unmet requirements for ref %q: %v", f.ref, err)
+		f.logger.Error("Unmet requirements", f.logger.Args("followerName", f.ref, "reason", err.Error()))
 		return
 	}
 
-	f.Verbosef("pulling artifact from remote repository...")
+	f.logger.Debug("Pulling artifact", f.logger.Args("followerName", f.ref))
 	// Pull the artifact from the repository.
 	filePaths, res, err := f.pull(ctx)
 	if err != nil {
-		f.Error.Printfln("an error occurred while pulling artifact from remote repository: %v", err)
+		f.logger.Error("Unable to pull artifact", f.logger.Args("followerName", f.ref, "reason", err.Error()))
 		return
 	}
-	f.Verbosef("artifact correctly pulled")
+	f.logger.Debug("Artifact correctly pulled", f.logger.Args("followerName", f.ref))
 
 	dstDir := f.destinationDir(res)
 
 	// Check if directory exists and is writable.
 	err = utils.ExistsAndIsWritable(dstDir)
 	if err != nil {
-		f.Error.Printfln("cannot use directory %q as install destination: %v", dstDir, err)
+		f.logger.Error("Invalid destination", f.logger.Args("followerName", f.ref, "directory", dstDir, "reason", err.Error()))
 		return
 	}
 
 	// Install the artifacts if necessary.
 	for _, path := range filePaths {
 		baseName := filepath.Base(path)
-		f.Verbosef("installing file %q...", baseName)
+		f.logger.Debug("Installing file", f.logger.Args("followerName", f.ref, "fileName", baseName))
 		dstPath := filepath.Join(dstDir, baseName)
 		// Check if the file exists.
-		f.Verbosef("checking if file %q already exists in %q", baseName, dstDir)
+		f.logger.Debug("Checking if file already exists", f.logger.Args("followerName", f.ref, "fileName", baseName, "directory", dstDir))
 		exists, err := fileExists(dstPath)
 		if err != nil {
-			f.Error.Printfln("an error occurred while checking %q existence: %v", baseName, err)
+			f.logger.Error("Unable to check existence for file", f.logger.Args("followerName", f.ref, "fileName", baseName, "reason", err.Error()))
 			return
 		}
 
 		if !exists {
-			f.Verbosef("file %q does not exist in %q, moving it", baseName, dstDir)
+			f.logger.Debug("Moving file", f.logger.Args("followerName", f.ref, "fileName", baseName, "destDirectory", dstDir))
 			if err = utils.Move(path, dstPath); err != nil {
-				f.Error.Printfln("an error occurred while moving file %q to %q: %v", baseName, dstDir, err)
+				f.logger.Error("Unable to move file", f.logger.Args("followerName", f.ref, "fileName", baseName, "destDirectory", dstDir, "reason", err.Error()))
 				return
 			}
-			f.Verbosef("file %q correctly installed", path)
+			f.logger.Debug("File correctly installed", f.logger.Args("followerName", f.ref, "path", path))
 			// It's done, move to the next file.
 			continue
 		}
-		f.Verbosef("file %q already exists in %q, checking if it is equal to the existing one", baseName, dstDir)
+		f.logger.Debug(fmt.Sprintf("file %q already exists in %q, checking if it is equal to the existing one", baseName, dstDir),
+			f.logger.Args("followerName", f.ref))
 		// Check if the files are equal.
 		eq, err := equal([]string{path, dstPath})
 		if err != nil {
-			f.Error.Printfln("an error occurred while comparing files %q and %q: %v", path, dstPath, err)
+			f.logger.Error("Unable to compare files", f.logger.Args("followerName", f.ref, "newFile", path, "existingFile", dstPath, "reason", err.Error()))
 			return
 		}
 
 		if !eq {
-			f.Verbosef("overwriting file %q with file %q", dstPath, path)
+			f.logger.Debug(fmt.Sprintf("Overwriting file %q with file %q", dstPath, path), f.logger.Args("followerName", f.ref))
 			if err = utils.Move(path, dstPath); err != nil {
-				f.Error.Printfln("an error occurred while overwriting file %q: %v", dstPath, err)
+				f.logger.Error("Unable to overwrite file", f.logger.Args("followerName", f.ref, "existingFile", dstPath, "reason", err.Error()))
 				return
 			}
 		} else {
-			f.Verbosef("the two file are equal, nothing to be done")
+			f.logger.Debug("The two file are equal, nothing to be done")
 		}
 	}
 
-	f.Info.Printfln("artifact with tag %q correctly installed", f.tag)
+	f.logger.Info("Artifact correctly installed",
+		f.logger.Args("followerName", f.ref, "artifactName", f.ref, "type", res.Type, "digest", res.Digest, "directory", dstDir))
 	f.currentDigest = desc.Digest.String()
 }
 
 // pull downloads, extracts, and installs the artifact.
 func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.RegistryResult, err error) {
-	f.Verbosef("check if pulling an allowed type of artifact")
+	f.logger.Debug("Check if pulling an allowed type of artifact", f.logger.Args("followerName", f.ref))
 	if err := f.Puller.CheckAllowedType(ctx, f.ref, f.Config.AllowedTypes.Types); err != nil {
 		return nil, nil, err
 	}
 
 	// Pull the artifact from the repository.
-	f.Verbosef("pulling artifact %q", f.ref)
+	f.logger.Debug("Pulling artifact %q", f.logger.Args("followerName", f.ref, "artifactName", f.ref))
 	res, err = f.Pull(ctx, f.ref, f.tmpDir, runtime.GOOS, runtime.GOARCH)
 	if err != nil {
 		return filePaths, res, fmt.Errorf("unable to pull artifact %q: %w", f.ref, err)
@@ -272,15 +272,15 @@ func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.Regis
 
 	// Verify the signature if needed
 	if f.Config.Signature != nil {
-		f.Verbosef("verifying signature for %s", digestRef)
+		f.logger.Debug("Verifying signature", f.logger.Args("followerName", f.ref, "digest", digestRef))
 		err = signature.Verify(ctx, digestRef, f.Config.Signature)
 		if err != nil {
 			return filePaths, res, fmt.Errorf("could not verify signature for %s: %w", res.RootDigest, err)
 		}
-		f.Verbosef("signature successfully verified")
+		f.logger.Debug("Signature successfully verified")
 	}
 
-	f.Verbosef("extracting artifact")
+	f.logger.Debug("Extracting artifact", f.logger.Args("followerName", f.ref))
 	res.Filename = filepath.Join(f.tmpDir, res.Filename)
 
 	file, err := os.Open(res.Filename)
@@ -294,7 +294,7 @@ func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.Regis
 		return filePaths, res, fmt.Errorf("unable to extract %q to %q: %w", res.Filename, f.tmpDir, err)
 	}
 
-	f.Verbosef("cleaning up leftovers files")
+	f.logger.Debug("Cleaning up leftovers files", f.logger.Args("followerName", f.ref))
 	err = os.Remove(res.Filename)
 	if err != nil {
 		return filePaths, res, fmt.Errorf("unable to remove file %q: %w", res.Filename, err)
@@ -364,7 +364,7 @@ func (f *Follower) checkRequirements(artifactConfig *oci.ArtifactConfig) error {
 
 func (f *Follower) cleanUp() {
 	if err := os.RemoveAll(f.tmpDir); err != nil {
-		f.DefaultText.Printfln("an error occurred while removing working directory %q:%v", f.tmpDir, err)
+		f.logger.Warn("Unable to clean working directory", f.logger.Args("followerName", f.ref, "directory", f.tmpDir, "reason", err))
 	}
 }
 
diff --git a/internal/utils/credentials.go b/internal/utils/credentials.go
index a726470c..7ca6a3c5 100644
--- a/internal/utils/credentials.go
+++ b/internal/utils/credentials.go
@@ -29,20 +29,18 @@ import (
 func GetCredentials(p *output.Printer) (username, password string, err error) {
 	reader := bufio.NewReader(os.Stdin)
 
-	p.DefaultText.Print("Username: ")
+	p.DefaultText.Print(p.FormatTitleAsLoggerInfo("Enter username:"))
 	username, err = reader.ReadString('\n')
 	if err != nil {
 		return "", "", err
 	}
 
-	p.DefaultText.Print("Password: ")
+	p.Logger.Info("Enter password: ")
 	bytePassword, err := term.ReadPassword(int(os.Stdin.Fd()))
 	if err != nil {
 		return "", "", err
 	}
 
-	p.DefaultText.Println()
-
 	password = string(bytePassword)
 	return strings.TrimSpace(username), strings.TrimSpace(password), nil
 }
diff --git a/main.go b/main.go
index f491ddf8..d9b9a416 100644
--- a/main.go
+++ b/main.go
@@ -36,7 +36,11 @@ func main() {
 	// If the ctx is marked as done then we reset the signals.
 	go func() {
 		<-ctx.Done()
-		opt.Printer.Info.Println("Received signal, terminating...")
+		// Stop all the printers if any is active
+		if opt.Printer != nil && opt.Printer.ProgressBar != nil && opt.Printer.ProgressBar.IsActive {
+			_, _ = opt.Printer.ProgressBar.Stop()
+		}
+		opt.Printer.Logger.Info("Received signal, terminating...")
 		stop()
 	}()
 
@@ -44,7 +48,7 @@ func main() {
 	rootCmd := cmd.New(ctx, opt)
 
 	// Execute the command.
-	if err := cmd.Execute(rootCmd, opt.Printer); err != nil {
+	if err := cmd.Execute(rootCmd, opt); err != nil {
 		os.Exit(1)
 	}
 	os.Exit(0)
diff --git a/pkg/install/tls/generator.go b/pkg/install/tls/generator.go
index 308e7ba0..b89dc6ec 100644
--- a/pkg/install/tls/generator.go
+++ b/pkg/install/tls/generator.go
@@ -28,7 +28,7 @@ import (
 	"path/filepath"
 	"time"
 
-	"github.com/falcosecurity/falcoctl/pkg/output"
+	"github.com/pterm/pterm"
 )
 
 // A GRPCTLS represents a TLS Generator for Falco.
@@ -242,7 +242,7 @@ func (g *GRPCTLS) GenerateClient(caTemplate *x509.Certificate, caKey DSAKey, not
 }
 
 // FlushToDisk is used to persist the cert material from a GRPCTLS to disk given a path.
-func (g *GRPCTLS) FlushToDisk(path string, logger *output.Printer) error {
+func (g *GRPCTLS) FlushToDisk(path string, logger *pterm.Logger) error {
 	p, err := satisfyDir(path)
 	if err != nil {
 		return fmt.Errorf("invalid path: %w", err)
@@ -251,13 +251,13 @@ func (g *GRPCTLS) FlushToDisk(path string, logger *output.Printer) error {
 
 	for _, name := range certsFilenames {
 		f := filepath.Join(path, name)
-		logger.Info.Printf("Saving %s to %s\n", name, path)
+		logger.Info("Saving file", logger.Args("name", name, "directory", path))
 		if err := os.WriteFile(f, g.certs[name].Bytes(), 0o600); err != nil {
 			return fmt.Errorf("unable to write %q: %w", name, err)
 		}
 	}
 
-	logger.Info.Println("Done generating the TLS certificates")
+	logger.Info("Done generating the TLS certificates")
 	return nil
 }
 
diff --git a/pkg/install/tls/handler.go b/pkg/install/tls/handler.go
index 79ae2a0f..979b87db 100644
--- a/pkg/install/tls/handler.go
+++ b/pkg/install/tls/handler.go
@@ -40,6 +40,7 @@ type Options struct {
 
 // Run executes the business logic of the `install tls` command.
 func (o *Options) Run() error {
+	logger := o.Common.Printer.Logger
 	// If the output path is not given then get the current working directory.
 	if o.Path == "" {
 		cwd, err := os.Getwd()
@@ -49,7 +50,7 @@ func (o *Options) Run() error {
 		o.Path = cwd
 	}
 
-	o.Common.Printer.Info.Printf("Generating certificates in %s directory\n", o.Path)
+	logger.Info("Generating certificates", logger.Args("directory", o.Path))
 
 	keyGenerator := NewKeyGenerator(DSAType(o.Algorithm))
 
@@ -84,5 +85,5 @@ func (o *Options) Run() error {
 		return err
 	}
 
-	return generator.FlushToDisk(o.Path, o.Common.Printer)
+	return generator.FlushToDisk(o.Path, logger)
 }
diff --git a/pkg/options/common.go b/pkg/options/common.go
index 9b377320..dca5dfb4 100644
--- a/pkg/options/common.go
+++ b/pkg/options/common.go
@@ -18,6 +18,7 @@ package options
 import (
 	"io"
 
+	"github.com/pterm/pterm"
 	"github.com/spf13/pflag"
 
 	"github.com/falcosecurity/falcoctl/internal/config"
@@ -31,37 +32,35 @@ import (
 type Common struct {
 	// Printer used by all commands to output messages.
 	Printer *output.Printer
-	// printerScope contains the data of the optional scope of a prefix.
-	// It used to add a prefix to the output of a printer.
-	printerScope string
 	// writer is used to write the output of the printer.
 	writer io.Writer
 	// Used to store the verbose flag, and then passed to the printer.
+	// Deprecated: will be removed in the future
 	verbose bool
 	// Disable the styling if set to true.
+	// Deprecated: will be removed in the future
 	disableStyling bool
 	// Config file. It must not be possible to be reinitialized by subcommands,
 	// using the Initialize function. It will be attached as global flags.
 	ConfigFile string
 	// IndexCache caches the entries for the configured indexes.
 	IndexCache *cache.Cache
+
+	logLevel  *LogLevel
+	logFormat *LogFormat
 }
 
 // NewOptions returns a new Common struct.
 func NewOptions() *Common {
-	return &Common{}
+	return &Common{
+		logLevel:  NewLogLevel(),
+		logFormat: NewLogFormat(),
+	}
 }
 
 // Configs type of the configs accepted by the Initialize function.
 type Configs func(options *Common)
 
-// WithPrinterScope sets the scope for the printer.
-func WithPrinterScope(scope string) Configs {
-	return func(options *Common) {
-		options.printerScope = scope
-	}
-}
-
 // WithWriter sets the writer for the printer.
 func WithWriter(writer io.Writer) Configs {
 	return func(options *Common) {
@@ -83,20 +82,35 @@ func (o *Common) Initialize(cfgs ...Configs) {
 		cfg(o)
 	}
 
-	// create the printer. The value of verbose is a flag value.
-	o.Printer = output.NewPrinter(o.printerScope, o.disableStyling, o.verbose, o.writer)
-}
+	// TODO(alacuku): remove once we remove the old flags
+	var logLevel pterm.LogLevel
+	if o.verbose {
+		logLevel = pterm.LogLevelDebug
+	} else {
+		logLevel = o.logLevel.ToPtermLogLevel()
+	}
 
-// IsVerbose used to check if the verbose flag is set or not.
-func (o *Common) IsVerbose() bool {
-	return o.verbose
+	var logFormatter pterm.LogFormatter
+	if o.disableStyling {
+		logFormatter = pterm.LogFormatterJSON
+	} else {
+		logFormatter = o.logFormat.ToPtermFormatter()
+	}
+
+	// create the printer. The value of verbose is a flag value.
+	o.Printer = output.NewPrinter(logLevel, logFormatter, o.writer)
 }
 
 // AddFlags registers the common flags.
 func (o *Common) AddFlags(flags *pflag.FlagSet) {
 	flags.BoolVarP(&o.verbose, "verbose", "v", false, "Enable verbose logs (default false)")
+	// Mark the verbose flag as deprecated.
+	_ = flags.MarkDeprecated("verbose", "please use --log-level")
 	flags.BoolVar(&o.disableStyling, "disable-styling", false, "Disable output styling such as spinners, progress bars and colors. "+
-		"Styling is automatically disabled if not attacched to a tty (default false)")
-	// Add global config
+		"Styling is automatically disabled if not attached to a tty (default false)")
+	// Mark the disableStyling as deprecated.
+	_ = flags.MarkDeprecated("disable-styling", "please use --log-format")
 	flags.StringVar(&o.ConfigFile, "config", config.ConfigPath, "config file to be used for falcoctl")
+	flags.Var(o.logFormat, "log-format", "Set formatting for logs (color, text, json)")
+	flags.Var(o.logLevel, "log-level", "Set level for logs (info, warn, debug, trace)")
 }
diff --git a/pkg/options/enum.go b/pkg/options/enum.go
new file mode 100644
index 00000000..a9e53143
--- /dev/null
+++ b/pkg/options/enum.go
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"fmt"
+	"strings"
+
+	"golang.org/x/exp/slices"
+)
+
+// Enum implements the flag interface. It can be used as a base for new flags that
+// can have a limited set of values.
+type Enum struct {
+	allowed []string
+	value   string
+}
+
+// NewEnum returns an enum struct. The firs argument is a set of values allowed for the flag.
+// The second argument is the default value of the flag.
+func NewEnum(allowed []string, d string) *Enum {
+	return &Enum{
+		allowed: allowed,
+		value:   d,
+	}
+}
+
+// String returns the value.
+func (e *Enum) String() string {
+	return e.value
+}
+
+// Set the value for the flag.
+func (e *Enum) Set(p string) error {
+	if !slices.Contains(e.allowed, p) {
+		return fmt.Errorf("invalid argument %q, please provide one of (%s)", p, strings.Join(e.allowed, ", "))
+	}
+	e.value = p
+	return nil
+}
+
+// Type returns the type of the flag.
+func (e *Enum) Type() string {
+	return "string"
+}
diff --git a/pkg/options/enum_test.go b/pkg/options/enum_test.go
new file mode 100644
index 00000000..f36efe3e
--- /dev/null
+++ b/pkg/options/enum_test.go
@@ -0,0 +1,96 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("Enum", func() {
+	var (
+		allowed  = []string{"val1", "val2", "val3"}
+		defValue = "val1"
+		enum     *Enum
+	)
+
+	BeforeEach(func() {
+		enum = NewEnum(allowed, defValue)
+	})
+
+	JustAfterEach(func() {
+		enum = nil
+	})
+
+	Context("NewEnum Func", func() {
+		It("should return a not nil struct", func() {
+			Expect(enum).ShouldNot(BeNil())
+		})
+
+		It("should set the default values", func() {
+			Expect(enum.value).Should(Equal(defValue))
+		})
+
+		It("should set the allowed values", func() {
+			Expect(enum.allowed).Should(Equal(allowed))
+		})
+	})
+
+	Context("Set Func", func() {
+		var val string
+		var err error
+		newVal := "val2"
+		newValWrong := "WrongVal"
+
+		JustBeforeEach(func() {
+			err = enum.Set(val)
+		})
+
+		Context("Setting an allowed value", func() {
+			BeforeEach(func() {
+				val = newVal
+			})
+
+			It("Should set the correct val", func() {
+				Expect(err).ShouldNot(HaveOccurred())
+				Expect(enum.value).Should(Equal(newVal))
+			})
+		})
+
+		Context("Setting a not allowed value", func() {
+			BeforeEach(func() {
+				val = newValWrong
+			})
+
+			It("Should error", func() {
+				Expect(err).Should(HaveOccurred())
+			})
+		})
+	})
+
+	Context("String Func", func() {
+		It("Should return the setted value", func() {
+			Expect(enum.String()).Should(Equal(defValue))
+		})
+	})
+
+	Context("Type Func", func() {
+		It("Should return the string type", func() {
+			Expect(enum.Type()).Should(Equal("string"))
+		})
+	})
+
+})
diff --git a/pkg/options/logFormat.go b/pkg/options/logFormat.go
new file mode 100644
index 00000000..a8e8b951
--- /dev/null
+++ b/pkg/options/logFormat.go
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"github.com/pterm/pterm"
+)
+
+const (
+	// LogFormatColor formatting option for logs.
+	LogFormatColor = "color"
+	// LogFormatText formatting option for logs.
+	LogFormatText = "text"
+	// LogFormatJSON formatting otion for logs.
+	LogFormatJSON = "json"
+)
+
+var logFormats = []string{LogFormatColor, LogFormatText, LogFormatJSON}
+
+// LogFormat data structure for log-format flag.
+type LogFormat struct {
+	*Enum
+}
+
+// NewLogFormat returns a new Enum configured for the log formats flag.
+func NewLogFormat() *LogFormat {
+	return &LogFormat{
+		Enum: NewEnum(logFormats, LogFormatColor),
+	}
+}
+
+// ToPtermFormatter converts the current formatter to pterm.LogFormatter.
+func (lg *LogFormat) ToPtermFormatter() pterm.LogFormatter {
+	var formatter pterm.LogFormatter
+
+	switch lg.value {
+	case LogFormatColor:
+		formatter = pterm.LogFormatterColorful
+	case LogFormatText:
+		pterm.DisableColor()
+		formatter = pterm.LogFormatterColorful
+	case LogFormatJSON:
+		formatter = pterm.LogFormatterJSON
+	}
+	return formatter
+}
diff --git a/pkg/options/logFormat_test.go b/pkg/options/logFormat_test.go
new file mode 100644
index 00000000..fed1a6dd
--- /dev/null
+++ b/pkg/options/logFormat_test.go
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"github.com/gookit/color"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/pterm/pterm"
+)
+
+var _ = Describe("LogFormat", func() {
+	var (
+		logFormatter *LogFormat
+	)
+	BeforeEach(func() {
+		logFormatter = NewLogFormat()
+	})
+
+	Context("NewLogFormat Func", func() {
+		It("should return a new logFormatter", func() {
+			Expect(logFormatter).ShouldNot(BeNil())
+			Expect(logFormatter.value).Should(Equal(LogFormatColor))
+			Expect(logFormatter.allowed).Should(Equal(logFormats))
+		})
+	})
+
+	Context("ToPtermFormatter Func", func() {
+		var output pterm.LogFormatter
+
+		JustBeforeEach(func() {
+			output = logFormatter.ToPtermFormatter()
+		})
+
+		Context("Color", func() {
+			BeforeEach(func() {
+				Expect(logFormatter.Set(LogFormatColor)).ShouldNot(HaveOccurred())
+			})
+
+			It("should return the color logFormatter", func() {
+				Expect(output).Should(Equal(pterm.LogFormatterColorful))
+				Expect(pterm.PrintColor).Should(BeTrue())
+				Expect(color.Enable).Should(BeTrue())
+			})
+		})
+
+		Context("Text", func() {
+			BeforeEach(func() {
+				Expect(logFormatter.Set(LogFormatText)).ShouldNot(HaveOccurred())
+			})
+
+			AfterEach(func() {
+				pterm.EnableColor()
+			})
+
+			It("should return the text logFormatter", func() {
+				Expect(output).Should(Equal(pterm.LogFormatterColorful))
+				Expect(pterm.PrintColor).Should(BeFalse())
+				Expect(color.Enable).Should(BeFalse())
+			})
+		})
+
+		Context("JSON", func() {
+			BeforeEach(func() {
+				Expect(logFormatter.Set(LogFormatJSON)).ShouldNot(HaveOccurred())
+			})
+
+			It("should return the json logFormatter", func() {
+				Expect(output).Should(Equal(pterm.LogFormatterJSON))
+				Expect(pterm.PrintColor).Should(BeTrue())
+				Expect(color.Enable).Should(BeTrue())
+			})
+		})
+	})
+})
diff --git a/pkg/options/logLevel.go b/pkg/options/logLevel.go
new file mode 100644
index 00000000..b574696a
--- /dev/null
+++ b/pkg/options/logLevel.go
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"github.com/pterm/pterm"
+)
+
+const (
+	// LogLevelInfo level option for logs.
+	LogLevelInfo = "info"
+	// LogLevelWarn level opiton for logs.
+	LogLevelWarn = "warn"
+	// LogLevelDebug level option for logs.
+	LogLevelDebug = "debug"
+	// LogLevelTrace level option for logs.
+	LogLevelTrace = "trace"
+)
+
+var logLevels = []string{LogLevelInfo, LogLevelWarn, LogLevelDebug, LogLevelTrace}
+
+// LogLevel data structure for log-level flag.
+type LogLevel struct {
+	*Enum
+}
+
+// NewLogLevel returns a new Enum configured for the log level flag.
+func NewLogLevel() *LogLevel {
+	return &LogLevel{
+		Enum: NewEnum(logLevels, LogLevelInfo),
+	}
+}
+
+// ToPtermLogLevel converts the current log level to pterm.LogLevel.
+func (ll *LogLevel) ToPtermLogLevel() pterm.LogLevel {
+	var level pterm.LogLevel
+	switch ll.value {
+	case LogLevelInfo:
+		level = pterm.LogLevelInfo
+	case LogLevelWarn:
+		level = pterm.LogLevelWarn
+	case LogLevelDebug:
+		level = pterm.LogLevelDebug
+	case LogLevelTrace:
+		level = pterm.LogLevelTrace
+	}
+	return level
+}
diff --git a/pkg/options/logLevel_test.go b/pkg/options/logLevel_test.go
new file mode 100644
index 00000000..5f7b579b
--- /dev/null
+++ b/pkg/options/logLevel_test.go
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+	"github.com/pterm/pterm"
+)
+
+var _ = Describe("LogLevel", func() {
+	var (
+		logLevel *LogLevel
+	)
+	BeforeEach(func() {
+		logLevel = NewLogLevel()
+	})
+
+	Context("NewLogLevel Func", func() {
+		It("should return a new LogLevel", func() {
+			Expect(logLevel).ShouldNot(BeNil())
+			Expect(logLevel.value).Should(Equal(LogLevelInfo))
+			Expect(logLevel.allowed).Should(Equal(logLevels))
+		})
+	})
+
+	Context("ToPtermLogLevel Func", func() {
+		var output pterm.LogLevel
+
+		JustBeforeEach(func() {
+			output = logLevel.ToPtermLogLevel()
+		})
+
+		Context("Info", func() {
+			BeforeEach(func() {
+				Expect(logLevel.Set(LogLevelInfo)).ShouldNot(HaveOccurred())
+			})
+
+			It("should return the Info level", func() {
+				Expect(output).Should(Equal(pterm.LogLevelInfo))
+			})
+		})
+
+		Context("Warn", func() {
+			BeforeEach(func() {
+				Expect(logLevel.Set(LogLevelWarn)).ShouldNot(HaveOccurred())
+			})
+
+			It("should return the Warn level", func() {
+				Expect(output).Should(Equal(pterm.LogLevelWarn))
+			})
+		})
+
+		Context("Debug", func() {
+			BeforeEach(func() {
+				Expect(logLevel.Set(LogLevelDebug)).ShouldNot(HaveOccurred())
+			})
+
+			It("should return the Debug level", func() {
+				Expect(output).Should(Equal(pterm.LogLevelDebug))
+			})
+		})
+
+		Context("Trace", func() {
+			BeforeEach(func() {
+				Expect(logLevel.Set(LogLevelTrace)).ShouldNot(HaveOccurred())
+			})
+
+			It("should return the Info level", func() {
+				Expect(output).Should(Equal(pterm.LogLevelTrace))
+			})
+		})
+	})
+})
diff --git a/pkg/options/options_suite_test.go b/pkg/options/options_suite_test.go
new file mode 100644
index 00000000..f4e4c6a7
--- /dev/null
+++ b/pkg/options/options_suite_test.go
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2023 The Falco Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package options
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestOptions(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Options Suite")
+}
diff --git a/pkg/output/output.go b/pkg/output/output.go
index 40be3e25..3f073e51 100644
--- a/pkg/output/output.go
+++ b/pkg/output/output.go
@@ -16,6 +16,7 @@
 package output
 
 import (
+	"bytes"
 	"fmt"
 	"io"
 	"os"
@@ -40,100 +41,73 @@ const (
 
 var spinnerCharset = []string{"⠈⠁", "⠈⠑", "⠈⠱", "⠈⡱", "⢀⡱", "⢄⡱", "⢄⡱", "⢆⡱", "⢎⡱", "⢎⡰", "⢎⡠", "⢎⡀", "⢎⠁", "⠎⠁", "⠊⠁"}
 
+// NewProgressBar returns a new progress bar printer.
+func NewProgressBar() pterm.ProgressbarPrinter {
+	return *pterm.DefaultProgressbar.
+		WithTitleStyle(pterm.NewStyle(pterm.FgDefault)).
+		WithBarStyle(pterm.NewStyle(pterm.FgDefault)).
+		WithBarCharacter("#").
+		WithLastCharacter("#").
+		WithShowElapsedTime(false).
+		WithShowCount(false).
+		WithMaxWidth(90).
+		WithRemoveWhenDone(true)
+}
+
 // Printer used by all commands to output messages.
 // If a commands needs a new format for its output add it here.
 type Printer struct {
-	Info    *pterm.PrefixPrinter
-	Success *pterm.PrefixPrinter
-	Warning *pterm.PrefixPrinter
-	Error   *pterm.PrefixPrinter
-
-	DefaultText  *pterm.BasicTextPrinter
-	TablePrinter *pterm.TablePrinter
-
-	ProgressBar *pterm.ProgressbarPrinter
-
-	Spinner *pterm.SpinnerPrinter
-
+	Logger         *pterm.Logger
+	DefaultText    *pterm.BasicTextPrinter
+	TablePrinter   *pterm.TablePrinter
+	ProgressBar    *pterm.ProgressbarPrinter
+	Spinner        *pterm.SpinnerPrinter
 	DisableStyling bool
-	verbose        bool
 }
 
 // NewPrinter returns a printer ready to be used.
-func NewPrinter(scope string, disableStyling, verbose bool, writer io.Writer) *Printer {
+func NewPrinter(logLevel pterm.LogLevel, logFormatter pterm.LogFormatter, writer io.Writer) *Printer {
+	var disableStyling bool
 	// If we are not in a tty then make sure that the disableStyling variable is set to true since
 	// we use it elsewhere to check if we are in a tty or not. We force the disableStyling to true
 	// only if it is set to false and we are not in a tty. Otherwise let it as it is, false if the
 	// user has not set it (default) otherwise true.
-	if !disableStyling && !isatty.IsTerminal(os.Stdout.Fd()) {
+	if (logFormatter != pterm.LogFormatterJSON && !isatty.IsTerminal(os.Stdout.Fd())) || logFormatter == pterm.LogFormatterJSON {
 		disableStyling = true
 	}
 
-	generic := &pterm.PrefixPrinter{MessageStyle: pterm.NewStyle(pterm.FgDefault)}
+	logger := pterm.DefaultLogger.
+		WithLevel(logLevel).WithFormatter(logFormatter).
+		WithMaxWidth(150)
+
 	basicText := &pterm.BasicTextPrinter{}
-	progressBar := pterm.DefaultProgressbar.
-		WithTitleStyle(pterm.NewStyle(pterm.FgDefault)).
-		WithBarStyle(pterm.NewStyle(pterm.FgDefault)).
-		WithBarCharacter("#").
-		WithLastCharacter("#").
-		WithShowElapsedTime(false)
+
 	tablePrinter := pterm.DefaultTable.WithHasHeader().WithSeparator("\t")
 	spinner := &pterm.SpinnerPrinter{
 		Sequence:            spinnerCharset,
 		Style:               pterm.NewStyle(pterm.FgDefault),
 		Delay:               time.Millisecond * 100,
 		MessageStyle:        pterm.NewStyle(pterm.FgDefault),
-		RemoveWhenDone:      false,
+		RemoveWhenDone:      true,
 		ShowTimer:           true,
 		TimerRoundingFactor: time.Second,
 		TimerStyle:          &pterm.ThemeDefault.TimerStyle,
 	}
 
 	printer := Printer{
-		verbose: verbose,
-		Info: generic.WithPrefix(pterm.Prefix{
-			Text:  "INFO",
-			Style: pterm.NewStyle(pterm.FgDefault),
-		}),
-
-		Success: generic.WithPrefix(pterm.Prefix{
-			Text:  "INFO",
-			Style: pterm.NewStyle(pterm.FgLightGreen),
-		}),
-
-		Warning: generic.WithPrefix(pterm.Prefix{
-			Text:  "WARN",
-			Style: pterm.NewStyle(pterm.FgYellow),
-		}),
-
-		Error: generic.WithPrefix(pterm.Prefix{
-			Text:  "ERRO",
-			Style: pterm.NewStyle(pterm.FgRed),
-		}),
-
-		DefaultText: basicText,
-
-		ProgressBar: progressBar,
-
-		TablePrinter: tablePrinter,
-
-		Spinner: spinner,
-
+		DefaultText:    basicText,
+		TablePrinter:   tablePrinter,
+		Spinner:        spinner,
 		DisableStyling: disableStyling,
+		Logger:         logger,
 	}
 
-	// Populate the printers for the spinner. We use the same one define in the printer.
-	printer.Spinner.FailPrinter = printer.Error
-	printer.Spinner.WarningPrinter = printer.Warning
-	printer.Spinner.SuccessPrinter = printer.Info
-	printer.Spinner.InfoPrinter = printer.Info
-
 	// We disable styling when the program is not attached to a tty or when requested by the user.
 	if disableStyling {
 		pterm.DisableStyling()
 	}
 
-	return printer.WithScope(scope).WithWriter(writer)
+	return printer.WithWriter(writer)
 }
 
 // CheckErr prints a user-friendly error based on the active printer.
@@ -143,20 +117,28 @@ func (p *Printer) CheckErr(err error) {
 	case err == nil:
 		return
 
-	// Print the error through the spinner, if active.
+	// Stop the spinner, if active.
 	case p != nil && p.Spinner.IsActive:
 		handlerFunc = func(msg string) {
-			p.Spinner.Fail(msg)
+			_ = p.Spinner.Stop()
+			p.Logger.Error(msg)
+		}
+		// Stop the progress bar, if active.
+	case p != nil && p.ProgressBar != nil && p.ProgressBar.IsActive:
+
+		handlerFunc = func(msg string) {
+			_, _ = p.ProgressBar.Stop()
+			p.Logger.Error(msg)
 		}
 
 	// If the printer is initialized then print the error through it.
 	case p != nil:
 		handlerFunc = func(msg string) {
-			msg = strings.TrimPrefix(msg, "error: ")
-			p.Error.Println(strings.TrimRight(msg, "\n"))
+			p.Logger.Error(msg)
 		}
 
 	// Otherwise, restore the default behavior.
+	// It should never happen.
 	default:
 		handlerFunc = func(msg string) {
 			fmt.Printf("%s (it seems that the printer has not been initialized, that's why you are seeing this message", msg)
@@ -166,13 +148,6 @@ func (p *Printer) CheckErr(err error) {
 	handlerFunc(err.Error())
 }
 
-// Verbosef outputs verbose messages if the verbose flags is set.
-func (p *Printer) Verbosef(format string, args ...interface{}) {
-	if p.verbose {
-		p.Info.Printfln(strings.TrimRight(format, "\n"), args...)
-	}
-}
-
 // PrintTable is a helper used to print data in table format.
 func (p *Printer) PrintTable(header TableHeader, data [][]string) error {
 	var table [][]string
@@ -196,50 +171,14 @@ func (p *Printer) PrintTable(header TableHeader, data [][]string) error {
 // WithWriter sets the writer for the current printer.
 func (p Printer) WithWriter(writer io.Writer) *Printer {
 	if writer != nil {
-		p.Info = p.Info.WithWriter(writer)
-		p.Success = p.Success.WithWriter(writer)
-		p.Warning = p.Warning.WithWriter(writer)
-		p.Error = p.Error.WithWriter(writer)
 		p.Spinner = p.Spinner.WithWriter(writer)
 		p.DefaultText = p.DefaultText.WithWriter(writer)
-		p.ProgressBar = p.ProgressBar.WithWriter(writer)
 		p.TablePrinter = p.TablePrinter.WithWriter(writer)
+		p.Logger = p.Logger.WithWriter(writer)
 	}
-
 	return &p
 }
 
-// WithScope sets the scope for the current printer.
-func (p Printer) WithScope(scope string) *Printer {
-	if scope != "" {
-		s := pterm.Scope{Text: scope, Style: pterm.NewStyle(pterm.FgGray)}
-
-		p.Info = p.Info.WithScope(s)
-		p.Error = p.Error.WithScope(s)
-		p.Warning = p.Warning.WithScope(s)
-
-		p.Spinner.FailPrinter = p.Error
-		p.Spinner.InfoPrinter = p.Info
-		p.Spinner.SuccessPrinter = p.Info
-		p.Spinner.WarningPrinter = p.Warning
-	}
-
-	return &p
-}
-
-// DisableStylingf disables styling globally for all existing printers.
-func (p *Printer) DisableStylingf() {
-	pterm.DisableStyling()
-}
-
-// EnableStyling enables styling globally for all existing printers.
-func (p *Printer) EnableStyling() {
-	pterm.EnableStyling()
-	if p.DisableStyling {
-		pterm.DisableColor()
-	}
-}
-
 // ExitOnErr aborts the execution in case of errors, and prints the error using the configured printer.
 func ExitOnErr(p *Printer, err error) {
 	if err != nil {
@@ -247,3 +186,11 @@ func ExitOnErr(p *Printer, err error) {
 		os.Exit(1)
 	}
 }
+
+// FormatTitleAsLoggerInfo returns the msg formatted as been printed by
+// the Info logger.
+func (p *Printer) FormatTitleAsLoggerInfo(msg string) string {
+	buf := &bytes.Buffer{}
+	p.Logger.WithWriter(buf).Info(msg)
+	return strings.TrimRight(buf.String(), "\n")
+}
diff --git a/pkg/output/output_test.go b/pkg/output/output_test.go
index 4fe0fe8d..e96a8a90 100644
--- a/pkg/output/output_test.go
+++ b/pkg/output/output_test.go
@@ -17,98 +17,335 @@ package output
 
 import (
 	"bytes"
+	"errors"
+	"fmt"
 	"io"
 
+	"github.com/gookit/color"
 	. "github.com/onsi/ginkgo/v2"
 	. "github.com/onsi/gomega"
+	"github.com/onsi/gomega/gbytes"
+	"github.com/pterm/pterm"
 )
 
-var _ = Describe("Output", func() {
+var _ = Describe("NewPrinter func", func() {
 	var (
-		printer *Printer
-		scope   string
-		verbose bool
-		writer  io.Writer
+		printer      *Printer
+		logFormatter pterm.LogFormatter
+		logLevel     pterm.LogLevel
+		writer       io.Writer
 	)
 
 	JustBeforeEach(func() {
-		printer = NewPrinter(scope, false, verbose, writer)
+		printer = NewPrinter(logLevel, logFormatter, writer)
 	})
 
 	JustAfterEach(func() {
 		printer = nil
-		scope = ""
-		verbose = false
+		logFormatter = 1000
+		logLevel = 1000
 		writer = nil
 	})
 
-	Context("a new printer is created", func() {
-		Context("with scope", func() {
+	Context("with writer", func() {
+		BeforeEach(func() {
+			writer = &bytes.Buffer{}
+		})
+
+		It("should correctly set the writer for each printer object", func() {
+			Expect(printer.Logger.Writer).Should(Equal(writer))
+			Expect(printer.Spinner.Writer).Should(Equal(writer))
+			Expect(printer.DefaultText.Writer).Should(Equal(writer))
+			Expect(printer.TablePrinter.Writer).Should(Equal(writer))
+		})
+	})
+
+	Context("with log-level", func() {
+		Describe("info", func() {
+			BeforeEach(func() {
+				logLevel = pterm.LogLevelInfo
+			})
+			It("should correctly set the log level to info", func() {
+				Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelInfo))
+			})
+		})
+
+		Describe("warn", func() {
 			BeforeEach(func() {
-				scope = "CustomScope"
+				logLevel = pterm.LogLevelWarn
 			})
+			It("should correctly set the log level to warn", func() {
+				Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelWarn))
+			})
+		})
 
-			It("should correctly set the scope for each printer object", func() {
-				Expect(printer.Error.Scope.Text).Should(Equal(scope))
-				Expect(printer.Info.Scope.Text).Should(Equal(scope))
-				Expect(printer.Warning.Scope.Text).Should(Equal(scope))
+		Describe("debug", func() {
+			BeforeEach(func() {
+				logLevel = pterm.LogLevelDebug
+			})
+			It("should correctly set the log level to debug", func() {
+				Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelDebug))
 			})
 		})
 
-		Context("with writer", func() {
+		Describe("error", func() {
 			BeforeEach(func() {
-				writer = &bytes.Buffer{}
+				logLevel = pterm.LogLevelError
 			})
+			It("should correctly set the log level to error", func() {
+				Expect(printer.Logger.Level).Should(Equal(pterm.LogLevelError))
+			})
+		})
+	})
 
-			It("should correctly set the writer for each printer object", func() {
-				Expect(printer.Error.Writer).Should(Equal(writer))
-				Expect(printer.Info.Writer).Should(Equal(writer))
-				Expect(printer.Warning.Writer).Should(Equal(writer))
-				Expect(printer.DefaultText.Writer).Should(Equal(writer))
+	Context("with log-formatter", func() {
+		Describe("colorful", func() {
+			BeforeEach(func() {
+				logFormatter = pterm.LogFormatterColorful
+			})
+			It("should correctly set the log formatter to colorful", func() {
+				Expect(printer.Logger.Formatter).Should(Equal(pterm.LogFormatterColorful))
 			})
 		})
 
-		Context("with verbose", func() {
+		Describe("json", func() {
 			BeforeEach(func() {
-				verbose = true
+				logFormatter = pterm.LogFormatterJSON
+			})
+
+			It("should correctly set the log level to json", func() {
+				Expect(printer.Logger.Formatter).Should(Equal(pterm.LogFormatterJSON))
+			})
+
+			It("should correctly disable styling at pterm package level", func() {
+				Expect(pterm.RawOutput).Should(BeTrue())
+			})
+
+			It("should correctly disable color at pterm package level", func() {
+				Expect(pterm.PrintColor).Should(BeFalse())
 			})
-			It("should correctly set the verbose variable to true", func() {
-				Expect(printer.verbose).Should(BeTrue())
+
+			It("should correctly disable color at color package level", func() {
+				Expect(color.Enable).Should(BeFalse())
 			})
 		})
 	})
+})
+
+var _ = Describe("CheckErr func", func() {
+	var (
+		buf     *gbytes.Buffer
+		printer *Printer
+		err     error
+	)
+
+	JustBeforeEach(func() {
+		printer.CheckErr(err)
+	})
+
+	JustAfterEach(func() {
+		printer = nil
+		Expect(buf.Clear()).ShouldNot(HaveOccurred())
+		err = nil
+	})
+
+	Context("printer is nil", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			// Set printer to nil.
+			printer = nil
+			err = errors.New("printer is set to nil")
+		})
+
+		It("should print using fmt", func() {
+			Expect(buf).ShouldNot(gbytes.Say(
+				fmt.Sprintf("%s (it seems that the printer has not been initialized, that's why you are seeing this message)", err.Error())))
+		})
+	})
+
+	Context("only printer is active and defined", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			// Set printer to nil.
+			printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf)
+			// Make sure that no other printers are active.
+			Expect(printer.ProgressBar).Should(BeNil())
+			Expect(printer.Spinner.IsActive).Should(BeFalse())
+			err = errors.New("only printers without effects")
+		})
+
+		It("should print using fmt", func() {
+			Expect(buf).Should(gbytes.Say(err.Error()))
+		})
+	})
 
-	Context("testing output using the verbose function", func() {
-		var (
-			msg          = "Testing verbose mode"
-			customWriter *bytes.Buffer
-		)
+	Context("error is nil", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			// Set printer to nil.
+			printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf)
+			// Make sure that no other printers are active.
+			Expect(printer.ProgressBar).Should(BeNil())
+			Expect(printer.Spinner.IsActive).Should(BeFalse())
+			err = nil
+		})
+
+		It("should print nothing", func() {
+			Expect(len(buf.Contents())).Should(BeZero())
+		})
+	})
 
+	Context("spinner is active", func() {
 		BeforeEach(func() {
-			// set the output writer.
-			customWriter = &bytes.Buffer{}
-			writer = customWriter
+			buf = gbytes.NewBuffer()
+			printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf)
+			printer.Spinner, _ = printer.Spinner.Start()
+			// Check that the spinner is active.
+			Expect(printer.Spinner.IsActive).Should(BeTrue())
+			err = errors.New("spinner is active")
 		})
 
-		JustBeforeEach(func() {
-			// call the output function
-			printer.Verbosef("%s", msg)
+		It("should print using logger", func() {
+			Expect(buf).Should(gbytes.Say(err.Error()))
 		})
 
-		Context("verbose mode is disabled", func() {
-			It("should not output the message", func() {
-				Expect(customWriter.String()).Should(BeEmpty())
-			})
+		It("should stop the spinner", func() {
+			Expect(printer.Spinner.IsActive).Should(BeFalse())
 		})
+	})
 
-		Context("verbose mode is enabled", func() {
-			BeforeEach(func() {
-				verbose = true
-			})
+	Context("spinner progress bar is active", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf)
+			printer.ProgressBar, _ = NewProgressBar().Start()
+			// Check that the progress bar is active.
+			Expect(printer.ProgressBar.IsActive).Should(BeTrue())
+			err = errors.New("progress bar is active")
+		})
 
-			It("should output the message", func() {
-				Expect(customWriter.String()).Should(ContainSubstring(msg))
-			})
+		It("should print using logger", func() {
+			Expect(buf).Should(gbytes.Say(err.Error()))
+		})
+
+		It("should stop the progress bar", func() {
+			Expect(printer.ProgressBar.IsActive).Should(BeFalse())
+		})
+	})
+
+})
+
+var _ = Describe("PrintTable func", func() {
+	var (
+		buf     *gbytes.Buffer
+		printer *Printer
+		header  TableHeader
+		err     error
+	)
+
+	JustBeforeEach(func() {
+		printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, buf)
+		err = printer.PrintTable(header, nil)
+	})
+
+	JustAfterEach(func() {
+		printer = nil
+		Expect(buf.Clear()).ShouldNot(HaveOccurred())
+		header = 1000
+	})
+
+	Context("artifact search header", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			header = ArtifactSearch
+		})
+
+		It("should print header", func() {
+			header := []string{"INDEX", "ARTIFACT", "TYPE", "REGISTRY", "REPOSITORY"}
+			for _, col := range header {
+				Expect(buf).Should(gbytes.Say(col))
+			}
+		})
+	})
+
+	Context("index list header", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			header = IndexList
+		})
+
+		It("should print header", func() {
+			header := []string{"NAME", "URL", "ADDED", "UPDATED"}
+			for _, col := range header {
+				Expect(buf).Should(gbytes.Say(col))
+			}
+		})
+	})
+
+	Context("index list header", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			header = ArtifactInfo
+		})
+
+		It("should print header", func() {
+			header := []string{"REF", "TAGS"}
+			for _, col := range header {
+				Expect(buf).Should(gbytes.Say(col))
+			}
+		})
+	})
+
+	Context("header is not defined", func() {
+		BeforeEach(func() {
+			buf = gbytes.NewBuffer()
+			header = 1000
+		})
+
+		It("should error", func() {
+			Expect(err).ShouldNot(BeNil())
+		})
+	})
+
+})
+
+var _ = Describe("FormatTitleAsLoggerInfo func", func() {
+	var (
+		printer      *Printer
+		msg          string
+		formattedMsg string
+	)
+
+	JustBeforeEach(func() {
+		printer = NewPrinter(pterm.LogLevelInfo, pterm.LogFormatterColorful, nil)
+		formattedMsg = printer.FormatTitleAsLoggerInfo(msg)
+	})
+
+	JustAfterEach(func() {
+		printer = nil
+	})
+
+	Context("message without trailing new line", func() {
+		BeforeEach(func() {
+			msg = "Testing message without new line"
+		})
+
+		It("should format according to the INFO logger", func() {
+			output := fmt.Sprintf("INFO  %s", msg)
+			Expect(formattedMsg).Should(ContainSubstring(output))
+		})
+	})
+
+	Context("message with trailing new line", func() {
+		BeforeEach(func() {
+			msg = "Testing message with new line"
+		})
+
+		It("should format according to the INFO logger and remove newline", func() {
+			output := fmt.Sprintf("INFO  %s", msg)
+			Expect(formattedMsg).Should(ContainSubstring(output))
+			Expect(formattedMsg).ShouldNot(ContainSubstring("\n"))
 		})
 	})
+
 })
diff --git a/pkg/output/tracker.go b/pkg/output/tracker.go
index b9383119..b827e70a 100644
--- a/pkg/output/tracker.go
+++ b/pkg/output/tracker.go
@@ -57,28 +57,30 @@ func NewProgressTracker(printer *Printer, target oras.Target, msg string) *Progr
 
 // Push reimplements the Push function of the oras.Target interface adding the needed logic for the progress bar.
 func (t *ProgressTracker) Push(ctx context.Context, expected v1.Descriptor, content io.Reader) error { //nolint:gocritic,lll // needed to implement the oras.Target interface
-	var progressBar *pterm.ProgressbarPrinter
 	d := expected.Digest.Encoded()[:12]
 
+	t.Logger.Info(fmt.Sprintf("%s layer %s", t.msg, d))
+
 	if !t.Printer.DisableStyling {
-		progressBar, _ = t.ProgressBar.WithTotal(int(expected.Size)).WithTitle(fmt.Sprintf(" INFO  %s %s:", t.msg, d)).WithShowCount(false).Start()
-	} else {
-		t.Info.Printfln("%s %s", t.msg, d)
+		t.ProgressBar, _ = NewProgressBar().
+			WithTotal(int(expected.Size)).
+			WithTitle(fmt.Sprintf("%s layer %s", t.msg, d)).
+			Start()
 	}
 
 	reader := &trackedReader{
 		Reader:      content,
 		descriptor:  expected,
-		progressBar: progressBar,
-	}
-	err := t.Target.Push(ctx, expected, reader)
-	if !t.Printer.DisableStyling {
-		_, _ = progressBar.Stop()
+		progressBar: t.ProgressBar,
 	}
-	if err != nil {
-		t.Error.Printfln("unable to push artifact %s", err)
+	if err := t.Target.Push(ctx, expected, reader); err != nil {
 		return err
 	}
+
+	if !t.Printer.DisableStyling {
+		_, _ = t.ProgressBar.Stop()
+	}
+
 	return nil
 }
 
@@ -90,7 +92,7 @@ func (t *ProgressTracker) Exists(ctx context.Context, target v1.Descriptor) (boo
 		return ok, err
 	}
 	if ok {
-		t.Info.Printfln("%s: layer already exists", d)
+		t.Logger.Info(fmt.Sprintf("%s: layer already exists", d))
 	}
 	return ok, err
 }