1package logging
2
3import (
4 "context"
5 "errors"
6 "io"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14 "go.uber.org/mock/gomock"
15 "gordon/internal/config"
16 "gordon/internal/testutils/mocks"
17)
18
19// mockLogStream implements io.ReadCloser for testing
20type mockLogStream struct {
21 data string
22 closed bool
23 reader *strings.Reader
24 ctx context.Context
25}
26
27func newMockLogStream(data string) *mockLogStream {
28 return &mockLogStream{
29 data: data,
30 reader: strings.NewReader(data),
31 }
32}
33
34func newContextAwareMockLogStream(ctx context.Context) *mockLogStream {
35 return &mockLogStream{
36 data: "",
37 reader: strings.NewReader(""),
38 ctx: ctx,
39 }
40}
41
42func (m *mockLogStream) Read(p []byte) (n int, err error) {
43 if m.closed {
44 return 0, io.EOF
45 }
46
47 // If we have a context, check if it's cancelled
48 if m.ctx != nil {
49 select {
50 case <-m.ctx.Done():
51 return 0, io.EOF
52 default:
53 // Block until context is cancelled
54 <-m.ctx.Done()
55 return 0, io.EOF
56 }
57 }
58
59 return m.reader.Read(p)
60}
61
62func (m *mockLogStream) Close() error {
63 m.closed = true
64 return nil
65}
66
67func TestNewLogManager(t *testing.T) {
68 ctrl := gomock.NewController(t)
69 defer ctrl.Finish()
70
71 cfg := &config.Config{}
72 runtime := mocks.NewMockRuntime(ctrl)
73
74 manager := NewLogManager(cfg, runtime)
75
76 assert.NotNil(t, manager)
77 assert.NotNil(t, manager.collectors)
78 assert.Equal(t, cfg, manager.config)
79 assert.Equal(t, runtime, manager.runtime)
80 assert.Empty(t, manager.collectors)
81}
82
83func TestLogManager_StartCollection_LoggingDisabled(t *testing.T) {
84 ctrl := gomock.NewController(t)
85 defer ctrl.Finish()
86
87 cfg := &config.Config{
88 Logging: config.LoggingConfig{
89 Enabled: false,
90 },
91 }
92 runtime := mocks.NewMockRuntime(ctrl)
93 manager := NewLogManager(cfg, runtime)
94
95 err := manager.StartCollection("container123", "test-container")
96 assert.NoError(t, err)
97
98 // Should not create any collectors
99 assert.Equal(t, 0, manager.GetActiveCollectors())
100}
101
102func TestLogManager_StartCollection_Success(t *testing.T) {
103 ctrl := gomock.NewController(t)
104 defer ctrl.Finish()
105
106 tempDir := t.TempDir()
107 cfg := &config.Config{
108 Logging: config.LoggingConfig{
109 Enabled: true,
110 Dir: tempDir,
111 ContainerLogDir: "containers",
112 MainLogFile: "gordon.log",
113 ProxyLogFile: "proxy.log",
114 Level: "info",
115 MaxSize: 10,
116 MaxBackups: 3,
117 MaxAge: 7,
118 Compress: false,
119 },
120 }
121
122 runtime := mocks.NewMockRuntime(ctrl)
123 manager := NewLogManager(cfg, runtime)
124
125 containerID := "container123"
126 containerName := "test-container"
127
128 err := manager.StartCollection(containerID, containerName)
129 require.NoError(t, err)
130
131 // Should create one collector
132 assert.Equal(t, 1, manager.GetActiveCollectors())
133
134 // Check collector exists
135 manager.mu.RLock()
136 collector, exists := manager.collectors[containerID]
137 manager.mu.RUnlock()
138
139 assert.True(t, exists)
140 assert.Equal(t, containerID, collector.containerID)
141 assert.Equal(t, containerName, collector.containerName)
142 assert.NotNil(t, collector.logWriter)
143 assert.NotNil(t, collector.ctx)
144 assert.NotNil(t, collector.cancel)
145
146 // Clean up
147 manager.StopAll()
148}
149
150func TestLogManager_StartCollection_ReplaceExisting(t *testing.T) {
151 ctrl := gomock.NewController(t)
152 defer ctrl.Finish()
153
154 tempDir := t.TempDir()
155 cfg := &config.Config{
156 Logging: config.LoggingConfig{
157 Enabled: true,
158 Dir: tempDir,
159 ContainerLogDir: "containers",
160 MainLogFile: "gordon.log",
161 ProxyLogFile: "proxy.log",
162 Level: "info",
163 MaxSize: 10,
164 MaxBackups: 3,
165 MaxAge: 7,
166 Compress: false,
167 },
168 }
169
170 runtime := mocks.NewMockRuntime(ctrl)
171 manager := NewLogManager(cfg, runtime)
172
173 containerID := "container123"
174
175 // Start first collector
176 err := manager.StartCollection(containerID, "first-container")
177 require.NoError(t, err)
178 assert.Equal(t, 1, manager.GetActiveCollectors())
179
180 // Get reference to first collector
181 manager.mu.RLock()
182 firstCollector := manager.collectors[containerID]
183 manager.mu.RUnlock()
184
185 // Start second collector with same ID (should replace)
186 err = manager.StartCollection(containerID, "second-container")
187 require.NoError(t, err)
188 assert.Equal(t, 1, manager.GetActiveCollectors())
189
190 // Get reference to second collector
191 manager.mu.RLock()
192 secondCollector := manager.collectors[containerID]
193 manager.mu.RUnlock()
194
195 // Should be different collectors
196 assert.NotEqual(t, firstCollector, secondCollector)
197 assert.Equal(t, "second-container", secondCollector.containerName)
198
199 // Clean up
200 manager.StopAll()
201}
202
203func TestLogManager_StopCollection(t *testing.T) {
204 ctrl := gomock.NewController(t)
205 defer ctrl.Finish()
206
207 tempDir := t.TempDir()
208 cfg := &config.Config{
209 Logging: config.LoggingConfig{
210 Enabled: true,
211 Dir: tempDir,
212 ContainerLogDir: "containers",
213 MainLogFile: "gordon.log",
214 ProxyLogFile: "proxy.log",
215 Level: "info",
216 MaxSize: 10,
217 MaxBackups: 3,
218 MaxAge: 7,
219 Compress: false,
220 },
221 }
222
223 runtime := mocks.NewMockRuntime(ctrl)
224 manager := NewLogManager(cfg, runtime)
225
226 containerID := "container123"
227
228 // Start collector
229 err := manager.StartCollection(containerID, "test-container")
230 require.NoError(t, err)
231 assert.Equal(t, 1, manager.GetActiveCollectors())
232
233 // Stop collector
234 manager.StopCollection(containerID)
235 assert.Equal(t, 0, manager.GetActiveCollectors())
236
237 // Stopping non-existent collector should not panic
238 manager.StopCollection("non-existent")
239 assert.Equal(t, 0, manager.GetActiveCollectors())
240}
241
242func TestLogManager_StopAll(t *testing.T) {
243 ctrl := gomock.NewController(t)
244 defer ctrl.Finish()
245
246 tempDir := t.TempDir()
247 cfg := &config.Config{
248 Logging: config.LoggingConfig{
249 Enabled: true,
250 Dir: tempDir,
251 ContainerLogDir: "containers",
252 MainLogFile: "gordon.log",
253 ProxyLogFile: "proxy.log",
254 Level: "info",
255 MaxSize: 10,
256 MaxBackups: 3,
257 MaxAge: 7,
258 Compress: false,
259 },
260 }
261
262 runtime := mocks.NewMockRuntime(ctrl)
263
264 // Mock GetContainerLogs calls for each container - return context-aware streams
265 runtime.EXPECT().GetContainerLogs(gomock.Any(), "container1", true).DoAndReturn(
266 func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
267 return newContextAwareMockLogStream(ctx), nil
268 }).AnyTimes()
269 runtime.EXPECT().GetContainerLogs(gomock.Any(), "container2", true).DoAndReturn(
270 func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
271 return newContextAwareMockLogStream(ctx), nil
272 }).AnyTimes()
273 runtime.EXPECT().GetContainerLogs(gomock.Any(), "container3", true).DoAndReturn(
274 func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
275 return newContextAwareMockLogStream(ctx), nil
276 }).AnyTimes()
277
278 manager := NewLogManager(cfg, runtime)
279
280 // Start multiple collectors
281 err := manager.StartCollection("container1", "test-container1")
282 require.NoError(t, err)
283 err = manager.StartCollection("container2", "test-container2")
284 require.NoError(t, err)
285 err = manager.StartCollection("container3", "test-container3")
286 require.NoError(t, err)
287
288 assert.Equal(t, 3, manager.GetActiveCollectors())
289
290 // Stop all collectors
291 manager.StopAll()
292 assert.Equal(t, 0, manager.GetActiveCollectors())
293
294 // Stopping again should be safe
295 manager.StopAll()
296 assert.Equal(t, 0, manager.GetActiveCollectors())
297}
298
299func TestLogManager_GetActiveCollectors(t *testing.T) {
300 ctrl := gomock.NewController(t)
301 defer ctrl.Finish()
302
303 cfg := &config.Config{
304 Logging: config.LoggingConfig{
305 Enabled: false,
306 },
307 }
308 runtime := mocks.NewMockRuntime(ctrl)
309 manager := NewLogManager(cfg, runtime)
310
311 // Initially no collectors
312 assert.Equal(t, 0, manager.GetActiveCollectors())
313
314 // Test concurrent access
315 var wg sync.WaitGroup
316 for i := 0; i < 10; i++ {
317 wg.Add(1)
318 go func() {
319 defer wg.Done()
320 count := manager.GetActiveCollectors()
321 assert.GreaterOrEqual(t, count, 0)
322 }()
323 }
324 wg.Wait()
325}
326
327func TestLogCollector_Stop(t *testing.T) {
328 tempDir := t.TempDir()
329 cfg := &config.Config{
330 Logging: config.LoggingConfig{
331 Enabled: true,
332 Dir: tempDir,
333 ContainerLogDir: "containers",
334 MaxSize: 10,
335 MaxBackups: 3,
336 MaxAge: 7,
337 Compress: false,
338 },
339 }
340
341 logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
342 require.NoError(t, err)
343
344 ctrl := gomock.NewController(t)
345 defer ctrl.Finish()
346
347 ctx, cancel := context.WithCancel(context.Background())
348 collector := &LogCollector{
349 containerID: "test123",
350 containerName: "test-container",
351 runtime: mocks.NewMockRuntime(ctrl),
352 logWriter: logWriter,
353 ctx: ctx,
354 cancel: cancel,
355 }
356
357 // Test that Stop doesn't panic
358 assert.NotPanics(t, func() {
359 collector.Stop()
360 })
361
362 // Test that Stop can be called multiple times
363 assert.NotPanics(t, func() {
364 collector.Stop()
365 })
366}
367
368func TestLogCollector_CollectLogsOnce_Success(t *testing.T) {
369 ctrl := gomock.NewController(t)
370 defer ctrl.Finish()
371
372 tempDir := t.TempDir()
373 cfg := &config.Config{
374 Logging: config.LoggingConfig{
375 Enabled: true,
376 Dir: tempDir,
377 ContainerLogDir: "containers",
378 MaxSize: 10,
379 MaxBackups: 3,
380 MaxAge: 7,
381 Compress: false,
382 },
383 }
384
385 logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
386 require.NoError(t, err)
387
388 // Create mock runtime that returns log stream
389 logData := "line 1\nline 2\nline 3"
390 mockStream := newMockLogStream(logData)
391
392 runtime := mocks.NewMockRuntime(ctrl)
393 runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(mockStream, nil)
394
395 ctx, cancel := context.WithCancel(context.Background())
396 defer cancel()
397
398 collector := &LogCollector{
399 containerID: "test123",
400 containerName: "test-container",
401 runtime: runtime,
402 logWriter: logWriter,
403 ctx: ctx,
404 cancel: cancel,
405 }
406
407 err = collector.collectLogsOnce()
408 assert.NoError(t, err)
409 assert.True(t, mockStream.closed)
410}
411
412func TestLogCollector_CollectLogsOnce_Error(t *testing.T) {
413 ctrl := gomock.NewController(t)
414 defer ctrl.Finish()
415
416 tempDir := t.TempDir()
417 cfg := &config.Config{
418 Logging: config.LoggingConfig{
419 Enabled: true,
420 Dir: tempDir,
421 ContainerLogDir: "containers",
422 MaxSize: 10,
423 MaxBackups: 3,
424 MaxAge: 7,
425 Compress: false,
426 },
427 }
428
429 logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
430 require.NoError(t, err)
431
432 // Create mock runtime that returns error
433 runtime := mocks.NewMockRuntime(ctrl)
434 runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(nil, errors.New("failed to get logs"))
435
436 ctx, cancel := context.WithCancel(context.Background())
437 defer cancel()
438
439 collector := &LogCollector{
440 containerID: "test123",
441 containerName: "test-container",
442 runtime: runtime,
443 logWriter: logWriter,
444 ctx: ctx,
445 cancel: cancel,
446 }
447
448 err = collector.collectLogsOnce()
449 assert.Error(t, err)
450 assert.Contains(t, err.Error(), "failed to get log stream")
451}
452
453func TestLogCollector_CollectLogsOnce_ContextCancelled(t *testing.T) {
454 ctrl := gomock.NewController(t)
455 defer ctrl.Finish()
456
457 tempDir := t.TempDir()
458 cfg := &config.Config{
459 Logging: config.LoggingConfig{
460 Enabled: true,
461 Dir: tempDir,
462 ContainerLogDir: "containers",
463 MaxSize: 10,
464 MaxBackups: 3,
465 MaxAge: 7,
466 Compress: false,
467 },
468 }
469
470 logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
471 require.NoError(t, err)
472
473 // Create mock stream that never ends
474 mockStream := newMockLogStream("line 1\nline 2\n")
475
476 runtime := mocks.NewMockRuntime(ctrl)
477 runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).Return(mockStream, nil)
478
479 ctx, cancel := context.WithCancel(context.Background())
480
481 collector := &LogCollector{
482 containerID: "test123",
483 containerName: "test-container",
484 runtime: runtime,
485 logWriter: logWriter,
486 ctx: ctx,
487 cancel: cancel,
488 }
489
490 // Cancel context immediately
491 cancel()
492
493 err = collector.collectLogsOnce()
494 assert.NoError(t, err) // Should return nil when context is cancelled
495}
496
497func TestLogCollector_CollectLogs_RetryLogic(t *testing.T) {
498 ctrl := gomock.NewController(t)
499 defer ctrl.Finish()
500
501 tempDir := t.TempDir()
502 cfg := &config.Config{
503 Logging: config.LoggingConfig{
504 Enabled: true,
505 Dir: tempDir,
506 ContainerLogDir: "containers",
507 MaxSize: 10,
508 MaxBackups: 3,
509 MaxAge: 7,
510 Compress: false,
511 },
512 }
513
514 logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
515 require.NoError(t, err)
516
517 // Create mock runtime that fails first time, succeeds second time
518 runtime := mocks.NewMockRuntime(ctrl)
519 runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).
520 Return(nil, errors.New("temporary failure")).Times(1)
521 runtime.EXPECT().GetContainerLogs(gomock.Any(), "test123", true).DoAndReturn(
522 func(ctx context.Context, containerID string, follow bool) (io.ReadCloser, error) {
523 return newContextAwareMockLogStream(ctx), nil
524 }).AnyTimes()
525
526 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
527 defer cancel()
528
529 collector := &LogCollector{
530 containerID: "test123",
531 containerName: "test-container",
532 runtime: runtime,
533 logWriter: logWriter,
534 ctx: ctx,
535 cancel: cancel,
536 }
537
538 // Start collection in goroutine
539 collector.wg.Add(1)
540 go collector.collectLogs()
541
542 // Wait a bit for retry to happen
543 time.Sleep(1500 * time.Millisecond)
544
545 // Cancel and wait for completion
546 cancel()
547 collector.wg.Wait()
548
549 // Test passes if no panic occurs and the mocks were called as expected
550}
551
552func TestLogCollector_CollectLogs_ContextCancellation(t *testing.T) {
553 ctrl := gomock.NewController(t)
554 defer ctrl.Finish()
555
556 tempDir := t.TempDir()
557 cfg := &config.Config{
558 Logging: config.LoggingConfig{
559 Enabled: true,
560 Dir: tempDir,
561 ContainerLogDir: "containers",
562 MaxSize: 10,
563 MaxBackups: 3,
564 MaxAge: 7,
565 Compress: false,
566 },
567 }
568
569 logWriter, err := GetContainerLogWriter(cfg, "test123", "test-container")
570 require.NoError(t, err)
571
572 runtime := mocks.NewMockRuntime(ctrl)
573 // Should not be called if context is cancelled immediately
574
575 ctx, cancel := context.WithCancel(context.Background())
576
577 collector := &LogCollector{
578 containerID: "test123",
579 containerName: "test-container",
580 runtime: runtime,
581 logWriter: logWriter,
582 ctx: ctx,
583 cancel: cancel,
584 }
585
586 // Cancel context immediately
587 cancel()
588
589 // Start collection
590 collector.wg.Add(1)
591 go collector.collectLogs()
592
593 // Wait for completion
594 collector.wg.Wait()
595
596 // Test passes if no panic occurs and goroutine exits cleanly
597}