github.com/argoproj/argo-cd/v2@v2.10.9/controller/metrics/metrics_test.go (about)

     1  package metrics
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"net/http"
     8  	"net/http/httptest"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	gitopsCache "github.com/argoproj/gitops-engine/pkg/cache"
    14  	"github.com/argoproj/gitops-engine/pkg/sync/common"
    15  	"github.com/stretchr/testify/assert"
    16  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    17  	"k8s.io/apimachinery/pkg/runtime"
    18  	"k8s.io/client-go/tools/cache"
    19  	"k8s.io/client-go/util/workqueue"
    20  	"sigs.k8s.io/yaml"
    21  
    22  	argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    23  	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake"
    24  	appinformer "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions"
    25  	applister "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
    26  
    27  	"sigs.k8s.io/controller-runtime/pkg/controller"
    28  )
    29  
    30  const fakeApp = `
    31  apiVersion: argoproj.io/v1alpha1
    32  kind: Application
    33  metadata:
    34    name: my-app
    35    namespace: argocd
    36    labels:
    37      team-name: my-team
    38      team-bu: bu-id
    39      argoproj.io/cluster: test-cluster
    40  spec:
    41    destination:
    42      namespace: dummy-namespace
    43      server: https://localhost:6443
    44    project: important-project
    45    source:
    46      path: some/path
    47      repoURL: https://github.com/argoproj/argocd-example-apps.git
    48  status:
    49    sync:
    50      status: Synced
    51    health:
    52      status: Healthy
    53  `
    54  
    55  const fakeApp2 = `
    56  apiVersion: argoproj.io/v1alpha1
    57  kind: Application
    58  metadata:
    59    name: my-app-2
    60    namespace: argocd
    61    labels:
    62      team-name: my-team
    63      team-bu: bu-id
    64      argoproj.io/cluster: test-cluster
    65  spec:
    66    destination:
    67      namespace: dummy-namespace
    68      server: https://localhost:6443
    69    project: important-project
    70    source:
    71      path: some/path
    72      repoURL: https://github.com/argoproj/argocd-example-apps.git
    73    syncPolicy:
    74      automated:
    75        selfHeal: false
    76        prune: true
    77  status:
    78    sync:
    79      status: Synced
    80    health:
    81      status: Healthy
    82  operation:
    83    sync:
    84      revision: 041eab7439ece92c99b043f0e171788185b8fc1d
    85      syncStrategy:
    86        hook: {}
    87  `
    88  
    89  const fakeApp3 = `
    90  apiVersion: argoproj.io/v1alpha1
    91  kind: Application
    92  metadata:
    93    name: my-app-3
    94    namespace: argocd
    95    deletionTimestamp: "2020-03-16T09:17:45Z"
    96    labels:
    97      team-name: my-team
    98      team-bu: bu-id
    99      argoproj.io/cluster: test-cluster
   100  spec:
   101    destination:
   102      namespace: dummy-namespace
   103      server: https://localhost:6443
   104    project: important-project
   105    source:
   106      path: some/path
   107      repoURL: https://github.com/argoproj/argocd-example-apps.git
   108    syncPolicy:
   109      automated:
   110        selfHeal: true
   111        prune: false
   112  status:
   113    sync:
   114      status: OutOfSync
   115    health:
   116      status: Degraded
   117  `
   118  
   119  const fakeDefaultApp = `
   120  apiVersion: argoproj.io/v1alpha1
   121  kind: Application
   122  metadata:
   123    name: my-app
   124    namespace: argocd
   125  spec:
   126    destination:
   127      namespace: dummy-namespace
   128      server: https://localhost:6443
   129    source:
   130      path: some/path
   131      repoURL: https://github.com/argoproj/argocd-example-apps.git
   132  status:
   133    sync:
   134      status: Synced
   135    health:
   136      status: Healthy
   137  `
   138  
   139  var noOpHealthCheck = func(r *http.Request) error {
   140  	return nil
   141  }
   142  
   143  var appFilter = func(obj interface{}) bool {
   144  	return true
   145  }
   146  
   147  func init() {
   148  	// Create a fake controller so we initialize the internal controller metrics.
   149  	// https://github.com/kubernetes-sigs/controller-runtime/blob/4000e996a202917ad7d40f02ed8a2079a9ce25e9/pkg/internal/controller/metrics/metrics.go
   150  	_, _ = controller.New("test-controller", nil, controller.Options{})
   151  }
   152  
   153  func newFakeApp(fakeAppYAML string) *argoappv1.Application {
   154  	var app argoappv1.Application
   155  	err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
   156  	if err != nil {
   157  		panic(err)
   158  	}
   159  	return &app
   160  }
   161  
   162  func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.ApplicationLister) {
   163  	ctx, cancel := context.WithCancel(context.Background())
   164  	defer cancel()
   165  	var fakeApps []runtime.Object
   166  	for _, appYAML := range fakeAppYAMLs {
   167  		a := newFakeApp(appYAML)
   168  		fakeApps = append(fakeApps, a)
   169  	}
   170  	appClientset := appclientset.NewSimpleClientset(fakeApps...)
   171  	factory := appinformer.NewSharedInformerFactoryWithOptions(appClientset, 0, appinformer.WithNamespace("argocd"), appinformer.WithTweakListOptions(func(options *metav1.ListOptions) {}))
   172  	appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
   173  	go appInformer.Run(ctx.Done())
   174  	if !cache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
   175  		log.Fatal("Timed out waiting for caches to sync")
   176  	}
   177  	return cancel, factory.Argoproj().V1alpha1().Applications().Lister()
   178  }
   179  
   180  func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
   181  	t.Helper()
   182  	testMetricServer(t, fakeAppYAMLs, expectedResponse, []string{})
   183  }
   184  
   185  type fakeClusterInfo struct {
   186  	clustersInfo []gitopsCache.ClusterInfo
   187  }
   188  
   189  func (f *fakeClusterInfo) GetClustersInfo() []gitopsCache.ClusterInfo {
   190  	return f.clustersInfo
   191  }
   192  
   193  type TestMetricServerConfig struct {
   194  	FakeAppYAMLs     []string
   195  	ExpectedResponse string
   196  	AppLabels        []string
   197  	ClustersInfo     []gitopsCache.ClusterInfo
   198  }
   199  
   200  func testMetricServer(t *testing.T, fakeAppYAMLs []string, expectedResponse string, appLabels []string) {
   201  	t.Helper()
   202  	cfg := TestMetricServerConfig{
   203  		FakeAppYAMLs:     fakeAppYAMLs,
   204  		ExpectedResponse: expectedResponse,
   205  		AppLabels:        appLabels,
   206  		ClustersInfo:     []gitopsCache.ClusterInfo{},
   207  	}
   208  	runTest(t, cfg)
   209  }
   210  
   211  func runTest(t *testing.T, cfg TestMetricServerConfig) {
   212  	t.Helper()
   213  	cancel, appLister := newFakeLister(cfg.FakeAppYAMLs...)
   214  	defer cancel()
   215  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, cfg.AppLabels)
   216  	assert.NoError(t, err)
   217  
   218  	if len(cfg.ClustersInfo) > 0 {
   219  		ci := &fakeClusterInfo{clustersInfo: cfg.ClustersInfo}
   220  		collector := &clusterCollector{
   221  			infoSource: ci,
   222  			info:       ci.GetClustersInfo(),
   223  		}
   224  		metricsServ.registry.MustRegister(collector)
   225  	}
   226  
   227  	req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
   228  	assert.NoError(t, err)
   229  	rr := httptest.NewRecorder()
   230  	metricsServ.Handler.ServeHTTP(rr, req)
   231  	assert.Equal(t, rr.Code, http.StatusOK)
   232  	body := rr.Body.String()
   233  	assertMetricsPrinted(t, cfg.ExpectedResponse, body)
   234  }
   235  
   236  type testCombination struct {
   237  	applications     []string
   238  	responseContains string
   239  }
   240  
   241  func TestMetrics(t *testing.T) {
   242  	combinations := []testCombination{
   243  		{
   244  			applications: []string{fakeApp, fakeApp2, fakeApp3},
   245  			responseContains: `
   246  # HELP argocd_app_info Information about application.
   247  # TYPE argocd_app_info gauge
   248  argocd_app_info{autosync_enabled="true",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Degraded",name="my-app-3",namespace="argocd",operation="delete",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="OutOfSync"} 1
   249  argocd_app_info{autosync_enabled="false",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
   250  argocd_app_info{autosync_enabled="true",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app-2",namespace="argocd",operation="sync",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
   251  `,
   252  		},
   253  		{
   254  			applications: []string{fakeDefaultApp},
   255  			responseContains: `
   256  # HELP argocd_app_info Information about application.
   257  # TYPE argocd_app_info gauge
   258  argocd_app_info{autosync_enabled="false",dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="default",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
   259  `,
   260  		},
   261  	}
   262  
   263  	for _, combination := range combinations {
   264  		testApp(t, combination.applications, combination.responseContains)
   265  	}
   266  }
   267  
   268  func TestMetricLabels(t *testing.T) {
   269  	type testCases struct {
   270  		testCombination
   271  		description  string
   272  		metricLabels []string
   273  	}
   274  	cases := []testCases{
   275  		{
   276  			description:  "will return the labels metrics successfully",
   277  			metricLabels: []string{"team-name", "team-bu", "argoproj.io/cluster"},
   278  			testCombination: testCombination{
   279  				applications: []string{fakeApp, fakeApp2, fakeApp3},
   280  				responseContains: `
   281  # TYPE argocd_app_labels gauge
   282  argocd_app_labels{label_argoproj_io_cluster="test-cluster",label_team_bu="bu-id",label_team_name="my-team",name="my-app",namespace="argocd",project="important-project"} 1
   283  argocd_app_labels{label_argoproj_io_cluster="test-cluster",label_team_bu="bu-id",label_team_name="my-team",name="my-app-2",namespace="argocd",project="important-project"} 1
   284  argocd_app_labels{label_argoproj_io_cluster="test-cluster",label_team_bu="bu-id",label_team_name="my-team",name="my-app-3",namespace="argocd",project="important-project"} 1
   285  `,
   286  			},
   287  		},
   288  		{
   289  			description:  "metric will have empty label value if not present in the application",
   290  			metricLabels: []string{"non-existing"},
   291  			testCombination: testCombination{
   292  				applications: []string{fakeApp, fakeApp2, fakeApp3},
   293  				responseContains: `
   294  # TYPE argocd_app_labels gauge
   295  argocd_app_labels{label_non_existing="",name="my-app",namespace="argocd",project="important-project"} 1
   296  argocd_app_labels{label_non_existing="",name="my-app-2",namespace="argocd",project="important-project"} 1
   297  argocd_app_labels{label_non_existing="",name="my-app-3",namespace="argocd",project="important-project"} 1
   298  `,
   299  			},
   300  		},
   301  	}
   302  
   303  	for _, c := range cases {
   304  		c := c
   305  		t.Run(c.description, func(t *testing.T) {
   306  			testMetricServer(t, c.applications, c.responseContains, c.metricLabels)
   307  		})
   308  	}
   309  }
   310  
   311  func TestLegacyMetrics(t *testing.T) {
   312  	t.Setenv(EnvVarLegacyControllerMetrics, "true")
   313  
   314  	expectedResponse := `
   315  # HELP argocd_app_created_time Creation time in unix timestamp for an application.
   316  # TYPE argocd_app_created_time gauge
   317  argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
   318  # HELP argocd_app_health_status The application current health status.
   319  # TYPE argocd_app_health_status gauge
   320  argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
   321  argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
   322  argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
   323  argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
   324  argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
   325  argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
   326  # HELP argocd_app_sync_status The application current sync status.
   327  # TYPE argocd_app_sync_status gauge
   328  argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
   329  argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
   330  argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
   331  `
   332  	testApp(t, []string{fakeApp}, expectedResponse)
   333  }
   334  
   335  func TestMetricsSyncCounter(t *testing.T) {
   336  	cancel, appLister := newFakeLister()
   337  	defer cancel()
   338  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{})
   339  	assert.NoError(t, err)
   340  
   341  	appSyncTotal := `
   342  # HELP argocd_app_sync_total Number of application syncs.
   343  # TYPE argocd_app_sync_total counter
   344  argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
   345  argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
   346  argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
   347  `
   348  
   349  	fakeApp := newFakeApp(fakeApp)
   350  	metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationRunning})
   351  	metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationFailed})
   352  	metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationError})
   353  	metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationSucceeded})
   354  	metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationSucceeded})
   355  
   356  	req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
   357  	assert.NoError(t, err)
   358  	rr := httptest.NewRecorder()
   359  	metricsServ.Handler.ServeHTTP(rr, req)
   360  	assert.Equal(t, rr.Code, http.StatusOK)
   361  	body := rr.Body.String()
   362  	log.Println(body)
   363  	assertMetricsPrinted(t, appSyncTotal, body)
   364  }
   365  
   366  // assertMetricsPrinted asserts every line in the expected lines appears in the body
   367  func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
   368  	t.Helper()
   369  	for _, line := range strings.Split(expectedLines, "\n") {
   370  		if line == "" {
   371  			continue
   372  		}
   373  		assert.Contains(t, body, line, fmt.Sprintf("expected metrics mismatch for line: %s", line))
   374  	}
   375  }
   376  
   377  // assertMetricNotPrinted
   378  func assertMetricsNotPrinted(t *testing.T, expectedLines, body string) {
   379  	for _, line := range strings.Split(expectedLines, "\n") {
   380  		if line == "" {
   381  			continue
   382  		}
   383  		assert.False(t, strings.Contains(body, expectedLines))
   384  	}
   385  }
   386  
   387  func TestReconcileMetrics(t *testing.T) {
   388  	cancel, appLister := newFakeLister()
   389  	defer cancel()
   390  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{})
   391  	assert.NoError(t, err)
   392  
   393  	appReconcileMetrics := `
   394  # HELP argocd_app_reconcile Application reconciliation performance.
   395  # TYPE argocd_app_reconcile histogram
   396  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.25"} 0
   397  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.5"} 0
   398  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="1"} 0
   399  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="2"} 0
   400  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="4"} 0
   401  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="8"} 1
   402  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="16"} 1
   403  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="+Inf"} 1
   404  argocd_app_reconcile_sum{dest_server="https://localhost:6443",namespace="argocd"} 5
   405  argocd_app_reconcile_count{dest_server="https://localhost:6443",namespace="argocd"} 1
   406  `
   407  	fakeApp := newFakeApp(fakeApp)
   408  	metricsServ.IncReconcile(fakeApp, 5*time.Second)
   409  
   410  	req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
   411  	assert.NoError(t, err)
   412  	rr := httptest.NewRecorder()
   413  	metricsServ.Handler.ServeHTTP(rr, req)
   414  	assert.Equal(t, rr.Code, http.StatusOK)
   415  	body := rr.Body.String()
   416  	log.Println(body)
   417  	assertMetricsPrinted(t, appReconcileMetrics, body)
   418  }
   419  
   420  func TestMetricsReset(t *testing.T) {
   421  	cancel, appLister := newFakeLister()
   422  	defer cancel()
   423  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{})
   424  	assert.NoError(t, err)
   425  
   426  	appSyncTotal := `
   427  # HELP argocd_app_sync_total Number of application syncs.
   428  # TYPE argocd_app_sync_total counter
   429  argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
   430  argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
   431  argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
   432  `
   433  
   434  	req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
   435  	assert.NoError(t, err)
   436  	rr := httptest.NewRecorder()
   437  	metricsServ.Handler.ServeHTTP(rr, req)
   438  	assert.Equal(t, rr.Code, http.StatusOK)
   439  	body := rr.Body.String()
   440  	assertMetricsPrinted(t, appSyncTotal, body)
   441  
   442  	err = metricsServ.SetExpiration(time.Second)
   443  	assert.NoError(t, err)
   444  	time.Sleep(2 * time.Second)
   445  	req, err = http.NewRequest(http.MethodGet, "/metrics", nil)
   446  	assert.NoError(t, err)
   447  	rr = httptest.NewRecorder()
   448  	metricsServ.Handler.ServeHTTP(rr, req)
   449  	assert.Equal(t, rr.Code, http.StatusOK)
   450  	body = rr.Body.String()
   451  	log.Println(body)
   452  	assertMetricsNotPrinted(t, appSyncTotal, body)
   453  	err = metricsServ.SetExpiration(time.Second)
   454  	assert.Error(t, err)
   455  }
   456  
   457  func TestWorkqueueMetrics(t *testing.T) {
   458  	cancel, appLister := newFakeLister()
   459  	defer cancel()
   460  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{})
   461  	assert.NoError(t, err)
   462  
   463  	expectedMetrics := `
   464  # TYPE workqueue_adds_total counter
   465  workqueue_adds_total{name="test"}
   466  
   467  # TYPE workqueue_depth gauge
   468  workqueue_depth{name="test"}
   469  
   470  # TYPE workqueue_longest_running_processor_seconds gauge
   471  workqueue_longest_running_processor_seconds{name="test"}
   472  
   473  # TYPE workqueue_queue_duration_seconds histogram
   474  
   475  # TYPE workqueue_unfinished_work_seconds gauge
   476  workqueue_unfinished_work_seconds{name="test"}
   477  
   478  # TYPE workqueue_work_duration_seconds histogram
   479  `
   480  	workqueue.NewNamed("test")
   481  
   482  	req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
   483  	assert.NoError(t, err)
   484  	rr := httptest.NewRecorder()
   485  	metricsServ.Handler.ServeHTTP(rr, req)
   486  	assert.Equal(t, rr.Code, http.StatusOK)
   487  	body := rr.Body.String()
   488  	log.Println(body)
   489  	assertMetricsPrinted(t, expectedMetrics, body)
   490  }
   491  
   492  func TestGoMetrics(t *testing.T) {
   493  	cancel, appLister := newFakeLister()
   494  	defer cancel()
   495  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{})
   496  	assert.NoError(t, err)
   497  
   498  	expectedMetrics := `
   499  # TYPE go_gc_duration_seconds summary
   500  go_gc_duration_seconds_sum
   501  go_gc_duration_seconds_count
   502  # TYPE go_goroutines gauge
   503  go_goroutines
   504  # TYPE go_info gauge
   505  go_info
   506  # TYPE go_memstats_alloc_bytes gauge
   507  go_memstats_alloc_bytes
   508  # TYPE go_memstats_sys_bytes gauge
   509  go_memstats_sys_bytes
   510  # TYPE go_threads gauge
   511  go_threads
   512  `
   513  
   514  	req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
   515  	assert.NoError(t, err)
   516  	rr := httptest.NewRecorder()
   517  	metricsServ.Handler.ServeHTTP(rr, req)
   518  	assert.Equal(t, rr.Code, http.StatusOK)
   519  	body := rr.Body.String()
   520  	log.Println(body)
   521  	assertMetricsPrinted(t, expectedMetrics, body)
   522  }