Nouvelles vues de détection (sql/views.sql) : - Identification hosts par IP/JA4 (view_host_identification, view_host_ja4_anomalies) - Détection brute force POST et query params variables - Header fingerprinting (ordre, headers modernes manquants, Sec-CH-UA) - ALPN mismatch detection (h2 déclaré mais HTTP/1.1 parlé) - Rate limiting & burst detection (50 req/min, 20 req/10s) - Path enumeration/scanning (paths sensibles) - Payload attacks (SQLi, XSS, path traversal) - JA4 botnet detection (même fingerprint sur 20+ IPs) - Correlation quality (orphan ratio >80%) ClickHouse (sql/init.sql) : - Compression ZSTD(3) sur champs texte (path, query, headers, ja3/ja4) - TTL automatique : 1 jour (raw) + 7 jours (http_logs) - Paramètre ttl_only_drop_parts = 1 Shutdown simplifié (internal/app/orchestrator.go) : - Suppression ShutdownTimeout et logique de flush/attente - Stop() = cancel() + Close() uniquement - systemd TimeoutStopSec gère l'arrêt forcé si besoin File output toggle (internal/config/*.go) : - Ajout champ Enabled dans FileOutputConfig - Le sink fichier n'est créé que si enabled && path != '' - Tests : TestValidate_FileOutputDisabled, TestLoadConfig_FileOutputDisabled RPM packaging (packaging/rpm/logcorrelator.spec) : - Changelog 1.1.18 → 1.1.22 - Suppression logcorrelator-tmpfiles.conf (redondant RuntimeDirectory=) Nettoyage : - idees.txt → idees/ (dossier) - Suppression 91.224.92.185.txt (logs exemple) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1011 lines
22 KiB
Go
1011 lines
22 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoad_ValidConfig(t *testing.T) {
|
|
content := `
|
|
inputs:
|
|
unix_sockets:
|
|
- name: http
|
|
path: /var/run/logcorrelator/http.socket
|
|
format: json
|
|
- name: network
|
|
path: /var/run/logcorrelator/network.socket
|
|
format: json
|
|
|
|
outputs:
|
|
file:
|
|
path: /var/log/logcorrelator/correlated.log
|
|
clickhouse:
|
|
dsn: clickhouse://user:pass@localhost:9000/db
|
|
table: correlated_logs
|
|
|
|
correlation:
|
|
time_window_s: 1
|
|
emit_orphans: true
|
|
`
|
|
|
|
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)
|
|
}
|
|
|
|
cfg, err := Load(configPath)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(cfg.Inputs.UnixSockets) != 2 {
|
|
t.Errorf("expected 2 unix sockets, got %d", len(cfg.Inputs.UnixSockets))
|
|
}
|
|
if cfg.Outputs.File.Path != "/var/log/logcorrelator/correlated.log" {
|
|
t.Errorf("expected file path /var/log/logcorrelator/correlated.log, got %s", cfg.Outputs.File.Path)
|
|
}
|
|
if !cfg.Outputs.File.Enabled {
|
|
t.Error("expected file output to be enabled by default when path is set")
|
|
}
|
|
}
|
|
|
|
func TestLoad_InvalidPath(t *testing.T) {
|
|
_, err := Load("/nonexistent/path/config.yml")
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent path")
|
|
}
|
|
}
|
|
|
|
func TestLoad_InvalidYAML(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yml")
|
|
content := `invalid: yaml: content: [`
|
|
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 invalid YAML")
|
|
}
|
|
}
|
|
|
|
func TestLoad_DefaultValues(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.yml")
|
|
content := `
|
|
inputs:
|
|
unix_sockets:
|
|
- name: a
|
|
path: /tmp/a.sock
|
|
- name: b
|
|
path: /tmp/b.sock
|
|
outputs:
|
|
file:
|
|
path: /var/log/test.log
|
|
`
|
|
if err := os.WriteFile(configPath, []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to write config: %v", err)
|
|
}
|
|
|
|
cfg, err := Load(configPath)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Check defaults
|
|
if cfg.Correlation.TimeWindowS != 1 {
|
|
t.Errorf("expected default time window value 1, got %d", cfg.Correlation.TimeWindowS)
|
|
}
|
|
if cfg.Correlation.EmitOrphans != true {
|
|
t.Error("expected default emit_orphans to be true")
|
|
}
|
|
}
|
|
|
|
func TestValidate_MinimumInputs(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "only_one", Path: "/tmp/test.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: "/var/log/test.log"},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for less than 2 inputs")
|
|
}
|
|
}
|
|
|
|
func TestValidate_AtLeastOneOutput(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: false},
|
|
ClickHouse: ClickHouseOutputConfig{Enabled: false},
|
|
Stdout: StdoutOutputConfig{Enabled: false},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for no outputs enabled")
|
|
}
|
|
}
|
|
|
|
func TestGetTimeWindow(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config CorrelationConfig
|
|
expected time.Duration
|
|
}{
|
|
{
|
|
name: "1 second",
|
|
config: CorrelationConfig{
|
|
TimeWindowS: 1,
|
|
},
|
|
expected: time.Second,
|
|
},
|
|
{
|
|
name: "5 seconds",
|
|
config: CorrelationConfig{
|
|
TimeWindowS: 5,
|
|
},
|
|
expected: 5 * time.Second,
|
|
},
|
|
{
|
|
name: "default",
|
|
config: CorrelationConfig{
|
|
TimeWindowS: 0,
|
|
},
|
|
expected: time.Second,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.config.GetTimeWindow()
|
|
if result != tt.expected {
|
|
t.Errorf("expected %v, got %v", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidate_DuplicateNames(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "same", Path: "/tmp/a.sock"},
|
|
{Name: "same", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: "/var/log/test.log"},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for duplicate names")
|
|
}
|
|
}
|
|
|
|
func TestValidate_DuplicatePaths(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/same.sock"},
|
|
{Name: "b", Path: "/tmp/same.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: "/var/log/test.log"},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for duplicate paths")
|
|
}
|
|
}
|
|
|
|
func TestValidate_EmptyName(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: "/var/log/test.log"},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for empty name")
|
|
}
|
|
}
|
|
|
|
func TestValidate_EmptyPath(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: ""},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: "/var/log/test.log"},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for empty path")
|
|
}
|
|
}
|
|
|
|
func TestValidate_EmptyFilePath(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: ""},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for empty file path")
|
|
}
|
|
}
|
|
|
|
func TestValidate_ClickHouseMissingDSN(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: ""},
|
|
ClickHouse: ClickHouseOutputConfig{
|
|
Enabled: true,
|
|
DSN: "",
|
|
Table: "test",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for missing ClickHouse DSN")
|
|
}
|
|
}
|
|
|
|
func TestValidate_ClickHouseMissingTable(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: ""},
|
|
ClickHouse: ClickHouseOutputConfig{
|
|
Enabled: true,
|
|
DSN: "clickhouse://localhost:9000/db",
|
|
Table: "",
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for missing ClickHouse table")
|
|
}
|
|
}
|
|
|
|
func TestValidate_ClickHouseInvalidBatchSize(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: ""},
|
|
ClickHouse: ClickHouseOutputConfig{
|
|
Enabled: true,
|
|
DSN: "clickhouse://localhost:9000/db",
|
|
Table: "test",
|
|
BatchSize: 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for invalid batch size")
|
|
}
|
|
}
|
|
|
|
func TestValidate_ClickHouseInvalidMaxBufferSize(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: ""},
|
|
ClickHouse: ClickHouseOutputConfig{
|
|
Enabled: true,
|
|
DSN: "clickhouse://localhost:9000/db",
|
|
Table: "test",
|
|
BatchSize: 100,
|
|
MaxBufferSize: 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for invalid max buffer size")
|
|
}
|
|
}
|
|
|
|
func TestValidate_ClickHouseInvalidTimeout(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: ""},
|
|
ClickHouse: ClickHouseOutputConfig{
|
|
Enabled: true,
|
|
DSN: "clickhouse://localhost:9000/db",
|
|
Table: "test",
|
|
BatchSize: 100,
|
|
TimeoutMs: 0,
|
|
},
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for invalid timeout")
|
|
}
|
|
}
|
|
|
|
func TestValidate_EmptyCorrelationKey(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: true, Path: "/var/log/test.log"},
|
|
},
|
|
Correlation: CorrelationConfig{
|
|
TimeWindowS: 0,
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Error("expected error for invalid time window")
|
|
}
|
|
}
|
|
|
|
func TestGetSocketPermissions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config UnixSocketConfig
|
|
expected os.FileMode
|
|
}{
|
|
{
|
|
name: "default",
|
|
config: UnixSocketConfig{
|
|
SocketPermissions: "",
|
|
},
|
|
expected: 0666,
|
|
},
|
|
{
|
|
name: "explicit 0660",
|
|
config: UnixSocketConfig{
|
|
SocketPermissions: "0660",
|
|
},
|
|
expected: 0660,
|
|
},
|
|
{
|
|
name: "explicit 0666",
|
|
config: UnixSocketConfig{
|
|
SocketPermissions: "0666",
|
|
},
|
|
expected: 0666,
|
|
},
|
|
{
|
|
name: "without leading zero",
|
|
config: UnixSocketConfig{
|
|
SocketPermissions: "660",
|
|
},
|
|
expected: 0660,
|
|
},
|
|
{
|
|
name: "invalid value",
|
|
config: UnixSocketConfig{
|
|
SocketPermissions: "invalid",
|
|
},
|
|
expected: 0666,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.config.GetSocketPermissions()
|
|
if result != tt.expected {
|
|
t.Errorf("expected %o, got %o", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogConfig_GetLevel(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config LogConfig
|
|
expected string
|
|
}{
|
|
{
|
|
name: "default",
|
|
config: LogConfig{Level: ""},
|
|
expected: "INFO",
|
|
},
|
|
{
|
|
name: "DEBUG uppercase",
|
|
config: LogConfig{Level: "DEBUG"},
|
|
expected: "DEBUG",
|
|
},
|
|
{
|
|
name: "debug lowercase",
|
|
config: LogConfig{Level: "debug"},
|
|
expected: "DEBUG",
|
|
},
|
|
{
|
|
name: "WARN",
|
|
config: LogConfig{Level: "WARN"},
|
|
expected: "WARN",
|
|
},
|
|
{
|
|
name: "ERROR",
|
|
config: LogConfig{Level: "ERROR"},
|
|
expected: "ERROR",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := tt.config.GetLevel()
|
|
if result != tt.expected {
|
|
t.Errorf("GetLevel() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoad_LogLevel(t *testing.T) {
|
|
content := `
|
|
log:
|
|
level: DEBUG
|
|
|
|
inputs:
|
|
unix_sockets:
|
|
- name: http
|
|
path: /var/run/logcorrelator/http.socket
|
|
- name: network
|
|
path: /var/run/logcorrelator/network.socket
|
|
|
|
outputs:
|
|
file:
|
|
path: /var/log/test.log
|
|
|
|
correlation:
|
|
time_window_s: 1
|
|
emit_orphans: true
|
|
`
|
|
|
|
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)
|
|
}
|
|
|
|
cfg, err := Load(configPath)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if cfg.Log.GetLevel() != "DEBUG" {
|
|
t.Errorf("expected log level DEBUG, got %s", cfg.Log.GetLevel())
|
|
}
|
|
}
|
|
|
|
func TestLoad_StdoutEnabledObject(t *testing.T) {
|
|
content := `
|
|
inputs:
|
|
unix_sockets:
|
|
- name: http
|
|
path: /var/run/logcorrelator/http.socket
|
|
- name: network
|
|
path: /var/run/logcorrelator/network.socket
|
|
|
|
outputs:
|
|
stdout:
|
|
enabled: true
|
|
|
|
correlation:
|
|
time_window_s: 1
|
|
emit_orphans: true
|
|
`
|
|
|
|
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)
|
|
}
|
|
|
|
cfg, err := Load(configPath)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if !cfg.Outputs.Stdout.Enabled {
|
|
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 != 120 {
|
|
t.Errorf("expected default 120, 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")
|
|
}
|
|
}
|
|
|
|
func TestValidate_FileOutputDisabled(t *testing.T) {
|
|
cfg := &Config{
|
|
Inputs: InputsConfig{
|
|
UnixSockets: []UnixSocketConfig{
|
|
{Name: "a", Path: "/tmp/a.sock"},
|
|
{Name: "b", Path: "/tmp/b.sock"},
|
|
},
|
|
},
|
|
Outputs: OutputsConfig{
|
|
File: FileOutputConfig{Enabled: false, Path: "/var/log/test.log"},
|
|
ClickHouse: ClickHouseOutputConfig{Enabled: false},
|
|
Stdout: StdoutOutputConfig{Enabled: true},
|
|
},
|
|
Correlation: CorrelationConfig{
|
|
TimeWindowS: 1,
|
|
},
|
|
}
|
|
|
|
err := cfg.Validate()
|
|
if err != nil {
|
|
t.Errorf("expected no error when file is disabled but stdout is enabled, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfig_FileOutputDisabled(t *testing.T) {
|
|
content := `
|
|
inputs:
|
|
unix_sockets:
|
|
- name: http
|
|
path: /var/run/logcorrelator/http.socket
|
|
- name: network
|
|
path: /var/run/logcorrelator/network.socket
|
|
|
|
outputs:
|
|
file:
|
|
enabled: false
|
|
path: /var/log/logcorrelator/correlated.log
|
|
stdout:
|
|
enabled: true
|
|
|
|
correlation:
|
|
time_window_s: 1
|
|
emit_orphans: true
|
|
`
|
|
|
|
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)
|
|
}
|
|
|
|
cfg, err := Load(configPath)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if cfg.Outputs.File.Enabled {
|
|
t.Error("expected file output to be disabled")
|
|
}
|
|
if cfg.Outputs.File.Path != "/var/log/logcorrelator/correlated.log" {
|
|
t.Errorf("expected file path to be preserved, got %s", cfg.Outputs.File.Path)
|
|
}
|
|
if !cfg.Outputs.Stdout.Enabled {
|
|
t.Error("expected stdout output to be enabled")
|
|
}
|
|
}
|