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(¶ms.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 }