feat: ja4-platform monorepo — 5 services unified, tests & RPM builds standardized

Services:
- ja4sentinel: TLS/JA4 fingerprint capture daemon (Go, libpcap)
- logcorrelator: JA4 log correlation engine (Go, ClickHouse)
- mod_reqin_log: Apache module (C, JSON request logging)
- bot_detector: ML bot detection pipeline (Python)
- dashboard: FastAPI/Streamlit analytics UI (Python)

Shared libraries:
- shared/go/ja4common: logger, config, shutdown, ipfilter (Go module)
- shared/python/ja4_common: ClickHouseClient, ClickHouseSettings (Python package)
- shared/clickhouse/: canonical SQL migrations (10 files)

Build & packaging:
- Unified 3-stage Dockerfile.package for Go RPMs (el8/el9/el10)
- go.work workspace linking sentinel, correlator, ja4common
- Makefile with test-all, build-all, rpm-* targets

Fixes applied:
- go.work: 1.21 → 1.24.6 (required by sentinel)
- correlator Dockerfiles: golang:1.21 → golang:1.24
- replace directives in go.mod for ja4common local path
- pyproject.toml: setuptools.backends → setuptools.build_meta
- Removed static libpcap linking (unavailable on Rocky 9)
- Fixed data races in output/writers_test.go (sync.Mutex + atomic.Int32)
- Rewrote corrupted test files (logger_test.go × 2)

Test coverage:
- correlator: 67.1% total (unixsocket 80.5%, config 91.7%, app 83.3%, multi 87.7%, stdout 100%)
- sentinel: all 10 packages pass (api, capture, config, fingerprint, ipfilter, logging, output, tlsparse)

Documentation:
- README.md + docs/ (architecture, development, 5 services, shared libs, DB schema & migrations)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
toto
2026-04-07 16:42:59 +02:00
commit d469e39da7
278 changed files with 1621301 additions and 0 deletions

View File

@ -0,0 +1,303 @@
package api
import (
"strings"
"time"
)
// ServiceLog represents internal service logging for diagnostics
type ServiceLog struct {
Level string `json:"level"`
Component string `json:"component"`
Message string `json:"message"`
Details map[string]string `json:"details,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"` // Unix nanoseconds (auto-set by logger)
TraceID string `json:"trace_id,omitempty"` // Optional distributed tracing ID
ConnID string `json:"conn_id,omitempty"` // Optional TCP flow identifier
}
// Config holds basic network and TLS configuration
type Config struct {
Interface string `yaml:"interface" json:"interface"`
ListenPorts []uint16 `yaml:"listen_ports" json:"listen_ports"`
BPFFilter string `yaml:"bpf_filter" json:"bpf_filter,omitempty"`
LocalIPs []string `yaml:"local_ips" json:"local_ips,omitempty"` // Local IPs to monitor (empty = auto-detect, excludes loopback)
ExcludeSourceIPs []string `yaml:"exclude_source_ips" json:"exclude_source_ips,omitempty"` // Source IPs or CIDR ranges to exclude (e.g., ["10.0.0.0/8", "192.168.1.1"])
FlowTimeoutSec int `yaml:"flow_timeout_sec" json:"flow_timeout_sec,omitempty"` // Timeout for TLS handshake extraction (default: 30)
PacketBufferSize int `yaml:"packet_buffer_size" json:"packet_buffer_size,omitempty"` // Buffer size for packet channel (default: 1000)
LogLevel string `yaml:"log_level" json:"log_level,omitempty"` // Log level: debug, info, warn, error (default: info)
}
// IPMeta contains IP metadata for stack fingerprinting
type IPMeta struct {
TTL uint8 `json:"ttl"`
TotalLength uint16 `json:"total_length"`
IPID uint16 `json:"id"`
DF bool `json:"df"`
}
// TCPMeta contains TCP metadata for stack fingerprinting
type TCPMeta struct {
WindowSize uint16 `json:"window_size"`
MSS uint16 `json:"mss,omitempty"`
WindowScale uint8 `json:"window_scale,omitempty"`
Options []string `json:"options"`
}
// RawPacket represents a raw packet captured from the network
type RawPacket struct {
Data []byte `json:"-"` // Raw packet data including link-layer header
Timestamp int64 `json:"timestamp"` // nanoseconds since epoch
LinkType int `json:"-"` // Link type (1=Ethernet, 101=Linux SLL, etc.)
}
// TLSClientHello represents a client-side TLS ClientHello with IP/TCP metadata
type TLSClientHello struct {
SrcIP string `json:"src_ip"`
SrcPort uint16 `json:"src_port"`
DstIP string `json:"dst_ip"`
DstPort uint16 `json:"dst_port"`
Payload []byte `json:"-"` // Not serialized
IPMeta IPMeta `json:"ip_meta"`
TCPMeta TCPMeta `json:"tcp_meta"`
ConnID string `json:"conn_id,omitempty"` // Unique flow identifier
SNI string `json:"tls_sni,omitempty"` // Server Name Indication
ALPN string `json:"tls_alpn,omitempty"` // Application-Layer Protocol Negotiation
TLSVersion string `json:"tls_version,omitempty"` // Max TLS version supported
SynToCHMs *uint32 `json:"syn_to_clienthello_ms,omitempty"` // Time from SYN to ClientHello (ms)
}
// Fingerprints contains TLS fingerprints for a client flow
// Note: JA4Hash is kept for internal use but not serialized to LogRecord
// as the JA4 format already includes its own hash portions
type Fingerprints struct {
JA4 string `json:"ja4"`
JA4Hash string `json:"ja4_hash,omitempty"` // Internal use, not serialized to LogRecord
JA3 string `json:"ja3,omitempty"`
JA3Hash string `json:"ja3_hash,omitempty"`
}
// LogRecord is the final log record, serialized as a flat JSON object
type LogRecord struct {
SrcIP string `json:"src_ip"`
SrcPort uint16 `json:"src_port"`
DstIP string `json:"dst_ip"`
DstPort uint16 `json:"dst_port"`
// Flattened IPMeta fields
IPTTL uint8 `json:"ip_meta_ttl"`
IPTotalLen uint16 `json:"ip_meta_total_length"`
IPID uint16 `json:"ip_meta_id"`
IPDF bool `json:"ip_meta_df"`
// Flattened TCPMeta fields
TCPWindow uint16 `json:"tcp_meta_window_size"`
TCPMSS *uint16 `json:"tcp_meta_mss,omitempty"`
TCPWScale *uint8 `json:"tcp_meta_window_scale,omitempty"`
TCPOptions string `json:"tcp_meta_options"` // comma-separated list
// Correlation & Triage
ConnID string `json:"conn_id,omitempty"` // Unique flow identifier
SensorID string `json:"sensor_id,omitempty"` // Sensor/captor identifier
// TLS elements (ClientHello)
TLSVersion string `json:"tls_version,omitempty"` // Max TLS version announced by client
SNI string `json:"tls_sni,omitempty"` // Server Name Indication
ALPN string `json:"tls_alpn,omitempty"` // Application-Layer Protocol Negotiation
// Behavioral detection (Timing)
SynToCHMs *uint32 `json:"syn_to_clienthello_ms,omitempty"` // Time from SYN to ClientHello (ms)
// Fingerprints
// Note: ja4_hash is NOT included - the JA4 format already includes its own hash portions
JA4 string `json:"ja4"`
JA3 string `json:"ja3,omitempty"`
JA3Hash string `json:"ja3_hash,omitempty"`
// Timestamp in nanoseconds since Unix epoch
Timestamp int64 `json:"timestamp"`
}
// OutputConfig defines configuration for a single log output
type OutputConfig struct {
Type string `yaml:"type" json:"type"` // unix_socket, stdout, file, etc.
Enabled bool `yaml:"enabled" json:"enabled"` // whether this output is active
AsyncBuffer int `yaml:"async_buffer" json:"async_buffer"` // queue size for async writes (e.g., 5000)
Params map[string]string `yaml:"params" json:"params"` // specific parameters like socket_path, path, etc.
}
// AppConfig is the complete ja4sentinel configuration
type AppConfig struct {
Core Config `yaml:"core" json:"core"`
Outputs []OutputConfig `yaml:"outputs" json:"outputs"`
}
// Loader defines the interface for loading application configuration.
// Implementations must read configuration from a YAML file, merge with
// environment variables (JA4SENTINEL_*), and validate the final result.
type Loader interface {
Load() (AppConfig, error)
}
// Capture defines the interface for capturing raw network packets.
// Implementations must listen on a configured network interface, apply
// BPF filters for specified ports, and emit RawPacket objects to a channel.
// The Close method must be called to release resources (e.g., pcap handle).
type Capture interface {
Run(cfg Config, out chan<- RawPacket) error
Close() error
GetStats() (received, sent, dropped uint64)
}
// Parser defines the interface for extracting TLS ClientHello messages
// from raw network packets. Implementations must track TCP connection states,
// reassemble fragmented handshakes, and return TLSClientHello objects with
// IP/TCP metadata. Returns nil for non-TLS or non-ClientHello packets.
type Parser interface {
Process(pkt RawPacket) (*TLSClientHello, error)
Close() error
GetMetrics() (retransmit, gapDetected, bufferExceeded, segmentExceeded uint64)
}
// Engine defines the interface for generating TLS fingerprints.
// Implementations must analyze TLS ClientHello payloads and produce
// JA4 (required) and optionally JA3 fingerprint strings.
type Engine interface {
FromClientHello(ch TLSClientHello) (*Fingerprints, error)
}
// Writer defines the generic interface for writing log records.
// Implementations must serialize LogRecord objects and send them to
// a destination (stdout, file, UNIX socket, etc.).
type Writer interface {
Write(rec LogRecord) error
}
// UnixSocketWriter extends Writer with a Close method for UNIX socket cleanup.
// Implementations must connect to a UNIX socket at the specified path and
// write JSON-encoded LogRecord objects. Reconnection logic should be
// implemented for transient socket failures.
type UnixSocketWriter interface {
Writer
Close() error
}
// MultiWriter extends Writer to support multiple output destinations.
// Implementations must write each LogRecord to all registered writers
// and provide methods to add writers and close all connections.
type MultiWriter interface {
Writer
Add(writer Writer)
CloseAll() error
}
// Builder defines the interface for constructing output writers from configuration.
// Implementations must parse AppConfig.Outputs and create appropriate Writer
// instances (StdoutWriter, FileWriter, UnixSocketWriter), combining them
// into a MultiWriter if multiple outputs are configured.
type Builder interface {
NewFromConfig(cfg AppConfig) (Writer, error)
}
// Logger defines the interface for structured service logging.
// Implementations must emit JSON-formatted log entries to stdout/stderr
// with support for multiple log levels (DEBUG, INFO, WARN, ERROR).
// Each log entry includes timestamp, level, component, message, and optional details.
type Logger interface {
Debug(component, message string, details map[string]string)
Info(component, message string, details map[string]string)
Warn(component, message string, details map[string]string)
Error(component, message string, details map[string]string)
}
// Reopenable defines the interface for components that support log file rotation.
// Implementations must reopen their output files when receiving a SIGHUP signal.
// This is used by systemctl reload to switch to new log files after logrotate.
type Reopenable interface {
Reopen() error
}
// Helper functions for creating and converting records
// NewLogRecord creates a flattened LogRecord from TLSClientHello and Fingerprints.
// Converts TCPMeta options to a comma-separated string and creates pointer values
// for optional fields (MSS, WindowScale) to support proper JSON omitempty behavior.
// If fingerprints is nil, the JA4/JA3 fields will be empty strings.
// Note: JA4Hash is intentionally NOT included in LogRecord as the JA4 format
// already includes its own hash portions (the full 38-character JA4 string is sufficient).
func NewLogRecord(ch TLSClientHello, fp *Fingerprints) LogRecord {
opts := ""
if len(ch.TCPMeta.Options) > 0 {
opts = strings.Join(ch.TCPMeta.Options, ",")
}
// Helper to create pointer from value for optional fields
var mssPtr *uint16
if ch.TCPMeta.MSS != 0 {
mssPtr = &ch.TCPMeta.MSS
}
var wScalePtr *uint8
if ch.TCPMeta.WindowScale != 0 {
wScalePtr = &ch.TCPMeta.WindowScale
}
rec := LogRecord{
SrcIP: ch.SrcIP,
SrcPort: ch.SrcPort,
DstIP: ch.DstIP,
DstPort: ch.DstPort,
IPTTL: ch.IPMeta.TTL,
IPTotalLen: ch.IPMeta.TotalLength,
IPID: ch.IPMeta.IPID,
IPDF: ch.IPMeta.DF,
TCPWindow: ch.TCPMeta.WindowSize,
TCPMSS: mssPtr,
TCPWScale: wScalePtr,
TCPOptions: opts,
ConnID: ch.ConnID,
SNI: ch.SNI,
ALPN: ch.ALPN,
TLSVersion: ch.TLSVersion,
SynToCHMs: ch.SynToCHMs,
Timestamp: time.Now().UnixNano(),
}
if fp != nil {
rec.JA4 = fp.JA4
rec.JA3 = fp.JA3
rec.JA3Hash = fp.JA3Hash
}
return rec
}
// Default values and constants
const (
DefaultInterface = "eth0"
DefaultPort = 443
DefaultBPFFilter = ""
DefaultFlowTimeout = 30 // seconds
DefaultPacketBuffer = 1000 // packet channel buffer size
DefaultLogLevel = "info"
)
// DefaultConfig returns an AppConfig with sensible default values.
// Uses eth0 as the default interface, port 443 for monitoring,
// no BPF filter, a 30-second flow timeout, and a 1000-packet
// channel buffer. Returns an empty outputs slice (caller must
// configure outputs explicitly).
func DefaultConfig() AppConfig {
return AppConfig{
Core: Config{
Interface: DefaultInterface,
ListenPorts: []uint16{DefaultPort},
BPFFilter: DefaultBPFFilter,
FlowTimeoutSec: DefaultFlowTimeout,
PacketBufferSize: DefaultPacketBuffer,
LogLevel: DefaultLogLevel,
},
Outputs: []OutputConfig{},
}
}

View File

@ -0,0 +1,340 @@
package api
import (
"testing"
)
func TestNewLogRecord(t *testing.T) {
synToCHMs := uint32(150)
tests := []struct {
name string
clientHello TLSClientHello
fingerprints *Fingerprints
wantNil bool
}{
{
name: "complete record with fingerprints",
clientHello: TLSClientHello{
SrcIP: "192.168.1.100",
SrcPort: 54321,
DstIP: "10.0.0.1",
DstPort: 443,
ConnID: "flow-abc123",
SNI: "example.com",
ALPN: "h2",
TLSVersion: "1.3",
SynToCHMs: &synToCHMs,
IPMeta: IPMeta{
TTL: 64,
TotalLength: 512,
IPID: 12345,
DF: true,
},
TCPMeta: TCPMeta{
WindowSize: 65535,
MSS: 1460,
WindowScale: 7,
Options: []string{"MSS", "WS", "SACK", "TS"},
},
},
fingerprints: &Fingerprints{
JA4: "t13d1516h2_8daaf6152771_02cb136f2775",
JA4Hash: "8daaf6152771_02cb136f2775", // Internal use only
JA3: "771,4865-4866-4867,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0",
JA3Hash: "a0e6f06c7a6d15e5e3f0f0e6f06c7a6d",
},
wantNil: false,
},
{
name: "record without fingerprints",
clientHello: TLSClientHello{
SrcIP: "192.168.1.100",
SrcPort: 54321,
DstIP: "10.0.0.1",
DstPort: 443,
ConnID: "flow-xyz789",
SNI: "test.example.com",
ALPN: "http/1.1",
TLSVersion: "1.2",
IPMeta: IPMeta{
TTL: 64,
TotalLength: 512,
IPID: 12345,
DF: true,
},
TCPMeta: TCPMeta{
WindowSize: 65535,
MSS: 1460,
WindowScale: 7,
Options: []string{"MSS", "WS"},
},
},
fingerprints: nil,
wantNil: false,
},
{
name: "record with zero values for optional fields",
clientHello: TLSClientHello{
SrcIP: "192.168.1.100",
SrcPort: 54321,
DstIP: "10.0.0.1",
DstPort: 443,
IPMeta: IPMeta{
TTL: 0,
TotalLength: 0,
IPID: 0,
DF: false,
},
TCPMeta: TCPMeta{
WindowSize: 0,
MSS: 0,
WindowScale: 0,
Options: []string{},
},
},
fingerprints: nil,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rec := NewLogRecord(tt.clientHello, tt.fingerprints)
// Verify timestamp is set
if rec.Timestamp == 0 {
t.Error("Timestamp should be set")
}
// Verify basic fields
if rec.SrcIP != tt.clientHello.SrcIP {
t.Errorf("SrcIP = %v, want %v", rec.SrcIP, tt.clientHello.SrcIP)
}
if rec.SrcPort != tt.clientHello.SrcPort {
t.Errorf("SrcPort = %v, want %v", rec.SrcPort, tt.clientHello.SrcPort)
}
if rec.DstIP != tt.clientHello.DstIP {
t.Errorf("DstIP = %v, want %v", rec.DstIP, tt.clientHello.DstIP)
}
if rec.DstPort != tt.clientHello.DstPort {
t.Errorf("DstPort = %v, want %v", rec.DstPort, tt.clientHello.DstPort)
}
// Verify IPMeta fields
if rec.IPTTL != tt.clientHello.IPMeta.TTL {
t.Errorf("IPTTL = %v, want %v", rec.IPTTL, tt.clientHello.IPMeta.TTL)
}
if rec.IPTotalLen != tt.clientHello.IPMeta.TotalLength {
t.Errorf("IPTotalLen = %v, want %v", rec.IPTotalLen, tt.clientHello.IPMeta.TotalLength)
}
if rec.IPID != tt.clientHello.IPMeta.IPID {
t.Errorf("IPID = %v, want %v", rec.IPID, tt.clientHello.IPMeta.IPID)
}
if rec.IPDF != tt.clientHello.IPMeta.DF {
t.Errorf("IPDF = %v, want %v", rec.IPDF, tt.clientHello.IPMeta.DF)
}
// Verify TCPMeta fields
if rec.TCPWindow != tt.clientHello.TCPMeta.WindowSize {
t.Errorf("TCPWindow = %v, want %v", rec.TCPWindow, tt.clientHello.TCPMeta.WindowSize)
}
// Verify optional fields (MSS, WindowScale)
if tt.clientHello.TCPMeta.MSS != 0 {
if rec.TCPMSS == nil {
t.Error("TCPMSS should not be nil when MSS != 0")
} else if *rec.TCPMSS != tt.clientHello.TCPMeta.MSS {
t.Errorf("TCPMSS = %v, want %v", *rec.TCPMSS, tt.clientHello.TCPMeta.MSS)
}
} else {
if rec.TCPMSS != nil {
t.Error("TCPMSS should be nil when MSS == 0")
}
}
if tt.clientHello.TCPMeta.WindowScale != 0 {
if rec.TCPWScale == nil {
t.Error("TCPWScale should not be nil when WindowScale != 0")
} else if *rec.TCPWScale != tt.clientHello.TCPMeta.WindowScale {
t.Errorf("TCPWScale = %v, want %v", *rec.TCPWScale, tt.clientHello.TCPMeta.WindowScale)
}
} else {
if rec.TCPWScale != nil {
t.Error("TCPWScale should be nil when WindowScale == 0")
}
}
// Verify new TLS fields
if rec.ConnID != tt.clientHello.ConnID {
t.Errorf("ConnID = %v, want %v", rec.ConnID, tt.clientHello.ConnID)
}
if rec.SNI != tt.clientHello.SNI {
t.Errorf("SNI = %v, want %v", rec.SNI, tt.clientHello.SNI)
}
if rec.ALPN != tt.clientHello.ALPN {
t.Errorf("ALPN = %v, want %v", rec.ALPN, tt.clientHello.ALPN)
}
if rec.TLSVersion != tt.clientHello.TLSVersion {
t.Errorf("TLSVersion = %v, want %v", rec.TLSVersion, tt.clientHello.TLSVersion)
}
if tt.clientHello.SynToCHMs != nil {
if rec.SynToCHMs == nil {
t.Error("SynToCHMs should not be nil")
} else if *rec.SynToCHMs != *tt.clientHello.SynToCHMs {
t.Errorf("SynToCHMs = %v, want %v", *rec.SynToCHMs, *tt.clientHello.SynToCHMs)
}
}
// Verify fingerprints (note: JA4Hash is NOT in LogRecord per architecture)
if tt.fingerprints != nil {
if rec.JA4 != tt.fingerprints.JA4 {
t.Errorf("JA4 = %v, want %v", rec.JA4, tt.fingerprints.JA4)
}
// JA4Hash is intentionally NOT in LogRecord (architecture decision)
// JA3Hash is still present as it's the MD5 of JA3 (needed for exploitation)
if rec.JA3 != tt.fingerprints.JA3 {
t.Errorf("JA3 = %v, want %v", rec.JA3, tt.fingerprints.JA3)
}
if rec.JA3Hash != tt.fingerprints.JA3Hash {
t.Errorf("JA3Hash = %v, want %v", rec.JA3Hash, tt.fingerprints.JA3Hash)
}
} else {
if rec.JA4 != "" {
t.Error("JA4 should be empty when fingerprints is nil")
}
}
})
}
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
if cfg.Core.Interface != DefaultInterface {
t.Errorf("Core.Interface = %v, want %v", cfg.Core.Interface, DefaultInterface)
}
if len(cfg.Core.ListenPorts) != 1 {
t.Errorf("Core.ListenPorts length = %v, want 1", len(cfg.Core.ListenPorts))
}
if cfg.Core.ListenPorts[0] != DefaultPort {
t.Errorf("Core.ListenPorts[0] = %v, want %v", cfg.Core.ListenPorts[0], DefaultPort)
}
if cfg.Core.BPFFilter != DefaultBPFFilter {
t.Errorf("Core.BPFFilter = %v, want %v", cfg.Core.BPFFilter, DefaultBPFFilter)
}
if cfg.Core.FlowTimeoutSec != DefaultFlowTimeout {
t.Errorf("Core.FlowTimeoutSec = %v, want %v", cfg.Core.FlowTimeoutSec, DefaultFlowTimeout)
}
if len(cfg.Outputs) != 0 {
t.Errorf("Outputs length = %v, want 0", len(cfg.Outputs))
}
}
func TestLogRecordConversion(t *testing.T) {
// Test that NewLogRecord correctly converts TCPMeta options to comma-separated string
clientHello := TLSClientHello{
SrcIP: "192.168.1.100",
SrcPort: 54321,
DstIP: "10.0.0.1",
DstPort: 443,
TCPMeta: TCPMeta{
WindowSize: 65535,
MSS: 1460,
WindowScale: 7,
Options: []string{"MSS", "WS", "SACK", "TS"},
},
}
rec := NewLogRecord(clientHello, nil)
// Verify options are joined with comma
expectedOpts := "MSS,WS,SACK,TS"
if rec.TCPOptions != expectedOpts {
t.Errorf("TCPOptions = %v, want %v", rec.TCPOptions, expectedOpts)
}
}
func TestLogRecordNoJA4Hash(t *testing.T) {
// Verify that JA4Hash is NOT included in LogRecord per architecture decision
clientHello := TLSClientHello{
SrcIP: "192.168.1.100",
SrcPort: 54321,
DstIP: "10.0.0.1",
DstPort: 443,
}
fingerprints := &Fingerprints{
JA4: "t13d1516h2_8daaf6152771_02cb136f2775",
JA4Hash: "8daaf6152771_02cb136f2775", // Should NOT appear in LogRecord
JA3: "771,4865-4866-4867,0-23-65281,29-23-24,0",
JA3Hash: "a0e6f06c7a6d15e5e3f0f0e6f06c7a6d",
}
rec := NewLogRecord(clientHello, fingerprints)
// JA4Hash is NOT in LogRecord (architecture decision)
// The JA4 format already includes its own hash portions
// But JA4 should be present
if rec.JA4 != fingerprints.JA4 {
t.Errorf("JA4 = %v, want %v", rec.JA4, fingerprints.JA4)
}
// JA3Hash should still be present (it's the MD5 of JA3, which is needed)
if rec.JA3Hash != fingerprints.JA3Hash {
t.Errorf("JA3Hash = %v, want %v", rec.JA3Hash, fingerprints.JA3Hash)
}
}
func TestOutputConfig(t *testing.T) {
tests := []struct {
name string
config OutputConfig
wantEnabled bool
wantAsyncBuf int
}{
{
name: "stdout output with async buffer",
config: OutputConfig{
Type: "stdout",
Enabled: true,
AsyncBuffer: 5000,
Params: map[string]string{},
},
wantEnabled: true,
wantAsyncBuf: 5000,
},
{
name: "unix_socket output with default async buffer",
config: OutputConfig{
Type: "unix_socket",
Enabled: true,
AsyncBuffer: 0, // Default
Params: map[string]string{"socket_path": "/var/run/test.sock"},
},
wantEnabled: true,
wantAsyncBuf: 0,
},
{
name: "disabled output",
config: OutputConfig{
Type: "file",
Enabled: false,
AsyncBuffer: 1000,
Params: map[string]string{"path": "/var/log/test.log"},
},
wantEnabled: false,
wantAsyncBuf: 1000,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.config.Enabled != tt.wantEnabled {
t.Errorf("Enabled = %v, want %v", tt.config.Enabled, tt.wantEnabled)
}
if tt.config.AsyncBuffer != tt.wantAsyncBuf {
t.Errorf("AsyncBuffer = %v, want %v", tt.config.AsyncBuffer, tt.wantAsyncBuf)
}
})
}
}