feat: Implémenter le système de logging complet avec interfaces et structures de données

Co-authored-by: aider (openrouter/qwen/qwen3-coder-plus) <aider@aider.chat>
This commit is contained in:
Jacquin Antoine
2026-02-25 04:17:40 +01:00
parent 87d47324fb
commit 3b09f9416e
3 changed files with 300 additions and 39 deletions

View File

@ -1,6 +1,10 @@
package api package api
// ServiceLog représente un log interne du service ja4sentinel (diagnostic). import (
"time"
)
// ServiceLog represents internal service logging for diagnostics
type ServiceLog struct { type ServiceLog struct {
Level string `json:"level"` Level string `json:"level"`
Component string `json:"component"` Component string `json:"component"`
@ -8,14 +12,14 @@ type ServiceLog struct {
Details map[string]string `json:"details,omitempty"` Details map[string]string `json:"details,omitempty"`
} }
// Config contient la configuration réseau et TLS de base. // Config holds basic network and TLS configuration
type Config struct { type Config struct {
Interface string `json:"interface"` Interface string `json:"interface"`
ListenPorts []uint16 `json:"listen_ports"` ListenPorts []uint16 `json:"listen_ports"`
BPFFilter string `json:"bpf_filter"` BPFFilter string `json:"bpf_filter,omitempty"`
} }
// IPMeta contient les métadonnées IP pour fingerprinting de stack. // IPMeta contains IP metadata for stack fingerprinting
type IPMeta struct { type IPMeta struct {
TTL uint8 `json:"ttl"` TTL uint8 `json:"ttl"`
TotalLength uint16 `json:"total_length"` TotalLength uint16 `json:"total_length"`
@ -23,33 +27,32 @@ type IPMeta struct {
DF bool `json:"df"` DF bool `json:"df"`
} }
// TCPMeta contient les métadonnées TCP pour fingerprinting de stack. // TCPMeta contains TCP metadata for stack fingerprinting
type TCPMeta struct { type TCPMeta struct {
WindowSize uint16 `json:"window_size"` WindowSize uint16 `json:"window_size"`
MSS uint16 `json:"mss"` MSS uint16 `json:"mss,omitempty"`
WindowScale uint8 `json:"window_scale"` WindowScale uint8 `json:"window_scale,omitempty"`
Options []string `json:"options"` Options []string `json:"options"`
} }
// RawPacket représente un paquet brut capturé sur le réseau. // RawPacket represents a raw packet captured from the network
type RawPacket struct { type RawPacket struct {
Data []byte `json:"data"` Data []byte `json:"-"` // Not serialized
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"` // nanoseconds since epoch
} }
// TLSClientHello représente un ClientHello TLS client, avec meta IP/TCP. // TLSClientHello represents a client-side TLS ClientHello with IP/TCP metadata
type TLSClientHello struct { type TLSClientHello struct {
SrcIP string `json:"src_ip"` SrcIP string `json:"src_ip"`
SrcPort uint16 `json:"src_port"` SrcPort uint16 `json:"src_port"`
DstIP string `json:"dst_ip"` DstIP string `json:"dst_ip"`
DstPort uint16 `json:"dst_port"` DstPort uint16 `json:"dst_port"`
Payload []byte `json:"payload"` Payload []byte `json:"-"` // Not serialized
IPMeta IPMeta `json:"ip_meta"` IPMeta IPMeta `json:"ip_meta"`
TCPMeta TCPMeta `json:"tcp_meta"` TCPMeta TCPMeta `json:"tcp_meta"`
} }
// Fingerprints contient les empreintes TLS pour un flux client. // Fingerprints contains TLS fingerprints for a client flow
type Fingerprints struct { type Fingerprints struct {
JA4 string `json:"ja4"` JA4 string `json:"ja4"`
JA4Hash string `json:"ja4_hash"` JA4Hash string `json:"ja4_hash"`
@ -57,45 +60,164 @@ type Fingerprints struct {
JA3Hash string `json:"ja3_hash,omitempty"` JA3Hash string `json:"ja3_hash,omitempty"`
} }
// LogRecord est un enregistrement de log final, sérialisé en JSON objet plat. // LogRecord is the final log record, serialized as a flat JSON object
type LogRecord struct { type LogRecord struct {
// Adresse IP source (client)
SrcIP string `json:"src_ip"` SrcIP string `json:"src_ip"`
// Port source (client)
SrcPort uint16 `json:"src_port"` SrcPort uint16 `json:"src_port"`
// Adresse IP destination (serveur)
DstIP string `json:"dst_ip"` DstIP string `json:"dst_ip"`
// Port destination (serveur)
DstPort uint16 `json:"dst_port"` DstPort uint16 `json:"dst_port"`
// Métadonnées IP (flatten) // Flattened IPMeta fields
IPTTL uint8 `json:"ip_meta_ttl"` IPTTL uint8 `json:"ip_meta_ttl"`
IPTotalLen uint16 `json:"ip_meta_total_length"` IPTotalLen uint16 `json:"ip_meta_total_length"`
IPID uint16 `json:"ip_meta_id"` IPID uint16 `json:"ip_meta_id"`
IPDF bool `json:"ip_meta_df"` IPDF bool `json:"ip_meta_df"`
// Métadonnées TCP (flatten) // Flattened TCPMeta fields
TCPWindow uint16 `json:"tcp_meta_window_size"` TCPWindow uint16 `json:"tcp_meta_window_size"`
TCPMSS uint16 `json:"tcp_meta_mss"` TCPMSS uint16 `json:"tcp_meta_mss,omitempty"`
TCPWScale uint8 `json:"tcp_meta_window_scale"` TCPWScale uint8 `json:"tcp_meta_window_scale,omitempty"`
TCPOptions string `json:"tcp_meta_options"` TCPOptions string `json:"tcp_meta_options"` // comma-separated list
// Empreintes // Fingerprints
JA4 string `json:"ja4"` JA4 string `json:"ja4"`
JA4Hash string `json:"ja4_hash"` JA4Hash string `json:"ja4_hash"`
JA3 string `json:"ja3,omitempty"` JA3 string `json:"ja3,omitempty"`
JA3Hash string `json:"ja3_hash,omitempty"` JA3Hash string `json:"ja3_hash,omitempty"`
} }
// OutputConfig configure une sortie de logs. // OutputConfig defines configuration for a single log output
type OutputConfig struct { type OutputConfig struct {
Type string `json:"type"` Type string `json:"type"` // unix_socket, stdout, file, etc.
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"` // whether this output is active
Params map[string]string `json:"params"` Params map[string]string `json:"params"` // specific parameters like socket_path, path, etc.
} }
// AppConfig est la configuration complète de ja4sentinel. // AppConfig is the complete ja4sentinel configuration
type AppConfig struct { type AppConfig struct {
Core Config `json:"core"` Core Config `json:"core"`
Outputs []OutputConfig `json:"outputs"` Outputs []OutputConfig `json:"outputs"`
} }
// Loader interface loads configuration from file/env/CLI
type Loader interface {
Load() (AppConfig, error)
}
// Capture interface provides raw network packets
type Capture interface {
Run(cfg Config, out chan<- RawPacket) error
}
// Parser converts RawPacket to TLSClientHello
type Parser interface {
Process(pkt RawPacket) (*TLSClientHello, error)
}
// Engine generates JA4 fingerprints from TLS ClientHello
type Engine interface {
FromClientHello(ch TLSClientHello) (*Fingerprints, error)
}
// Writer is the generic interface for writing results
type Writer interface {
Write(rec LogRecord) error
}
// UnixSocketWriter implements Writer sending logs to a UNIX socket
type UnixSocketWriter interface {
Writer
Close() error
}
// MultiWriter combines multiple Writers
type MultiWriter interface {
Writer
Add(writer Writer)
CloseAll() error
}
// Builder constructs Writers from AppConfig
type Builder interface {
NewFromConfig(cfg AppConfig) (Writer, error)
}
// Logger interface for service logging
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)
}
// Helper functions for creating and converting records
// NewLogRecord creates a LogRecord from TLSClientHello and Fingerprints
func NewLogRecord(ch TLSClientHello, fp *Fingerprints) LogRecord {
opts := ""
if len(ch.TCPMeta.Options) > 0 {
opts = joinStringSlice(ch.TCPMeta.Options, ",")
}
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: ch.TCPMeta.MSS,
TCPWScale: ch.TCPMeta.WindowScale,
TCPOptions: opts,
}
if fp != nil {
rec.JA4 = fp.JA4
rec.JA4Hash = fp.JA4Hash
rec.JA3 = fp.JA3
rec.JA3Hash = fp.JA3Hash
}
return rec
}
// Helper to join string slice with separator
func joinStringSlice(slice []string, sep string) string {
if len(slice) == 0 {
return ""
}
result := slice[0]
for _, s := range slice[1:] {
result += sep + s
}
return result
}
// Default values and constants
const (
DefaultInterface = "eth0"
DefaultPort = 443
DefaultBPFFilter = ""
// Logging levels
LogLevelDebug = "DEBUG"
LogLevelInfo = "INFO"
LogLevelWarn = "WARN"
LogLevelError = "ERROR"
)
// DefaultConfig returns a configuration with sensible defaults
func DefaultConfig() AppConfig {
return AppConfig{
Core: Config{
Interface: DefaultInterface,
ListenPorts: []uint16{DefaultPort},
BPFFilter: DefaultBPFFilter,
},
Outputs: []OutputConfig{},
}
}

View File

@ -0,0 +1,19 @@
// Package logging provides a factory for creating loggers
package logging
import (
"github.com/your-repo/ja4sentinel/api"
)
// LoggerFactory creates logger instances
type LoggerFactory struct{}
// NewLogger creates a new logger based on configuration
func (f *LoggerFactory) NewLogger(level string) api.Logger {
return NewServiceLogger(level)
}
// NewDefaultLogger creates a logger with default settings
func (f *LoggerFactory) NewDefaultLogger() api.Logger {
return NewServiceLogger("info")
}

View File

@ -0,0 +1,120 @@
// Package logging provides structured logging for ja4sentinel service components
package logging
import (
"encoding/json"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/your-repo/ja4sentinel/api"
)
// ServiceLogger handles structured logging for the ja4sentinel service
type ServiceLogger struct {
level string
mutex sync.Mutex
out *log.Logger
formatter func(api.ServiceLog) ([]byte, error)
}
// NewServiceLogger creates a new service logger
func NewServiceLogger(level string) *ServiceLogger {
logger := &ServiceLogger{
level: strings.ToLower(level),
out: log.New(os.Stdout, "", 0),
formatter: func(s api.ServiceLog) ([]byte, error) {
logData := map[string]interface{}{
"timestamp": time.Now().UnixNano(),
"level": strings.ToUpper(s.Level),
"component": s.Component,
"message": s.Message,
}
if s.Details != nil && len(s.Details) > 0 {
for k, v := range s.Details {
logData[k] = v
}
}
return json.Marshal(logData)
},
}
return logger
}
// Log emits a structured log entry to stdout in JSON format
func (l *ServiceLogger) Log(component, level, message string, details map[string]string) {
if !l.isLogLevelEnabled(level) {
return
}
// Lock to prevent concurrent writes to stdout
l.mutex.Lock()
defer l.mutex.Unlock()
serviceLog := api.ServiceLog{
Level: level,
Component: component,
Message: message,
Details: details,
}
jsonData, err := l.formatter(serviceLog)
if err != nil {
// Fallback to simple logging if JSON formatting fails
fmt.Printf(`{"timestamp":%d,"level":"ERROR","component":"logging","message":"%s","original_message":"%s"}`,
time.Now().UnixNano(), err.Error(), message)
return
}
fmt.Println(string(jsonData))
}
// Debug logs a debug level entry
func (l *ServiceLogger) Debug(component, message string, details map[string]string) {
if l.isLogLevelEnabled("debug") {
l.Log(component, "DEBUG", message, details)
}
}
// Info logs an info level entry
func (l *ServiceLogger) Info(component, message string, details map[string]string) {
if l.isLogLevelEnabled("info") {
l.Log(component, "INFO", message, details)
}
}
// Warn logs a warning level entry
func (l *ServiceLogger) Warn(component, message string, details map[string]string) {
if l.isLogLevelEnabled("warn") {
l.Log(component, "WARN", message, details)
}
}
// Error logs an error level entry
func (l *ServiceLogger) Error(component, message string, details map[string]string) {
if l.isLogLevelEnabled("error") {
l.Log(component, "ERROR", message, details)
}
}
// isLogLevelEnabled checks if a log level should be emitted based on configured level
func (l *ServiceLogger) isLogLevelEnabled(messageLevel string) bool {
switch l.level {
case "debug":
return true
case "info":
return messageLevel != "debug"
case "warn":
return messageLevel != "debug" && messageLevel != "info"
case "error":
return messageLevel == "error"
default:
return false // If level is invalid, don't log anything
}
}