github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/logsink/logsink_test.go (about)

     1  // Copyright 2015-2017 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package logsink_test
     5  
     6  import (
     7  	"errors"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"net/url"
    11  	"runtime/debug"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/gorilla/websocket"
    16  	"github.com/juju/clock/testclock"
    17  	"github.com/juju/loggo"
    18  	"github.com/juju/testing"
    19  	jc "github.com/juju/testing/checkers"
    20  	"github.com/juju/utils/v3"
    21  	"go.uber.org/mock/gomock"
    22  	gc "gopkg.in/check.v1"
    23  
    24  	"github.com/juju/juju/apiserver/logsink"
    25  	"github.com/juju/juju/apiserver/logsink/mocks"
    26  	"github.com/juju/juju/apiserver/websocket/websockettest"
    27  	"github.com/juju/juju/rpc/params"
    28  	coretesting "github.com/juju/juju/testing"
    29  )
    30  
    31  var shortAttempt = &utils.AttemptStrategy{
    32  	Total: coretesting.ShortWait,
    33  	Delay: 10 * time.Millisecond,
    34  }
    35  
    36  var longAttempt = &utils.AttemptStrategy{
    37  	Total: coretesting.LongWait,
    38  	Delay: 10 * time.Millisecond,
    39  }
    40  
    41  type logsinkSuite struct {
    42  	testing.IsolationSuite
    43  
    44  	abort chan struct{}
    45  
    46  	stub    *testing.Stub
    47  	written chan params.LogRecord
    48  
    49  	lastStack []byte
    50  	stackMu   sync.Mutex
    51  }
    52  
    53  var _ = gc.Suite(&logsinkSuite{})
    54  
    55  func (s *logsinkSuite) SetUpTest(c *gc.C) {
    56  	s.IsolationSuite.SetUpTest(c)
    57  	s.abort = make(chan struct{})
    58  	s.written = make(chan params.LogRecord, 1)
    59  	s.stub = &testing.Stub{}
    60  	s.stackMu.Lock()
    61  	s.lastStack = nil
    62  	s.stackMu.Unlock()
    63  }
    64  
    65  func (s *logsinkSuite) dialWebsocket(c *gc.C, srv *httptest.Server) *websocket.Conn {
    66  	u, err := url.Parse(srv.URL)
    67  	c.Assert(err, jc.ErrorIsNil)
    68  	u.Scheme = "ws"
    69  	conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    70  	c.Assert(err, jc.ErrorIsNil)
    71  	s.AddCleanup(func(*gc.C) { conn.Close() })
    72  	return conn
    73  }
    74  
    75  func (s *logsinkSuite) TestSuccess(c *gc.C) {
    76  	srv, finish := s.createServer(c)
    77  	defer finish()
    78  
    79  	conn := s.dialWebsocket(c, srv)
    80  	websockettest.AssertJSONInitialErrorNil(c, conn)
    81  
    82  	t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC)
    83  	record := params.LogRecord{
    84  		Time:     t0,
    85  		Module:   "some.where",
    86  		Location: "foo.go:42",
    87  		Level:    loggo.INFO.String(),
    88  		Message:  "all is well",
    89  	}
    90  	err := conn.WriteJSON(&record)
    91  	c.Assert(err, jc.ErrorIsNil)
    92  
    93  	select {
    94  	case written, ok := <-s.written:
    95  		c.Assert(ok, jc.IsTrue)
    96  		c.Assert(written, jc.DeepEquals, record)
    97  	case <-time.After(coretesting.LongWait):
    98  		c.Fatal("timed out waiting for log record to be written")
    99  	}
   100  	select {
   101  	case <-s.written:
   102  		c.Fatal("unexpected log record")
   103  	case <-time.After(coretesting.ShortWait):
   104  	}
   105  	s.stub.CheckCallNames(c, "Open", "WriteLog")
   106  
   107  	s.stackMu.Lock()
   108  	if s.lastStack != nil {
   109  		c.Logf("last Close call stack: \n%s", string(s.lastStack))
   110  	}
   111  	s.stackMu.Unlock()
   112  
   113  	err = conn.Close()
   114  	c.Assert(err, jc.ErrorIsNil)
   115  	for a := longAttempt.Start(); a.Next(); {
   116  		if len(s.stub.Calls()) == 3 {
   117  			break
   118  		}
   119  	}
   120  	s.stub.CheckCallNames(c, "Open", "WriteLog", "Close")
   121  }
   122  
   123  func (s *logsinkSuite) TestLogMessages(c *gc.C) {
   124  	srv, finish := s.createServer(c)
   125  	defer finish()
   126  
   127  	var logs loggo.TestWriter
   128  	writer := loggo.NewMinimumLevelWriter(&logs, loggo.INFO)
   129  	c.Assert(loggo.RegisterWriter("logsink-tests", writer), jc.ErrorIsNil)
   130  
   131  	// Open, then close connection.
   132  	conn := s.dialWebsocket(c, srv)
   133  	websockettest.AssertJSONInitialErrorNil(c, conn)
   134  	err := conn.Close()
   135  	c.Assert(err, jc.ErrorIsNil)
   136  
   137  	// Ensure that no error is logged when the connection is closed normally.
   138  	for a := shortAttempt.Start(); a.Next(); {
   139  		for _, log := range logs.Log() {
   140  			c.Assert(log.Level, jc.LessThan, loggo.ERROR, gc.Commentf("log: %#v", log))
   141  		}
   142  	}
   143  }
   144  
   145  func (s *logsinkSuite) TestSuccessWithLabels(c *gc.C) {
   146  	srv, finish := s.createServer(c)
   147  	defer finish()
   148  
   149  	var logs loggo.TestWriter
   150  	writer := loggo.NewMinimumLevelWriter(&logs, loggo.INFO)
   151  	c.Assert(loggo.RegisterWriter("logsink-tests", writer), jc.ErrorIsNil)
   152  
   153  	conn := s.dialWebsocket(c, srv)
   154  	websockettest.AssertJSONInitialErrorNil(c, conn)
   155  
   156  	t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC)
   157  	records := []params.LogRecord{{
   158  		Time:     t0,
   159  		Module:   "some.where",
   160  		Location: "foo.go:42",
   161  		Level:    loggo.INFO.String(),
   162  		Message:  "all is well",
   163  	}, {
   164  		Time:     t0,
   165  		Module:   "some.where.else",
   166  		Location: "bar.go:122",
   167  		Level:    loggo.INFO.String(),
   168  		Message:  "all is nice",
   169  		Entity:   "entity.name",
   170  		Labels:   []string{"bar"},
   171  	}, {
   172  		Time:     t0,
   173  		Module:   "some.where",
   174  		Location: "foo.go:33",
   175  		Level:    loggo.INFO.String(),
   176  		Message:  "all is fine",
   177  	}}
   178  	for _, record := range records {
   179  		err := conn.WriteJSON(&record)
   180  		c.Assert(err, jc.ErrorIsNil)
   181  
   182  		select {
   183  		case written, ok := <-s.written:
   184  			c.Assert(ok, jc.IsTrue)
   185  			c.Assert(written, jc.DeepEquals, record)
   186  		case <-time.After(coretesting.LongWait):
   187  			c.Fatal("timed out waiting for log record to be written")
   188  		}
   189  		select {
   190  		case <-s.written:
   191  			c.Fatal("unexpected log record")
   192  		case <-time.After(coretesting.ShortWait):
   193  		}
   194  
   195  	}
   196  
   197  	s.stub.CheckCallNames(c, "Open", "WriteLog", "WriteLog", "WriteLog")
   198  
   199  	s.stackMu.Lock()
   200  	if s.lastStack != nil {
   201  		c.Logf("last Close call stack: \n%s", string(s.lastStack))
   202  	}
   203  	s.stackMu.Unlock()
   204  
   205  	err := conn.Close()
   206  	c.Assert(err, jc.ErrorIsNil)
   207  	for a := longAttempt.Start(); a.Next(); {
   208  		if len(s.stub.Calls()) == 3 {
   209  			break
   210  		}
   211  	}
   212  	s.stub.CheckCallNames(c, "Open", "WriteLog", "WriteLog", "WriteLog", "Close")
   213  }
   214  
   215  func (s *logsinkSuite) TestLogOpenFails(c *gc.C) {
   216  	srv, finish := s.createServer(c)
   217  	defer finish()
   218  
   219  	s.stub.SetErrors(errors.New("rats"))
   220  	conn := s.dialWebsocket(c, srv)
   221  	websockettest.AssertJSONError(c, conn, "rats")
   222  	websockettest.AssertWebsocketClosed(c, conn)
   223  }
   224  
   225  func (s *logsinkSuite) TestLogWriteFails(c *gc.C) {
   226  	srv, finish := s.createServer(c)
   227  	defer finish()
   228  
   229  	s.stub.SetErrors(nil, errors.New("cannae write"))
   230  	conn := s.dialWebsocket(c, srv)
   231  	websockettest.AssertJSONInitialErrorNil(c, conn)
   232  
   233  	err := conn.WriteJSON(&params.LogRecord{})
   234  	c.Assert(err, jc.ErrorIsNil)
   235  
   236  	websockettest.AssertJSONError(c, conn, "cannae write")
   237  	websockettest.AssertWebsocketClosed(c, conn)
   238  }
   239  
   240  func (s *logsinkSuite) TestReceiveErrorBreaksConn(c *gc.C) {
   241  	srv, finish := s.createServer(c)
   242  	defer finish()
   243  
   244  	conn := s.dialWebsocket(c, srv)
   245  	websockettest.AssertJSONInitialErrorNil(c, conn)
   246  
   247  	// The logsink handler expects JSON messages. Send some
   248  	// junk to verify that the server closes the connection.
   249  	err := conn.WriteMessage(websocket.TextMessage, []byte("junk!"))
   250  	c.Assert(err, jc.ErrorIsNil)
   251  
   252  	websockettest.AssertWebsocketClosed(c, conn)
   253  }
   254  
   255  func (s *logsinkSuite) TestRateLimit(c *gc.C) {
   256  	modelUUID, err := utils.NewUUID()
   257  	c.Assert(err, jc.ErrorIsNil)
   258  
   259  	metricsCollector, finish := createMockMetrics(c, modelUUID.String())
   260  	defer finish()
   261  
   262  	testClock := testclock.NewClock(time.Time{})
   263  	srv := httptest.NewServer(logsink.NewHTTPHandler(
   264  		func(req *http.Request) (logsink.LogWriteCloser, error) {
   265  			s.stub.AddCall("Open")
   266  			return &mockLogWriteCloser{
   267  				s.stub,
   268  				s.written,
   269  				nil,
   270  			}, s.stub.NextErr()
   271  		},
   272  		s.abort,
   273  		&logsink.RateLimitConfig{
   274  			Burst:  2,
   275  			Refill: time.Second,
   276  			Clock:  testClock,
   277  		},
   278  		metricsCollector,
   279  		modelUUID.String(),
   280  	))
   281  	defer srv.Close()
   282  
   283  	conn := s.dialWebsocket(c, srv)
   284  	websockettest.AssertJSONInitialErrorNil(c, conn)
   285  
   286  	record := params.LogRecord{
   287  		Time:     time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC),
   288  		Module:   "some.where",
   289  		Location: "foo.go:42",
   290  		Level:    loggo.INFO.String(),
   291  		Message:  "all is well",
   292  	}
   293  	for i := 0; i < 4; i++ {
   294  		err := conn.WriteJSON(&record)
   295  		c.Assert(err, jc.ErrorIsNil)
   296  	}
   297  
   298  	expectRecord := func() {
   299  		select {
   300  		case written, ok := <-s.written:
   301  			c.Assert(ok, jc.IsTrue)
   302  			c.Assert(written, jc.DeepEquals, record)
   303  		case <-time.After(coretesting.LongWait):
   304  			c.Fatal("timed out waiting for log record to be written")
   305  		}
   306  	}
   307  	expectNoRecord := func() {
   308  		select {
   309  		case <-s.written:
   310  			c.Fatal("unexpected log record")
   311  		case <-time.After(coretesting.ShortWait):
   312  		}
   313  	}
   314  
   315  	// There should be 2 records received immediately,
   316  	// and then rate-limiting should kick in.
   317  	expectRecord()
   318  	expectRecord()
   319  	expectNoRecord()
   320  	testClock.WaitAdvance(time.Second, coretesting.LongWait, 1)
   321  	expectRecord()
   322  	expectNoRecord()
   323  	testClock.WaitAdvance(time.Second, coretesting.LongWait, 1)
   324  	expectRecord()
   325  	expectNoRecord()
   326  }
   327  
   328  func (s *logsinkSuite) TestReceiverStopsWhenAsked(c *gc.C) {
   329  	myStopCh := make(chan struct{})
   330  
   331  	modelUUID, err := utils.NewUUID()
   332  	c.Assert(err, jc.ErrorIsNil)
   333  
   334  	metricsCollector, finish := createMockMetrics(c, modelUUID.String())
   335  	defer finish()
   336  
   337  	handler := logsink.NewHTTPHandlerForTest(
   338  		func(req *http.Request) (logsink.LogWriteCloser, error) {
   339  			s.stub.AddCall("Open")
   340  			return &slowWriteCloser{}, s.stub.NextErr()
   341  		},
   342  		s.abort,
   343  		nil,
   344  		metricsCollector,
   345  		modelUUID.String(),
   346  		func() (chan struct{}, func()) {
   347  			return myStopCh, func() {}
   348  		},
   349  	)
   350  	srv := httptest.NewServer(handler)
   351  	defer srv.Close()
   352  
   353  	conn := s.dialWebsocket(c, srv)
   354  	websockettest.AssertJSONInitialErrorNil(c, conn)
   355  
   356  	close(myStopCh)
   357  
   358  	// Send 2 log messages so we're sure the receiver gets a chance to
   359  	// go down the stop channel leg, since the writes are slow.
   360  	t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC)
   361  	record := params.LogRecord{
   362  		Time:     t0,
   363  		Module:   "some.where",
   364  		Location: "foo.go:42",
   365  		Level:    loggo.INFO.String(),
   366  		Message:  "all is well",
   367  	}
   368  	err = conn.WriteJSON(&record)
   369  	c.Assert(err, jc.ErrorIsNil)
   370  	// The second write might error (if the receiver stopped after the
   371  	// first message).
   372  	_ = conn.WriteJSON(&record)
   373  
   374  	for a := longAttempt.Start(); a.Next(); {
   375  		if logsink.ReceiverStopped(c, handler) {
   376  			break
   377  		}
   378  	}
   379  	c.Assert(logsink.ReceiverStopped(c, handler), gc.Equals, true)
   380  
   381  	err = conn.Close()
   382  	c.Assert(err, jc.ErrorIsNil)
   383  }
   384  
   385  func (s *logsinkSuite) TestHandlerClosesStopChannel(c *gc.C) {
   386  	modelUUID, err := utils.NewUUID()
   387  	c.Assert(err, jc.ErrorIsNil)
   388  
   389  	metricsCollector, finish := createMockMetrics(c, modelUUID.String())
   390  	defer finish()
   391  
   392  	var stub testing.Stub
   393  	handler := logsink.NewHTTPHandlerForTest(
   394  		func(req *http.Request) (logsink.LogWriteCloser, error) {
   395  			return &mockLogWriteCloser{
   396  				s.stub,
   397  				s.written,
   398  				nil,
   399  			}, s.stub.NextErr()
   400  		},
   401  		s.abort,
   402  		nil,
   403  		metricsCollector,
   404  		modelUUID.String(),
   405  		func() (chan struct{}, func()) {
   406  			ch := make(chan struct{})
   407  			return ch, func() {
   408  				stub.AddCall("close stop channel")
   409  				close(ch)
   410  			}
   411  		},
   412  	)
   413  	srv := httptest.NewServer(handler)
   414  	defer srv.Close()
   415  	conn := s.dialWebsocket(c, srv)
   416  	websockettest.AssertJSONInitialErrorNil(c, conn)
   417  
   418  	t0 := time.Date(2015, time.June, 1, 23, 2, 1, 0, time.UTC)
   419  	record := params.LogRecord{
   420  		Time:     t0,
   421  		Module:   "some.where",
   422  		Location: "foo.go:42",
   423  		Level:    loggo.INFO.String(),
   424  		Message:  "all is well",
   425  	}
   426  	err = conn.WriteJSON(&record)
   427  	c.Assert(err, jc.ErrorIsNil)
   428  
   429  	select {
   430  	case written, ok := <-s.written:
   431  		c.Assert(ok, jc.IsTrue)
   432  		c.Assert(written, jc.DeepEquals, record)
   433  	case <-time.After(coretesting.LongWait):
   434  		c.Fatal("timed out waiting for log record to be written")
   435  	}
   436  
   437  	err = conn.Close()
   438  	c.Assert(err, jc.ErrorIsNil)
   439  	for a := longAttempt.Start(); a.Next(); {
   440  		if len(stub.Calls()) == 1 {
   441  			break
   442  		}
   443  	}
   444  	stub.CheckCallNames(c, "close stop channel")
   445  }
   446  
   447  func (s *logsinkSuite) createServer(c *gc.C) (*httptest.Server, func()) {
   448  	recordStack := func() {
   449  		s.stackMu.Lock()
   450  		defer s.stackMu.Unlock()
   451  		s.lastStack = debug.Stack()
   452  	}
   453  
   454  	modelUUID, err := utils.NewUUID()
   455  	c.Assert(err, jc.ErrorIsNil)
   456  
   457  	metricsCollector, finish := createMockMetrics(c, modelUUID.String())
   458  
   459  	srv := httptest.NewServer(logsink.NewHTTPHandler(
   460  		func(req *http.Request) (logsink.LogWriteCloser, error) {
   461  			s.stub.AddCall("Open")
   462  			return &mockLogWriteCloser{
   463  				s.stub,
   464  				s.written,
   465  				recordStack,
   466  			}, s.stub.NextErr()
   467  		},
   468  		s.abort,
   469  		nil, // no rate-limiting
   470  		metricsCollector,
   471  		modelUUID.String(),
   472  	))
   473  	return srv, func() {
   474  		finish()
   475  		srv.Close()
   476  	}
   477  }
   478  
   479  type mockLogWriteCloser struct {
   480  	*testing.Stub
   481  	written  chan<- params.LogRecord
   482  	callback func()
   483  }
   484  
   485  func (m *mockLogWriteCloser) Close() error {
   486  	m.MethodCall(m, "Close")
   487  	if m.callback != nil {
   488  		m.callback()
   489  	}
   490  	return m.NextErr()
   491  }
   492  
   493  func (m *mockLogWriteCloser) WriteLog(r params.LogRecord) error {
   494  	m.MethodCall(m, "WriteLog", r)
   495  	m.written <- r
   496  	return m.NextErr()
   497  }
   498  
   499  type slowWriteCloser struct{}
   500  
   501  func (slowWriteCloser) Close() error {
   502  	return nil
   503  }
   504  
   505  func (slowWriteCloser) WriteLog(params.LogRecord) error {
   506  	// This makes it more likely that the goroutine will notice the
   507  	// stop channel is closed, because logCh won't be ready for
   508  	// sending.
   509  	time.Sleep(testing.ShortWait)
   510  	return nil
   511  }
   512  
   513  func createMockMetrics(c *gc.C, modelUUID string) (*mocks.MockMetricsCollector, func()) {
   514  	ctrl := gomock.NewController(c)
   515  
   516  	counter := mocks.NewMockCounter(ctrl)
   517  	counter.EXPECT().Inc().AnyTimes()
   518  
   519  	gauge := mocks.NewMockGauge(ctrl)
   520  	gauge.EXPECT().Inc().AnyTimes()
   521  	gauge.EXPECT().Dec().AnyTimes()
   522  
   523  	metricsCollector := mocks.NewMockMetricsCollector(ctrl)
   524  	metricsCollector.EXPECT().TotalConnections().Return(counter).AnyTimes()
   525  	metricsCollector.EXPECT().Connections().Return(gauge).AnyTimes()
   526  	metricsCollector.EXPECT().LogWriteCount(modelUUID, gomock.Any()).Return(counter).AnyTimes()
   527  	metricsCollector.EXPECT().LogReadCount(modelUUID, gomock.Any()).Return(counter).AnyTimes()
   528  
   529  	return metricsCollector, ctrl.Finish
   530  }