
Provide an administrator a streaming view of journal logs on Linux systems using journalctl, and event logs on Windows systems using the Get-WinEvent PowerShell cmdlet without them having to implement a client side reader. Only available to cluster admins. The implementation for journald on Linux was originally done by Clayton Coleman. Introduce a heuristics approach to query logs The logs query for node objects will follow a heuristics approach when asked to query for logs from a service. If asked to get the logs from a service foobar, it will first check if foobar logs to the native OS service log provider. If unable to get logs from these, it will attempt to get logs from /var/foobar, /var/log/foobar.log or /var/log/foobar/foobar.log in that order. The logs sub-command can also directly serve a file if the query looks like a file. Co-authored-by: Clayton Coleman <ccoleman@redhat.com> Co-authored-by: Christian Glombek <cglombek@redhat.com>
216 lines
7.9 KiB
Go
216 lines
7.9 KiB
Go
/*
|
|
Copyright 2022 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package kubelet
|
|
|
|
import (
|
|
"net/url"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func Test_getLoggingCmd(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args nodeLogQuery
|
|
wantLinux []string
|
|
wantWindows []string
|
|
wantOtherOS []string
|
|
}{
|
|
{
|
|
args: nodeLogQuery{},
|
|
wantLinux: []string{"--utc", "--no-pager", "--output=short-precise"},
|
|
wantWindows: []string{"-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", "Get-WinEvent -FilterHashtable @{LogName='Application'} | Sort-Object TimeCreated | Format-Table -AutoSize -Wrap"},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, got, err := getLoggingCmd(&tt.args, []string{})
|
|
switch os := runtime.GOOS; os {
|
|
case "linux":
|
|
if !reflect.DeepEqual(got, tt.wantLinux) {
|
|
t.Errorf("getLoggingCmd() = %v, want %v", got, tt.wantLinux)
|
|
}
|
|
case "windows":
|
|
if !reflect.DeepEqual(got, tt.wantWindows) {
|
|
t.Errorf("getLoggingCmd() = %v, want %v", got, tt.wantWindows)
|
|
}
|
|
default:
|
|
if err == nil {
|
|
t.Errorf("getLoggingCmd() = %v, want err", got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_newNodeLogQuery(t *testing.T) {
|
|
validTimeValue := "2019-12-04T02:00:00Z"
|
|
validT, _ := time.Parse(time.RFC3339, validTimeValue)
|
|
tests := []struct {
|
|
name string
|
|
query url.Values
|
|
want *nodeLogQuery
|
|
wantErr bool
|
|
}{
|
|
{name: "empty", query: url.Values{}, want: nil},
|
|
{query: url.Values{"unknown": []string{"true"}}, want: nil},
|
|
|
|
{query: url.Values{"sinceTime": []string{""}}, want: nil},
|
|
{query: url.Values{"sinceTime": []string{"2019-12-04 02:00:00"}}, wantErr: true},
|
|
{query: url.Values{"sinceTime": []string{"2019-12-04 02:00:00.000"}}, wantErr: true},
|
|
{query: url.Values{"sinceTime": []string{"2019-12-04 02"}}, wantErr: true},
|
|
{query: url.Values{"sinceTime": []string{"2019-12-04 02:00"}}, wantErr: true},
|
|
{query: url.Values{"sinceTime": []string{validTimeValue}},
|
|
want: &nodeLogQuery{options: options{SinceTime: &validT}}},
|
|
|
|
{query: url.Values{"untilTime": []string{""}}, want: nil},
|
|
{query: url.Values{"untilTime": []string{"2019-12-04 02:00:00"}}, wantErr: true},
|
|
{query: url.Values{"untilTime": []string{"2019-12-04 02:00:00.000"}}, wantErr: true},
|
|
{query: url.Values{"untilTime": []string{"2019-12-04 02"}}, wantErr: true},
|
|
{query: url.Values{"untilTime": []string{"2019-12-04 02:00"}}, wantErr: true},
|
|
{query: url.Values{"untilTime": []string{validTimeValue}},
|
|
want: &nodeLogQuery{options: options{UntilTime: &validT}}},
|
|
|
|
{query: url.Values{"tailLines": []string{"100"}}, want: &nodeLogQuery{options: options{TailLines: intPtr(100)}}},
|
|
{query: url.Values{"tailLines": []string{"foo"}}, wantErr: true},
|
|
{query: url.Values{"tailLines": []string{" "}}, wantErr: true},
|
|
|
|
{query: url.Values{"pattern": []string{"foo"}}, want: &nodeLogQuery{options: options{Pattern: "foo"}}},
|
|
|
|
{query: url.Values{"boot": []string{""}}, want: nil},
|
|
{query: url.Values{"boot": []string{"0"}}, want: &nodeLogQuery{options: options{Boot: intPtr(0)}}},
|
|
{query: url.Values{"boot": []string{"-23"}}, want: &nodeLogQuery{options: options{Boot: intPtr(-23)}}},
|
|
{query: url.Values{"boot": []string{"foo"}}, wantErr: true},
|
|
{query: url.Values{"boot": []string{" "}}, wantErr: true},
|
|
|
|
{query: url.Values{"query": []string{""}}, wantErr: true},
|
|
{query: url.Values{"query": []string{" ", " "}}, wantErr: true},
|
|
{query: url.Values{"query": []string{"foo"}}, want: &nodeLogQuery{Services: []string{"foo"}}},
|
|
{query: url.Values{"query": []string{"foo", "bar"}}, want: &nodeLogQuery{Services: []string{"foo", "bar"}}},
|
|
{query: url.Values{"query": []string{"foo", "/bar"}}, want: &nodeLogQuery{Services: []string{"foo"},
|
|
Files: []string{"/bar"}}},
|
|
{query: url.Values{"query": []string{"/foo", `\bar`}}, want: &nodeLogQuery{Files: []string{"/foo", `\bar`}}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.query.Encode(), func(t *testing.T) {
|
|
got, err := newNodeLogQuery(tt.query)
|
|
if len(err) > 0 != tt.wantErr {
|
|
t.Errorf("newNodeLogQuery() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("different: %s", cmp.Diff(tt.want, got, cmpopts.IgnoreUnexported(nodeLogQuery{})))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_validateServices(t *testing.T) {
|
|
var (
|
|
service1 = "svc1"
|
|
service2 = "svc2"
|
|
invalid1 = "svc\n"
|
|
invalid2 = "svc.foo\n"
|
|
)
|
|
tests := []struct {
|
|
name string
|
|
services []string
|
|
wantErr bool
|
|
}{
|
|
{name: "one service", services: []string{service1}},
|
|
{name: "two services", services: []string{service1, service2}},
|
|
{name: "invalid service new line", services: []string{invalid1}, wantErr: true},
|
|
{name: "invalid service with dot", services: []string{invalid2}, wantErr: true},
|
|
{name: "long service", services: []string{strings.Repeat(service1, 100)}, wantErr: true},
|
|
{name: "max number of services", services: []string{service1, service2, service1, service2, service1}, wantErr: true},
|
|
}
|
|
for _, tt := range tests {
|
|
errs := validateServices(tt.services)
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if len(errs) > 0 != tt.wantErr {
|
|
t.Errorf("validateServices() error = %v, wantErr %v", errs, tt.wantErr)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_nodeLogQuery_validate(t *testing.T) {
|
|
var (
|
|
service1 = "svc1"
|
|
service2 = "svc2"
|
|
file1 = "/test1.log"
|
|
file2 = "/test2.log"
|
|
pattern = "foo"
|
|
invalid = "foo\\"
|
|
)
|
|
since, err := time.Parse(time.RFC3339, "2023-01-04T02:00:00Z")
|
|
assert.NoError(t, err)
|
|
until, err := time.Parse(time.RFC3339, "2023-02-04T02:00:00Z")
|
|
assert.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
Services []string
|
|
Files []string
|
|
options options
|
|
wantErr bool
|
|
}{
|
|
{name: "empty", wantErr: true},
|
|
{name: "empty with options", options: options{SinceTime: &since}, wantErr: true},
|
|
{name: "one service", Services: []string{service1}},
|
|
{name: "two services", Services: []string{service1, service2}},
|
|
{name: "one service one file", Services: []string{service1}, Files: []string{file1}, wantErr: true},
|
|
{name: "two files", Files: []string{file1, file2}, wantErr: true},
|
|
{name: "one file options", Files: []string{file1}, options: options{Pattern: pattern}, wantErr: true},
|
|
{name: "invalid pattern", Services: []string{service1}, options: options{Pattern: invalid}, wantErr: true},
|
|
{name: "since", Services: []string{service1}, options: options{SinceTime: &since}},
|
|
{name: "until", Services: []string{service1}, options: options{UntilTime: &until}},
|
|
{name: "since until", Services: []string{service1}, options: options{SinceTime: &until, UntilTime: &since},
|
|
wantErr: true},
|
|
{name: "boot", Services: []string{service1}, options: options{Boot: intPtr(-1)}},
|
|
{name: "boot out of range", Services: []string{service1}, options: options{Boot: intPtr(1)}, wantErr: true},
|
|
{name: "tailLines", Services: []string{service1}, options: options{TailLines: intPtr(100)}},
|
|
{name: "tailLines out of range", Services: []string{service1}, options: options{TailLines: intPtr(100000)}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
n := &nodeLogQuery{
|
|
Services: tt.Services,
|
|
Files: tt.Files,
|
|
options: tt.options,
|
|
}
|
|
errs := n.validate()
|
|
if len(errs) > 0 != tt.wantErr {
|
|
t.Errorf("nodeLogQuery.validate() error = %v, wantErr %v", errs, tt.wantErr)
|
|
return
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|