github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/monitor/loglevel_test.go (about)

     1  // Copyright (c) 2022 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  package monitor
     6  
     7  import (
     8  	"io"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/aristanetworks/glog"
    16  )
    17  
    18  func req(method string, params ...string) *http.Request {
    19  	req := httptest.NewRequest(method, "/whatever", nil)
    20  	q := req.URL.Query()
    21  	for i := 0; i < len(params); i += 2 {
    22  		q.Add(params[i], params[i+1])
    23  	}
    24  	req.URL.RawQuery = q.Encode()
    25  	return req
    26  }
    27  
    28  func call(t *testing.T, srv *logsetSrv, req *http.Request) *http.Response {
    29  	t.Helper()
    30  	w := httptest.NewRecorder()
    31  	srv.ServeHTTP(w, req)
    32  	resp := w.Result()
    33  	body, _ := io.ReadAll(resp.Body)
    34  	t.Logf("req = %#v, resp = %q", req, string(body))
    35  	return resp
    36  }
    37  
    38  func hasUpdate[T comparable](t *testing.T, req loglevelReq, key string, want *T) {
    39  	t.Helper()
    40  	got, ok := req.updates[key].(T)
    41  	if !ok {
    42  		if want == nil {
    43  			return
    44  		}
    45  		t.Fatalf("hasUpdate(%q): got updates %#v, should contain %#v", key, req.updates, *want)
    46  	}
    47  	if want == nil {
    48  		t.Fatalf("hasUpdate(%q): got updates %#v, should contain %#v", key, req.updates, want)
    49  	}
    50  	if got != *want {
    51  		t.Fatalf("hasUpdate(%q): got updates %#v, should contain %#v", key, req.updates, *want)
    52  	}
    53  }
    54  
    55  func TestRequestParsing(t *testing.T) {
    56  	tcases := map[string]struct {
    57  		req         *http.Request
    58  		wantErr     string
    59  		wantGlog    *glogUpdater
    60  		wantVModule *vModuleUpdater
    61  	}{
    62  		"GET": {
    63  			req:     req("GET"),
    64  			wantErr: "method must be POST",
    65  		},
    66  		"empty POST": {
    67  			req:     req("POST"),
    68  			wantErr: "empty request",
    69  		},
    70  		"only timeout": {
    71  			req:     req("POST", "timeout", "5m"),
    72  			wantErr: "empty request",
    73  		},
    74  		"error small": {
    75  			req:     req("POST", "timeout", ".1s"),
    76  			wantErr: "timeout too small",
    77  		},
    78  		"error large": {
    79  			req:     req("POST", "timeout", "24h1s"),
    80  			wantErr: "timeout too large",
    81  		},
    82  
    83  		// glog parsing
    84  		"invalid glog": {
    85  			req:     req("POST", glogV, "??"),
    86  			wantErr: "invalid glog argument",
    87  		},
    88  		"negative glog": {
    89  			req:     req("POST", glogV, "-1"),
    90  			wantErr: "invalid glog argument",
    91  		},
    92  		"glog": {
    93  			req:      req("POST", glogV, "0"),
    94  			wantGlog: &glogUpdater{v: 0},
    95  		},
    96  		"glog with timeout": {
    97  			req:      req("POST", glogV, "1", "timeout", "10s"),
    98  			wantGlog: &glogUpdater{v: 1},
    99  		},
   100  		"glog with 24h timeout": {
   101  			req:      req("POST", glogV, "2", "timeout", "24h"),
   102  			wantGlog: &glogUpdater{v: 2},
   103  		},
   104  		"glog with 1s timeout": {
   105  			req:      req("POST", glogV, "3", "timeout", "1s"),
   106  			wantGlog: &glogUpdater{v: 3},
   107  		},
   108  
   109  		// vmodule parsing
   110  		"invalid vmodule": {
   111  			req:     req("POST", glogVModule, "not valid"),
   112  			wantErr: "invalid glog-vmodule argument",
   113  		},
   114  		"invalid vmodule 2": {
   115  			req:     req("POST", glogVModule, "x="),
   116  			wantErr: "invalid glog-vmodule argument",
   117  		},
   118  		"invalid vmodule 3": {
   119  			req:     req("POST", glogVModule, "x=09,asdf"),
   120  			wantErr: "invalid glog-vmodule argument",
   121  		},
   122  		"valid vmodule": {
   123  			req:         req("POST", glogVModule, "x=09,y=100,x/y/z=0"),
   124  			wantVModule: &vModuleUpdater{v: "x=09,y=100,x/y/z=0"},
   125  		},
   126  		"valid vmodule that should not work": {
   127  			req:         req("POST", glogVModule, "invalid:;path:-_=10"),
   128  			wantVModule: &vModuleUpdater{v: "invalid:;path:-_=10"},
   129  		},
   130  	}
   131  
   132  	for name, tcase := range tcases {
   133  		t.Run(name, func(t *testing.T) {
   134  			req, err := parseLoglevelReq(tcase.req)
   135  			if tcase.wantErr != "" && err == nil {
   136  				t.Fatalf("expected error %v: got nil", tcase.wantErr)
   137  			} else if err != nil && !strings.Contains(err.Error(), tcase.wantErr) {
   138  				t.Fatalf("expected error to contain %q: got %q", tcase.wantErr, err.Error())
   139  			} else if tcase.wantErr == "" && err != nil {
   140  				t.Fatalf("unexpected error: %v", err)
   141  			}
   142  			hasUpdate(t, req, glogV, tcase.wantGlog)
   143  			hasUpdate(t, req, glogVModule, tcase.wantVModule)
   144  
   145  		})
   146  	}
   147  }
   148  
   149  func TestGlogLogset(t *testing.T) {
   150  	t.Run("updater", func(t *testing.T) {
   151  		defer glog.SetVGlobal(glog.SetVGlobal(42)) // init and reset
   152  		updater := glogUpdater{v: glog.Level(100)}
   153  		resetter, err := updater.Apply()
   154  		if err != nil {
   155  			t.Fatalf("error applying update: %v", err)
   156  		}
   157  		if got := glog.VGlobal(); got != 100 {
   158  			t.Fatalf("glog verbosity should be 100, got %#v", got)
   159  		}
   160  		resetter()
   161  		if got := glog.VGlobal(); got != 42 {
   162  			t.Fatalf("glog verbosity should be 42, got %#v", got)
   163  		}
   164  
   165  	})
   166  
   167  	t.Run("request", func(t *testing.T) {
   168  		defer glog.SetVGlobal(glog.SetVGlobal(0)) // init and reset
   169  		ls := newLogsetSrv()
   170  		resp := call(t, ls, req("POST", glogV, "1"))
   171  		if resp.StatusCode != 200 {
   172  			t.Fatalf("expected status 200, wanted %v", resp.StatusCode)
   173  		}
   174  		if v := glog.VGlobal(); v != 1 {
   175  			t.Fatalf("expected glog %v, got %v", v, 1)
   176  		}
   177  	})
   178  }
   179  
   180  func TestVModuleSet(t *testing.T) {
   181  	t.Run("updater", func(t *testing.T) {
   182  		// reset vmodule at end of test
   183  		prev, err := glog.SetVModule("prev=99")
   184  		if err != nil {
   185  			t.Fatalf("vmodule call failed: %v", err)
   186  		}
   187  		defer glog.SetVModule(prev)
   188  		updater := vModuleUpdater{v: "next=100"}
   189  		resetter, err := updater.Apply()
   190  		if err != nil {
   191  			t.Fatalf("error applying vmodule update: %v", err)
   192  		}
   193  		if got := glog.VModule(); got != "next=100" {
   194  			t.Fatalf("vmodule should be 'next=100', got %#v", got)
   195  		}
   196  		resetter()
   197  		if got := glog.VModule(); got != "prev=99" {
   198  			t.Fatalf("vmodule should be 'prev=99', got %#v", got)
   199  		}
   200  
   201  	})
   202  	t.Run("request", func(t *testing.T) {
   203  		// reset vmodule at end of test
   204  		prev, err := glog.SetVModule("")
   205  		if err != nil {
   206  			t.Fatalf("vmodule call failed: %v", err)
   207  		}
   208  		defer glog.SetVModule(prev)
   209  		// make request
   210  		ls := newLogsetSrv()
   211  		resp := call(t, ls, req("POST", glogVModule, "y=001"))
   212  		if resp.StatusCode != 200 {
   213  			t.Fatalf("expected status 200, wanted %v", resp.StatusCode)
   214  		}
   215  		if v := glog.VModule(); v != "y=1" {
   216  			t.Fatalf("expected vmodule x=001, got %q", v)
   217  		}
   218  	})
   219  }
   220  
   221  type mockedRequest struct {
   222  	timerCreated   chan time.Duration // recevies the time Duration when timer is created
   223  	timerTrigger   chan time.Time     // channel that you can send a time to trigger timer
   224  	timerCancelled chan struct{}      // will be closed if timer is cancelled
   225  	logApplied     chan struct{}      // request has been applied when this channel is closed
   226  	logReset       chan struct{}      // request has been reset when this channeel is closed
   227  }
   228  
   229  // Apply implements mockLogUpdater
   230  func (m mockedRequest) Apply() (func(), error) {
   231  	close(m.logApplied)
   232  	return func() {
   233  		close(m.logReset)
   234  	}, nil
   235  }
   236  
   237  type mockTimerImpl struct {
   238  	c chan time.Time
   239  }
   240  
   241  func (m *mockTimerImpl) C() <-chan time.Time {
   242  	return m.c
   243  }
   244  
   245  func (m *mockTimerImpl) Stop() bool {
   246  	return true // returning true means we won't try and drain the channel; thats okay
   247  }
   248  
   249  func newMockedRequest(t *testing.T, ls *logsetSrv, opts ...string) mockedRequest {
   250  	m := mockedRequest{
   251  		timerCreated: make(chan time.Duration, 1),
   252  		timerTrigger: make(chan time.Time),
   253  		logApplied:   make(chan struct{}),
   254  		logReset:     make(chan struct{}),
   255  	}
   256  	// setup dependency injected timer
   257  	newTimer := func(d time.Duration) timer {
   258  		m.timerCreated <- d
   259  		return &mockTimerImpl{c: m.timerTrigger}
   260  	}
   261  
   262  	ls.mu.Lock()
   263  	ls.timer = newTimer // has to be set on each new request
   264  	ls.mu.Unlock()
   265  
   266  	args := []string{glogV, "1"}
   267  	args = append(args, opts...)
   268  	req := req("POST", args...)
   269  	request, err := parseLoglevelReq(req)
   270  	if err != nil {
   271  		t.Fatalf("could not create glog request: %v", err)
   272  	}
   273  	request.updates[glogV] = m
   274  
   275  	t.Log("send logrequest", opts)
   276  	if err := ls.handle(request); err != nil {
   277  		t.Fatalf("unexpected error: %v", err)
   278  	}
   279  
   280  	ls.mu.Lock()
   281  	if v, exists := ls.resetTo[glogV]; exists {
   282  		m.timerCancelled = v.cancel
   283  	}
   284  	ls.mu.Unlock()
   285  	return m
   286  }
   287  
   288  func TestResetBehavior(t *testing.T) {
   289  	t.Run("reset is called", func(t *testing.T) {
   290  		ls := newLogsetSrv()
   291  		req := newMockedRequest(t, ls, "timeout", "1s")
   292  		<-req.logApplied
   293  		if timerD := <-req.timerCreated; timerD != time.Second {
   294  			t.Fatalf("expected timer to be set for %v, got %v", time.Second, timerD)
   295  		}
   296  
   297  		req.timerTrigger <- time.Time{}
   298  		<-req.logReset
   299  
   300  		t.Log("wait until all ongoing resets are done")
   301  		ls.wg.Wait()
   302  	})
   303  
   304  	t.Run("updating loglevel twice with no overlapping timeout works", func(t *testing.T) {
   305  		ls := newLogsetSrv()
   306  
   307  		req1 := newMockedRequest(t, ls, "timeout", "33s")
   308  		<-req1.logApplied
   309  		if timerD := <-req1.timerCreated; timerD != time.Second*33 {
   310  			t.Fatalf("expected timer to be set for %v, got %v", time.Second*33, timerD)
   311  		}
   312  
   313  		req1.timerTrigger <- time.Time{}
   314  		t.Log("request 1 should be reset after timer is triggered")
   315  		<-req1.logReset
   316  
   317  		req2 := newMockedRequest(t, ls, "timeout", "45s")
   318  		<-req2.logApplied
   319  		if timerD := <-req2.timerCreated; timerD != 45*time.Second {
   320  			t.Fatalf("expected timer to be set for %v, got %v", 45*time.Second, timerD)
   321  		}
   322  		req2.timerTrigger <- time.Time{}
   323  		t.Log("request 2 should be reset after timer is triggered")
   324  		<-req2.logReset
   325  
   326  		t.Log("wait until all ongoing resets are done")
   327  		ls.wg.Wait()
   328  	})
   329  
   330  	t.Run("updating loglevel with overlapping timeout behaves correctly", func(t *testing.T) {
   331  		ls := newLogsetSrv()
   332  
   333  		req1 := newMockedRequest(t, ls, "timeout", "10s")
   334  		<-req1.logApplied
   335  		if timerD := <-req1.timerCreated; timerD != time.Second*10 {
   336  			t.Fatalf("expected timer to be set for %v, got %v", time.Second*10, timerD)
   337  		}
   338  
   339  		req2 := newMockedRequest(t, ls, "timeout", "100s")
   340  		<-req2.logApplied
   341  		if timerD := <-req2.timerCreated; timerD != 100*time.Second {
   342  			t.Fatalf("expected timer to be set for %v, got %v", 100*time.Second, timerD)
   343  		}
   344  
   345  		t.Log("timer1 should be cancelled")
   346  		<-req1.timerCancelled
   347  		t.Log("timer should be triggered")
   348  		select {
   349  		case req1.timerTrigger <- time.Time{}:
   350  			t.Fatal("first request timer goroutine should not be running after cancellation")
   351  		default:
   352  		}
   353  
   354  		t.Log("triggering timer 2 should call the original reset function")
   355  		req2.timerTrigger <- time.Time{}
   356  		<-req1.logReset
   357  
   358  		t.Log("wait until all ongoing resets are done")
   359  		ls.wg.Wait()
   360  		select {
   361  		case <-req2.logReset:
   362  			t.Fatal("should not call second reset function")
   363  		default:
   364  		}
   365  	})
   366  
   367  	t.Run("updating loglevel clears waiting reset timers", func(t *testing.T) {
   368  		ls := newLogsetSrv()
   369  
   370  		req1 := newMockedRequest(t, ls, "timeout", "11s")
   371  		<-req1.logApplied
   372  		if timerD := <-req1.timerCreated; timerD != time.Second*11 {
   373  			t.Fatalf("expected timer to be set for %v, got %v", time.Second*11, timerD)
   374  		}
   375  
   376  		req2 := newMockedRequest(t, ls)
   377  		<-req2.logApplied
   378  
   379  		t.Log("timer 1 should be cancelled")
   380  		<-req1.timerCancelled
   381  
   382  		req3 := newMockedRequest(t, ls, "timeout", "12s")
   383  		<-req3.logApplied
   384  
   385  		if timerD := <-req3.timerCreated; timerD != time.Second*12 {
   386  			t.Fatalf("expected timer to be set for %v, got %v", time.Second*12, timerD)
   387  		}
   388  
   389  		t.Log("triggering timer 3 should call the new reset function")
   390  		req3.timerTrigger <- time.Time{}
   391  		<-req3.logReset
   392  
   393  		t.Log("wait until all ongoing resets are done")
   394  		ls.wg.Wait()
   395  
   396  		select {
   397  		case req1.timerTrigger <- time.Time{}:
   398  			t.Fatal("first request timer goroutine should not be running after cancellation")
   399  		default:
   400  		}
   401  
   402  		t.Log("timer 2 should never be created")
   403  		select {
   404  		case <-req2.timerCreated:
   405  			t.Fatal("timer should never be created on request with no timeout set")
   406  		default:
   407  		}
   408  
   409  		select {
   410  		case req2.timerTrigger <- time.Time{}:
   411  			t.Fatal("second request timer should not be able to receive a time")
   412  		default:
   413  		}
   414  
   415  		t.Log("ensure we did not call either reset function")
   416  		select {
   417  		case <-req1.logReset:
   418  			t.Fatal("should not call first reset function")
   419  		case <-req2.logReset:
   420  			t.Fatal("should not call second reset function")
   421  		default:
   422  		}
   423  	})
   424  }