diff --git a/cmd/tk/bootstrap.go b/cmd/tk/bootstrap.go
index b1376372b7c7aaf97fb79a0c72d3e339a7b5b448..9f02ecfb5455b51ebdf0532f62fdf38bc3aa3d36 100644
--- a/cmd/tk/bootstrap.go
+++ b/cmd/tk/bootstrap.go
@@ -179,7 +179,7 @@ func applySyncManifests(ctx context.Context, kubeClient client.Client, name, nam
 		return err
 	}
 
-	logWaiting("waiting for cluster sync")
+	logger.Waitingf("waiting for cluster sync")
 
 	if err := wait.PollImmediate(pollInterval, timeout,
 		isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
diff --git a/cmd/tk/bootstrap_github.go b/cmd/tk/bootstrap_github.go
index 872c915ed70c9a88ae089a36f438880ffb40ccff..bf455ab1004739d1ee9264a6f28700903059c1d1 100644
--- a/cmd/tk/bootstrap_github.go
+++ b/cmd/tk/bootstrap_github.go
@@ -118,13 +118,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 	defer cancel()
 
 	// create GitHub repository if doesn't exists
-	logAction("connecting to %s", ghHostname)
+	logger.Actionf("connecting to %s", ghHostname)
 	changed, err := provider.CreateRepository(ctx, repository)
 	if err != nil {
 		return err
 	}
 	if changed {
-		logSuccess("repository created")
+		logger.Successf("repository created")
 	}
 
 	withErrors := false
@@ -132,10 +132,10 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 	if !ghPersonal {
 		for _, team := range ghTeams {
 			if changed, err := provider.AddTeam(ctx, repository, team, ghDefaultPermission); err != nil {
-				logFailure(err.Error())
+				logger.Failuref(err.Error())
 				withErrors = true
 			} else if changed {
-				logSuccess("%s team access granted", team)
+				logger.Successf("%s team access granted", team)
 			}
 		}
 	}
@@ -144,10 +144,10 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 	if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
 		return err
 	}
-	logSuccess("repository cloned")
+	logger.Successf("repository cloned")
 
 	// generate install manifests
-	logGenerate("generating manifests")
+	logger.Generatef("generating manifests")
 	manifest, err := generateInstallManifests(ghPath, namespace, tmpDir)
 	if err != nil {
 		return err
@@ -164,9 +164,9 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 		if err := repository.Push(ctx); err != nil {
 			return err
 		}
-		logSuccess("components manifests pushed")
+		logger.Successf("components manifests pushed")
 	} else {
-		logSuccess("components are up to date")
+		logger.Successf("components are up to date")
 	}
 
 	// determine if repo synchronization is working
@@ -174,16 +174,16 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 
 	if isInstall {
 		// apply install manifests
-		logAction("installing components in %s namespace", namespace)
+		logger.Actionf("installing components in %s namespace", namespace)
 		if err := applyInstallManifests(ctx, manifest, components); err != nil {
 			return err
 		}
-		logSuccess("install completed")
+		logger.Successf("install completed")
 	}
 
 	// setup SSH deploy key
 	if shouldCreateDeployKey(ctx, kubeClient, namespace) {
-		logAction("configuring deploy key")
+		logger.Actionf("configuring deploy key")
 		u, err := url.Parse(repository.GetSSH())
 		if err != nil {
 			return fmt.Errorf("git URL parse failed: %w", err)
@@ -202,14 +202,14 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 		if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
 			return err
 		} else if changed {
-			logSuccess("deploy key configured")
+			logger.Successf("deploy key configured")
 		}
 	}
 
 	// configure repo synchronization
 	if isInstall {
 		// generate source and kustomization manifests
-		logAction("generating sync manifests")
+		logger.Actionf("generating sync manifests")
 		if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, ghPath, tmpDir, ghInterval); err != nil {
 			return err
 		}
@@ -221,11 +221,11 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 			if err := repository.Push(ctx); err != nil {
 				return err
 			}
-			logSuccess("sync manifests pushed")
+			logger.Successf("sync manifests pushed")
 		}
 
 		// apply manifests and waiting for sync
-		logAction("applying sync manifests")
+		logger.Actionf("applying sync manifests")
 		if err := applySyncManifests(ctx, kubeClient, namespace, namespace, ghPath, tmpDir); err != nil {
 			return err
 		}
@@ -235,6 +235,6 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("bootstrap completed with errors")
 	}
 
-	logSuccess("bootstrap finished")
+	logger.Successf("bootstrap finished")
 	return nil
 }
diff --git a/cmd/tk/bootstrap_gitlab.go b/cmd/tk/bootstrap_gitlab.go
index 10d4343ec059678be08dd95c451e8c18c028a6ed..9fd3af4b7bb0e2624fa9d9bb1a3bd6966d42d63a 100644
--- a/cmd/tk/bootstrap_gitlab.go
+++ b/cmd/tk/bootstrap_gitlab.go
@@ -109,23 +109,23 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
 	defer cancel()
 
 	// create GitLab project if doesn't exists
-	logAction("connecting to %s", glHostname)
+	logger.Actionf("connecting to %s", glHostname)
 	changed, err := provider.CreateRepository(ctx, repository)
 	if err != nil {
 		return err
 	}
 	if changed {
-		logSuccess("repository created")
+		logger.Successf("repository created")
 	}
 
 	// clone repository and checkout the master branch
 	if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
 		return err
 	}
-	logSuccess("repository cloned")
+	logger.Successf("repository cloned")
 
 	// generate install manifests
-	logGenerate("generating manifests")
+	logger.Generatef("generating manifests")
 	manifest, err := generateInstallManifests(glPath, namespace, tmpDir)
 	if err != nil {
 		return err
@@ -142,9 +142,9 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
 		if err := repository.Push(ctx); err != nil {
 			return err
 		}
-		logSuccess("components manifests pushed")
+		logger.Successf("components manifests pushed")
 	} else {
-		logSuccess("components are up to date")
+		logger.Successf("components are up to date")
 	}
 
 	// determine if repo synchronization is working
@@ -152,16 +152,16 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
 
 	if isInstall {
 		// apply install manifests
-		logAction("installing components in %s namespace", namespace)
+		logger.Actionf("installing components in %s namespace", namespace)
 		if err := applyInstallManifests(ctx, manifest, components); err != nil {
 			return err
 		}
-		logSuccess("install completed")
+		logger.Successf("install completed")
 	}
 
 	// setup SSH deploy key
 	if shouldCreateDeployKey(ctx, kubeClient, namespace) {
-		logAction("configuring deploy key")
+		logger.Actionf("configuring deploy key")
 		u, err := url.Parse(repository.GetSSH())
 		if err != nil {
 			return fmt.Errorf("git URL parse failed: %w", err)
@@ -180,14 +180,14 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
 		if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
 			return err
 		} else if changed {
-			logSuccess("deploy key configured")
+			logger.Successf("deploy key configured")
 		}
 	}
 
 	// configure repo synchronization
 	if isInstall {
 		// generate source and kustomization manifests
-		logAction("generating sync manifests")
+		logger.Actionf("generating sync manifests")
 		if err := generateSyncManifests(repository.GetSSH(), namespace, namespace, glPath, tmpDir, glInterval); err != nil {
 			return err
 		}
@@ -199,16 +199,16 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
 			if err := repository.Push(ctx); err != nil {
 				return err
 			}
-			logSuccess("sync manifests pushed")
+			logger.Successf("sync manifests pushed")
 		}
 
 		// apply manifests and waiting for sync
-		logAction("applying sync manifests")
+		logger.Actionf("applying sync manifests")
 		if err := applySyncManifests(ctx, kubeClient, namespace, namespace, glPath, tmpDir); err != nil {
 			return err
 		}
 	}
 
-	logSuccess("bootstrap finished")
+	logger.Successf("bootstrap finished")
 	return nil
 }
diff --git a/cmd/tk/check.go b/cmd/tk/check.go
index 184921377c169fb7f7102ebe84d5dfac602acdc6..ca26f972c536c96e5134d6e8dfd04363da9fa577 100644
--- a/cmd/tk/check.go
+++ b/cmd/tk/check.go
@@ -58,7 +58,7 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 
-	logAction("checking prerequisites")
+	logger.Actionf("checking prerequisites")
 	checkFailed := false
 
 	if !kubectlCheck(ctx, ">=1.18.0") {
@@ -73,83 +73,83 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
 		if checkFailed {
 			os.Exit(1)
 		}
-		logSuccess("prerequisites checks passed")
+		logger.Successf("prerequisites checks passed")
 		return nil
 	}
 
-	logAction("checking controllers")
+	logger.Actionf("checking controllers")
 	if !componentsCheck() {
 		checkFailed = true
 	}
 	if checkFailed {
 		os.Exit(1)
 	}
-	logSuccess("all checks passed")
+	logger.Successf("all checks passed")
 	return nil
 }
 
 func kubectlCheck(ctx context.Context, version string) bool {
 	_, err := exec.LookPath("kubectl")
 	if err != nil {
-		logFailure("kubectl not found")
+		logger.Failuref("kubectl not found")
 		return false
 	}
 
 	command := "kubectl version --client --short | awk '{ print $3 }'"
 	output, err := utils.execCommand(ctx, ModeCapture, command)
 	if err != nil {
-		logFailure("kubectl version can't be determined")
+		logger.Failuref("kubectl version can't be determined")
 		return false
 	}
 
 	v, err := semver.ParseTolerant(output)
 	if err != nil {
-		logFailure("kubectl version can't be parsed")
+		logger.Failuref("kubectl version can't be parsed")
 		return false
 	}
 
 	rng, _ := semver.ParseRange(version)
 	if !rng(v) {
-		logFailure("kubectl version must be %s", version)
+		logger.Failuref("kubectl version must be %s", version)
 		return false
 	}
 
-	logSuccess("kubectl %s %s", v.String(), version)
+	logger.Successf("kubectl %s %s", v.String(), version)
 	return true
 }
 
 func kubernetesCheck(version string) bool {
 	cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
 	if err != nil {
-		logFailure("Kubernetes client initialization failed: %s", err.Error())
+		logger.Failuref("Kubernetes client initialization failed: %s", err.Error())
 		return false
 	}
 
 	client, err := kubernetes.NewForConfig(cfg)
 	if err != nil {
-		logFailure("Kubernetes client initialization failed: %s", err.Error())
+		logger.Failuref("Kubernetes client initialization failed: %s", err.Error())
 		return false
 	}
 
 	ver, err := client.Discovery().ServerVersion()
 	if err != nil {
-		logFailure("Kubernetes API call failed: %s", err.Error())
+		logger.Failuref("Kubernetes API call failed: %s", err.Error())
 		return false
 	}
 
 	v, err := semver.ParseTolerant(ver.String())
 	if err != nil {
-		logFailure("Kubernetes version can't be determined")
+		logger.Failuref("Kubernetes version can't be determined")
 		return false
 	}
 
 	rng, _ := semver.ParseRange(version)
 	if !rng(v) {
-		logFailure("Kubernetes version must be %s", version)
+		logger.Failuref("Kubernetes version must be %s", version)
 		return false
 	}
 
-	logSuccess("Kubernetes %s %s", v.String(), version)
+	logger.Successf("Kubernetes %s %s", v.String(), version)
 	return true
 }
 
@@ -162,10 +162,10 @@ func componentsCheck() bool {
 		command := fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s",
 			namespace, deployment, timeout.String())
 		if output, err := utils.execCommand(ctx, ModeCapture, command); err != nil {
-			logFailure("%s: %s", deployment, strings.TrimSuffix(output, "\n"))
+			logger.Failuref("%s: %s", deployment, strings.TrimSuffix(output, "\n"))
 			ok = false
 		} else {
-			logSuccess("%s is healthy", deployment)
+			logger.Successf("%s is healthy", deployment)
 		}
 	}
 	return ok
diff --git a/cmd/tk/create_kustomization.go b/cmd/tk/create_kustomization.go
index 6bb9da7a8a687ab4d221d74496dd92811675a631..2319eea9201efc7572c98f53dc73bfe726158cb7 100644
--- a/cmd/tk/create_kustomization.go
+++ b/cmd/tk/create_kustomization.go
@@ -122,7 +122,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if !export {
-		logGenerate("generating kustomization")
+		logger.Generatef("generating kustomization")
 	}
 
 	emptyAPIGroup := ""
@@ -192,18 +192,18 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
 		return exportKs(kustomization)
 	}
 
-	logAction("applying kustomization")
+	logger.Actionf("applying kustomization")
 	if err := upsertKustomization(ctx, kubeClient, kustomization); err != nil {
 		return err
 	}
 
-	logWaiting("waiting for kustomization sync")
+	logger.Waitingf("waiting for kustomization sync")
 	if err := wait.PollImmediate(pollInterval, timeout,
 		isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
 		return err
 	}
 
-	logSuccess("kustomization %s is ready", name)
+	logger.Successf("kustomization %s is ready", name)
 
 	namespacedName := types.NamespacedName{
 		Namespace: namespace,
@@ -215,7 +215,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if kustomization.Status.LastAppliedRevision != "" {
-		logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision)
+		logger.Successf("applied revision %s", kustomization.Status.LastAppliedRevision)
 	} else {
 		return fmt.Errorf("kustomization sync failed")
 	}
@@ -236,7 +236,7 @@ func upsertKustomization(ctx context.Context, kubeClient client.Client, kustomiz
 			if err := kubeClient.Create(ctx, &kustomization); err != nil {
 				return err
 			} else {
-				logSuccess("kustomization created")
+				logger.Successf("kustomization created")
 				return nil
 			}
 		}
@@ -248,7 +248,7 @@ func upsertKustomization(ctx context.Context, kubeClient client.Client, kustomiz
 		return err
 	}
 
-	logSuccess("kustomization updated")
+	logger.Successf("kustomization updated")
 	return nil
 }
 
diff --git a/cmd/tk/create_source_git.go b/cmd/tk/create_source_git.go
index 3276ce64035d3d0b8ed1bf63ea46f59e3b846afd..b7fa8fdc70d0e98de6283853b8555a7dac1ad393 100644
--- a/cmd/tk/create_source_git.go
+++ b/cmd/tk/create_source_git.go
@@ -166,7 +166,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 	withAuth := false
 	// TODO(hidde): move all auth prep to separate func?
 	if u.Scheme == "ssh" {
-		logAction("generating deploy key pair")
+		logger.Actionf("generating deploy key pair")
 		pair, err := generateKeyPair(ctx)
 		if err != nil {
 			return err
@@ -181,15 +181,15 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 			return fmt.Errorf("aborting")
 		}
 
-		logAction("collecting preferred public key from SSH server")
+		logger.Actionf("collecting preferred public key from SSH server")
 		hostKey, err := scanHostKey(ctx, u)
 		if err != nil {
 			return err
 		}
-		logSuccess("collected public key from SSH server:")
+		logger.Successf("collected public key from SSH server:")
 		fmt.Printf("%s", hostKey)
 
-		logAction("applying secret with keys")
+		logger.Actionf("applying secret with keys")
 		secret := corev1.Secret{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      name,
@@ -206,7 +206,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 		}
 		withAuth = true
 	} else if sourceGitUsername != "" && sourceGitPassword != "" {
-		logAction("applying secret with basic auth credentials")
+		logger.Actionf("applying secret with basic auth credentials")
 		secret := corev1.Secret{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      name,
@@ -224,10 +224,10 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if withAuth {
-		logSuccess("authentication configured")
+		logger.Successf("authentication configured")
 	}
 
-	logGenerate("generating source")
+	logger.Generatef("generating source")
 
 	if withAuth {
 		gitRepository.Spec.SecretRef = &corev1.LocalObjectReference{
@@ -235,18 +235,18 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	logAction("applying source")
+	logger.Actionf("applying source")
 	if err := upsertGitRepository(ctx, kubeClient, gitRepository); err != nil {
 		return err
 	}
 
-	logWaiting("waiting for git sync")
+	logger.Waitingf("waiting for git sync")
 	if err := wait.PollImmediate(pollInterval, timeout,
 		isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
 		return err
 	}
 
-	logSuccess("git sync completed")
+	logger.Successf("git sync completed")
 
 	namespacedName := types.NamespacedName{
 		Namespace: namespace,
@@ -258,7 +258,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if gitRepository.Status.Artifact != nil {
-		logSuccess("fetched revision: %s", gitRepository.Status.Artifact.Revision)
+		logger.Successf("fetched revision: %s", gitRepository.Status.Artifact.Revision)
 	} else {
 		return fmt.Errorf("git sync failed, artifact not found")
 	}
@@ -336,7 +336,7 @@ func upsertGitRepository(ctx context.Context, kubeClient client.Client, gitRepos
 			if err := kubeClient.Create(ctx, &gitRepository); err != nil {
 				return err
 			} else {
-				logSuccess("source created")
+				logger.Successf("source created")
 				return nil
 			}
 		}
@@ -348,7 +348,7 @@ func upsertGitRepository(ctx context.Context, kubeClient client.Client, gitRepos
 		return err
 	}
 
-	logSuccess("source updated")
+	logger.Successf("source updated")
 	return nil
 }
 
diff --git a/cmd/tk/delete_kustomization.go b/cmd/tk/delete_kustomization.go
index 28d326b670b696a3245b5d321f3c252fe817b5f7..d72bf18637954637030899a99c77b04542d884c9 100644
--- a/cmd/tk/delete_kustomization.go
+++ b/cmd/tk/delete_kustomization.go
@@ -65,7 +65,7 @@ func deleteKsCmdRun(cmd *cobra.Command, args []string) error {
 
 	if !deleteSilent {
 		if !kustomization.Spec.Suspend {
-			logWaiting("This action will remove the Kubernetes objects previously applied by the %s kustomization!", name)
+			logger.Waitingf("This action will remove the Kubernetes objects previously applied by the %s kustomization!", name)
 		}
 		prompt := promptui.Prompt{
 			Label:     "Are you sure you want to delete this kustomization",
@@ -76,12 +76,12 @@ func deleteKsCmdRun(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	logAction("deleting kustomization %s in %s namespace", name, namespace)
+	logger.Actionf("deleting kustomization %s in %s namespace", name, namespace)
 	err = kubeClient.Delete(ctx, &kustomization)
 	if err != nil {
 		return err
 	}
-	logSuccess("kustomization deleted")
+	logger.Successf("kustomization deleted")
 
 	return nil
 }
diff --git a/cmd/tk/delete_source_git.go b/cmd/tk/delete_source_git.go
index f88ab69c0ce4a324036796ca4e2240819a740817..cbb4f092fde811872633257b25f80aa7c2dc69ee 100644
--- a/cmd/tk/delete_source_git.go
+++ b/cmd/tk/delete_source_git.go
@@ -72,12 +72,12 @@ func deleteSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 		}
 	}
 
-	logAction("deleting source %s in %s namespace", name, namespace)
+	logger.Actionf("deleting source %s in %s namespace", name, namespace)
 	err = kubeClient.Delete(ctx, &git)
 	if err != nil {
 		return err
 	}
-	logSuccess("source deleted")
+	logger.Successf("source deleted")
 
 	return nil
 }
diff --git a/cmd/tk/export_kustomization.go b/cmd/tk/export_kustomization.go
index 83c2a633c4ce0462899c83ae08bcdab60e2a802f..17fc28007306cbfb4e2a6d18137bd74fc31fe3c3 100644
--- a/cmd/tk/export_kustomization.go
+++ b/cmd/tk/export_kustomization.go
@@ -67,7 +67,7 @@ func exportKsCmdRun(cmd *cobra.Command, args []string) error {
 		}
 
 		if len(list.Items) == 0 {
-			logFailure("no kustomizations found in %s namespace", namespace)
+			logger.Failuref("no kustomizations found in %s namespace", namespace)
 			return nil
 		}
 
diff --git a/cmd/tk/export_source_git.go b/cmd/tk/export_source_git.go
index 714a8a1a8fa64fab588369724e13bf959c30f313..6b8dafa84f7ea052ff5055f5dcff0699ecaf6b40 100644
--- a/cmd/tk/export_source_git.go
+++ b/cmd/tk/export_source_git.go
@@ -67,7 +67,7 @@ func exportSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 		}
 
 		if len(list.Items) == 0 {
-			logFailure("no source found in %s namespace", namespace)
+			logger.Failuref("no source found in %s namespace", namespace)
 			return nil
 		}
 
diff --git a/cmd/tk/get_kustomization.go b/cmd/tk/get_kustomization.go
index 5e09a8cbb4198d71c5025137c601586c904d8a2e..f53d96006db247881673f7bd6a2f695d7c7495c7 100644
--- a/cmd/tk/get_kustomization.go
+++ b/cmd/tk/get_kustomization.go
@@ -53,13 +53,13 @@ func getKsCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if len(list.Items) == 0 {
-		logFailure("no kustomizations found in %s namespace", namespace)
+		logger.Failuref("no kustomizations found in %s namespace", namespace)
 		return nil
 	}
 
 	for _, kustomization := range list.Items {
 		if kustomization.Spec.Suspend {
-			logSuccess("%s is suspended", kustomization.GetName())
+			logger.Successf("%s is suspended", kustomization.GetName())
 			continue
 		}
 		isInitialized := false
@@ -67,19 +67,19 @@ func getKsCmdRun(cmd *cobra.Command, args []string) error {
 			if condition.Type == kustomizev1.ReadyCondition {
 				if condition.Status != corev1.ConditionFalse {
 					if kustomization.Status.LastAppliedRevision != "" {
-						logSuccess("%s last applied revision %s", kustomization.GetName(), kustomization.Status.LastAppliedRevision)
+						logger.Successf("%s last applied revision %s", kustomization.GetName(), kustomization.Status.LastAppliedRevision)
 					} else {
-						logSuccess("%s reconciling", kustomization.GetName())
+						logger.Successf("%s reconciling", kustomization.GetName())
 					}
 				} else {
-					logFailure("%s %s", kustomization.GetName(), condition.Message)
+					logger.Failuref("%s %s", kustomization.GetName(), condition.Message)
 				}
 				isInitialized = true
 				break
 			}
 		}
 		if !isInitialized {
-			logFailure("%s is not ready", kustomization.GetName())
+			logger.Failuref("%s is not ready", kustomization.GetName())
 		}
 	}
 	return nil
diff --git a/cmd/tk/get_source_git.go b/cmd/tk/get_source_git.go
index 7fe82ef3e2e9bfc50933ec7907f7bf97fb5a7efa..f490b66da82ac5d07eb687f041c555b517392a86 100644
--- a/cmd/tk/get_source_git.go
+++ b/cmd/tk/get_source_git.go
@@ -52,7 +52,7 @@ func getSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if len(list.Items) == 0 {
-		logFailure("no sources found in %s namespace", namespace)
+		logger.Failuref("no sources found in %s namespace", namespace)
 		return nil
 	}
 
@@ -61,16 +61,16 @@ func getSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 		for _, condition := range source.Status.Conditions {
 			if condition.Type == sourcev1.ReadyCondition {
 				if condition.Status != corev1.ConditionFalse {
-					logSuccess("%s last fetched revision: %s", source.GetName(), source.Status.Artifact.Revision)
+					logger.Successf("%s last fetched revision: %s", source.GetName(), source.Status.Artifact.Revision)
 				} else {
-					logFailure("%s %s", source.GetName(), condition.Message)
+					logger.Failuref("%s %s", source.GetName(), condition.Message)
 				}
 				isInitialized = true
 				break
 			}
 		}
 		if !isInitialized {
-			logFailure("%s is not ready", source.GetName())
+			logger.Failuref("%s is not ready", source.GetName())
 		}
 	}
 	return nil
diff --git a/cmd/tk/install.go b/cmd/tk/install.go
index 08c81d7e2f5893c4ce4a4055fc5df2a8ba1c2411..8cbbf716c3f9e127b108ce4c16c70c17d0b63491 100644
--- a/cmd/tk/install.go
+++ b/cmd/tk/install.go
@@ -81,7 +81,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
 	}
 	defer os.RemoveAll(tmpDir)
 
-	logGenerate("generating manifests")
+	logger.Generatef("generating manifests")
 	if kustomizePath == "" {
 		err = genInstallManifests(installVersion, namespace, components, tmpDir)
 		if err != nil {
@@ -103,9 +103,9 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
 			fmt.Print(yaml)
 		}
 	}
-	logSuccess("manifests build completed")
+	logger.Successf("manifests build completed")
 
-	logAction("installing components in %s namespace", namespace)
+	logger.Actionf("installing components in %s namespace", namespace)
 	applyOutput := ModeStderrOS
 	if verbose {
 		applyOutput = ModeOS
@@ -121,24 +121,24 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if installDryRun {
-		logSuccess("install dry-run finished")
+		logger.Successf("install dry-run finished")
 		return nil
 	} else {
-		logSuccess("install completed")
+		logger.Successf("install completed")
 	}
 
-	logWaiting("verifying installation")
+	logger.Waitingf("verifying installation")
 	for _, deployment := range components {
 		command = fmt.Sprintf("kubectl -n %s rollout status deployment %s --timeout=%s",
 			namespace, deployment, timeout.String())
 		if _, err := utils.execCommand(ctx, applyOutput, command); err != nil {
 			return fmt.Errorf("install failed")
 		} else {
-			logSuccess("%s ready", deployment)
+			logger.Successf("%s ready", deployment)
 		}
 	}
 
-	logSuccess("install finished")
+	logger.Successf("install finished")
 	return nil
 }
 
diff --git a/cmd/tk/log.go b/cmd/tk/log.go
index 7ff5c329af11fa6b282cb2f242ac06e187b2e758..984d18d1adb1a80e1b60095718550925a4eb42f4 100644
--- a/cmd/tk/log.go
+++ b/cmd/tk/log.go
@@ -18,22 +18,24 @@ package main
 
 import "fmt"
 
-func logAction(format string, a ...interface{}) {
+type printLogger struct{}
+
+func (l printLogger) Actionf(format string, a ...interface{}) {
 	fmt.Println(`►`, fmt.Sprintf(format, a...))
 }
 
-func logGenerate(format string, a ...interface{}) {
+func (l printLogger) Generatef(format string, a ...interface{}) {
 	fmt.Println(`✚`, fmt.Sprintf(format, a...))
 }
 
-func logWaiting(format string, a ...interface{}) {
+func (l printLogger) Waitingf(format string, a ...interface{}) {
 	fmt.Println(`◎`, fmt.Sprintf(format, a...))
 }
 
-func logSuccess(format string, a ...interface{}) {
+func (l printLogger) Successf(format string, a ...interface{}) {
 	fmt.Println(`✔`, fmt.Sprintf(format, a...))
 }
 
-func logFailure(format string, a ...interface{}) {
+func (l printLogger) Failuref(format string, a ...interface{}) {
 	fmt.Println(`✗`, fmt.Sprintf(format, a...))
 }
diff --git a/cmd/tk/main.go b/cmd/tk/main.go
index f188612259858b288978524d9b6f6a45230cf6e1..a25d1fb9c02090f9a2440f410e6f73215d2f6e22 100644
--- a/cmd/tk/main.go
+++ b/cmd/tk/main.go
@@ -25,6 +25,8 @@ import (
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra/doc"
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
+
+	tklog "github.com/fluxcd/toolkit/pkg/log"
 )
 
 var VERSION = "0.0.0-dev.0"
@@ -98,7 +100,8 @@ var (
 	verbose      bool
 	components   []string
 	utils        Utils
-	pollInterval = 2 * time.Second
+	pollInterval              = 2 * time.Second
+	logger       tklog.Logger = printLogger{}
 )
 
 func init() {
@@ -118,7 +121,7 @@ func main() {
 	generateDocs()
 	kubeconfigFlag()
 	if err := rootCmd.Execute(); err != nil {
-		logFailure("%v", err)
+		logger.Failuref("%v", err)
 		os.Exit(1)
 	}
 }
diff --git a/cmd/tk/resume_kustomization.go b/cmd/tk/resume_kustomization.go
index 0594953180c511f9deec71353a82c0de6d18c58a..a1d708a862d899e0e5169a5d401798e98a0249e8 100644
--- a/cmd/tk/resume_kustomization.go
+++ b/cmd/tk/resume_kustomization.go
@@ -66,20 +66,20 @@ func resumeKsCmdRun(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	logAction("resuming kustomization %s in %s namespace", name, namespace)
+	logger.Actionf("resuming kustomization %s in %s namespace", name, namespace)
 	kustomization.Spec.Suspend = false
 	if err := kubeClient.Update(ctx, &kustomization); err != nil {
 		return err
 	}
-	logSuccess("kustomization resumed")
+	logger.Successf("kustomization resumed")
 
-	logWaiting("waiting for kustomization sync")
+	logger.Waitingf("waiting for kustomization sync")
 	if err := wait.PollImmediate(pollInterval, timeout,
 		isKustomizationResumed(ctx, kubeClient, name, namespace)); err != nil {
 		return err
 	}
 
-	logSuccess("kustomization sync completed")
+	logger.Successf("kustomization sync completed")
 
 	err = kubeClient.Get(ctx, namespacedName, &kustomization)
 	if err != nil {
@@ -87,7 +87,7 @@ func resumeKsCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if kustomization.Status.LastAppliedRevision != "" {
-		logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision)
+		logger.Successf("applied revision %s", kustomization.Status.LastAppliedRevision)
 	} else {
 		return fmt.Errorf("kustomization sync failed")
 	}
diff --git a/cmd/tk/suspend_kustomization.go b/cmd/tk/suspend_kustomization.go
index 6c8dd41b63d47ccffdfa408db44eeafc937b1e17..d879a44e504eb6a5a95ea9dc01f9130c8486d276 100644
--- a/cmd/tk/suspend_kustomization.go
+++ b/cmd/tk/suspend_kustomization.go
@@ -60,12 +60,12 @@ func suspendKsCmdRun(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	logAction("suspending kustomization %s in %s namespace", name, namespace)
+	logger.Actionf("suspending kustomization %s in %s namespace", name, namespace)
 	kustomization.Spec.Suspend = true
 	if err := kubeClient.Update(ctx, &kustomization); err != nil {
 		return err
 	}
-	logSuccess("kustomization suspended")
+	logger.Successf("kustomization suspended")
 
 	return nil
 }
diff --git a/cmd/tk/sync_kustomization.go b/cmd/tk/sync_kustomization.go
index 492c88f0593fe0a7b863d3b026097047f5a66478..4631bff36ac0d5e574f07119212ef90da82be28f 100644
--- a/cmd/tk/sync_kustomization.go
+++ b/cmd/tk/sync_kustomization.go
@@ -83,7 +83,7 @@ func syncKsCmdRun(cmd *cobra.Command, args []string) error {
 			return err
 		}
 	} else {
-		logAction("annotating kustomization %s in %s namespace", name, namespace)
+		logger.Actionf("annotating kustomization %s in %s namespace", name, namespace)
 		if kustomization.Annotations == nil {
 			kustomization.Annotations = map[string]string{
 				kustomizev1.SyncAtAnnotation: time.Now().String(),
@@ -94,16 +94,16 @@ func syncKsCmdRun(cmd *cobra.Command, args []string) error {
 		if err := kubeClient.Update(ctx, &kustomization); err != nil {
 			return err
 		}
-		logSuccess("kustomization annotated")
+		logger.Successf("kustomization annotated")
 	}
 
-	logWaiting("waiting for kustomization sync")
+	logger.Waitingf("waiting for kustomization sync")
 	if err := wait.PollImmediate(pollInterval, timeout,
 		isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil {
 		return err
 	}
 
-	logSuccess("kustomization sync completed")
+	logger.Successf("kustomization sync completed")
 
 	err = kubeClient.Get(ctx, namespacedName, &kustomization)
 	if err != nil {
@@ -111,7 +111,7 @@ func syncKsCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if kustomization.Status.LastAppliedRevision != "" {
-		logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision)
+		logger.Successf("applied revision %s", kustomization.Status.LastAppliedRevision)
 	} else {
 		return fmt.Errorf("kustomization sync failed")
 	}
diff --git a/cmd/tk/sync_source_git.go b/cmd/tk/sync_source_git.go
index 610bcf7bba428fa79a988a0ad0572ed8dfe46130..72460bfe04e7d8b8a3526600fd22d4fea142fec8 100644
--- a/cmd/tk/sync_source_git.go
+++ b/cmd/tk/sync_source_git.go
@@ -59,7 +59,7 @@ func syncSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 		Name:      name,
 	}
 
-	logAction("annotating source %s in %s namespace", name, namespace)
+	logger.Actionf("annotating source %s in %s namespace", name, namespace)
 	var gitRepository sourcev1.GitRepository
 	err = kubeClient.Get(ctx, namespacedName, &gitRepository)
 	if err != nil {
@@ -76,15 +76,15 @@ func syncSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 	if err := kubeClient.Update(ctx, &gitRepository); err != nil {
 		return err
 	}
-	logSuccess("source annotated")
+	logger.Successf("source annotated")
 
-	logWaiting("waiting for git sync")
+	logger.Waitingf("waiting for git sync")
 	if err := wait.PollImmediate(pollInterval, timeout,
 		isGitRepositoryReady(ctx, kubeClient, name, namespace)); err != nil {
 		return err
 	}
 
-	logSuccess("git sync completed")
+	logger.Successf("git sync completed")
 
 	err = kubeClient.Get(ctx, namespacedName, &gitRepository)
 	if err != nil {
@@ -92,7 +92,7 @@ func syncSourceGitCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if gitRepository.Status.Artifact != nil {
-		logSuccess("fetched revision: %s", gitRepository.Status.Artifact.Revision)
+		logger.Successf("fetched revision %s", gitRepository.Status.Artifact.Revision)
 	} else {
 		return fmt.Errorf("git sync failed, artifact not found")
 	}
diff --git a/cmd/tk/uninstall.go b/cmd/tk/uninstall.go
index 26f8e145008d1e517db508a81c3cb1fd0f26f5cf..7e145c73ead38370661df0f6a6d13b88b220bc72 100644
--- a/cmd/tk/uninstall.go
+++ b/cmd/tk/uninstall.go
@@ -76,7 +76,7 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error {
 	}
 
 	if uninstallKustomizations {
-		logAction("uninstalling kustomizations")
+		logger.Actionf("uninstalling kustomizations")
 		command := fmt.Sprintf("kubectl -n %s delete kustomizations --all --timeout=%s %s",
 			namespace, timeout.String(), dryRun)
 		if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
@@ -85,7 +85,7 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error {
 
 		// TODO: use the kustomizations snapshots to create a list of objects
 		// that are subject to deletion and wait for all of them to be terminated
-		logWaiting("waiting on GC")
+		logger.Waitingf("waiting on GC")
 		time.Sleep(30 * time.Second)
 	}
 
@@ -94,13 +94,13 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error {
 		kinds += ",crds"
 	}
 
-	logAction("uninstalling components")
+	logger.Actionf("uninstalling components")
 	command := fmt.Sprintf("kubectl delete %s -l app.kubernetes.io/instance=%s --timeout=%s %s",
 		kinds, namespace, timeout.String(), dryRun)
 	if _, err := utils.execCommand(ctx, ModeOS, command); err != nil {
 		return fmt.Errorf("uninstall failed")
 	}
 
-	logSuccess("uninstall finished")
+	logger.Successf("uninstall finished")
 	return nil
 }
diff --git a/pkg/log/log.go b/pkg/log/log.go
new file mode 100644
index 0000000000000000000000000000000000000000..ad87bb96ca237a88e8aabe4b8ebacb3d3eaf0c03
--- /dev/null
+++ b/pkg/log/log.go
@@ -0,0 +1,29 @@
+/*
+Copyright 2020 The Flux CD contributors.
+
+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 log
+
+type Logger interface {
+	// Actionf logs a formatted action message.
+	Actionf(format string, a ...interface{})
+	// Generatef logs a formatted generate message.
+	Generatef(format string, a ...interface{})
+	// Waitingf logs a formatted waiting message.
+	Waitingf(format string, a ...interface{})
+	// Waitingf logs a formatted success message.
+	Successf(format string, a ...interface{})
+	// Failuref logs a formatted failure message.
+	Failuref(format string, a ...interface{})
+}