agones.dev/agones@v1.54.0/pkg/gameserverallocations/metrics_test.go (about)

     1  // Copyright 2022 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gameserverallocations
    16  
    17  import (
    18  	"bufio"
    19  	"context"
    20  	"net/http"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	agonesv1 "agones.dev/agones/pkg/apis/agones/v1"
    26  	allocationv1 "agones.dev/agones/pkg/apis/allocation/v1"
    27  	gameserverv1 "agones.dev/agones/pkg/client/listers/agones/v1"
    28  	mt "agones.dev/agones/pkg/metrics"
    29  	agtesting "agones.dev/agones/pkg/testing"
    30  	"agones.dev/agones/pkg/util/httpserver"
    31  	"agones.dev/agones/pkg/util/runtime"
    32  	"agones.dev/agones/test/e2e/framework"
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  	"go.opencensus.io/stats/view"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	"k8s.io/apimachinery/pkg/labels"
    38  	k8sruntime "k8s.io/apimachinery/pkg/runtime"
    39  	"k8s.io/apimachinery/pkg/util/wait"
    40  	"k8s.io/apimachinery/pkg/watch"
    41  	k8stesting "k8s.io/client-go/testing"
    42  )
    43  
    44  type mockGameServerLister struct {
    45  	gameServerNamespaceLister mockGameServerNamespaceLister
    46  	gameServersCalled         bool
    47  }
    48  
    49  type mockGameServerNamespaceLister struct {
    50  	gameServer *agonesv1.GameServer
    51  }
    52  
    53  func (s *mockGameServerLister) List(_ labels.Selector) (ret []*agonesv1.GameServer, err error) {
    54  	return ret, nil
    55  }
    56  
    57  func (s *mockGameServerLister) GameServers(_ string) gameserverv1.GameServerNamespaceLister {
    58  	s.gameServersCalled = true
    59  	return s.gameServerNamespaceLister
    60  }
    61  
    62  func (s mockGameServerNamespaceLister) Get(_ string) (*agonesv1.GameServer, error) {
    63  	return s.gameServer, nil
    64  }
    65  
    66  func (s mockGameServerNamespaceLister) List(_ labels.Selector) (ret []*agonesv1.GameServer, err error) {
    67  	return ret, nil
    68  }
    69  
    70  func resetMetrics() {
    71  	unRegisterViews()
    72  	registerViews()
    73  }
    74  
    75  func TestSetResponse(t *testing.T) {
    76  	subtests := []struct {
    77  		name           string
    78  		gameServer     *agonesv1.GameServer
    79  		err            error
    80  		allocation     *allocationv1.GameServerAllocation
    81  		expectedState  allocationv1.GameServerAllocationState
    82  		expectedCalled bool
    83  	}{
    84  		{
    85  			name: "Try to get gs from local cluster for local allocation",
    86  			gameServer: &agonesv1.GameServer{
    87  				ObjectMeta: metav1.ObjectMeta{
    88  					Labels: map[string]string{agonesv1.FleetNameLabel: "fleetName"},
    89  				},
    90  			},
    91  			allocation: &allocationv1.GameServerAllocation{
    92  				Status: allocationv1.GameServerAllocationStatus{
    93  					State:          allocationv1.GameServerAllocationAllocated,
    94  					GameServerName: "gameServerName",
    95  					Source:         "local",
    96  				},
    97  			},
    98  			expectedCalled: true,
    99  		},
   100  		{
   101  			name: "Do not try to get gs from local cluster for remote allocation",
   102  			gameServer: &agonesv1.GameServer{
   103  				ObjectMeta: metav1.ObjectMeta{
   104  					Labels: map[string]string{agonesv1.FleetNameLabel: "fleetName"},
   105  				},
   106  			},
   107  			allocation: &allocationv1.GameServerAllocation{
   108  				Status: allocationv1.GameServerAllocationStatus{
   109  					State:          allocationv1.GameServerAllocationAllocated,
   110  					GameServerName: "gameServerName",
   111  					Source:         "33.188.237.156:443",
   112  				},
   113  			},
   114  			expectedCalled: false,
   115  		},
   116  	}
   117  
   118  	for _, subtest := range subtests {
   119  		gsl := mockGameServerLister{
   120  			gameServerNamespaceLister: mockGameServerNamespaceLister{
   121  				gameServer: subtest.gameServer,
   122  			},
   123  		}
   124  
   125  		metrics := metrics{
   126  			ctx:              context.Background(),
   127  			gameServerLister: &gsl,
   128  			logger:           runtime.NewLoggerWithSource("metrics_test"),
   129  			start:            time.Now(),
   130  		}
   131  
   132  		t.Run(subtest.name, func(t *testing.T) {
   133  			metrics.setResponse(subtest.allocation)
   134  			assert.Equal(t, subtest.expectedCalled, gsl.gameServersCalled)
   135  		})
   136  	}
   137  }
   138  
   139  func TestAllocationMetrics(t *testing.T) {
   140  	resetMetrics()
   141  
   142  	runtime.FeatureTestMutex.Lock()
   143  	defer runtime.FeatureTestMutex.Unlock()
   144  
   145  	conf := mt.Config{
   146  		PrometheusMetrics: true,
   147  	}
   148  	server := &httpserver.Server{
   149  		Port:   "3001",
   150  		Logger: framework.TestLogger(t),
   151  	}
   152  
   153  	health, closer := mt.SetupMetrics(conf, server)
   154  	defer t.Cleanup(closer)
   155  
   156  	assert.NotNil(t, health, "Health check handler should not be nil")
   157  	server.Handle("/", health)
   158  
   159  	f, gsList := defaultFixtures(1)
   160  	a, m := newFakeAllocator()
   161  
   162  	m.AgonesClient.AddReactor("list", "gameservers", func(_ k8stesting.Action) (bool, k8sruntime.Object, error) {
   163  		return true, &agonesv1.GameServerList{Items: gsList}, nil
   164  	})
   165  
   166  	gsWatch := watch.NewFake()
   167  	m.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil))
   168  	m.AgonesClient.AddReactor("update", "gameservers", func(action k8stesting.Action) (bool, k8sruntime.Object, error) {
   169  		ua := action.(k8stesting.UpdateAction)
   170  		gs := ua.GetObject().(*agonesv1.GameServer)
   171  		assert.Equal(t, agonesv1.GameServerStateAllocated, gs.Status.State)
   172  		gsWatch.Modify(gs)
   173  
   174  		return true, gs, nil
   175  	})
   176  
   177  	ctxAlloc, cancelAlloc := agtesting.StartInformers(m, a.allocationCache.gameServerSynced)
   178  	defer cancelAlloc()
   179  
   180  	require.NoError(t, a.Run(ctxAlloc))
   181  	// wait for it to be up and running
   182  	err := wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, true, func(_ context.Context) (done bool, err error) {
   183  		return a.allocationCache.workerqueue.RunCount() == 1, nil
   184  	})
   185  	require.NoError(t, err)
   186  
   187  	gsa := allocationv1.GameServerAllocation{ObjectMeta: metav1.ObjectMeta{Name: "gsa-1", Namespace: defaultNs},
   188  		Spec: allocationv1.GameServerAllocationSpec{
   189  			Selectors: []allocationv1.GameServerSelector{{LabelSelector: metav1.LabelSelector{MatchLabels: map[string]string{agonesv1.FleetNameLabel: f.ObjectMeta.Name}}}},
   190  		}}
   191  	gsa.ApplyDefaults()
   192  	errs := gsa.Validate()
   193  	require.Len(t, errs, 0)
   194  
   195  	result, err := a.Allocate(ctxAlloc, &gsa)
   196  	require.NoError(t, err)
   197  	require.NotNil(t, result)
   198  
   199  	ctxHTTP, cancelHTTP := context.WithCancel(context.Background())
   200  	defer cancelHTTP()
   201  
   202  	// Start the HTTP server
   203  	go func() {
   204  		_ = server.Run(ctxHTTP, 0)
   205  	}()
   206  	time.Sleep(300 * time.Millisecond)
   207  
   208  	resp, err := http.Get("http://localhost:3001/metrics")
   209  	require.NoError(t, err, "Failed to GET metrics endpoint")
   210  	defer func() {
   211  		assert.NoError(t, resp.Body.Close())
   212  	}()
   213  
   214  	assert.Equal(t, http.StatusOK, resp.StatusCode, "Expected status code 200")
   215  
   216  	metricsSet := collectMetricNames(resp)
   217  	expectedMetrics := getMetricNames()
   218  
   219  	for _, metric := range expectedMetrics {
   220  		assert.Contains(t, metricsSet, metric, "Missing expected metric: %s", metric)
   221  	}
   222  }
   223  
   224  // getMetricNames returns all metric view names.
   225  func getMetricNames() []string {
   226  	var metricNames []string
   227  	for _, v := range stateViews {
   228  		metricName := "agones_" + v.Name
   229  
   230  		// Check if the aggregation type is Distribution
   231  		if v.Aggregation.Type == view.AggTypeDistribution {
   232  			// If it's a distribution, we append _bucket, _sum, and _count
   233  			metricNames = append(metricNames,
   234  				metricName+"_bucket",
   235  				metricName+"_sum",
   236  				metricName+"_count",
   237  			)
   238  		} else {
   239  			metricNames = append(metricNames, metricName)
   240  
   241  		}
   242  	}
   243  	return metricNames
   244  }
   245  
   246  func collectMetricNames(resp *http.Response) map[string]bool {
   247  	metrics := make(map[string]bool)
   248  	scanner := bufio.NewScanner(resp.Body)
   249  	for scanner.Scan() {
   250  		line := scanner.Text()
   251  		if strings.HasPrefix(line, "#") || line == "" {
   252  			continue
   253  		}
   254  		fields := strings.Fields(line)
   255  		if len(fields) > 0 {
   256  			// Extract only the metric name, excluding labels
   257  			metricName := fields[0]
   258  			if idx := strings.Index(metricName, "{"); idx != -1 {
   259  				metricName = metricName[:idx]
   260  			}
   261  			metrics[metricName] = true
   262  		}
   263  	}
   264  	return metrics
   265  }