summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/actions/spelling/allow.txt1
-rw-r--r--.github/actions/spelling/excludes.txt4
-rw-r--r--cmd/main.go25
-rw-r--r--config/example-grafana-dashboard.json685
-rw-r--r--docs/install/start.md40
-rw-r--r--go.mod1
-rw-r--r--go.sum6
-rw-r--r--pkg/argocd/argocd.go13
-rw-r--r--pkg/argocd/update.go1
-rw-r--r--pkg/client/kubernetes.go6
-rw-r--r--pkg/metrics/metrics.go187
-rw-r--r--pkg/registry/client.go7
12 files changed, 974 insertions, 2 deletions
diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt
index 5165668..a2609ed 100644
--- a/.github/actions/spelling/allow.txt
+++ b/.github/actions/spelling/allow.txt
@@ -77,6 +77,7 @@ gopkg
goreportcard
goroutine
goroutines
+Grafana
grpc
guestbook
healthport
diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt
index d066cfc..af56473 100644
--- a/.github/actions/spelling/excludes.txt
+++ b/.github/actions/spelling/excludes.txt
@@ -3,3 +3,7 @@
\/mocks\/
ignore$
\.png$
+^\.config/
+\.json$
+\.yaml$
+\.go$
diff --git a/cmd/main.go b/cmd/main.go
index c66da13..294d4e4 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -15,6 +15,7 @@ import (
"github.com/argoproj-labs/argocd-image-updater/pkg/health"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/metrics"
"github.com/argoproj-labs/argocd-image-updater/pkg/registry"
"github.com/argoproj-labs/argocd-image-updater/pkg/version"
@@ -41,6 +42,7 @@ type ImageUpdaterConfig struct {
KubeClient *client.KubernetesClient
MaxConcurrency int
HealthPort int
+ MetricsPort int
RegistriesConf string
AppNamePatterns []string
}
@@ -94,6 +96,8 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
return result, err
}
+ metrics.Applications().SetNumberOfApplications(len(appList))
+
if !warmUp {
log.Infof("Starting image update cycle, considering %d annotated application(s) for update", len(appList))
}
@@ -131,6 +135,11 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
result.NumImagesConsidered += res.NumImagesConsidered
result.NumImagesUpdated += res.NumImagesUpdated
result.NumSkipped += res.NumSkipped
+ if !warmUp && !cfg.DryRun {
+ metrics.Applications().IncreaseImageUpdate(app, res.NumImagesUpdated)
+ }
+ metrics.Applications().IncreaseUpdateErrors(app, res.NumErrors)
+ metrics.Applications().SetNumberOfImagesWatched(app, res.NumImagesConsidered)
wg.Done()
}(app, curApplication)
}
@@ -402,11 +411,17 @@ func newRunCommand() *cobra.Command {
// Health server will start in a go routine and run asynchronously
var hsErrCh chan error
+ var msErrCh chan error
if cfg.HealthPort > 0 {
log.Infof("Starting health probe server TCP port=%d", cfg.HealthPort)
hsErrCh = health.StartHealthServer(cfg.HealthPort)
}
+ if cfg.MetricsPort > 0 {
+ log.Infof("Starting metrics server on TCP port=%d", cfg.MetricsPort)
+ msErrCh = metrics.StartMetricsServer(cfg.MetricsPort)
+ }
+
if warmUpCache {
err := warmupImageCache(cfg)
if err != nil {
@@ -422,10 +437,17 @@ func newRunCommand() *cobra.Command {
case err := <-hsErrCh:
if err != nil {
log.Errorf("Health probe server exited with error: %v", err)
- return nil
} else {
log.Infof("Health probe server exited gracefully")
}
+ return nil
+ case err := <-msErrCh:
+ if err != nil {
+ log.Errorf("Metrics server exited with error: %v", err)
+ } else {
+ log.Infof("Metrics server exited gracefully")
+ }
+ return nil
default:
if lastRun.IsZero() || time.Since(lastRun) > cfg.CheckInterval {
result, err := runImageUpdater(cfg, false)
@@ -462,6 +484,7 @@ func newRunCommand() *cobra.Command {
runCmd.Flags().StringVar(&cfg.LogLevel, "loglevel", env.GetStringVal("IMAGE_UPDATER_LOGLEVEL", "info"), "set the loglevel to one of trace|debug|info|warn|error")
runCmd.Flags().StringVar(&kubeConfig, "kubeconfig", "", "full path to kubernetes client configuration, i.e. ~/.kube/config")
runCmd.Flags().IntVar(&cfg.HealthPort, "health-port", 8080, "port to start the health server on, 0 to disable")
+ runCmd.Flags().IntVar(&cfg.MetricsPort, "metrics-port", 8081, "port to start the metrics server on, 0 to disable")
runCmd.Flags().BoolVar(&once, "once", false, "run only once, same as specifying --interval=0 and --health-port=0")
runCmd.Flags().StringVar(&cfg.RegistriesConf, "registries-conf-path", defaultRegistriesConfPath, "path to registries configuration file")
runCmd.Flags().BoolVar(&disableKubernetes, "disable-kubernetes", false, "do not create and use a Kubernetes client")
diff --git a/config/example-grafana-dashboard.json b/config/example-grafana-dashboard.json
new file mode 100644
index 0000000..9a72737
--- /dev/null
+++ b/config/example-grafana-dashboard.json
@@ -0,0 +1,685 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "description": "Example dashboard for Argo CD Image Updater metrics",
+ "editable": true,
+ "gnetId": null,
+ "graphTooltip": 0,
+ "id": 25,
+ "links": [],
+ "panels": [
+ {
+ "collapsed": false,
+ "datasource": null,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 6,
+ "panels": [],
+ "title": "Configuration",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "The total number of applications watched by Argo CD Image Updater",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 0,
+ "y": 1
+ },
+ "hiddenSeries": false,
+ "id": 2,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(argocd_image_updater_applications_watched_total)",
+ "instant": false,
+ "interval": "",
+ "legendFormat": "Applications",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Number of applications watched",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "Number of apps",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "Total number of images watched for updates by Argo CD Image Updater",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 9,
+ "w": 12,
+ "x": 12,
+ "y": 1
+ },
+ "hiddenSeries": false,
+ "id": 4,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(argocd_image_updater_images_watched_total)",
+ "interval": "",
+ "legendFormat": "Images",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Number of images watched",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "Number of images",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "collapsed": false,
+ "datasource": null,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 10
+ },
+ "id": 12,
+ "panels": [],
+ "title": "Image updates",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "The total number and error of images updated at a given time",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 11
+ },
+ "hiddenSeries": false,
+ "id": 8,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(increase(argocd_image_updater_images_updated_total[1m]))",
+ "interval": "",
+ "legendFormat": "Updates",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(increase(argocd_image_updater_images_updated_error_total[1m]))",
+ "interval": "",
+ "legendFormat": "Errors",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Image updates (total)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "Amount",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 11
+ },
+ "hiddenSeries": false,
+ "id": 10,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (application) (increase(argocd_image_updater_images_updated_total[5m]))",
+ "interval": "",
+ "legendFormat": "{{application}}",
+ "refId": "A"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Image updates (per app)",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "collapsed": false,
+ "datasource": null,
+ "gridPos": {
+ "h": 1,
+ "w": 24,
+ "x": 0,
+ "y": 19
+ },
+ "id": 18,
+ "panels": [],
+ "title": "Operational metrics",
+ "type": "row"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "Number of API requests to registries in a specific time frame",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 20
+ },
+ "hiddenSeries": false,
+ "id": 14,
+ "legend": {
+ "alignAsTable": true,
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (registry) (increase(argocd_image_updater_registry_requests_total[1m]))",
+ "interval": "",
+ "legendFormat": "{{registry}} | requests",
+ "refId": "A"
+ },
+ {
+ "expr": "sum by (registry) (increase(argocd_image_updater_registry_errors_total[1m]))",
+ "interval": "",
+ "legendFormat": "{{registry}} | errors",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Registry API requests",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": null,
+ "description": "The number of requests to the Argo CD API server over time",
+ "fieldConfig": {
+ "defaults": {
+ "custom": {}
+ },
+ "overrides": []
+ },
+ "fill": 1,
+ "fillGradient": 0,
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 20
+ },
+ "hiddenSeries": false,
+ "id": 16,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 1,
+ "nullPointMode": "null",
+ "options": {
+ "alertThreshold": true
+ },
+ "percentage": false,
+ "pluginVersion": "7.2.1",
+ "pointradius": 2,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(increase(argocd_image_updater_argocd_api_requests_total[1m]))",
+ "interval": "",
+ "legendFormat": "Requests",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(increase(argocd_image_updater_argocd_api_errors_total[1m]))",
+ "interval": "",
+ "legendFormat": "Errors",
+ "refId": "B"
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Argo CD API requests",
+ "tooltip": {
+ "shared": true,
+ "sort": 0,
+ "value_type": "individual"
+ },
+ "type": "graph",
+ "xaxis": {
+ "buckets": null,
+ "mode": "time",
+ "name": null,
+ "show": true,
+ "values": []
+ },
+ "yaxes": [
+ {
+ "decimals": 0,
+ "format": "short",
+ "label": "",
+ "logBase": 1,
+ "max": null,
+ "min": "0",
+ "show": true
+ },
+ {
+ "format": "short",
+ "label": null,
+ "logBase": 1,
+ "max": null,
+ "min": null,
+ "show": true
+ }
+ ],
+ "yaxis": {
+ "align": false,
+ "alignLevel": null
+ }
+ }
+ ],
+ "schemaVersion": 26,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Argo CD Image Updater",
+ "uid": "M-U4ZLAMz",
+ "version": 9
+} \ No newline at end of file
diff --git a/docs/install/start.md b/docs/install/start.md
index 41a8c57..da2b085 100644
--- a/docs/install/start.md
+++ b/docs/install/start.md
@@ -221,3 +221,43 @@ If opting for such an approach, you should make sure that:
* Each instance has a dedicated user in Argo CD, with dedicated RBAC permissions
* RBAC permissions are set-up so that instances cannot interfere with each
others managed resources
+
+## Metrics
+
+Starting with v0.8.0, Argo CD Image Updater exports Prometheus-compatible
+metrics on a dedicated endpoint, which by default listens on TCP port 8082
+and serves data from `/metrics` path. This endpoint is exposed by a service
+named `argocd-image-updater` on a port named `metrics`.
+
+The following metrics are being made available:
+
+* Number of applications processed (i.e. those with an annotation)
+
+ * `argocd_image_updater_applications_watched_total`
+
+* Number of images watched for new tags
+
+ * `argocd_image_updater_images_watched_total`
+
+* Number of images updated (successful and failed)
+
+ * `argocd_image_updater_images_updated_total`
+ * `argocd_image_updater_images_errors_total`
+
+* Number of requests to Argo CD API (successful and failed)
+
+ * `argocd_image_updater_argocd_api_requests_total`
+ * `argocd_image_updater_argocd_api_errors_total`
+
+* Number of requests to K8s API (successful and failed)
+
+ * `argocd_image_updater_k8s_api_requests_total`
+ * `argocd_image_updater_k8s_api_errors_total`
+
+* Number of requests to the container registries (successful and failed)
+
+ * `argocd_image_updater_registry_requests_total`
+ * `argocd_image_updater_registry_errors_total`
+
+A (very) rudimentary example dashboard definition for Grafana is provided
+[here](https://github.com/argoproj-labs/argocd-image-updater/tree/master/config)
diff --git a/go.mod b/go.mod
index b58b0d0..3bbc39e 100644
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
github.com/gorilla/mux v1.7.4 // indirect
github.com/nokia/docker-registry-client v0.0.0-20201015093031-af1a6d3b4fb1
github.com/patrickmn/go-cache v2.1.0+incompatible
+ github.com/prometheus/client_golang v1.0.0
github.com/sirupsen/logrus v1.6.0
github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.6.1
diff --git a/go.sum b/go.sum
index 195542f..ce34c09 100644
--- a/go.sum
+++ b/go.sum
@@ -75,6 +75,7 @@ github.com/bazelbuild/buildtools v0.0.0-20190731111112-f720930ceb60/go.mod h1:5J
github.com/bazelbuild/buildtools v0.0.0-20190917191645-69366ca98f89/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU=
github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=
@@ -439,6 +440,7 @@ github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOA
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-shellwords v1.0.5/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mesos/mesos-go v0.0.9/go.mod h1:kPYCMQ9gsOXVAle1OsoY4I1+9kPu8GHkf88aV59fDr4=
github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
@@ -516,15 +518,19 @@ github.com/pquerna/cachecontrol v0.0.0-20180306154005-525d0eb5f91d/go.mod h1:prY
github.com/pquerna/ffjson v0.0.0-20180717144149-af8b230fcd20/go.mod h1:YARuvh7BUWHNhzDq2OM5tzR2RiCcN2D7sapiKyCel/M=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI=
diff --git a/pkg/argocd/argocd.go b/pkg/argocd/argocd.go
index 4832c6f..b1dcf45 100644
--- a/pkg/argocd/argocd.go
+++ b/pkg/argocd/argocd.go
@@ -10,6 +10,7 @@ import (
"github.com/argoproj-labs/argocd-image-updater/pkg/common"
"github.com/argoproj-labs/argocd-image-updater/pkg/image"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/metrics"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/pkg/apiclient/application"
@@ -143,13 +144,17 @@ func FilterApplicationsForUpdate(apps []v1alpha1.Application, patterns []string)
// GetApplication gets the application named appName from Argo CD API
func (client *argoCD) GetApplication(ctx context.Context, appName string) (*v1alpha1.Application, error) {
conn, appClient, err := client.Client.NewApplicationClient()
+ metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1)
if err != nil {
+ metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1)
return nil, err
}
defer conn.Close()
+ metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1)
app, err := appClient.Get(ctx, &application.ApplicationQuery{Name: &appName})
if err != nil {
+ metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1)
return nil, err
}
@@ -160,13 +165,17 @@ func (client *argoCD) GetApplication(ctx context.Context, appName string) (*v1al
// has access to.
func (client *argoCD) ListApplications() ([]v1alpha1.Application, error) {
conn, appClient, err := client.Client.NewApplicationClient()
+ metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1)
if err != nil {
+ metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1)
return nil, err
}
defer conn.Close()
+ metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1)
apps, err := appClient.List(context.TODO(), &application.ApplicationQuery{})
if err != nil {
+ metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1)
return nil, err
}
@@ -176,13 +185,17 @@ func (client *argoCD) ListApplications() ([]v1alpha1.Application, error) {
// UpdateSpec updates the spec for given application
func (client *argoCD) UpdateSpec(ctx context.Context, in *application.ApplicationUpdateSpecRequest) (*v1alpha1.ApplicationSpec, error) {
conn, appClient, err := client.Client.NewApplicationClient()
+ metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1)
if err != nil {
+ metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1)
return nil, err
}
defer conn.Close()
+ metrics.Clients().IncreaseArgoCDClientRequest(client.Client.ClientOptions().ServerAddr, 1)
spec, err := appClient.UpdateSpec(ctx, in)
if err != nil {
+ metrics.Clients().IncreaseArgoCDClientError(client.Client.ClientOptions().ServerAddr, 1)
return nil, err
}
diff --git a/pkg/argocd/update.go b/pkg/argocd/update.go
index cf68011..0ed2e0c 100644
--- a/pkg/argocd/update.go
+++ b/pkg/argocd/update.go
@@ -15,6 +15,7 @@ import (
// Stores some statistics about the results of a run
type ImageUpdaterResult struct {
NumApplicationsProcessed int
+ NumImagesFound int
NumImagesUpdated int
NumImagesConsidered int
NumSkipped int
diff --git a/pkg/client/kubernetes.go b/pkg/client/kubernetes.go
index 99e6d8b..5f4f512 100644
--- a/pkg/client/kubernetes.go
+++ b/pkg/client/kubernetes.go
@@ -6,6 +6,8 @@ import (
"context"
"fmt"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/metrics"
+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
@@ -46,7 +48,9 @@ func NewKubernetesClient(kubeconfig string) (*KubernetesClient, error) {
// GetSecretData returns the raw data from named K8s secret in given namespace
func (client *KubernetesClient) GetSecretData(namespace string, secretName string) (map[string][]byte, error) {
secret, err := client.Clientset.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, v1.GetOptions{})
+ metrics.Clients().IncreaseK8sClientRequest(1)
if err != nil {
+ metrics.Clients().IncreaseK8sClientRequest(1)
return nil, err
}
return secret.Data, nil
@@ -55,7 +59,9 @@ func (client *KubernetesClient) GetSecretData(namespace string, secretName strin
// GetSecretField returns the value of a field from named K8s secret in given namespace
func (client *KubernetesClient) GetSecretField(namespace string, secretName string, field string) (string, error) {
secret, err := client.GetSecretData(namespace, secretName)
+ metrics.Clients().IncreaseK8sClientRequest(1)
if err != nil {
+ metrics.Clients().IncreaseK8sClientRequest(1)
return "", err
}
if data, ok := secret[field]; !ok {
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
new file mode 100644
index 0000000..08c8f4b
--- /dev/null
+++ b/pkg/metrics/metrics.go
@@ -0,0 +1,187 @@
+package metrics
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/promauto"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+// TODO: These should not be global vars with this package
+var epm *EndpointMetrics
+var apm *ApplicationMetrics
+var cpm *ClientMetrics
+
+// EndpointMetrics stores metrics for registry endpoints
+type EndpointMetrics struct {
+ requestsTotal *prometheus.CounterVec
+ requestsFailed *prometheus.CounterVec
+}
+
+// ApplicationMetrics stores metrics for applications
+type ApplicationMetrics struct {
+ applicationsTotal prometheus.Gauge
+ imagesWatchedTotal *prometheus.GaugeVec
+ imagesUpdatedTotal *prometheus.CounterVec
+ imagesUpdatedErrorsTotal *prometheus.CounterVec
+}
+
+// ClientMetrics stores metrics for K8s and ArgoCD clients
+type ClientMetrics struct {
+ argoCDRequestsTotal *prometheus.CounterVec
+ argoCDRequestsErrorsTotal *prometheus.CounterVec
+ kubeAPIRequestsTotal prometheus.Counter
+ kubeAPIRequestsErrorsTotal prometheus.Counter
+}
+
+// StartMetricsServer starts a new HTTP server for metrics on given port
+func StartMetricsServer(port int) chan error {
+ errCh := make(chan error)
+ go func() {
+ http.Handle("/metrics", promhttp.Handler())
+ errCh <- http.ListenAndServe(fmt.Sprintf(":%d", port), nil)
+ }()
+ return errCh
+}
+
+// NewEndpointMetrics returns a new endpoint metrics object
+func NewEndpointMetrics() *EndpointMetrics {
+ metrics := &EndpointMetrics{}
+
+ metrics.requestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "argocd_image_updater_registry_requests_total",
+ Help: "The total number of requests to this endpoint",
+ }, []string{"registry"})
+ metrics.requestsFailed = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "argocd_image_updater_registry_requests_failed_total",
+ Help: "The number of failed requests to this endpoint",
+ }, []string{"registry"})
+
+ return metrics
+}
+
+// NewApplicationsMetrics returns a new application metrics object
+func NewApplicationsMetrics() *ApplicationMetrics {
+ metrics := &ApplicationMetrics{}
+
+ metrics.applicationsTotal = promauto.NewGauge(prometheus.GaugeOpts{
+ Name: "argocd_image_updater_applications_watched_total",
+ Help: "The total number of applications watched by Argo CD Image Updater",
+ })
+
+ metrics.imagesWatchedTotal = promauto.NewGaugeVec(prometheus.GaugeOpts{
+ Name: "argocd_image_updater_images_watched_total",
+ Help: "Number of images watched by Argo CD Image Updater",
+ }, []string{"application"})
+
+ metrics.imagesUpdatedTotal = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "argocd_image_updater_images_updated_total",
+ Help: "Number of images updates by Argo CD Image Updater",
+ }, []string{"application"})
+
+ metrics.imagesUpdatedErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "argocd_image_updater_images_errors_total",
+ Help: "Number of errors reported by Argo CD Image Updater",
+ }, []string{"application"})
+
+ return metrics
+}
+
+// NewClientMetrics returns a new client metrics object
+func NewClientMetrics() *ClientMetrics {
+ metrics := &ClientMetrics{}
+
+ metrics.argoCDRequestsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "argocd_image_updater_argocd_api_requests_total",
+ Help: "The total number of Argo CD API requests performed by the Argo CD Image Updater",
+ }, []string{"argocd_server"})
+
+ metrics.argoCDRequestsErrorsTotal = promauto.NewCounterVec(prometheus.CounterOpts{
+ Name: "argocd_image_updater_argocd_api_errors_total",
+ Help: "The total number of Argo CD API requests resulting in error",
+ }, []string{"argocd_server"})
+
+ metrics.kubeAPIRequestsTotal = promauto.NewCounter(prometheus.CounterOpts{
+ Name: "argocd_image_updater_k8s_api_requests_total",
+ Help: "The total number of Argo CD API requests resulting in error",
+ })
+
+ metrics.kubeAPIRequestsErrorsTotal = promauto.NewCounter(prometheus.CounterOpts{
+ Name: "argocd_image_updater_k8s_api_errors_total",
+ Help: "The total number of Argo CD API requests resulting in error",
+ })
+
+ return metrics
+}
+
+// Endpoint returns the global EndpointMetrics object
+func Endpoint() *EndpointMetrics {
+ return epm
+}
+
+// Applications returns the global ApplicationMetrics object
+func Applications() *ApplicationMetrics {
+ return apm
+}
+
+// Clients returns the global ClientMetrics object
+func Clients() *ClientMetrics {
+ return cpm
+}
+
+// IncreaseRequest increases the request counter of EndpointMetrics object
+func (epm *EndpointMetrics) IncreaseRequest(registryURL string, isFailed bool) {
+ epm.requestsTotal.WithLabelValues(registryURL).Inc()
+ if isFailed {
+ epm.requestsFailed.WithLabelValues(registryURL).Inc()
+ }
+}
+
+// SetNumberOfApplications sets the total number of currently watched applications
+func (apm *ApplicationMetrics) SetNumberOfApplications(num int) {
+ apm.applicationsTotal.Set(float64(num))
+}
+
+// SetNumberOfImagesWatched sets the total number of currently watched images for given application
+func (apm *ApplicationMetrics) SetNumberOfImagesWatched(application string, num int) {
+ apm.imagesWatchedTotal.WithLabelValues(application).Set(float64(num))
+}
+
+// IncreaseImageUpdate increases the number of image updates for given application
+func (apm *ApplicationMetrics) IncreaseImageUpdate(application string, by int) {
+ apm.imagesUpdatedTotal.WithLabelValues(application).Add(float64(by))
+}
+
+// IncreaseUpdateErrors increases the number of errors for given application occured during update process
+func (apm *ApplicationMetrics) IncreaseUpdateErrors(application string, by int) {
+ apm.imagesUpdatedErrorsTotal.WithLabelValues(application).Add(float64(by))
+}
+
+// IncreaseArgoCDClientRequest increases the number of Argo CD API requests for given server
+func (cpm *ClientMetrics) IncreaseArgoCDClientRequest(server string, by int) {
+ cpm.argoCDRequestsTotal.WithLabelValues(server).Add(float64(by))
+}
+
+// IncreaseArgoCDClientError increases the number of failed Argo CD API requests for given server
+func (cpm *ClientMetrics) IncreaseArgoCDClientError(server string, by int) {
+ cpm.argoCDRequestsErrorsTotal.WithLabelValues(server).Add(float64(by))
+}
+
+// IncreaseK8sClientRequest increases the number of K8s API requests
+func (cpm *ClientMetrics) IncreaseK8sClientRequest(by int) {
+ cpm.kubeAPIRequestsTotal.Add(float64(by))
+}
+
+// IncreaseK8sClientRequest increases the number of failed K8s API requests
+func (cpm *ClientMetrics) IncreaseK8sClientError(by int) {
+ cpm.kubeAPIRequestsErrorsTotal.Add(float64(by))
+}
+
+// TODO: This is a lazy workaround, better initialize it somehwere else
+func init() {
+ epm = NewEndpointMetrics()
+ apm = NewApplicationsMetrics()
+ cpm = NewClientMetrics()
+}
diff --git a/pkg/registry/client.go b/pkg/registry/client.go
index 7eac4ea..69992ee 100644
--- a/pkg/registry/client.go
+++ b/pkg/registry/client.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
+ "github.com/argoproj-labs/argocd-image-updater/pkg/metrics"
"github.com/argoproj-labs/argocd-image-updater/pkg/tag"
"github.com/docker/distribution"
@@ -41,13 +42,16 @@ type registryClient struct {
type rateLimitTransport struct {
limiter ratelimit.Limiter
transport http.RoundTripper
+ endpoint string
}
// RoundTrip is a custom RoundTrip method with rate-limiter
func (rlt *rateLimitTransport) RoundTrip(r *http.Request) (*http.Response, error) {
rlt.limiter.Take()
log.Tracef("%s", r.URL)
- return rlt.transport.RoundTrip(r)
+ resp, err := rlt.transport.RoundTrip(r)
+ metrics.Endpoint().IncreaseRequest(rlt.endpoint, err != nil)
+ return resp, err
}
// newRegistry is a wrapper for creating a registry client that is possibly
@@ -69,6 +73,7 @@ func newRegistry(ep *RegistryEndpoint, opts registry.Options) (*registry.Registr
rlt := &rateLimitTransport{
limiter: ep.Limiter,
transport: transport,
+ endpoint: ep.RegistryAPI,
}
logf := opts.Logf