k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/kubelet/kuberuntime/logs/logs_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package logs 18 19 import ( 20 "bufio" 21 "bytes" 22 "context" 23 "fmt" 24 "io" 25 "os" 26 "path/filepath" 27 goruntime "runtime" 28 "testing" 29 "time" 30 31 utiltesting "k8s.io/client-go/util/testing" 32 33 v1 "k8s.io/api/core/v1" 34 apitesting "k8s.io/cri-api/pkg/apis/testing" 35 "k8s.io/kubernetes/pkg/kubelet/types" 36 "k8s.io/utils/pointer" 37 38 "github.com/stretchr/testify/assert" 39 40 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 41 runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" 42 ) 43 44 func TestLogOptions(t *testing.T) { 45 var ( 46 line = int64(8) 47 bytes = int64(64) 48 timestamp = metav1.Now() 49 sinceseconds = int64(10) 50 ) 51 for c, test := range []struct { 52 apiOpts *v1.PodLogOptions 53 expect *LogOptions 54 }{ 55 { // empty options 56 apiOpts: &v1.PodLogOptions{}, 57 expect: &LogOptions{tail: -1, bytes: -1}, 58 }, 59 { // test tail lines 60 apiOpts: &v1.PodLogOptions{TailLines: &line}, 61 expect: &LogOptions{tail: line, bytes: -1}, 62 }, 63 { // test limit bytes 64 apiOpts: &v1.PodLogOptions{LimitBytes: &bytes}, 65 expect: &LogOptions{tail: -1, bytes: bytes}, 66 }, 67 { // test since timestamp 68 apiOpts: &v1.PodLogOptions{SinceTime: ×tamp}, 69 expect: &LogOptions{tail: -1, bytes: -1, since: timestamp.Time}, 70 }, 71 { // test since seconds 72 apiOpts: &v1.PodLogOptions{SinceSeconds: &sinceseconds}, 73 expect: &LogOptions{tail: -1, bytes: -1, since: timestamp.Add(-10 * time.Second)}, 74 }, 75 } { 76 t.Logf("TestCase #%d: %+v", c, test) 77 opts := NewLogOptions(test.apiOpts, timestamp.Time) 78 assert.Equal(t, test.expect, opts) 79 } 80 } 81 82 func TestReadLogs(t *testing.T) { 83 file, err := os.CreateTemp("", "TestFollowLogs") 84 if err != nil { 85 t.Fatalf("unable to create temp file") 86 } 87 defer utiltesting.CloseAndRemove(t, file) 88 file.WriteString(`{"log":"line1\n","stream":"stdout","time":"2020-09-27T11:18:01.00000000Z"}` + "\n") 89 file.WriteString(`{"log":"line2\n","stream":"stdout","time":"2020-09-27T11:18:02.00000000Z"}` + "\n") 90 file.WriteString(`{"log":"line3\n","stream":"stdout","time":"2020-09-27T11:18:03.00000000Z"}` + "\n") 91 92 testCases := []struct { 93 name string 94 podLogOptions v1.PodLogOptions 95 expected string 96 }{ 97 { 98 name: "default pod log options should output all lines", 99 podLogOptions: v1.PodLogOptions{}, 100 expected: "line1\nline2\nline3\n", 101 }, 102 { 103 name: "using TailLines 2 should output last 2 lines", 104 podLogOptions: v1.PodLogOptions{ 105 TailLines: pointer.Int64(2), 106 }, 107 expected: "line2\nline3\n", 108 }, 109 { 110 name: "using TailLines 4 should output all lines when the log has less than 4 lines", 111 podLogOptions: v1.PodLogOptions{ 112 TailLines: pointer.Int64(4), 113 }, 114 expected: "line1\nline2\nline3\n", 115 }, 116 { 117 name: "using TailLines 0 should output nothing", 118 podLogOptions: v1.PodLogOptions{ 119 TailLines: pointer.Int64(0), 120 }, 121 expected: "", 122 }, 123 { 124 name: "using LimitBytes 9 should output first 9 bytes", 125 podLogOptions: v1.PodLogOptions{ 126 LimitBytes: pointer.Int64(9), 127 }, 128 expected: "line1\nlin", 129 }, 130 { 131 name: "using LimitBytes 100 should output all bytes when the log has less than 100 bytes", 132 podLogOptions: v1.PodLogOptions{ 133 LimitBytes: pointer.Int64(100), 134 }, 135 expected: "line1\nline2\nline3\n", 136 }, 137 { 138 name: "using LimitBytes 0 should output nothing", 139 podLogOptions: v1.PodLogOptions{ 140 LimitBytes: pointer.Int64(0), 141 }, 142 expected: "", 143 }, 144 { 145 name: "using SinceTime should output lines with a time on or after the specified time", 146 podLogOptions: v1.PodLogOptions{ 147 SinceTime: &metav1.Time{Time: time.Date(2020, time.Month(9), 27, 11, 18, 02, 0, time.UTC)}, 148 }, 149 expected: "line2\nline3\n", 150 }, 151 { 152 name: "using SinceTime now should output nothing", 153 podLogOptions: v1.PodLogOptions{ 154 SinceTime: &metav1.Time{Time: time.Now()}, 155 }, 156 expected: "", 157 }, 158 { 159 name: "using follow should output all log lines", 160 podLogOptions: v1.PodLogOptions{ 161 Follow: true, 162 }, 163 expected: "line1\nline2\nline3\n", 164 }, 165 { 166 name: "using follow combined with TailLines 2 should output the last 2 lines", 167 podLogOptions: v1.PodLogOptions{ 168 Follow: true, 169 TailLines: pointer.Int64(2), 170 }, 171 expected: "line2\nline3\n", 172 }, 173 { 174 name: "using follow combined with SinceTime should output lines with a time on or after the specified time", 175 podLogOptions: v1.PodLogOptions{ 176 Follow: true, 177 SinceTime: &metav1.Time{Time: time.Date(2020, time.Month(9), 27, 11, 18, 02, 0, time.UTC)}, 178 }, 179 expected: "line2\nline3\n", 180 }, 181 } 182 for _, tc := range testCases { 183 t.Run(tc.name, func(t *testing.T) { 184 containerID := "fake-container-id" 185 fakeRuntimeService := &apitesting.FakeRuntimeService{ 186 Containers: map[string]*apitesting.FakeContainer{ 187 containerID: { 188 ContainerStatus: runtimeapi.ContainerStatus{ 189 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 190 }, 191 }, 192 }, 193 } 194 // If follow is specified, mark the container as exited or else ReadLogs will run indefinitely 195 if tc.podLogOptions.Follow { 196 fakeRuntimeService.Containers[containerID].State = runtimeapi.ContainerState_CONTAINER_EXITED 197 } 198 199 opts := NewLogOptions(&tc.podLogOptions, time.Now()) 200 stdoutBuf := bytes.NewBuffer(nil) 201 stderrBuf := bytes.NewBuffer(nil) 202 err = ReadLogs(context.TODO(), file.Name(), containerID, opts, fakeRuntimeService, stdoutBuf, stderrBuf) 203 204 if err != nil { 205 t.Fatalf(err.Error()) 206 } 207 if stderrBuf.Len() > 0 { 208 t.Fatalf("Stderr: %v", stderrBuf.String()) 209 } 210 if actual := stdoutBuf.String(); tc.expected != actual { 211 t.Fatalf("Actual output does not match expected.\nActual: %v\nExpected: %v\n", actual, tc.expected) 212 } 213 }) 214 } 215 } 216 217 func TestReadRotatedLog(t *testing.T) { 218 if goruntime.GOOS == "windows" { 219 // TODO: remove skip once the failing test has been fixed. 220 t.Skip("Skip failing test on Windows.") 221 } 222 tmpDir := t.TempDir() 223 file, err := os.CreateTemp(tmpDir, "logfile") 224 if err != nil { 225 assert.NoErrorf(t, err, "unable to create temp file") 226 } 227 stdoutBuf := &bytes.Buffer{} 228 stderrBuf := &bytes.Buffer{} 229 containerID := "fake-container-id" 230 fakeRuntimeService := &apitesting.FakeRuntimeService{ 231 Containers: map[string]*apitesting.FakeContainer{ 232 containerID: { 233 ContainerStatus: runtimeapi.ContainerStatus{ 234 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 235 }, 236 }, 237 }, 238 } 239 ctx, cancel := context.WithCancel(context.Background()) 240 defer cancel() 241 // Start to follow the container's log. 242 fileName := file.Name() 243 go func(ctx context.Context) { 244 podLogOptions := v1.PodLogOptions{ 245 Follow: true, 246 } 247 opts := NewLogOptions(&podLogOptions, time.Now()) 248 _ = ReadLogs(ctx, fileName, containerID, opts, fakeRuntimeService, stdoutBuf, stderrBuf) 249 }(ctx) 250 251 // log in stdout 252 expectedStdout := "line0\nline2\nline4\nline6\nline8\n" 253 // log in stderr 254 expectedStderr := "line1\nline3\nline5\nline7\nline9\n" 255 256 dir := filepath.Dir(file.Name()) 257 baseName := filepath.Base(file.Name()) 258 259 // Write 10 lines to log file. 260 // Let ReadLogs start. 261 time.Sleep(50 * time.Millisecond) 262 263 for line := 0; line < 10; line++ { 264 // Write the first three lines to log file 265 now := time.Now().Format(types.RFC3339NanoLenient) 266 if line%2 == 0 { 267 file.WriteString(fmt.Sprintf( 268 `{"log":"line%d\n","stream":"stdout","time":"%s"}`+"\n", line, now)) 269 } else { 270 file.WriteString(fmt.Sprintf( 271 `{"log":"line%d\n","stream":"stderr","time":"%s"}`+"\n", line, now)) 272 } 273 time.Sleep(1 * time.Millisecond) 274 275 if line == 5 { 276 file.Close() 277 // Pretend to rotate the log. 278 rotatedName := fmt.Sprintf("%s.%s", baseName, time.Now().Format("220060102-150405")) 279 rotatedName = filepath.Join(dir, rotatedName) 280 if err := os.Rename(filepath.Join(dir, baseName), rotatedName); err != nil { 281 assert.NoErrorf(t, err, "failed to rotate log %q to %q", file.Name(), rotatedName) 282 return 283 } 284 285 newF := filepath.Join(dir, baseName) 286 if file, err = os.Create(newF); err != nil { 287 assert.NoError(t, err, "unable to create new log file") 288 return 289 } 290 time.Sleep(20 * time.Millisecond) 291 } 292 } 293 294 time.Sleep(20 * time.Millisecond) 295 // Make the function ReadLogs end. 296 fakeRuntimeService.Lock() 297 fakeRuntimeService.Containers[containerID].State = runtimeapi.ContainerState_CONTAINER_EXITED 298 fakeRuntimeService.Unlock() 299 300 assert.Equal(t, expectedStdout, stdoutBuf.String()) 301 assert.Equal(t, expectedStderr, stderrBuf.String()) 302 } 303 304 func TestParseLog(t *testing.T) { 305 timestamp, err := time.Parse(timeFormatIn, "2016-10-20T18:39:20.57606443Z") 306 assert.NoError(t, err) 307 msg := &logMessage{} 308 for c, test := range []struct { 309 line string 310 msg *logMessage 311 err bool 312 }{ 313 { // Docker log format stdout 314 line: `{"log":"docker stdout test log","stream":"stdout","time":"2016-10-20T18:39:20.57606443Z"}` + "\n", 315 msg: &logMessage{ 316 timestamp: timestamp, 317 stream: runtimeapi.Stdout, 318 log: []byte("docker stdout test log"), 319 }, 320 }, 321 { // Docker log format stderr 322 line: `{"log":"docker stderr test log","stream":"stderr","time":"2016-10-20T18:39:20.57606443Z"}` + "\n", 323 msg: &logMessage{ 324 timestamp: timestamp, 325 stream: runtimeapi.Stderr, 326 log: []byte("docker stderr test log"), 327 }, 328 }, 329 { // CRI log format stdout 330 line: "2016-10-20T18:39:20.57606443Z stdout F cri stdout test log\n", 331 msg: &logMessage{ 332 timestamp: timestamp, 333 stream: runtimeapi.Stdout, 334 log: []byte("cri stdout test log\n"), 335 }, 336 }, 337 { // CRI log format stderr 338 line: "2016-10-20T18:39:20.57606443Z stderr F cri stderr test log\n", 339 msg: &logMessage{ 340 timestamp: timestamp, 341 stream: runtimeapi.Stderr, 342 log: []byte("cri stderr test log\n"), 343 }, 344 }, 345 { // Unsupported Log format 346 line: "unsupported log format test log\n", 347 msg: &logMessage{}, 348 err: true, 349 }, 350 { // Partial CRI log line 351 line: "2016-10-20T18:39:20.57606443Z stdout P cri stdout partial test log\n", 352 msg: &logMessage{ 353 timestamp: timestamp, 354 stream: runtimeapi.Stdout, 355 log: []byte("cri stdout partial test log"), 356 }, 357 }, 358 { // Partial CRI log line with multiple log tags. 359 line: "2016-10-20T18:39:20.57606443Z stdout P:TAG1:TAG2 cri stdout partial test log\n", 360 msg: &logMessage{ 361 timestamp: timestamp, 362 stream: runtimeapi.Stdout, 363 log: []byte("cri stdout partial test log"), 364 }, 365 }, 366 } { 367 t.Logf("TestCase #%d: %+v", c, test) 368 parse, err := getParseFunc([]byte(test.line)) 369 if test.err { 370 assert.Error(t, err) 371 continue 372 } 373 assert.NoError(t, err) 374 err = parse([]byte(test.line), msg) 375 assert.NoError(t, err) 376 assert.Equal(t, test.msg, msg) 377 } 378 } 379 380 func TestWriteLogs(t *testing.T) { 381 timestamp := time.Unix(1234, 43210) 382 log := "abcdefg\n" 383 384 for c, test := range []struct { 385 stream runtimeapi.LogStreamType 386 since time.Time 387 timestamp bool 388 expectStdout string 389 expectStderr string 390 }{ 391 { // stderr log 392 stream: runtimeapi.Stderr, 393 expectStderr: log, 394 }, 395 { // stdout log 396 stream: runtimeapi.Stdout, 397 expectStdout: log, 398 }, 399 { // since is after timestamp 400 stream: runtimeapi.Stdout, 401 since: timestamp.Add(1 * time.Second), 402 }, 403 { // timestamp enabled 404 stream: runtimeapi.Stderr, 405 timestamp: true, 406 expectStderr: timestamp.Format(timeFormatOut) + " " + log, 407 }, 408 } { 409 t.Logf("TestCase #%d: %+v", c, test) 410 msg := &logMessage{ 411 timestamp: timestamp, 412 stream: test.stream, 413 log: []byte(log), 414 } 415 stdoutBuf := bytes.NewBuffer(nil) 416 stderrBuf := bytes.NewBuffer(nil) 417 w := newLogWriter(stdoutBuf, stderrBuf, &LogOptions{since: test.since, timestamp: test.timestamp, bytes: -1}) 418 err := w.write(msg, true) 419 assert.NoError(t, err) 420 assert.Equal(t, test.expectStdout, stdoutBuf.String()) 421 assert.Equal(t, test.expectStderr, stderrBuf.String()) 422 } 423 } 424 425 func TestWriteLogsWithBytesLimit(t *testing.T) { 426 timestamp := time.Unix(1234, 4321) 427 timestampStr := timestamp.Format(timeFormatOut) 428 log := "abcdefg\n" 429 430 for c, test := range []struct { 431 stdoutLines int 432 stderrLines int 433 bytes int 434 timestamp bool 435 expectStdout string 436 expectStderr string 437 }{ 438 { // limit bytes less than one line 439 stdoutLines: 3, 440 bytes: 3, 441 expectStdout: "abc", 442 }, 443 { // limit bytes across lines 444 stdoutLines: 3, 445 bytes: len(log) + 3, 446 expectStdout: "abcdefg\nabc", 447 }, 448 { // limit bytes more than all lines 449 stdoutLines: 3, 450 bytes: 3 * len(log), 451 expectStdout: "abcdefg\nabcdefg\nabcdefg\n", 452 }, 453 { // limit bytes for stderr 454 stderrLines: 3, 455 bytes: len(log) + 3, 456 expectStderr: "abcdefg\nabc", 457 }, 458 { // limit bytes for both stdout and stderr, stdout first. 459 stdoutLines: 1, 460 stderrLines: 2, 461 bytes: len(log) + 3, 462 expectStdout: "abcdefg\n", 463 expectStderr: "abc", 464 }, 465 { // limit bytes with timestamp 466 stdoutLines: 3, 467 timestamp: true, 468 bytes: len(timestampStr) + 1 + len(log) + 2, 469 expectStdout: timestampStr + " " + log + timestampStr[:2], 470 }, 471 } { 472 t.Logf("TestCase #%d: %+v", c, test) 473 msg := &logMessage{ 474 timestamp: timestamp, 475 log: []byte(log), 476 } 477 stdoutBuf := bytes.NewBuffer(nil) 478 stderrBuf := bytes.NewBuffer(nil) 479 w := newLogWriter(stdoutBuf, stderrBuf, &LogOptions{timestamp: test.timestamp, bytes: int64(test.bytes)}) 480 for i := 0; i < test.stdoutLines; i++ { 481 msg.stream = runtimeapi.Stdout 482 if err := w.write(msg, true); err != nil { 483 assert.EqualError(t, err, errMaximumWrite.Error()) 484 } 485 } 486 for i := 0; i < test.stderrLines; i++ { 487 msg.stream = runtimeapi.Stderr 488 if err := w.write(msg, true); err != nil { 489 assert.EqualError(t, err, errMaximumWrite.Error()) 490 } 491 } 492 assert.Equal(t, test.expectStdout, stdoutBuf.String()) 493 assert.Equal(t, test.expectStderr, stderrBuf.String()) 494 } 495 } 496 497 func TestReadLogsLimitsWithTimestamps(t *testing.T) { 498 logLineFmt := "2022-10-29T16:10:22.592603036-05:00 stdout P %v\n" 499 logLineNewLine := "2022-10-29T16:10:22.592603036-05:00 stdout F \n" 500 501 tmpfile, err := os.CreateTemp("", "log.*.txt") 502 assert.NoError(t, err) 503 504 count := 10000 505 506 for i := 0; i < count; i++ { 507 tmpfile.WriteString(fmt.Sprintf(logLineFmt, i)) 508 } 509 tmpfile.WriteString(logLineNewLine) 510 511 for i := 0; i < count; i++ { 512 tmpfile.WriteString(fmt.Sprintf(logLineFmt, i)) 513 } 514 tmpfile.WriteString(logLineNewLine) 515 516 // two lines are in the buffer 517 518 defer os.Remove(tmpfile.Name()) // clean up 519 520 assert.NoError(t, err) 521 tmpfile.Close() 522 523 var buf bytes.Buffer 524 w := io.MultiWriter(&buf) 525 526 err = ReadLogs(context.Background(), tmpfile.Name(), "", &LogOptions{tail: -1, bytes: -1, timestamp: true}, nil, w, w) 527 assert.NoError(t, err) 528 529 lineCount := 0 530 scanner := bufio.NewScanner(bytes.NewReader(buf.Bytes())) 531 for scanner.Scan() { 532 lineCount++ 533 534 // Split the line 535 ts, logline, _ := bytes.Cut(scanner.Bytes(), []byte(" ")) 536 537 // Verification 538 // 1. The timestamp should exist 539 // 2. The last item in the log should be 9999 540 _, err = time.Parse(time.RFC3339, string(ts)) 541 assert.NoError(t, err, "timestamp not found") 542 assert.Equal(t, true, bytes.HasSuffix(logline, []byte("9999")), "is the complete log found") 543 } 544 545 assert.Equal(t, 2, lineCount, "should have two lines") 546 }