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 }