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: &timestamp},
    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  }