Repository: yonahd/kor
Pull request #87 (2023-09-21 10:46:14)

Export Prometheus metrics
adding option for "exporter"
this is an initial PR, simple way of exporting metrics
@yonahd  , @Yuni-sa  how you think we should proceed from here ? 


Patch:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3977912
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: 'Feature: Add this super cool feature '
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/README.md b/README.md
index 295648a..b1ba0fd 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,35 @@ Kor is a tool to discover unused Kubernetes resources. Currently, Kor can identi
 
 Download the binary for your operating system from the [releases page](https://github.com/yonahd/kor/releases) and add it to your system's PATH.
 
+### Homebrew
 For macOS users, you can install Kor using Homebrew:
 ```sh
 brew install kor
 ```
+### Build from source
+Install the binary to your `$GOBIN` or `$GOPATH/bin`:
+```sh
+go install github.com/yonahd/kor@latest
+```
+
+### Docker
+Run a container with your kubeconfig mounted:
+```sh
+docker run --rm -i yonahdissen/kor
+
+docker run --rm -i -v "/path/to/.kube/config:/root/.kube/config" yonahdissen/kor all
+```
+
+### Helm
+Run as a cronjob in your Cluster
+```sh
+helm upgrade -i kor \
+    --namespace kor \
+    --create-namespace \
+    ./charts/kor
+```
+
+For more information see [in cluster usage](#in-cluster-usage) 
 
 ## Usage
 
@@ -44,14 +69,18 @@ Kor provides various subcommands to identify and list unused resources. The avai
 - `pvc` - Gets unused PVCs for the specified namespace or all namespaces.
 - `ingress` - Gets unused Ingresses for the specified namespace or all namespaces.
 - `pdb` - Gets unused PDBs for the specified namespace or all namespaces.
+- `exporter` - Export Prometheus metrics.
 
 ### Supported Flags
 ```
--e, --exclude-namespaces string   Namespaces to be excluded, splited by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
+-e, --exclude-namespaces string   Namespaces to be excluded, split by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.
 -h, --help                        help for kor
--n, --include-namespaces string   Namespaces to run on, split by comma. Example: --include-namespace ns1,ns2,ns3. 
+-n, --include-namespaces string   Namespaces to run on, split by comma. Example: --include-namespace ns1,ns2,ns3.
 -k, --kubeconfig string           Path to kubeconfig file (optional)
-    --output string               Output format ("table" or "json") (default "table")
+    --output string               Output format (table or json) (default "table")
+    --slack-auth-token string     Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.
+    --slack-channel string        Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.
+    --slack-webhook-url string    Slack webhook URL to send notifications to
 ```
 
 To use a specific subcommand, run `kor [subcommand] [flags]`.
@@ -135,6 +164,7 @@ helm upgrade -i kor \
     --set cronJob.slackToken=<slack-token> \
     ./charts/kor
 ```
+> Note: To send it to Slack as a file it's required to set the `slackToken` and `slackChannel` values.
 
 It's set to run every Monday at 1 a.m. by default. You can change the schedule by setting the `cronJob.schedule` value.
 
diff --git a/charts/kor/templates/cronjob.yaml b/charts/kor/templates/cronjob.yaml
index 0a0100e..4f0454b 100644
--- a/charts/kor/templates/cronjob.yaml
+++ b/charts/kor/templates/cronjob.yaml
@@ -19,9 +19,21 @@ spec:
               image: {{ .Values.cronJob.image.repository }}:{{ .Values.cronJob.image.tag }}
               command: ["/bin/sh", "-c"]
               {{- if .Values.cronJob.slackWebhookUrl }}
-              args: ["{{ .Values.cronJob.command }} --slack-webhook-url {{ .Values.cronJob.slackWebhookUrl }}"]
+              args: ["{{ .Values.cronJob.command }} --slack-webhook-url $SLACK_WEBHOOK_URL"]
+              env:
+                - name: SLACK_WEBHOOK_URL
+                  valueFrom:
+                    secretKeyRef:
+                      name: {{ .Release.Name }}-slack-webhook-url-secret
+                      key: slack-webhook-url
               {{- else if and .Values.cronJob.slackChannel .Values.cronJob.slackAuthToken }}
-              args: ["{{ .Values.cronJob.command }} --slack-channel {{ .Values.cronJob.slackChannel }} --slack-auth-token {{ .Values.cronJob.slackAuthToken }}"]
+              args: ["{{ .Values.cronJob.command }} --slack-channel {{ .Values.cronJob.slackChannel }} --slack-auth-token $SLACK_AUTH_TOKEN"]
+              env:
+                - name: SLACK_AUTH_TOKEN
+                  valueFrom:
+                    secretKeyRef:
+                      name: {{ .Release.Name }}-slack-auth-token-secret
+                      key: slack-auth-token
               {{- else }}
               args: ["{{ .Values.cronJob.command }}"]
               {{- end }}
diff --git a/charts/kor/templates/secret.yaml b/charts/kor/templates/secret.yaml
new file mode 100644
index 0000000..9e27bdd
--- /dev/null
+++ b/charts/kor/templates/secret.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Release.Name }}-slack-auth-token-secret
+type: Opaque
+data:
+  slack-auth-token: {{ .Values.cronJob.slackAuthToken | b64enc | quote }}
+
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ .Release.Name }}-slack-webhook-url-secret
+type: Opaque
+data:
+  slack-webhook-url: {{ .Values.cronJob.slackWebhookUrl | b64enc | quote }}
diff --git a/cmd/kor/all.go b/cmd/kor/all.go
index 5b3490f..3aaf7e5 100644
--- a/cmd/kor/all.go
+++ b/cmd/kor/all.go
@@ -20,7 +20,7 @@ var allCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedAll(includeExcludeLists, clientset)
+			kor.GetUnusedAll(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/configmaps.go b/cmd/kor/configmaps.go
index 49ed5ce..4d52e88 100644
--- a/cmd/kor/configmaps.go
+++ b/cmd/kor/configmaps.go
@@ -21,7 +21,7 @@ var configmapCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedConfigmaps(includeExcludeLists, clientset)
+			kor.GetUnusedConfigmaps(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/deployments.go b/cmd/kor/deployments.go
index 61074f9..e56c01b 100644
--- a/cmd/kor/deployments.go
+++ b/cmd/kor/deployments.go
@@ -21,7 +21,7 @@ var deployCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedDeployments(includeExcludeLists, clientset)
+			kor.GetUnusedDeployments(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/exporter.go b/cmd/kor/exporter.go
new file mode 100644
index 0000000..5f8a800
--- /dev/null
+++ b/cmd/kor/exporter.go
@@ -0,0 +1,21 @@
+package kor
+
+import (
+	"github.com/spf13/cobra"
+	"github.com/yonahd/kor/pkg/kor"
+)
+
+var exporterCmd = &cobra.Command{
+	Use:   "exporter",
+	Short: "start prometheus exporter",
+	Args:  cobra.ExactArgs(0),
+	Run: func(cmd *cobra.Command, args []string) {
+		clientset := kor.GetKubeClient(kubeconfig)
+		kor.Exporter(includeExcludeLists, clientset, "json")
+
+	},
+}
+
+func init() {
+	rootCmd.AddCommand(exporterCmd)
+}
diff --git a/cmd/kor/hpas.go b/cmd/kor/hpas.go
index 1af3a3a..35f074e 100644
--- a/cmd/kor/hpas.go
+++ b/cmd/kor/hpas.go
@@ -21,7 +21,7 @@ var hpaCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedHpas(includeExcludeLists, clientset)
+			kor.GetUnusedHpas(includeExcludeLists, clientset, slackOpts)
 		}
 	},
 }
diff --git a/cmd/kor/ingresses.go b/cmd/kor/ingresses.go
index 97f0c61..f48c2cc 100644
--- a/cmd/kor/ingresses.go
+++ b/cmd/kor/ingresses.go
@@ -21,7 +21,7 @@ var ingressCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedIngresses(includeExcludeLists, clientset)
+			kor.GetUnusedIngresses(includeExcludeLists, clientset, slackOpts)
 		}
 	},
 }
diff --git a/cmd/kor/pdbs.go b/cmd/kor/pdbs.go
index 0cf774f..2b06049 100644
--- a/cmd/kor/pdbs.go
+++ b/cmd/kor/pdbs.go
@@ -21,7 +21,7 @@ var pdbCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedPdbs(includeExcludeLists, clientset)
+			kor.GetUnusedPdbs(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/pvc.go b/cmd/kor/pvc.go
index c0a02e0..2b4cfba 100644
--- a/cmd/kor/pvc.go
+++ b/cmd/kor/pvc.go
@@ -21,7 +21,7 @@ var pvcCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedPvcs(includeExcludeLists, clientset)
+			kor.GetUnusedPvcs(includeExcludeLists, clientset, slackOpts)
 		}
 	},
 }
diff --git a/cmd/kor/roles.go b/cmd/kor/roles.go
index 44e9e22..65087fe 100644
--- a/cmd/kor/roles.go
+++ b/cmd/kor/roles.go
@@ -21,7 +21,7 @@ var roleCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedRoles(includeExcludeLists, clientset)
+			kor.GetUnusedRoles(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/root.go b/cmd/kor/root.go
index 6c5c162..418a415 100644
--- a/cmd/kor/root.go
+++ b/cmd/kor/root.go
@@ -27,7 +27,7 @@ var rootCmd = &cobra.Command{
 					fmt.Println(response)
 				}
 			} else {
-				kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames)
+				kor.GetUnusedMulti(includeExcludeLists, kubeconfig, resourceNames, slackOpts)
 			}
 		} else {
 			fmt.Printf("Subcommand %q was not found, try using 'kor --help' for available subcommands", args[0])
@@ -39,6 +39,7 @@ var (
 	outputFormat        string
 	kubeconfig          string
 	includeExcludeLists kor.IncludeExcludeLists
+	slackOpts           kor.SlackOpts
 )
 
 func Execute() {
@@ -46,6 +47,9 @@ func Execute() {
 	rootCmd.PersistentFlags().StringVarP(&includeExcludeLists.IncludeListStr, "include-namespaces", "n", "", "Namespaces to run on, splited by comma. Example: --include-namespace ns1,ns2,ns3. ")
 	rootCmd.PersistentFlags().StringVarP(&includeExcludeLists.ExcludeListStr, "exclude-namespaces", "e", "", "Namespaces to be excluded, splited by comma. Example: --exclude-namespace ns1,ns2,ns3. If --include-namespace is set, --exclude-namespaces will be ignored.")
 	rootCmd.PersistentFlags().StringVar(&outputFormat, "output", "table", "Output format (table or json)")
+	rootCmd.PersistentFlags().StringVar(&slackOpts.WebhookURL, "slack-webhook-url", "", "Slack webhook URL to send notifications to")
+	rootCmd.PersistentFlags().StringVar(&slackOpts.Channel, "slack-channel", "", "Slack channel to send notifications to. --slack-channel requires --slack-auth-token to be set.")
+	rootCmd.PersistentFlags().StringVar(&slackOpts.Token, "slack-auth-token", "", "Slack auth token to send notifications to. --slack-auth-token requires --slack-channel to be set.")
 	if err := rootCmd.Execute(); err != nil {
 		fmt.Fprintf(os.Stderr, "Error while executing your CLI '%s'", err)
 		os.Exit(1)
diff --git a/cmd/kor/secrets.go b/cmd/kor/secrets.go
index fae67f7..7139728 100644
--- a/cmd/kor/secrets.go
+++ b/cmd/kor/secrets.go
@@ -21,7 +21,7 @@ var secretCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedSecrets(includeExcludeLists, clientset)
+			kor.GetUnusedSecrets(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/serviceaccounts.go b/cmd/kor/serviceaccounts.go
index 3b2472d..bdeca3d 100644
--- a/cmd/kor/serviceaccounts.go
+++ b/cmd/kor/serviceaccounts.go
@@ -21,7 +21,7 @@ var serviceAccountCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedServiceAccounts(includeExcludeLists, clientset)
+			kor.GetUnusedServiceAccounts(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/services.go b/cmd/kor/services.go
index d027495..d04ba6b 100644
--- a/cmd/kor/services.go
+++ b/cmd/kor/services.go
@@ -21,7 +21,7 @@ var serviceCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedServices(includeExcludeLists, clientset)
+			kor.GetUnusedServices(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/cmd/kor/statefulsets.go b/cmd/kor/statefulsets.go
index 130da02..e8ffb14 100644
--- a/cmd/kor/statefulsets.go
+++ b/cmd/kor/statefulsets.go
@@ -21,7 +21,7 @@ var stsCmd = &cobra.Command{
 				fmt.Println(response)
 			}
 		} else {
-			kor.GetUnusedStatefulSets(includeExcludeLists, clientset)
+			kor.GetUnusedStatefulSets(includeExcludeLists, clientset, slackOpts)
 		}
 
 	},
diff --git a/go.mod b/go.mod
index 3a4d873..18dd26b 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.20
 
 require (
 	github.com/olekukonko/tablewriter v0.0.5
+	github.com/prometheus/client_golang v1.16.0
 	github.com/spf13/cobra v1.7.0
 	k8s.io/api v0.28.2
 	k8s.io/apimachinery v0.28.2
@@ -13,6 +14,8 @@ require (
 )
 
 require (
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emicklei/go-restful/v3 v3.10.1 // indirect
 	github.com/evanphx/json-patch v5.6.0+incompatible // indirect
@@ -32,12 +35,16 @@ require (
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/mattn/go-runewidth v0.0.14 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/onsi/ginkgo/v2 v2.11.0 // indirect
 	github.com/onsi/gomega v1.27.10 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/prometheus/client_model v0.3.0 // indirect
+	github.com/prometheus/common v0.42.0 // indirect
+	github.com/prometheus/procfs v0.10.1 // indirect
 	github.com/rivo/uniseg v0.4.4 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	golang.org/x/net v0.13.0 // indirect
diff --git a/go.sum b/go.sum
index 423cae2..4f4e8cb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,7 @@
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -21,7 +25,9 @@ github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -57,6 +63,8 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ
 github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
 github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
+github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -74,6 +82,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
+github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
+github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
+github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
+github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
+github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
+github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
+github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
 github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
@@ -107,6 +123,7 @@ golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
 golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
 golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/pkg/kor/all.go b/pkg/kor/all.go
index 5952b20..abbd084 100644
--- a/pkg/kor/all.go
+++ b/pkg/kor/all.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -118,8 +119,11 @@ func getUnusedPdbs(clientset kubernetes.Interface, namespace string) ResourceDif
 	return namespacePdbDiff
 }
 
-func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
+
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		var allDiffs []ResourceDiff
 		namespaceCMDiff := getUnusedCMs(clientset, namespace)
@@ -144,9 +148,20 @@ func GetUnusedAll(includeExcludeLists IncludeExcludeLists, clientset kubernetes.
 		allDiffs = append(allDiffs, namespaceIngressDiff)
 		namespacePdbDiff := getUnusedPdbs(clientset, namespace)
 		allDiffs = append(allDiffs, namespacePdbDiff)
+
 		output := FormatOutputAll(namespace, allDiffs)
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/confimgmaps.go b/pkg/kor/confimgmaps.go
index 64ca796..8125e3c 100644
--- a/pkg/kor/confimgmaps.go
+++ b/pkg/kor/confimgmaps.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -128,9 +129,11 @@ func processNamespaceCM(clientset kubernetes.Interface, namespace string) ([]str
 
 }
 
-func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceCM(clientset, namespace)
 		if err != nil {
@@ -138,8 +141,18 @@ func GetUnusedConfigmaps(includeExcludeLists IncludeExcludeLists, clientset kube
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Config Maps")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/deployments.go b/pkg/kor/deployments.go
index 3285ea7..c5d84f2 100644
--- a/pkg/kor/deployments.go
+++ b/pkg/kor/deployments.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -32,9 +33,11 @@ func ProcessNamespaceDeployments(clientset kubernetes.Interface, namespace strin
 	return deploymentsWithoutReplicas, nil
 }
 
-func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := ProcessNamespaceDeployments(clientset, namespace)
 		if err != nil {
@@ -42,8 +45,18 @@ func GetUnusedDeployments(includeExcludeLists IncludeExcludeLists, clientset kub
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Deployments")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/exporter.go b/pkg/kor/exporter.go
new file mode 100644
index 0000000..e218068
--- /dev/null
+++ b/pkg/kor/exporter.go
@@ -0,0 +1,72 @@
+package kor
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"os"
+	"strconv"
+	"time"
+
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	"k8s.io/client-go/kubernetes"
+)
+
+var (
+	orphanedResourcesCounter = prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "kubernetes_orphaned_resources",
+			Help: "Orphaned resources in Kubernetes",
+		},
+		[]string{"kind", "namespace", "resourceName"},
+	)
+)
+
+func init() {
+	prometheus.MustRegister(orphanedResourcesCounter)
+}
+
+// TODO: add option to change port / url !?
+func Exporter(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) {
+	http.Handle("/metrics", promhttp.Handler())
+	fmt.Println("Server listening on :8080")
+	go exportMetrics(includeExcludeLists, clientset, outputFormat) // Start exporting metrics in the background
+	if err := http.ListenAndServe(":8080", nil); err != nil {
+		fmt.Println(err)
+	}
+}
+
+func exportMetrics(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) {
+	exporterInterval := os.Getenv("EXPORTER_INTERVAL")
+	if exporterInterval == "" {
+		exporterInterval = "10"
+	}
+	exporterIntervalValue, err := strconv.Atoi(exporterInterval)
+	if err != nil {
+		fmt.Println(err)
+		os.Exit(1)
+	}
+
+	for {
+		if korOutput, err := GetUnusedAllStructured(includeExcludeLists, clientset, outputFormat); err != nil {
+			fmt.Println(err)
+			os.Exit(1)
+		} else {
+			var data map[string]map[string][]string
+			if err := json.Unmarshal([]byte(korOutput), &data); err != nil {
+				fmt.Println("Error parsing JSON:", err)
+				return
+			}
+
+			for namespace, resources := range data {
+				for kind, resourceList := range resources {
+					for _, resourceName := range resourceList {
+						orphanedResourcesCounter.WithLabelValues(kind, namespace, resourceName).Set(1)
+					}
+				}
+			}
+			time.Sleep(time.Duration(exporterIntervalValue) * time.Minute)
+		}
+	}
+}
diff --git a/pkg/kor/hpas.go b/pkg/kor/hpas.go
index 635fb7e..7fc4bf1 100644
--- a/pkg/kor/hpas.go
+++ b/pkg/kor/hpas.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -79,9 +80,11 @@ func processNamespaceHpas(clientset kubernetes.Interface, namespace string) ([]s
 	return unusedHpas, nil
 }
 
-func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceHpas(clientset, namespace)
 		if err != nil {
@@ -89,10 +92,19 @@ func GetUnusedHpas(includeExcludeLists IncludeExcludeLists, clientset kubernetes
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Hpas")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
 	}
 
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
+	}
 }
 
 func GetUnusedHpasStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) {
diff --git a/pkg/kor/ingresses.go b/pkg/kor/ingresses.go
index b657dc4..22dbf8a 100644
--- a/pkg/kor/ingresses.go
+++ b/pkg/kor/ingresses.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -88,9 +89,11 @@ func processNamespaceIngresses(clientset kubernetes.Interface, namespace string)
 
 }
 
-func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceIngresses(clientset, namespace)
 		if err != nil {
@@ -98,8 +101,18 @@ func GetUnusedIngresses(includeExcludeLists IncludeExcludeLists, clientset kuber
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Ingresses")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/multi.go b/pkg/kor/multi.go
index 9caee0e..cb85f07 100644
--- a/pkg/kor/multi.go
+++ b/pkg/kor/multi.go
@@ -1,8 +1,10 @@
 package kor
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
+	"os"
 	"strings"
 
 	"k8s.io/client-go/kubernetes"
@@ -53,10 +55,12 @@ func retrieveNamespaceDiffs(clientset kubernetes.Interface, namespace string, re
 	return allDiffs
 }
 
-func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string) {
+func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourceNames string, slackOpts SlackOpts) {
 	var clientset kubernetes.Interface
 	var namespaces []string
 
+	var outputBuffer bytes.Buffer
+
 	clientset = GetKubeClient(kubeconfig)
 
 	resourceList := strings.Split(resourceNames, ",")
@@ -65,8 +69,18 @@ func GetUnusedMulti(includeExcludeLists IncludeExcludeLists, kubeconfig, resourc
 	for _, namespace := range namespaces {
 		allDiffs := retrieveNamespaceDiffs(clientset, namespace, resourceList)
 		output := FormatOutputAll(namespace, allDiffs)
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/pdbs.go b/pkg/kor/pdbs.go
index b0458a0..9407503 100644
--- a/pkg/kor/pdbs.go
+++ b/pkg/kor/pdbs.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -48,9 +49,11 @@ func processNamespacePdbs(clientset kubernetes.Interface, namespace string) ([]s
 	return unusedPdbs, nil
 }
 
-func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespacePdbs(clientset, namespace)
 		if err != nil {
@@ -58,8 +61,18 @@ func GetUnusedPdbs(includeExcludeLists IncludeExcludeLists, clientset kubernetes
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Pdbs")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/pvc.go b/pkg/kor/pvc.go
index 4303566..b5a9b13 100644
--- a/pkg/kor/pvc.go
+++ b/pkg/kor/pvc.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -53,9 +54,11 @@ func processNamespacePvcs(clientset kubernetes.Interface, namespace string) ([]s
 	return diff, nil
 }
 
-func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespacePvcs(clientset, namespace)
 		if err != nil {
@@ -63,10 +66,19 @@ func GetUnusedPvcs(includeExcludeLists IncludeExcludeLists, clientset kubernetes
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Pvcs")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
 	}
 
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
+	}
 }
 
 func GetUnusedPvcsStructured(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, outputFormat string) (string, error) {
diff --git a/pkg/kor/roles.go b/pkg/kor/roles.go
index 93ddc55..7bfb26d 100644
--- a/pkg/kor/roles.go
+++ b/pkg/kor/roles.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -66,9 +67,11 @@ func processNamespaceRoles(clientset kubernetes.Interface, namespace string) ([]
 
 }
 
-func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceRoles(clientset, namespace)
 		if err != nil {
@@ -76,8 +79,18 @@ func GetUnusedRoles(includeExcludeLists IncludeExcludeLists, clientset kubernete
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Roles")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/secrets.go b/pkg/kor/secrets.go
index 9a42133..e41a748 100644
--- a/pkg/kor/secrets.go
+++ b/pkg/kor/secrets.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -140,9 +141,11 @@ func processNamespaceSecret(clientset kubernetes.Interface, namespace string) ([
 
 }
 
-func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceSecret(clientset, namespace)
 		if err != nil {
@@ -150,8 +153,18 @@ func GetUnusedSecrets(includeExcludeLists IncludeExcludeLists, clientset kuberne
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Secrets")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/serviceaccounts.go b/pkg/kor/serviceaccounts.go
index b7e90a6..e031025 100644
--- a/pkg/kor/serviceaccounts.go
+++ b/pkg/kor/serviceaccounts.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -139,9 +140,11 @@ func processNamespaceSA(clientset kubernetes.Interface, namespace string) ([]str
 
 }
 
-func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := processNamespaceSA(clientset, namespace)
 		if err != nil {
@@ -149,8 +152,18 @@ func GetUnusedServiceAccounts(includeExcludeLists IncludeExcludeLists, clientset
 			continue
 		}
 		output := FormatOutput(namespace, diff, "ServiceAccount")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/services.go b/pkg/kor/services.go
index 14a2cb8..ca0059a 100644
--- a/pkg/kor/services.go
+++ b/pkg/kor/services.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -32,9 +33,11 @@ func ProcessNamespaceServices(clientset kubernetes.Interface, namespace string)
 	return endpointsWithoutSubsets, nil
 }
 
-func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := ProcessNamespaceServices(clientset, namespace)
 		if err != nil {
@@ -42,8 +45,18 @@ func GetUnusedServices(includeExcludeLists IncludeExcludeLists, clientset kubern
 			continue
 		}
 		output := FormatOutput(namespace, diff, "Services")
-		fmt.Println(output)
-		fmt.Println()
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 
diff --git a/pkg/kor/slack.go b/pkg/kor/slack.go
new file mode 100644
index 0000000..2eb8c92
--- /dev/null
+++ b/pkg/kor/slack.go
@@ -0,0 +1,112 @@
+package kor
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"path/filepath"
+)
+
+type SendMessageToSlack interface {
+	SendToSlack(slackOpts SlackOpts, outputBuffer string) error
+}
+
+type SlackOpts struct {
+	WebhookURL string
+	Channel    string
+	Token      string
+}
+
+type SlackMessage struct {
+}
+
+func SendToSlack(sm SendMessageToSlack, slackOpts SlackOpts, outputBuffer string) error {
+	return sm.SendToSlack(slackOpts, outputBuffer)
+}
+
+func (sm SlackMessage) SendToSlack(slackOpts SlackOpts, outputBuffer string) error {
+	if slackOpts.WebhookURL != "" {
+		payload := []byte(`{"text": "` + outputBuffer + `"}`)
+		_, err := http.Post(slackOpts.WebhookURL, "application/json", bytes.NewBuffer(payload))
+
+		if err != nil {
+			return err
+		}
+		return nil
+	} else if slackOpts.Channel != "" && slackOpts.Token != "" {
+		fmt.Printf("Sending message to Slack channel %s...", slackOpts.Channel)
+		outputFilePath, _ := writeOutputToFile(outputBuffer)
+
+		var formData bytes.Buffer
+		writer := multipart.NewWriter(&formData)
+
+		fileWriter, err := writer.CreateFormFile("file", outputFilePath)
+		if err != nil {
+			return err
+		}
+		file, err := os.Open(outputFilePath)
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+		_, err = io.Copy(fileWriter, file)
+		if err != nil {
+			return err
+		}
+
+		if err := writer.WriteField("channels", slackOpts.Channel); err != nil {
+			return err
+		}
+
+		writer.Close()
+
+		req, err := http.NewRequest("POST", "https://slack.com/api/files.upload", &formData)
+		if err != nil {
+			return err
+		}
+		req.Header.Set("Authorization", "Bearer "+slackOpts.Token)
+		req.Header.Set("Content-Type", writer.FormDataContentType())
+
+		client := &http.Client{}
+		resp, err := client.Do(req)
+		if err != nil {
+			return err
+		}
+		defer resp.Body.Close()
+
+		if resp.StatusCode != http.StatusOK {
+			return fmt.Errorf("slack API returned non-OK status code: %d", resp.StatusCode)
+		}
+
+		return nil
+	} else {
+		return errors.New("SlackOpts must contain either WebhookURL or Channel and Token")
+	}
+}
+
+func writeOutputToFile(outputBuffer string) (string, error) {
+	homeDir, err := os.UserHomeDir()
+	if err != nil {
+		return "", fmt.Errorf("failed to get user's home directory: %v", err)
+	}
+
+	outputFileName := "kor-scan-results.txt"
+	outputFilePath := filepath.Join(homeDir, outputFileName)
+
+	file, err := os.Create(outputFilePath)
+	if err != nil {
+		return "", fmt.Errorf("failed to create output file: %v", err)
+	}
+	defer file.Close()
+
+	_, err = file.WriteString(outputBuffer)
+	if err != nil {
+		return "", fmt.Errorf("failed to write output to file: %v", err)
+	}
+
+	return outputFilePath, nil
+}
diff --git a/pkg/kor/statefulsets.go b/pkg/kor/statefulsets.go
index 6c7102e..ead07c4 100644
--- a/pkg/kor/statefulsets.go
+++ b/pkg/kor/statefulsets.go
@@ -1,6 +1,7 @@
 package kor
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -28,18 +29,30 @@ func ProcessNamespaceStatefulSets(clientset kubernetes.Interface, namespace stri
 	return statefulSetsWithoutReplicas, nil
 }
 
-func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface) {
+func GetUnusedStatefulSets(includeExcludeLists IncludeExcludeLists, clientset kubernetes.Interface, slackOpts SlackOpts) {
 	namespaces := SetNamespaceList(includeExcludeLists, clientset)
 
+	var outputBuffer bytes.Buffer
+
 	for _, namespace := range namespaces {
 		diff, err := ProcessNamespaceStatefulSets(clientset, namespace)
 		if err != nil {
 			fmt.Fprintf(os.Stderr, "Failed to process namespace %s: %v\n", namespace, err)
 			continue
 		}
-		output := FormatOutput(namespace, diff, "StatefulSets")
-		fmt.Println(output)
-		fmt.Println()
+		output := FormatOutput(namespace, diff, "Statefulsets")
+
+		outputBuffer.WriteString(output)
+		outputBuffer.WriteString("\n")
+	}
+
+	if slackOpts != (SlackOpts{}) {
+		if err := SendToSlack(SlackMessage{}, slackOpts, outputBuffer.String()); err != nil {
+			fmt.Fprintf(os.Stderr, "Failed to send message to slack: %v\n", err)
+			os.Exit(1)
+		}
+	} else {
+		fmt.Println(outputBuffer.String())
 	}
 }
 


Test patch:
diff --git a/pkg/kor/slack_test.go b/pkg/kor/slack_test.go
new file mode 100644
index 0000000..1875b92
--- /dev/null
+++ b/pkg/kor/slack_test.go
@@ -0,0 +1,74 @@
+package kor
+
+import (
+	"bytes"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+)
+
+type SendToSlackTestCase struct {
+	Name         string
+	SlackOpts    SlackOpts
+	OutputBuffer string
+}
+
+var testCases = []SendToSlackTestCase{
+	{
+		Name: "Test using WebhookURL",
+		SlackOpts: SlackOpts{
+			WebhookURL: "slack.webhookurl.com",
+		},
+		OutputBuffer: "Test message",
+	},
+	{
+		Name: "Test using Channel and Token",
+		SlackOpts: SlackOpts{
+			Channel: "your_channel",
+			Token:   "your_token",
+		},
+		OutputBuffer: "Test message",
+	},
+	{
+		Name:         "Test with empty SlackOpts",
+		SlackOpts:    SlackOpts{},
+		OutputBuffer: "Test message",
+	},
+}
+
+func TestSendToSlack(t *testing.T) {
+	for _, tc := range testCases {
+		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			if err := SendToSlack(SlackMessage{}, tc.SlackOpts, tc.OutputBuffer); err != nil {
+				t.Errorf("Expected no error, got %v", err)
+			}
+		}))
+
+		defer server.Close()
+	}
+}
+
+func TestWriteOutputToFile(t *testing.T) {
+	outputBuffer := bytes.Buffer{}
+	outputBuffer.WriteString("This is a test output.\n")
+	expectedOutput := outputBuffer.String()
+
+	outputFilePath, err := writeOutputToFile(expectedOutput)
+	if err != nil {
+		t.Errorf("Expected no error, got %v", err)
+	}
+
+	if _, err := os.Stat(outputFilePath); os.IsNotExist(err) {
+		t.Errorf("Expected output file to exist, got error: %v", err)
+	}
+
+	fileContent, err := os.ReadFile(outputFilePath)
+	if err != nil {
+		t.Errorf("Failed to read output file: %v", err)
+	}
+
+	if string(fileContent) != expectedOutput {
+		t.Errorf("Expected file content:\n%s\nGot:\n%s", expectedOutput, string(fileContent))
+	}
+}