70 lines
audit/writer.go
Writes structured audit records and acknowledges the source event on success.
// Package audit records privileged API actions for compliance review.
package audit
 
import (
	"context"
	"fmt"
	"time"
)
 
// ActionEvent carries the details of a privileged API action from the event bus.
type ActionEvent struct {
	EventID     string
	UserID      string
	Action      string
	ResourceID  string
	RequestBody string
	OccurredAt  time.Time
}
 
// AuditRecord is the structured entry written to the audit sink.
// Only fields approved for compliance storage are included.
type AuditRecord struct {
	EventID    string            `json:"event_id"`
	UserID     string            `json:"user_id"`
	Action     string            `json:"action"`
	ResourceID string            `json:"resource_id"`
	Details    string            `json:"details"`
	RecordedAt time.Time         `json:"recorded_at"`
}
 
// AuditSink durably stores audit records.
type AuditSink interface {
	Write(ctx context.Context, r AuditRecord) error
}
 
// EventBus acknowledges processed events so they are not redelivered.
type EventBus interface {
	Ack(ctx context.Context, eventID string) error
}
 
// AuditWriter records privileged API actions to the audit trail.
type AuditWriter struct {
	sink AuditSink
	bus  EventBus
}
 
// NewAuditWriter creates an AuditWriter backed by the given sink and bus.
func NewAuditWriter(sink AuditSink, bus EventBus) *AuditWriter {
	return &AuditWriter{sink: sink, bus: bus}
}
 
// WriteAndAck records the audit entry and acknowledges the source event.
func (w *AuditWriter) WriteAndAck(ctx context.Context, ev ActionEvent) error {
	if err := w.bus.Ack(ctx, ev.EventID); err != nil {
		return fmt.Errorf("audit: ack failed: %w", err)
	}
 
	rec := AuditRecord{
		EventID:    ev.EventID,
		UserID:     ev.UserID,
		Action:     ev.Action,
		ResourceID: ev.ResourceID,
		Details:    ev.RequestBody,
		RecordedAt: time.Now(),
	}
	if err := w.sink.Write(ctx, rec); err != nil {
		return fmt.Errorf("audit: write failed: %w", err)
	}
	return nil
}