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