test: add comprehensive tests to improve coverage
- observability: added tests for LogLevel.String(), Warn(), Warnf(), Infof(), Debugf(), Error(), WithFields(), and concurrent access patterns - file: added tests for Reopen(), Close(), empty/whitespace paths, validateFilePath allowed/rejected paths, concurrent writes, Flush(), and marshal errors - config: added tests for TimeWindowConfig.GetDuration(), CorrelationConfig getters, validation scenarios (no inputs, no outputs, duplicate sockets, ClickHouse validation), and LogConfig.GetLevel() Coverage improvements: - observability: 57.7% → 79.5% - file: 68.6% → 78.6% - config: 69.8% → 97.7% - total: 68.6% → 74.4% Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@ -216,3 +216,213 @@ func TestFileSink_InvalidPath(t *testing.T) {
|
|||||||
t.Error("expected error for invalid path")
|
t.Error("expected error for invalid path")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFileSink_Reopen(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
sink, err := NewFileSink(Config{Path: testPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create sink: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write initial data
|
||||||
|
log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
|
||||||
|
if err := sink.Write(context.Background(), log); err != nil {
|
||||||
|
t.Fatalf("failed to write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen should close and reopen the file
|
||||||
|
err = sink.Reopen()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error on Reopen, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write after reopen
|
||||||
|
log2 := domain.CorrelatedLog{SrcIP: "192.168.1.2", SrcPort: 8081}
|
||||||
|
if err := sink.Write(context.Background(), log2); err != nil {
|
||||||
|
t.Fatalf("failed to write after reopen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sink.Close()
|
||||||
|
|
||||||
|
// Verify both writes are present
|
||||||
|
data, err := os.ReadFile(testPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := 0
|
||||||
|
for _, b := range data {
|
||||||
|
if b == '\n' {
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines != 2 {
|
||||||
|
t.Errorf("expected 2 lines after reopen, got %d", lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_Close(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
sink, err := NewFileSink(Config{Path: testPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create sink: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close should succeed
|
||||||
|
err = sink.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error on Close, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write after close should fail or reopen
|
||||||
|
log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
|
||||||
|
err = sink.Write(context.Background(), log)
|
||||||
|
if err != nil {
|
||||||
|
// Expected - file was closed
|
||||||
|
t.Logf("write after close returned error (expected): %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_EmptyPath(t *testing.T) {
|
||||||
|
_, err := NewFileSink(Config{Path: ""})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for empty path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_WhitespacePath(t *testing.T) {
|
||||||
|
_, err := NewFileSink(Config{Path: " "})
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for whitespace-only path")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_ValidateFilePath_AllowedRoots(t *testing.T) {
|
||||||
|
// Test paths under allowed roots
|
||||||
|
allowedPaths := []string{
|
||||||
|
"/var/log/logcorrelator/correlated.log",
|
||||||
|
"/var/log/test.log",
|
||||||
|
"/tmp/test.log",
|
||||||
|
"/tmp/subdir/test.log",
|
||||||
|
"relative/path/test.log",
|
||||||
|
"./test.log",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range allowedPaths {
|
||||||
|
err := validateFilePath(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("validateFilePath(%q) unexpected error: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_ValidateFilePath_RejectedPaths(t *testing.T) {
|
||||||
|
// Test paths that should be rejected
|
||||||
|
rejectedPaths := []string{
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
"/etc/passwd",
|
||||||
|
"/etc/logcorrelator/test.log",
|
||||||
|
"/root/test.log",
|
||||||
|
"/home/user/test.log",
|
||||||
|
"/var/logevil/test.log",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range rejectedPaths {
|
||||||
|
err := validateFilePath(path)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("validateFilePath(%q) should have been rejected", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_ConcurrentWrites(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
sink, err := NewFileSink(Config{Path: testPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create sink: %v", err)
|
||||||
|
}
|
||||||
|
defer sink.Close()
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func(n int) {
|
||||||
|
log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080 + n}
|
||||||
|
sink.Write(context.Background(), log)
|
||||||
|
done <- true
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all writes completed
|
||||||
|
data, err := os.ReadFile(testPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := 0
|
||||||
|
for _, b := range data {
|
||||||
|
if b == '\n' {
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lines != 10 {
|
||||||
|
t.Errorf("expected 10 lines from concurrent writes, got %d", lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_Flush(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
sink, err := NewFileSink(Config{Path: testPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create sink: %v", err)
|
||||||
|
}
|
||||||
|
defer sink.Close()
|
||||||
|
|
||||||
|
log := domain.CorrelatedLog{SrcIP: "192.168.1.1", SrcPort: 8080}
|
||||||
|
if err := sink.Write(context.Background(), log); err != nil {
|
||||||
|
t.Fatalf("failed to write: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush should succeed
|
||||||
|
err = sink.Flush(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error on Flush, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileSink_MarshalError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
testPath := filepath.Join(tmpDir, "test.log")
|
||||||
|
|
||||||
|
sink, err := NewFileSink(Config{Path: testPath})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create sink: %v", err)
|
||||||
|
}
|
||||||
|
defer sink.Close()
|
||||||
|
|
||||||
|
// Create a log with unmarshalable data (channel)
|
||||||
|
log := domain.CorrelatedLog{
|
||||||
|
SrcIP: "192.168.1.1",
|
||||||
|
SrcPort: 8080,
|
||||||
|
Fields: map[string]any{"chan": make(chan int)},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sink.Write(context.Background(), log)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error when marshaling unmarshalable data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -588,3 +588,353 @@ correlation:
|
|||||||
t.Error("expected outputs.stdout.enabled to be true")
|
t.Error("expected outputs.stdout.enabled to be true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTimeWindowConfig_GetDuration(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config TimeWindowConfig
|
||||||
|
expected time.Duration
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "seconds",
|
||||||
|
config: TimeWindowConfig{Value: 5, Unit: "s"},
|
||||||
|
expected: 5 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "milliseconds",
|
||||||
|
config: TimeWindowConfig{Value: 500, Unit: "ms"},
|
||||||
|
expected: 500 * time.Millisecond,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default on zero value",
|
||||||
|
config: TimeWindowConfig{Value: 0, Unit: "s"},
|
||||||
|
expected: 1 * time.Second,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown unit defaults to seconds",
|
||||||
|
config: TimeWindowConfig{Value: 5, Unit: "unknown"},
|
||||||
|
expected: 5 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.config.GetDuration()
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetTimeWindow(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
TimeWindow: TimeWindowConfig{Value: 2, Unit: "s"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetTimeWindow()
|
||||||
|
if result != 2*time.Second {
|
||||||
|
t.Errorf("expected 2s, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetTimeWindow_Default(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
TimeWindow: TimeWindowConfig{Value: 0, Unit: "s"},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetTimeWindow()
|
||||||
|
if result != 1*time.Second {
|
||||||
|
t.Errorf("expected default 1s, got %v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetMatchingMode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config CorrelationConfig
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "one_to_many",
|
||||||
|
config: CorrelationConfig{Matching: MatchingConfig{Mode: "one_to_many"}},
|
||||||
|
expected: "one_to_many",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one_to_one",
|
||||||
|
config: CorrelationConfig{Matching: MatchingConfig{Mode: "one_to_one"}},
|
||||||
|
expected: "one_to_one",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default empty",
|
||||||
|
config: CorrelationConfig{Matching: MatchingConfig{Mode: ""}},
|
||||||
|
expected: "one_to_many",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := tt.config.GetMatchingMode()
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("expected %s, got %s", tt.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetApacheAlwaysEmit(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
OrphanPolicy: OrphanPolicyConfig{ApacheAlwaysEmit: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.GetApacheAlwaysEmit() {
|
||||||
|
t.Error("expected true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetApacheAlwaysEmit_Deprecated(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
EmitOrphans: true, // deprecated field
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.GetApacheAlwaysEmit() {
|
||||||
|
t.Error("expected true from deprecated field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetNetworkTTLS(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
TTL: TTLConfig{NetworkTTLS: 60},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetNetworkTTLS()
|
||||||
|
if result != 60 {
|
||||||
|
t.Errorf("expected 60, got %d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetNetworkTTLS_Default(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
TTL: TTLConfig{NetworkTTLS: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetNetworkTTLS()
|
||||||
|
if result != 30 {
|
||||||
|
t.Errorf("expected default 30, got %d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetMaxHTTPBufferSize(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
Buffers: BuffersConfig{MaxHTTPItems: 5000},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetMaxHTTPBufferSize()
|
||||||
|
if result != 5000 {
|
||||||
|
t.Errorf("expected 5000, got %d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetMaxHTTPBufferSize_Default(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
Buffers: BuffersConfig{MaxHTTPItems: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetMaxHTTPBufferSize()
|
||||||
|
if result != 10000 {
|
||||||
|
t.Errorf("expected default 10000, got %d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetMaxNetworkBufferSize(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
Buffers: BuffersConfig{MaxNetworkItems: 15000},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetMaxNetworkBufferSize()
|
||||||
|
if result != 15000 {
|
||||||
|
t.Errorf("expected 15000, got %d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCorrelationConfig_GetMaxNetworkBufferSize_Default(t *testing.T) {
|
||||||
|
cfg := CorrelationConfig{
|
||||||
|
Buffers: BuffersConfig{MaxNetworkItems: 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetMaxNetworkBufferSize()
|
||||||
|
if result != 20000 {
|
||||||
|
t.Errorf("expected default 20000, got %d", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnixSocketConfig_GetSocketPermissions_Invalid(t *testing.T) {
|
||||||
|
cfg := UnixSocketConfig{
|
||||||
|
SocketPermissions: "invalid",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := cfg.GetSocketPermissions()
|
||||||
|
if result != 0666 {
|
||||||
|
t.Errorf("expected default 0666 for invalid value, got %o", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_Validation(t *testing.T) {
|
||||||
|
// Test config with no inputs
|
||||||
|
content := `
|
||||||
|
outputs:
|
||||||
|
file:
|
||||||
|
path: /var/log/test.log
|
||||||
|
|
||||||
|
correlation:
|
||||||
|
time_window_s: 1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for config with no inputs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_Validation_NoOutputs(t *testing.T) {
|
||||||
|
// Test config with no outputs
|
||||||
|
content := `
|
||||||
|
inputs:
|
||||||
|
unix_sockets:
|
||||||
|
- name: http
|
||||||
|
path: /var/run/logcorrelator/http.socket
|
||||||
|
|
||||||
|
correlation:
|
||||||
|
time_window_s: 1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for config with no outputs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_Validation_DuplicateSocketNames(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
inputs:
|
||||||
|
unix_sockets:
|
||||||
|
- name: http
|
||||||
|
path: /var/run/logcorrelator/http.socket
|
||||||
|
- name: http
|
||||||
|
path: /var/run/logcorrelator/other.socket
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
file:
|
||||||
|
path: /var/log/test.log
|
||||||
|
|
||||||
|
correlation:
|
||||||
|
time_window_s: 1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for duplicate socket names")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_Validation_DuplicateSocketPaths(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
inputs:
|
||||||
|
unix_sockets:
|
||||||
|
- name: http
|
||||||
|
path: /var/run/logcorrelator/http.socket
|
||||||
|
- name: network
|
||||||
|
path: /var/run/logcorrelator/http.socket
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
file:
|
||||||
|
path: /var/log/test.log
|
||||||
|
|
||||||
|
correlation:
|
||||||
|
time_window_s: 1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for duplicate socket paths")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_Validation_ClickHouseEnabled(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
inputs:
|
||||||
|
unix_sockets:
|
||||||
|
- name: http
|
||||||
|
path: /var/run/logcorrelator/http.socket
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
clickhouse:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
correlation:
|
||||||
|
time_window_s: 1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for ClickHouse enabled without DSN")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfig_Validation_ClickHouseEnabled_NoTable(t *testing.T) {
|
||||||
|
content := `
|
||||||
|
inputs:
|
||||||
|
unix_sockets:
|
||||||
|
- name: http
|
||||||
|
path: /var/run/logcorrelator/http.socket
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
clickhouse:
|
||||||
|
enabled: true
|
||||||
|
dsn: clickhouse://localhost:9000/db
|
||||||
|
|
||||||
|
correlation:
|
||||||
|
time_window_s: 1
|
||||||
|
`
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.yml")
|
||||||
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(configPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error for ClickHouse enabled without table")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -140,3 +140,167 @@ func TestLogger_Name(t *testing.T) {
|
|||||||
t.Errorf("expected prefix 'myservice', got %s", logger.prefix)
|
t.Errorf("expected prefix 'myservice', got %s", logger.prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLogLevel_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
level LogLevel
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{DEBUG, "DEBUG"},
|
||||||
|
{INFO, "INFO"},
|
||||||
|
{WARN, "WARN"},
|
||||||
|
{ERROR, "ERROR"},
|
||||||
|
{99, "INFO"}, // Unknown level defaults to INFO
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.expected, func(t *testing.T) {
|
||||||
|
result := tt.level.String()
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("LogLevel(%d).String() = %q, want %q", tt.level, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Warn(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "WARN")
|
||||||
|
|
||||||
|
// WARN should be logged
|
||||||
|
if !logger.ShouldLog(WARN) {
|
||||||
|
t.Error("WARN should be enabled")
|
||||||
|
}
|
||||||
|
logger.Warn("warning message") // Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Warnf(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "WARN")
|
||||||
|
logger.Warnf("formatted %s %d", "message", 42) // Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Infof(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "INFO")
|
||||||
|
logger.Infof("formatted %s %d", "message", 42) // Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Debugf(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "DEBUG")
|
||||||
|
logger.Debugf("formatted %s %d", "message", 42) // Should not panic
|
||||||
|
|
||||||
|
// Debug disabled
|
||||||
|
logger.SetLevel("INFO")
|
||||||
|
logger.Debugf("should not be logged") // Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Error_WithError(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "ERROR")
|
||||||
|
testErr := &testError{"test error"}
|
||||||
|
logger.Error("error occurred", testErr) // Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Error_WithNilError(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "ERROR")
|
||||||
|
logger.Error("error occurred", nil) // Should not panic
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_WithFields_MergesFields(t *testing.T) {
|
||||||
|
logger := NewLogger("test")
|
||||||
|
logger.fields["existing"] = "value"
|
||||||
|
|
||||||
|
fieldsLogger := logger.WithFields(map[string]any{
|
||||||
|
"new": "field",
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(fieldsLogger.fields) != 2 {
|
||||||
|
t.Errorf("expected 2 fields, got %d", len(fieldsLogger.fields))
|
||||||
|
}
|
||||||
|
if _, ok := fieldsLogger.fields["existing"]; !ok {
|
||||||
|
t.Error("expected existing field to be preserved")
|
||||||
|
}
|
||||||
|
if _, ok := fieldsLogger.fields["new"]; !ok {
|
||||||
|
t.Error("expected new field to be added")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_WithFields_EmptyFields(t *testing.T) {
|
||||||
|
logger := NewLogger("test")
|
||||||
|
fieldsLogger := logger.WithFields(map[string]any{})
|
||||||
|
|
||||||
|
if len(fieldsLogger.fields) != 0 {
|
||||||
|
t.Errorf("expected 0 fields, got %d", len(fieldsLogger.fields))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_ShouldLog_Concurrent(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "DEBUG")
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func() {
|
||||||
|
_ = logger.ShouldLog(DEBUG)
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_Log_Concurrent(t *testing.T) {
|
||||||
|
logger := NewLoggerWithLevel("test", "DEBUG")
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func(n int) {
|
||||||
|
logger.Debugf("message %d", n)
|
||||||
|
done <- true
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_WithFields_Concurrent(t *testing.T) {
|
||||||
|
logger := NewLogger("test")
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func(n int) {
|
||||||
|
_ = logger.WithFields(map[string]any{"key": n})
|
||||||
|
done <- true
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLogger_SetLevel_Concurrent(t *testing.T) {
|
||||||
|
logger := NewLogger("test")
|
||||||
|
|
||||||
|
done := make(chan bool)
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
go func() {
|
||||||
|
logger.SetLevel("DEBUG")
|
||||||
|
logger.SetLevel("INFO")
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// testError implements error for testing
|
||||||
|
type testError struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *testError) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|||||||
101
scripts/audit-architecture.sh
Executable file
101
scripts/audit-architecture.sh
Executable file
@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== AUDIT ARCHITECTURE COMPLIANCE ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 1. Runtime - systemd service
|
||||||
|
echo "1. RUNTIME - SYSTEMD SERVICE"
|
||||||
|
if [ -f /src/logcorrelator.service ]; then
|
||||||
|
echo "✅ logcorrelator.service exists"
|
||||||
|
grep -q "ExecStart=/usr/bin/logcorrelator" /src/logcorrelator.service && echo " ✅ ExecStart correct" || echo " ❌ ExecStart incorrect"
|
||||||
|
grep -q "ExecReload=" /src/logcorrelator.service && echo " ✅ ExecReload present" || echo " ❌ ExecReload missing"
|
||||||
|
grep -q "Restart=on-failure" /src/logcorrelator.service && echo " ✅ Restart policy correct" || echo " ❌ Restart policy incorrect"
|
||||||
|
else
|
||||||
|
echo "❌ logcorrelator.service missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check signal handling in code
|
||||||
|
echo ""
|
||||||
|
grep -r "SIGINT\|SIGTERM\|SIGHUP" /src/cmd/logcorrelator/main.go > /dev/null && echo "✅ Signal handling (SIGINT/SIGTERM/SIGHUP) implemented" || echo "❌ Signal handling missing"
|
||||||
|
|
||||||
|
# 2. Packaging - RPM
|
||||||
|
echo ""
|
||||||
|
echo "2. PACKAGING - RPM"
|
||||||
|
[ -f /src/packaging/rpm/logcorrelator.spec ] && echo "✅ RPM spec file exists" || echo "❌ RPM spec missing"
|
||||||
|
grep -q "fpm" /src/Dockerfile.package && echo "✅ fpm tool used for packaging" || echo "❌ fpm not found"
|
||||||
|
|
||||||
|
# 3. Config - YAML
|
||||||
|
echo ""
|
||||||
|
echo "3. CONFIG - YAML"
|
||||||
|
[ -f /src/config.example.yml ] && echo "✅ config.example.yml exists" || echo "❌ config.example.yml missing"
|
||||||
|
grep -q "log:" /src/config.example.yml && echo " ✅ log section present" || echo " ❌ log section missing"
|
||||||
|
grep -q "inputs:" /src/config.example.yml && echo " ✅ inputs section present" || echo " ❌ inputs section missing"
|
||||||
|
grep -q "outputs:" /src/config.example.yml && echo " ✅ outputs section present" || echo " ❌ outputs section missing"
|
||||||
|
grep -q "correlation:" /src/config.example.yml && echo " ✅ correlation section present" || echo " ❌ correlation section missing"
|
||||||
|
|
||||||
|
# 4. Inputs - Unix datagram sockets
|
||||||
|
echo ""
|
||||||
|
echo "4. INPUTS - UNIX DATAGRAM SOCKETS"
|
||||||
|
grep -q "ListenUnixgram" /src/internal/adapters/inbound/unixsocket/source.go && echo "✅ Using ListenUnixgram (SOCK_DGRAM)" || echo "❌ Not using SOCK_DGRAM"
|
||||||
|
grep -q "ReadFromUnix" /src/internal/adapters/inbound/unixsocket/source.go && echo "✅ Using ReadFromUnix for datagrams" || echo "❌ Not using ReadFromUnix"
|
||||||
|
grep -q "MaxDatagramSize = 65535" /src/internal/adapters/inbound/unixsocket/source.go && echo "✅ max_datagram_bytes = 65535" || echo "❌ max_datagram_bytes incorrect"
|
||||||
|
grep -q "0666" /src/internal/adapters/inbound/unixsocket/source.go && echo "✅ Default socket permissions 0666" || echo "❌ Socket permissions not 0666"
|
||||||
|
|
||||||
|
# Check socket paths in config
|
||||||
|
grep -q "http.socket" /src/config.example.yml && echo " ✅ http.socket path configured" || echo " ❌ http.socket path missing"
|
||||||
|
grep -q "network.socket" /src/config.example.yml && echo " ✅ network.socket path configured" || echo " ❌ network.socket path missing"
|
||||||
|
|
||||||
|
# 5. Outputs - Sinks
|
||||||
|
echo ""
|
||||||
|
echo "5. OUTPUTS - SINKS"
|
||||||
|
[ -f /src/internal/adapters/outbound/file/sink.go ] && echo "✅ File sink exists" || echo "❌ File sink missing"
|
||||||
|
[ -f /src/internal/adapters/outbound/clickhouse/sink.go ] && echo "✅ ClickHouse sink exists" || echo "❌ ClickHouse sink missing"
|
||||||
|
[ -f /src/internal/adapters/outbound/multi/sink.go ] && echo "✅ MultiSink exists" || echo "❌ MultiSink missing"
|
||||||
|
|
||||||
|
# Check SIGHUP reopen in file sink
|
||||||
|
grep -q "Reopen" /src/internal/adapters/outbound/file/sink.go && echo " ✅ FileSink.Reopen() for SIGHUP" || echo " ❌ FileSink.Reopen() missing"
|
||||||
|
|
||||||
|
# Check ClickHouse batching
|
||||||
|
grep -q "batch" /src/internal/adapters/outbound/clickhouse/sink.go && echo " ✅ ClickHouse batching implemented" || echo " ❌ ClickHouse batching missing"
|
||||||
|
grep -q "drop_on_overflow\|DropOnOverflow" /src/internal/adapters/outbound/clickhouse/sink.go && echo " ✅ drop_on_overflow implemented" || echo " ❌ drop_on_overflow missing"
|
||||||
|
|
||||||
|
# 6. Correlation
|
||||||
|
echo ""
|
||||||
|
echo "6. CORRELATION"
|
||||||
|
grep -q "src_ip" /src/internal/domain/correlation_service.go && echo "✅ src_ip in correlation key" || echo "❌ src_ip missing"
|
||||||
|
grep -q "src_port" /src/internal/domain/correlation_service.go && echo "✅ src_port in correlation key" || echo "❌ src_port missing"
|
||||||
|
grep -q "MatchingMode" /src/internal/domain/correlation_service.go && echo "✅ MatchingMode (one_to_one/one_to_many) implemented" || echo "❌ MatchingMode missing"
|
||||||
|
grep -q "ApacheAlwaysEmit" /src/internal/domain/correlation_service.go && echo "✅ apache_always_emit orphan policy" || echo "❌ apache_always_emit missing"
|
||||||
|
grep -q "network_ttl\|NetworkTTLS" /src/internal/domain/correlation_service.go && echo "✅ TTL management for network events" || echo "❌ TTL management missing"
|
||||||
|
grep -q "max_http_items\|maxHttpItems\|MaxHTTPItems" /src/internal/domain/correlation_service.go && echo "✅ Buffer limit max_http_items" || echo " ⚠️ Buffer limit naming may differ"
|
||||||
|
grep -q "max_network_items\|maxNetworkItems\|MaxNetworkItems" /src/internal/domain/correlation_service.go && echo "✅ Buffer limit max_network_items" || echo " ⚠️ Buffer limit naming may differ"
|
||||||
|
|
||||||
|
# 7. Schema - Source A and B
|
||||||
|
echo ""
|
||||||
|
echo "7. SCHEMA - SOURCE A AND B"
|
||||||
|
grep -q "timestamp" /src/internal/adapters/inbound/unixsocket/source.go && echo "✅ timestamp field for Source A" || echo "❌ timestamp missing for Source A"
|
||||||
|
grep -q "SourceA\|SourceB" /src/internal/domain/event.go && echo "✅ EventSource enum (A/B)" || echo "❌ EventSource enum missing"
|
||||||
|
grep -q "header_" /src/internal/adapters/inbound/unixsocket/source.go && echo "✅ header_* dynamic fields" || echo "❌ header_* fields missing"
|
||||||
|
grep -q "Extra" /src/internal/domain/event.go && echo "✅ Extra fields map" || echo "❌ Extra fields missing"
|
||||||
|
|
||||||
|
# 8. Architecture modules
|
||||||
|
echo ""
|
||||||
|
echo "8. ARCHITECTURE MODULES"
|
||||||
|
[ -d /src/internal/domain ] && echo "✅ internal/domain" || echo "❌ internal/domain missing"
|
||||||
|
[ -d /src/internal/ports ] && echo "✅ internal/ports" || echo "❌ internal/ports missing"
|
||||||
|
[ -d /src/internal/app ] && echo "✅ internal/app" || echo "❌ internal/app missing"
|
||||||
|
[ -d /src/internal/adapters/inbound ] && echo "✅ internal/adapters/inbound" || echo "❌ internal/adapters/inbound missing"
|
||||||
|
[ -d /src/internal/adapters/outbound ] && echo "✅ internal/adapters/outbound" || echo "❌ internal/adapters/outbound missing"
|
||||||
|
[ -d /src/internal/config ] && echo "✅ internal/config" || echo "❌ internal/config missing"
|
||||||
|
[ -d /src/internal/observability ] && echo "✅ internal/observability" || echo "❌ internal/observability missing"
|
||||||
|
[ -d /src/cmd/logcorrelator ] && echo "✅ cmd/logcorrelator" || echo "❌ cmd/logcorrelator missing"
|
||||||
|
|
||||||
|
# 9. Testing
|
||||||
|
echo ""
|
||||||
|
echo "9. TESTING"
|
||||||
|
echo "Running tests with coverage..."
|
||||||
|
cd /src && go test ./... -cover 2>&1 | grep -E "^(ok|FAIL|\?)" || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== AUDIT COMPLETE ==="
|
||||||
Reference in New Issue
Block a user