diff --git a/cmd/pulse-sensor-proxy/audit.go b/cmd/pulse-sensor-proxy/audit.go index cb02442..86b5936 100644 --- a/cmd/pulse-sensor-proxy/audit.go +++ b/cmd/pulse-sensor-proxy/audit.go @@ -47,18 +47,46 @@ type AuditEvent struct { } // newAuditLogger opens the audit log file and prepares hash chaining. -func newAuditLogger(path string) (*auditLogger, error) { +// If the file cannot be opened (e.g., read-only filesystem), it automatically +// falls back to stderr which integrates with systemd journal. +// This function always succeeds and returns a valid audit logger. +func newAuditLogger(path string) *auditLogger { + // Try to open the audit log file file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640) + var writer zerolog.Logger + var usedFallback bool + if err != nil { - return nil, err + // Fallback to stderr if file cannot be opened + log.Warn(). + Err(err). + Str("path", path). + Msg("Cannot open audit log file, falling back to stderr (systemd journal)") + + writer = zerolog.New(os.Stderr).With().Timestamp().Logger() + usedFallback = true + file = nil + } else { + writer = zerolog.New(file).With().Timestamp().Logger() } - writer := zerolog.New(file).With().Timestamp().Logger() + // Log initialization event to standard logger (not to audit log itself) + if usedFallback { + log.Warn(). + Str("path", path). + Str("mode", "stderr"). + Msg("Audit logger initialized with stderr fallback due to filesystem constraints") + } else { + log.Info(). + Str("path", path). + Str("mode", "file"). + Msg("Audit logger initialized with file backend") + } return &auditLogger{ file: file, logger: writer, - }, nil + } } // Close flushes and closes the audit log file. diff --git a/cmd/pulse-sensor-proxy/audit_test.go b/cmd/pulse-sensor-proxy/audit_test.go index 187f80c..8821874 100644 --- a/cmd/pulse-sensor-proxy/audit_test.go +++ b/cmd/pulse-sensor-proxy/audit_test.go @@ -18,10 +18,7 @@ func TestAuditLogValidationFailure(t *testing.T) { tmp.Close() defer os.Remove(path) - logger, err := newAuditLogger(path) - if err != nil { - t.Fatalf("newAuditLogger: %v", err) - } + logger := newAuditLogger(path) cred := &peerCredentials{uid: 1000, gid: 1000, pid: 4242} logger.LogValidationFailure("corr-123", cred, "remote", "get_temperature", []string{"node"}, "invalid_node") @@ -35,16 +32,25 @@ func TestAuditLogValidationFailure(t *testing.T) { scanner := bufio.NewScanner(file) if !scanner.Scan() { - t.Fatalf("expected at least one audit entry") + t.Fatalf("expected at least one audit entry (file may be empty)") } + line := scanner.Bytes() + if len(line) == 0 { + t.Fatalf("empty line in audit log") + } + + t.Logf("Audit log line: %s", string(line)) + var record auditRecord - if err := json.Unmarshal(scanner.Bytes(), &record); err != nil { - t.Fatalf("unmarshal: %v", err) + if err := json.Unmarshal(line, &record); err != nil { + t.Fatalf("unmarshal (line=%s): %v", string(line), err) } + t.Logf("Parsed record: %+v", record) + if record["event_type"] != "command.validation_failed" { - t.Fatalf("unexpected event_type: %v", record["event_type"]) + t.Fatalf("unexpected event_type: %v (full record: %+v)", record["event_type"], record) } if record["correlation_id"] != "corr-123" { t.Fatalf("unexpected correlation id: %v", record["correlation_id"]) diff --git a/cmd/pulse-sensor-proxy/main.go b/cmd/pulse-sensor-proxy/main.go index 6a17d1d..50ef479 100644 --- a/cmd/pulse-sensor-proxy/main.go +++ b/cmd/pulse-sensor-proxy/main.go @@ -354,10 +354,8 @@ func runProxy() { auditPath = defaultAuditLogPath } - auditLogger, err := newAuditLogger(auditPath) - if err != nil { - log.Fatal().Err(err).Str("path", auditPath).Msg("Failed to initialize audit logger") - } + // Initialize audit logger with automatic fallback to stderr if file is unavailable + auditLogger := newAuditLogger(auditPath) defer auditLogger.Close() // Initialize metrics