diff options
| -rw-r--r-- | .github/actions/spelling/allow.txt | 1 | ||||
| -rw-r--r-- | .github/actions/spelling/excludes.txt | 4 | ||||
| -rw-r--r-- | cmd/main.go | 25 | ||||
| -rw-r--r-- | config/example-grafana-dashboard.json | 685 | ||||
| -rw-r--r-- | docs/install/start.md | 40 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 6 | ||||
| -rw-r--r-- | pkg/argocd/argocd.go | 13 | ||||
| -rw-r--r-- | pkg/argocd/update.go | 1 | ||||
| -rw-r--r-- | pkg/client/kubernetes.go | 6 | ||||
| -rw-r--r-- | pkg/metrics/metrics.go | 187 | ||||
| -rw-r--r-- | pkg/registry/client.go | 7 |
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) @@ -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 @@ -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 |
