mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
fdba14b9a3
The "func" field in log output is incorrect in some cases and showing internal logging methods instead of the actual calling function. This is happening because the caller information is being captured statically (5 call frames) which can be different for different call patterns. Changes: * Move the caller capture to the event creation * Use deterministic skip (3 frames) for event creation * Add test cases to verify that the caller is captured correctly Note: tests needed to written outside convey to avoid call insertion Signed-off-by: Ravi Chamarthy <ravi@chamarthy.dev>
425 lines
11 KiB
Go
425 lines
11 KiB
Go
//go:build sync && scrub && metrics && search
|
|
// +build sync,scrub,metrics,search
|
|
|
|
package log_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
godigest "github.com/opencontainers/go-digest"
|
|
. "github.com/smartystreets/goconvey/convey"
|
|
"gopkg.in/resty.v1"
|
|
|
|
"zotregistry.dev/zot/v2/pkg/api"
|
|
"zotregistry.dev/zot/v2/pkg/api/config"
|
|
"zotregistry.dev/zot/v2/pkg/api/constants"
|
|
"zotregistry.dev/zot/v2/pkg/log"
|
|
test "zotregistry.dev/zot/v2/pkg/test/common"
|
|
)
|
|
|
|
type AuditLog struct {
|
|
Level string `json:"level"`
|
|
ClientIP string `json:"clientIP"` //nolint:tagliatelle // keep IP
|
|
Subject string `json:"subject"`
|
|
Action string `json:"action"`
|
|
Object string `json:"object"`
|
|
Status int `json:"status"`
|
|
Time string `json:"time"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
func TestAuditLogMessages(t *testing.T) {
|
|
Convey("Make a new controller", t, func() {
|
|
dir := t.TempDir()
|
|
|
|
port := test.GetFreePort()
|
|
baseURL := test.GetBaseURL(port)
|
|
conf := config.New()
|
|
|
|
outputPath := dir + "/zot.log"
|
|
auditPath := dir + "/zot-audit.log"
|
|
conf.Log = &config.LogConfig{Level: "debug", Output: outputPath, Audit: auditPath}
|
|
|
|
conf.HTTP.Port = port
|
|
|
|
username, seedUser := test.GenerateRandomString()
|
|
password, seedPass := test.GenerateRandomString()
|
|
htpasswdPath := test.MakeHtpasswdFileFromString(test.GetCredString(username, password))
|
|
|
|
defer os.Remove(htpasswdPath)
|
|
|
|
conf.HTTP.Auth = &config.AuthConfig{
|
|
HTPasswd: config.AuthHTPasswd{
|
|
Path: htpasswdPath,
|
|
},
|
|
}
|
|
|
|
ctlr := api.NewController(conf)
|
|
ctlr.Log.Info().Int64("seedUser", seedUser).Int64("seedPass", seedPass).Msg("random seed for username & password")
|
|
ctlr.Config.Storage.RootDirectory = dir
|
|
|
|
ctlrManager := test.NewControllerManager(ctlr)
|
|
ctlrManager.StartAndWait(port)
|
|
defer ctlrManager.StopServer()
|
|
|
|
Convey("Open auditLog file", func() {
|
|
auditFile, err := os.Open(auditPath)
|
|
if err != nil {
|
|
t.Log("Cannot open file")
|
|
panic(err)
|
|
}
|
|
defer auditFile.Close()
|
|
|
|
Convey("Test GET request", func() {
|
|
resp, err := resty.R().SetBasicAuth(username, password).Get(baseURL + "/v2/")
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusOK)
|
|
|
|
byteValue, _ := io.ReadAll(auditFile)
|
|
So(len(byteValue), ShouldEqual, 0)
|
|
})
|
|
|
|
Convey("Test POST request", func() {
|
|
repoName := "everyone/isallowed"
|
|
path := "/v2/" + repoName + "/blobs/uploads/"
|
|
resp, err := resty.R().SetBasicAuth(username, password).Post(baseURL + path)
|
|
So(err, ShouldBeNil)
|
|
So(resp, ShouldNotBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// wait until the file is populated
|
|
byteValue, _ := io.ReadAll(auditFile)
|
|
|
|
for {
|
|
if len(byteValue) != 0 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
}
|
|
|
|
var auditLog AuditLog
|
|
|
|
err = json.Unmarshal(byteValue, &auditLog)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(auditLog.Subject, ShouldEqual, username)
|
|
So(auditLog.Action, ShouldEqual, http.MethodPost)
|
|
So(auditLog.Status, ShouldEqual, http.StatusAccepted)
|
|
So(auditLog.Object, ShouldEqual, path)
|
|
})
|
|
|
|
Convey("Test PUT and DELETE request", func() {
|
|
// create upload
|
|
path := "/v2/repo/blobs/uploads/"
|
|
resp, err := resty.R().SetBasicAuth(username, password).Post(baseURL + path)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
So(loc, ShouldNotBeEmpty)
|
|
location := resp.Header().Get("Location")
|
|
So(location, ShouldNotBeEmpty)
|
|
|
|
// wait until the file is populated
|
|
byteValue, _ := io.ReadAll(auditFile)
|
|
|
|
for {
|
|
if len(byteValue) != 0 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
}
|
|
|
|
var auditLog AuditLog
|
|
|
|
err = json.Unmarshal(byteValue, &auditLog)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(auditLog.Subject, ShouldEqual, username)
|
|
So(auditLog.Action, ShouldEqual, http.MethodPost)
|
|
So(auditLog.Status, ShouldEqual, http.StatusAccepted)
|
|
So(auditLog.Object, ShouldEqual, path)
|
|
|
|
content := []byte("this is a blob")
|
|
digest := godigest.FromBytes(content)
|
|
So(digest, ShouldNotBeNil)
|
|
|
|
// blob upload
|
|
resp, err = resty.R().SetQueryParam("digest", digest.String()).
|
|
SetBasicAuth(username, password).
|
|
SetHeader("Content-Type", "application/octet-stream").SetBody(content).Put(loc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusCreated)
|
|
blobLoc := test.Location(baseURL, resp)
|
|
So(blobLoc, ShouldNotBeEmpty)
|
|
So(resp.Header().Get(constants.DistContentDigestKey), ShouldNotBeEmpty)
|
|
|
|
// wait until the file is populated
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
|
|
for {
|
|
if len(byteValue) != 0 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
}
|
|
|
|
err = json.Unmarshal(byteValue, &auditLog)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(auditLog.Subject, ShouldEqual, username)
|
|
So(auditLog.Action, ShouldEqual, http.MethodPut)
|
|
So(auditLog.Status, ShouldEqual, http.StatusCreated)
|
|
|
|
putPath := location + "?digest=" + strings.ReplaceAll(digest.String(), ":", "%3A")
|
|
So(auditLog.Object, ShouldEqual, putPath)
|
|
|
|
// delete this blob
|
|
resp, err = resty.R().SetBasicAuth(username, password).Delete(blobLoc)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
So(resp.Header().Get("Content-Length"), ShouldEqual, "0")
|
|
|
|
// wait until the file is populated
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
|
|
for {
|
|
if len(byteValue) != 0 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
}
|
|
|
|
err = json.Unmarshal(byteValue, &auditLog)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(auditLog.Subject, ShouldEqual, username)
|
|
So(auditLog.Action, ShouldEqual, http.MethodDelete)
|
|
So(auditLog.Status, ShouldEqual, http.StatusAccepted)
|
|
|
|
deletePath := strings.ReplaceAll(path, "uploads/", digest.String())
|
|
So(auditLog.Object, ShouldEqual, deletePath)
|
|
})
|
|
|
|
Convey("Test PATCH request", func() {
|
|
path := "/v2/repo/blobs/uploads/"
|
|
resp, err := resty.R().SetBasicAuth(username, password).Post(baseURL + path)
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
loc := test.Location(baseURL, resp)
|
|
So(loc, ShouldNotBeEmpty)
|
|
location := resp.Header().Get("Location")
|
|
So(location, ShouldNotBeEmpty)
|
|
|
|
// wait until the file is populated
|
|
byteValue, _ := io.ReadAll(auditFile)
|
|
|
|
for {
|
|
if len(byteValue) != 0 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
}
|
|
|
|
var auditLog AuditLog
|
|
|
|
err = json.Unmarshal(byteValue, &auditLog)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(auditLog.Subject, ShouldEqual, username)
|
|
So(auditLog.Action, ShouldEqual, http.MethodPost)
|
|
So(auditLog.Status, ShouldEqual, http.StatusAccepted)
|
|
So(auditLog.Object, ShouldEqual, path)
|
|
|
|
var buf bytes.Buffer
|
|
chunk := []byte("this is a chunk")
|
|
n, err := buf.Write(chunk)
|
|
So(n, ShouldEqual, len(chunk))
|
|
So(err, ShouldBeNil)
|
|
|
|
// write a chunk
|
|
contentRange := fmt.Sprintf("%d-%d", 0, len(chunk)-1)
|
|
resp, err = resty.R().SetBasicAuth(username, password).
|
|
SetHeader("Content-Type", "application/octet-stream").
|
|
SetHeader("Content-Range", contentRange).SetBody(chunk).Patch(loc)
|
|
|
|
So(err, ShouldBeNil)
|
|
So(resp.StatusCode(), ShouldEqual, http.StatusAccepted)
|
|
|
|
// wait until the file is populated
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
|
|
for {
|
|
if len(byteValue) != 0 {
|
|
break
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
byteValue, _ = io.ReadAll(auditFile)
|
|
}
|
|
|
|
err = json.Unmarshal(byteValue, &auditLog)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
So(auditLog.Subject, ShouldEqual, username)
|
|
So(auditLog.Action, ShouldEqual, http.MethodPatch)
|
|
So(auditLog.Status, ShouldEqual, http.StatusAccepted)
|
|
|
|
patchPath := location
|
|
So(auditLog.Object, ShouldEqual, patchPath)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestLogErrors(t *testing.T) {
|
|
Convey("Get error with unknown log level", t, func() {
|
|
So(func() { _ = log.NewLogger("invalid", "test.out") }, ShouldPanic)
|
|
})
|
|
|
|
Convey("Get error when opening log file", t, func() {
|
|
dir := t.TempDir()
|
|
logPath := path.Join(dir, "logFile")
|
|
err := os.WriteFile(logPath, []byte{}, 0o000)
|
|
So(err, ShouldBeNil)
|
|
So(func() {
|
|
_ = log.NewLogger("debug", logPath)
|
|
}, ShouldPanic)
|
|
})
|
|
}
|
|
|
|
func TestNewAuditLogger(t *testing.T) {
|
|
Convey("Get error with unknown audit log level", t, func() {
|
|
So(func() { _ = log.NewAuditLogger("invalid", "test.out") }, ShouldPanic)
|
|
})
|
|
|
|
Convey("Get error when opening audit file", t, func() {
|
|
dir := t.TempDir()
|
|
logPath := path.Join(dir, "logFile")
|
|
err := os.WriteFile(logPath, []byte{}, 0o000)
|
|
So(err, ShouldBeNil)
|
|
So(func() {
|
|
_ = log.NewAuditLogger("debug", logPath)
|
|
}, ShouldPanic)
|
|
})
|
|
}
|
|
|
|
func TestCallerFunction(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
logger := log.NewLoggerWithWriter("debug", &buf)
|
|
|
|
// Test case 1: Simple log without chaining
|
|
logger.Info().Msg("test message 1")
|
|
output1 := buf.String()
|
|
|
|
if !strings.Contains(output1, "test message 1") {
|
|
t.Errorf("Expected output to contain 'test message 1', got: %s", output1)
|
|
}
|
|
|
|
if !strings.Contains(output1, "TestCallerFunction") {
|
|
t.Errorf("Expected output to contain 'TestCallerFunction', got: %s", output1)
|
|
}
|
|
|
|
if strings.Contains(output1, "(*Event).Str") {
|
|
t.Errorf("Expected output NOT to contain '(*Event).Str', got: %s", output1)
|
|
}
|
|
|
|
// Reset buffer
|
|
buf.Reset()
|
|
|
|
// Test case 2: Log with Str() chaining (this was the problematic case)
|
|
logger.Info().Str("module", "test").Msg("test message 2")
|
|
output2 := buf.String()
|
|
|
|
if !strings.Contains(output2, "test message 2") {
|
|
t.Errorf("Expected output to contain 'test message 2', got: %s", output2)
|
|
}
|
|
|
|
if !strings.Contains(output2, "module") {
|
|
t.Errorf("Expected output to contain 'module', got: %s", output2)
|
|
}
|
|
|
|
if !strings.Contains(output2, "TestCallerFunction") {
|
|
t.Errorf("Expected output to contain 'TestCallerFunction', got: %s", output2)
|
|
}
|
|
|
|
if strings.Contains(output2, "(*Event).Str") {
|
|
t.Errorf("Expected output NOT to contain '(*Event).Str', got: %s", output2)
|
|
}
|
|
|
|
// Reset buffer
|
|
buf.Reset()
|
|
|
|
// Test case 3: Log with multiple chainings
|
|
logger.Error().Str("module", "test").Str("error", "something").Msg("test error message")
|
|
output3 := buf.String()
|
|
|
|
if !strings.Contains(output3, "test error message") {
|
|
t.Errorf("Expected output to contain 'test error message', got: %s", output3)
|
|
}
|
|
|
|
if !strings.Contains(output3, "TestCallerFunction") {
|
|
t.Errorf("Expected output to contain 'TestCallerFunction', got: %s", output3)
|
|
}
|
|
|
|
if strings.Contains(output3, "(*Event).Str") {
|
|
t.Errorf("Expected output NOT to contain '(*Event).Str', got: %s", output3)
|
|
}
|
|
|
|
// Reset buffer
|
|
buf.Reset()
|
|
|
|
// Test case 4: Debug with Int
|
|
logger.Debug().Int("count", 42).Msg("test debug message")
|
|
output4 := buf.String()
|
|
|
|
if !strings.Contains(output4, "test debug message") {
|
|
t.Errorf("Expected output to contain 'test debug message', got: %s", output4)
|
|
}
|
|
|
|
if !strings.Contains(output4, "TestCallerFunction") {
|
|
t.Errorf("Expected output to contain 'TestCallerFunction', got: %s", output4)
|
|
}
|
|
|
|
if strings.Contains(output4, "(*Event).Int") {
|
|
t.Errorf("Expected output NOT to contain '(*Event).Int', got: %s", output4)
|
|
}
|
|
}
|