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

     1  package metrics
     2  
     3  import (
     4  	"context"
     5  	"log"
     6  	"net/http"
     7  	"net/http/httptest"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/stretchr/testify/mock"
    13  
    14  	"github.com/argoproj/argo-cd/v3/util/db/mocks"
    15  
    16  	gitopsCache "github.com/argoproj/gitops-engine/pkg/cache"
    17  	"github.com/argoproj/gitops-engine/pkg/sync/common"
    18  	"github.com/stretchr/testify/assert"
    19  	"github.com/stretchr/testify/require"
    20  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    21  	"k8s.io/apimachinery/pkg/runtime"
    22  	"k8s.io/client-go/rest"
    23  	"k8s.io/client-go/tools/cache"
    24  	"k8s.io/client-go/util/workqueue"
    25  	"sigs.k8s.io/yaml"
    26  
    27  	argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    28  	appclientset "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
    29  	appinformer "github.com/argoproj/argo-cd/v3/pkg/client/informers/externalversions"
    30  	applister "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
    31  
    32  	"sigs.k8s.io/controller-runtime/pkg/controller"
    33  	"sigs.k8s.io/controller-runtime/pkg/manager"
    34  )
    35  
    36  const fakeApp = `
    37  apiVersion: argoproj.io/v1alpha1
    38  kind: Application
    39  metadata:
    40    name: my-app
    41    namespace: argocd
    42    labels:
    43      team-name: my-team
    44      team-bu: bu-id
    45      argoproj.io/cluster: test-cluster
    46  spec:
    47    destination:
    48      namespace: dummy-namespace
    49      name: cluster1
    50    project: important-project
    51    source:
    52      path: some/path
    53      repoURL: https://github.com/argoproj/argocd-example-apps.git
    54  status:
    55    sync:
    56      status: Synced
    57    health:
    58      status: Healthy
    59  `
    60  
    61  const fakeApp2 = `
    62  apiVersion: argoproj.io/v1alpha1
    63  kind: Application
    64  metadata:
    65    name: my-app-2
    66    namespace: argocd
    67    labels:
    68      team-name: my-team
    69      team-bu: bu-id
    70      argoproj.io/cluster: test-cluster
    71  spec:
    72    destination:
    73      namespace: dummy-namespace
    74      name: cluster1
    75    project: important-project
    76    source:
    77      path: some/path
    78      repoURL: https://github.com/argoproj/argocd-example-apps.git
    79    syncPolicy:
    80      automated:
    81        selfHeal: false
    82        prune: true
    83  status:
    84    sync:
    85      status: Synced
    86    health:
    87      status: Healthy
    88  operation:
    89    sync:
    90      dryRun: true
    91      revision: 041eab7439ece92c99b043f0e171788185b8fc1d
    92      syncStrategy:
    93        hook: {}
    94  `
    95  
    96  const fakeApp3 = `
    97  apiVersion: argoproj.io/v1alpha1
    98  kind: Application
    99  metadata:
   100    name: my-app-3
   101    namespace: argocd
   102    deletionTimestamp: "2020-03-16T09:17:45Z"
   103    labels:
   104      team-name: my-team
   105      team-bu: bu-id
   106      argoproj.io/cluster: test-cluster
   107  spec:
   108    destination:
   109      namespace: dummy-namespace
   110      name: cluster1
   111    project: important-project
   112    source:
   113      path: some/path
   114      repoURL: https://github.com/argoproj/argocd-example-apps.git
   115    syncPolicy:
   116      automated:
   117        selfHeal: true
   118        prune: false
   119  status:
   120    sync:
   121      status: OutOfSync
   122    health:
   123      status: Degraded
   124  `
   125  
   126  const fakeApp4 = `
   127  apiVersion: argoproj.io/v1alpha1
   128  kind: Application
   129  metadata:
   130    name: my-app-4
   131    namespace: argocd
   132    labels:
   133      team-name: my-team
   134      team-bu: bu-id
   135      argoproj.io/cluster: test-cluster
   136  spec:
   137    destination:
   138      namespace: dummy-namespace
   139      name: cluster1
   140    project: important-project
   141    source:
   142      path: some/path
   143      repoURL: https://github.com/argoproj/argocd-example-apps.git
   144  status:
   145    sync:
   146      status: OutOfSync
   147    health:
   148      status: Degraded
   149    conditions:
   150    - lastTransitionTime: "2024-08-07T12:25:40Z"
   151      message: Application has 1 orphaned resources
   152      type: OrphanedResourceWarning
   153    - lastTransitionTime: "2024-08-07T12:25:40Z"
   154      message: Resource Pod standalone-pod is excluded in the settings
   155      type: ExcludedResourceWarning
   156    - lastTransitionTime: "2024-08-07T12:25:40Z"
   157      message: Resource Endpoint raw-endpoint is excluded in the settings
   158      type: ExcludedResourceWarning
   159  `
   160  
   161  const fakeDefaultApp = `
   162  apiVersion: argoproj.io/v1alpha1
   163  kind: Application
   164  metadata:
   165    name: my-app
   166    namespace: argocd
   167  spec:
   168    destination:
   169      namespace: dummy-namespace
   170      name: cluster1
   171    source:
   172      path: some/path
   173      repoURL: https://github.com/argoproj/argocd-example-apps.git
   174  status:
   175    sync:
   176      status: Synced
   177    health:
   178      status: Healthy
   179  `
   180  
   181  const fakeAppOperationRunning = `
   182  apiVersion: argoproj.io/v1alpha1
   183  kind: Application
   184  metadata:
   185    name: my-app
   186    namespace: argocd
   187    labels:
   188      team-name: my-team
   189      team-bu: bu-id
   190      argoproj.io/cluster: test-cluster
   191  spec:
   192    destination:
   193      namespace: dummy-namespace
   194      name: cluster1
   195    project: important-project
   196    source:
   197      path: some/path
   198      repoURL: https://github.com/argoproj/argocd-example-apps.git
   199  status:
   200    sync:
   201      status: OutOfSync
   202    health:
   203      status: Progressing
   204    operationState:
   205      phase: Running
   206      startedAt: "2025-01-29T08:42:34Z"
   207  `
   208  
   209  const fakeAppOperationFinished = `
   210  apiVersion: argoproj.io/v1alpha1
   211  kind: Application
   212  metadata:
   213    name: my-app
   214    namespace: argocd
   215    labels:
   216      team-name: my-team
   217      team-bu: bu-id
   218      argoproj.io/cluster: test-cluster
   219  spec:
   220    destination:
   221      namespace: dummy-namespace
   222      name: cluster1
   223    project: important-project
   224    source:
   225      path: some/path
   226      repoURL: https://github.com/argoproj/argocd-example-apps.git
   227  status:
   228    sync:
   229      status: Synced
   230    health:
   231      status: Healthy
   232    operationState:
   233      phase: Succeeded
   234      startedAt: "2025-01-29T08:42:34Z"
   235      finishedAt: "2025-01-29T08:42:35Z"
   236  `
   237  
   238  var noOpHealthCheck = func(_ *http.Request) error {
   239  	return nil
   240  }
   241  
   242  var appFilter = func(_ any) bool {
   243  	return true
   244  }
   245  
   246  func init() {
   247  	// Create a fake manager
   248  	mgr, err := manager.New(&rest.Config{}, manager.Options{})
   249  	if err != nil {
   250  		panic(err)
   251  	}
   252  	// Create a fake controller so we initialize the internal controller metrics.
   253  	// https://github.com/kubernetes-sigs/controller-runtime/blob/4000e996a202917ad7d40f02ed8a2079a9ce25e9/pkg/internal/controller/metrics/metrics.go
   254  	_, _ = controller.New("test-controller", mgr, controller.Options{})
   255  }
   256  
   257  func newFakeApp(fakeAppYAML string) *argoappv1.Application {
   258  	var app argoappv1.Application
   259  	err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
   260  	if err != nil {
   261  		panic(err)
   262  	}
   263  	return &app
   264  }
   265  
   266  func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.ApplicationLister) {
   267  	ctx, cancel := context.WithCancel(context.Background())
   268  	defer cancel()
   269  	var fakeApps []runtime.Object
   270  	for _, appYAML := range fakeAppYAMLs {
   271  		a := newFakeApp(appYAML)
   272  		fakeApps = append(fakeApps, a)
   273  	}
   274  	appClientset := appclientset.NewSimpleClientset(fakeApps...)
   275  	factory := appinformer.NewSharedInformerFactoryWithOptions(appClientset, 0, appinformer.WithNamespace("argocd"), appinformer.WithTweakListOptions(func(_ *metav1.ListOptions) {}))
   276  	appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
   277  	go appInformer.Run(ctx.Done())
   278  	if !cache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
   279  		log.Fatal("Timed out waiting for caches to sync")
   280  	}
   281  	return cancel, factory.Argoproj().V1alpha1().Applications().Lister()
   282  }
   283  
   284  func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
   285  	t.Helper()
   286  	testMetricServer(t, fakeAppYAMLs, expectedResponse, []string{}, []string{})
   287  }
   288  
   289  type fakeClusterInfo struct {
   290  	clustersInfo []gitopsCache.ClusterInfo
   291  }
   292  
   293  func (f *fakeClusterInfo) GetClustersInfo() []gitopsCache.ClusterInfo {
   294  	return f.clustersInfo
   295  }
   296  
   297  type TestMetricServerConfig struct {
   298  	FakeAppYAMLs     []string
   299  	ExpectedResponse string
   300  	AppLabels        []string
   301  	AppConditions    []string
   302  	ClusterLabels    []string
   303  	ClustersInfo     []gitopsCache.ClusterInfo
   304  	ClusterLister    ClusterLister
   305  }
   306  
   307  func testMetricServer(t *testing.T, fakeAppYAMLs []string, expectedResponse string, appLabels []string, appConditions []string) {
   308  	t.Helper()
   309  	cfg := TestMetricServerConfig{
   310  		FakeAppYAMLs:     fakeAppYAMLs,
   311  		ExpectedResponse: expectedResponse,
   312  		AppLabels:        appLabels,
   313  		AppConditions:    appConditions,
   314  		ClusterLabels:    []string{},
   315  		ClustersInfo:     []gitopsCache.ClusterInfo{},
   316  	}
   317  	runTest(t, cfg)
   318  }
   319  
   320  func runTest(t *testing.T, cfg TestMetricServerConfig) {
   321  	t.Helper()
   322  	cancel, appLister := newFakeLister(cfg.FakeAppYAMLs...)
   323  	defer cancel()
   324  	mockDB := mocks.NewArgoDB(t)
   325  	mockDB.On("GetClusterServersByName", mock.Anything, "cluster1").Return([]string{"https://localhost:6443"}, nil)
   326  	mockDB.On("GetCluster", mock.Anything, "https://localhost:6443").Return(&argoappv1.Cluster{Name: "cluster1", Server: "https://localhost:6443"}, nil)
   327  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, cfg.AppLabels, cfg.AppConditions, mockDB)
   328  	require.NoError(t, err)
   329  
   330  	if len(cfg.ClustersInfo) > 0 {
   331  		ci := &fakeClusterInfo{clustersInfo: cfg.ClustersInfo}
   332  		collector := NewClusterCollector(t.Context(), ci, cfg.ClusterLister, cfg.ClusterLabels)
   333  		metricsServ.registry.MustRegister(collector)
   334  	}
   335  
   336  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   337  	require.NoError(t, err)
   338  	rr := httptest.NewRecorder()
   339  	metricsServ.Handler.ServeHTTP(rr, req)
   340  	assert.Equal(t, http.StatusOK, rr.Code)
   341  	body := rr.Body.String()
   342  	assertMetricsPrinted(t, cfg.ExpectedResponse, body)
   343  }
   344  
   345  type testCombination struct {
   346  	applications     []string
   347  	responseContains string
   348  }
   349  
   350  func TestMetrics(t *testing.T) {
   351  	combinations := []testCombination{
   352  		{
   353  			applications: []string{fakeApp, fakeApp2, fakeApp3},
   354  			responseContains: `
   355  # HELP argocd_app_info Information about application.
   356  # TYPE argocd_app_info gauge
   357  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
   358  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
   359  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
   360  `,
   361  		},
   362  		{
   363  			applications: []string{fakeDefaultApp},
   364  			responseContains: `
   365  # HELP argocd_app_info Information about application.
   366  # TYPE argocd_app_info gauge
   367  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
   368  `,
   369  		},
   370  	}
   371  
   372  	for _, combination := range combinations {
   373  		testApp(t, combination.applications, combination.responseContains)
   374  	}
   375  }
   376  
   377  func TestMetricLabels(t *testing.T) {
   378  	type testCases struct {
   379  		testCombination
   380  		description  string
   381  		metricLabels []string
   382  	}
   383  	cases := []testCases{
   384  		{
   385  			description:  "will return the labels metrics successfully",
   386  			metricLabels: []string{"team-name", "team-bu", "argoproj.io/cluster"},
   387  			testCombination: testCombination{
   388  				applications: []string{fakeApp, fakeApp2, fakeApp3},
   389  				responseContains: `
   390  # TYPE argocd_app_labels gauge
   391  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
   392  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
   393  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
   394  `,
   395  			},
   396  		},
   397  		{
   398  			description:  "metric will have empty label value if not present in the application",
   399  			metricLabels: []string{"non-existing"},
   400  			testCombination: testCombination{
   401  				applications: []string{fakeApp, fakeApp2, fakeApp3},
   402  				responseContains: `
   403  # TYPE argocd_app_labels gauge
   404  argocd_app_labels{label_non_existing="",name="my-app",namespace="argocd",project="important-project"} 1
   405  argocd_app_labels{label_non_existing="",name="my-app-2",namespace="argocd",project="important-project"} 1
   406  argocd_app_labels{label_non_existing="",name="my-app-3",namespace="argocd",project="important-project"} 1
   407  `,
   408  			},
   409  		},
   410  	}
   411  
   412  	for _, c := range cases {
   413  		c := c
   414  		t.Run(c.description, func(t *testing.T) {
   415  			testMetricServer(t, c.applications, c.responseContains, c.metricLabels, []string{})
   416  		})
   417  	}
   418  }
   419  
   420  func TestMetricConditions(t *testing.T) {
   421  	type testCases struct {
   422  		testCombination
   423  		description      string
   424  		metricConditions []string
   425  	}
   426  	cases := []testCases{
   427  		{
   428  			description:      "metric will only output OrphanedResourceWarning",
   429  			metricConditions: []string{"OrphanedResourceWarning"},
   430  			testCombination: testCombination{
   431  				applications: []string{fakeApp4},
   432  				responseContains: `
   433  # HELP argocd_app_condition Report application conditions.
   434  # TYPE argocd_app_condition gauge
   435  argocd_app_condition{condition="OrphanedResourceWarning",name="my-app-4",namespace="argocd",project="important-project"} 1
   436  `,
   437  			},
   438  		},
   439  		{
   440  			description:      "metric will only output ExcludedResourceWarning",
   441  			metricConditions: []string{"ExcludedResourceWarning"},
   442  			testCombination: testCombination{
   443  				applications: []string{fakeApp4},
   444  				responseContains: `
   445  # HELP argocd_app_condition Report application conditions.
   446  # TYPE argocd_app_condition gauge
   447  argocd_app_condition{condition="ExcludedResourceWarning",name="my-app-4",namespace="argocd",project="important-project"} 2
   448  `,
   449  			},
   450  		},
   451  		{
   452  			description:      "metric will only output both OrphanedResourceWarning and ExcludedResourceWarning",
   453  			metricConditions: []string{"ExcludedResourceWarning", "OrphanedResourceWarning"},
   454  			testCombination: testCombination{
   455  				applications: []string{fakeApp4},
   456  				responseContains: `
   457  # HELP argocd_app_condition Report application conditions.
   458  # TYPE argocd_app_condition gauge
   459  argocd_app_condition{condition="OrphanedResourceWarning",name="my-app-4",namespace="argocd",project="important-project"} 1
   460  argocd_app_condition{condition="ExcludedResourceWarning",name="my-app-4",namespace="argocd",project="important-project"} 2
   461  `,
   462  			},
   463  		},
   464  	}
   465  
   466  	for _, c := range cases {
   467  		c := c
   468  		t.Run(c.description, func(t *testing.T) {
   469  			testMetricServer(t, c.applications, c.responseContains, []string{}, c.metricConditions)
   470  		})
   471  	}
   472  }
   473  
   474  func TestMetricsSyncCounter(t *testing.T) {
   475  	cancel, appLister := newFakeLister()
   476  	defer cancel()
   477  	mockDB := mocks.NewArgoDB(t)
   478  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   479  	require.NoError(t, err)
   480  
   481  	appSyncTotal := `
   482  # HELP argocd_app_sync_total Number of application syncs.
   483  # TYPE argocd_app_sync_total counter
   484  argocd_app_sync_total{dest_server="https://localhost:6443",dry_run="false",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
   485  argocd_app_sync_total{dest_server="https://localhost:6443",dry_run="false",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
   486  argocd_app_sync_total{dest_server="https://localhost:6443",dry_run="false",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
   487  `
   488  
   489  	fakeApp := newFakeApp(fakeApp)
   490  	metricsServ.IncSync(fakeApp, "https://localhost:6443", &argoappv1.OperationState{Phase: common.OperationRunning})
   491  	metricsServ.IncSync(fakeApp, "https://localhost:6443", &argoappv1.OperationState{Phase: common.OperationFailed})
   492  	metricsServ.IncSync(fakeApp, "https://localhost:6443", &argoappv1.OperationState{Phase: common.OperationError})
   493  	metricsServ.IncSync(fakeApp, "https://localhost:6443", &argoappv1.OperationState{Phase: common.OperationSucceeded})
   494  	metricsServ.IncSync(fakeApp, "https://localhost:6443", &argoappv1.OperationState{Phase: common.OperationSucceeded})
   495  
   496  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   497  	require.NoError(t, err)
   498  	rr := httptest.NewRecorder()
   499  	metricsServ.Handler.ServeHTTP(rr, req)
   500  	assert.Equal(t, http.StatusOK, rr.Code)
   501  	body := rr.Body.String()
   502  	log.Println(body)
   503  	assertMetricsPrinted(t, appSyncTotal, body)
   504  }
   505  
   506  // assertMetricsPrinted asserts every line in the expected lines appears in the body
   507  func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
   508  	t.Helper()
   509  	for _, line := range strings.Split(expectedLines, "\n") {
   510  		if line == "" {
   511  			continue
   512  		}
   513  		assert.Contains(t, body, line, "expected metrics mismatch for line: %s", line)
   514  	}
   515  }
   516  
   517  // assertMetricsNotPrinted
   518  func assertMetricsNotPrinted(t *testing.T, expectedLines, body string) {
   519  	t.Helper()
   520  	for _, line := range strings.Split(expectedLines, "\n") {
   521  		if line == "" {
   522  			continue
   523  		}
   524  		assert.NotContains(t, body, expectedLines)
   525  	}
   526  }
   527  
   528  func TestMetricsSyncDuration(t *testing.T) {
   529  	cancel, appLister := newFakeLister()
   530  	defer cancel()
   531  	mockDB := mocks.NewArgoDB(t)
   532  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   533  	require.NoError(t, err)
   534  
   535  	t.Run("metric is not generated during Operation Running.", func(t *testing.T) {
   536  		fakeAppOperationRunning := newFakeApp(fakeAppOperationRunning)
   537  		metricsServ.IncAppSyncDuration(fakeAppOperationRunning, "https://localhost:6443", fakeAppOperationRunning.Status.OperationState)
   538  
   539  		req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   540  		require.NoError(t, err)
   541  		rr := httptest.NewRecorder()
   542  		metricsServ.Handler.ServeHTTP(rr, req)
   543  		assert.Equal(t, http.StatusOK, rr.Code)
   544  		body := rr.Body.String()
   545  		log.Println(body)
   546  		assertMetricsNotPrinted(t, "argocd_app_sync_duration_seconds_total", body)
   547  	})
   548  
   549  	t.Run("metric is created when Operation Finished.", func(t *testing.T) {
   550  		fakeAppOperationFinished := newFakeApp(fakeAppOperationFinished)
   551  		metricsServ.IncAppSyncDuration(fakeAppOperationFinished, "https://localhost:6443", fakeAppOperationFinished.Status.OperationState)
   552  
   553  		req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   554  		require.NoError(t, err)
   555  		rr := httptest.NewRecorder()
   556  		metricsServ.Handler.ServeHTTP(rr, req)
   557  		assert.Equal(t, http.StatusOK, rr.Code)
   558  		body := rr.Body.String()
   559  		appSyncDurationTotal := `
   560  # HELP argocd_app_sync_duration_seconds_total Application sync performance in seconds total.
   561  # TYPE argocd_app_sync_duration_seconds_total counter
   562  argocd_app_sync_duration_seconds_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project"} 1
   563  `
   564  		log.Println(body)
   565  		assertMetricsPrinted(t, appSyncDurationTotal, body)
   566  	})
   567  }
   568  
   569  func TestReconcileMetrics(t *testing.T) {
   570  	cancel, appLister := newFakeLister()
   571  	defer cancel()
   572  	mockDB := mocks.NewArgoDB(t)
   573  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   574  	require.NoError(t, err)
   575  
   576  	appReconcileMetrics := `
   577  # HELP argocd_app_reconcile Application reconciliation performance in seconds.
   578  # TYPE argocd_app_reconcile histogram
   579  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.25"} 0
   580  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.5"} 0
   581  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="1"} 0
   582  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="2"} 0
   583  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="4"} 0
   584  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="8"} 1
   585  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="16"} 1
   586  argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="+Inf"} 1
   587  argocd_app_reconcile_sum{dest_server="https://localhost:6443",namespace="argocd"} 5
   588  argocd_app_reconcile_count{dest_server="https://localhost:6443",namespace="argocd"} 1
   589  `
   590  	fakeApp := newFakeApp(fakeApp)
   591  	metricsServ.IncReconcile(fakeApp, "https://localhost:6443", 5*time.Second)
   592  
   593  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   594  	require.NoError(t, err)
   595  	rr := httptest.NewRecorder()
   596  	metricsServ.Handler.ServeHTTP(rr, req)
   597  	assert.Equal(t, http.StatusOK, rr.Code)
   598  	body := rr.Body.String()
   599  	log.Println(body)
   600  	assertMetricsPrinted(t, appReconcileMetrics, body)
   601  }
   602  
   603  func TestOrphanedResourcesMetric(t *testing.T) {
   604  	cancel, appLister := newFakeLister()
   605  	defer cancel()
   606  	mockDB := mocks.NewArgoDB(t)
   607  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   608  	require.NoError(t, err)
   609  
   610  	expectedMetrics := `
   611  # HELP argocd_app_orphaned_resources_count Number of orphaned resources per application
   612  # TYPE argocd_app_orphaned_resources_count gauge
   613  argocd_app_orphaned_resources_count{name="my-app-4",namespace="argocd",project="important-project"} 1
   614  `
   615  	app := newFakeApp(fakeApp4)
   616  	numOrphanedResources := 1
   617  	metricsServ.SetOrphanedResourcesMetric(app, numOrphanedResources)
   618  
   619  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   620  	require.NoError(t, err)
   621  	rr := httptest.NewRecorder()
   622  	metricsServ.Handler.ServeHTTP(rr, req)
   623  	assert.Equal(t, http.StatusOK, rr.Code)
   624  	body := rr.Body.String()
   625  	log.Println(body)
   626  	assertMetricsPrinted(t, expectedMetrics, body)
   627  }
   628  
   629  func TestMetricsReset(t *testing.T) {
   630  	cancel, appLister := newFakeLister()
   631  	defer cancel()
   632  	mockDB := mocks.NewArgoDB(t)
   633  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   634  	require.NoError(t, err)
   635  
   636  	appSyncTotal := `
   637  # HELP argocd_app_sync_total Number of application syncs.
   638  # TYPE argocd_app_sync_total counter
   639  argocd_app_sync_total{dest_server="https://localhost:6443",dry_run="false",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
   640  argocd_app_sync_total{dest_server="https://localhost:6443",dry_run="false",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
   641  argocd_app_sync_total{dest_server="https://localhost:6443",dry_run="false",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
   642  `
   643  
   644  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   645  	require.NoError(t, err)
   646  	rr := httptest.NewRecorder()
   647  	metricsServ.Handler.ServeHTTP(rr, req)
   648  	assert.Equal(t, http.StatusOK, rr.Code)
   649  	body := rr.Body.String()
   650  	assertMetricsPrinted(t, appSyncTotal, body)
   651  
   652  	err = metricsServ.SetExpiration(time.Second)
   653  	require.NoError(t, err)
   654  	time.Sleep(2 * time.Second)
   655  	req, err = http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   656  	require.NoError(t, err)
   657  	rr = httptest.NewRecorder()
   658  	metricsServ.Handler.ServeHTTP(rr, req)
   659  	assert.Equal(t, http.StatusOK, rr.Code)
   660  	body = rr.Body.String()
   661  	log.Println(body)
   662  	assertMetricsNotPrinted(t, appSyncTotal, body)
   663  	err = metricsServ.SetExpiration(time.Second)
   664  	require.Error(t, err)
   665  }
   666  
   667  func TestWorkqueueMetrics(t *testing.T) {
   668  	cancel, appLister := newFakeLister()
   669  	defer cancel()
   670  	mockDB := mocks.NewArgoDB(t)
   671  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   672  	require.NoError(t, err)
   673  
   674  	expectedMetrics := `
   675  # TYPE workqueue_adds_total counter
   676  workqueue_adds_total{controller="test",name="test"}
   677  # TYPE workqueue_depth gauge
   678  workqueue_depth{controller="test",name="test",priority=""}
   679  # TYPE workqueue_longest_running_processor_seconds gauge
   680  workqueue_longest_running_processor_seconds{controller="test",name="test"}
   681  # TYPE workqueue_queue_duration_seconds histogram
   682  # TYPE workqueue_unfinished_work_seconds gauge
   683  workqueue_unfinished_work_seconds{controller="test",name="test"}
   684  # TYPE workqueue_work_duration_seconds histogram
   685  `
   686  	workqueue.NewNamed("test")
   687  
   688  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   689  	require.NoError(t, err)
   690  	rr := httptest.NewRecorder()
   691  	metricsServ.Handler.ServeHTTP(rr, req)
   692  	assert.Equal(t, http.StatusOK, rr.Code)
   693  	body := rr.Body.String()
   694  	log.Println(body)
   695  	assertMetricsPrinted(t, expectedMetrics, body)
   696  }
   697  
   698  func TestGoMetrics(t *testing.T) {
   699  	cancel, appLister := newFakeLister()
   700  	defer cancel()
   701  	mockDB := mocks.NewArgoDB(t)
   702  	metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}, []string{}, mockDB)
   703  	require.NoError(t, err)
   704  
   705  	expectedMetrics := `
   706  # TYPE go_gc_duration_seconds summary
   707  go_gc_duration_seconds_sum
   708  go_gc_duration_seconds_count
   709  # TYPE go_goroutines gauge
   710  go_goroutines
   711  # TYPE go_info gauge
   712  go_info
   713  # TYPE go_memstats_alloc_bytes gauge
   714  go_memstats_alloc_bytes
   715  # TYPE go_memstats_sys_bytes gauge
   716  go_memstats_sys_bytes
   717  # TYPE go_threads gauge
   718  go_threads
   719  `
   720  
   721  	req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
   722  	require.NoError(t, err)
   723  	rr := httptest.NewRecorder()
   724  	metricsServ.Handler.ServeHTTP(rr, req)
   725  	assert.Equal(t, http.StatusOK, rr.Code)
   726  	body := rr.Body.String()
   727  	log.Println(body)
   728  	assertMetricsPrinted(t, expectedMetrics, body)
   729  }