Audit Logging Integration Guide

Current State

The audit service is currently passive - it provides infrastructure but doesn’t automatically capture changes. You need to manually integrate it into your business logic.

Integration Approaches

Approach 1: Manual Audit Logging (Current - Simple)

Manually call CreateAuditLog in your service methods. Example: Adding audit to ApprovalService
// approval_service.go
type ApprovalService struct {
    leaveRequestRepo ports.LeaveRequestRepository
    leaveBalanceRepo ports.LeaveBalanceRepository
    leaveTypeRepo    ports.LeaveTypeRepository
    userRepo         ports.UserRepository
    auditService     *AuditService  // Add this
}

func (s *ApprovalService) ApproveRequest(ctx context.Context, requestID, approverID, approverName, comment string) error {
    // 1. Get the leave request (BEFORE state)
    request, err := s.leaveRequestRepo.FindByID(ctx, requestID)
    if err != nil {
        return err
    }

    // Capture BEFORE state
    beforeState := map[string]interface{}{
        "status": request.Status,
        "approver_id": request.ApproverID,
    }

    // 2. Perform the approval
    request.Status = domain.LeaveRequestStatusApproved
    request.ApproverID = &approverID
    request.ApproverName = &approverName

    if err := s.leaveRequestRepo.Update(ctx, request); err != nil {
        return err
    }

    // 3. Create audit log (AFTER state)
    afterState := map[string]interface{}{
        "status": request.Status,
        "approver_id": approverID,
        "approver_name": approverName,
    }

    s.auditService.CreateAuditLog(ctx, CreateAuditLogParams{
        ID:         "audit_" + generateID(),
        UserID:     approverID,
        UserName:   approverName,
        UserRole:   "manager", // Get from context
        Action:     coreDomain.AuditActionApprove,
        EntityType: coreDomain.AuditEntityLeaveRequest,
        EntityID:   requestID,
        Before:     beforeState,
        After:      afterState,
        Details: map[string]interface{}{
            "comment": comment,
            "total_days": request.TotalDays,
        },
        IPAddress:  getIPFromContext(ctx),
        UserAgent:  getUserAgentFromContext(ctx),
    })

    return nil
}
Pros:
  • ✅ Simple and explicit
  • ✅ Full control over what’s logged
  • ✅ Easy to understand
Cons:
  • ❌ Manual work for each operation
  • ❌ Easy to forget
  • ❌ Repetitive code

Wrap services with audit decorators that automatically log operations.
// audit_decorator.go
type AuditedApprovalService struct {
    inner        *ApprovalService
    auditService *AuditService
}

func NewAuditedApprovalService(inner *ApprovalService, audit *AuditService) *AuditedApprovalService {
    return &AuditedApprovalService{
        inner:        inner,
        auditService: audit,
    }
}

func (s *AuditedApprovalService) ApproveRequest(ctx context.Context, requestID, approverID, approverName, comment string) error {
    // 1. Get BEFORE state
    request, _ := s.inner.leaveRequestRepo.FindByID(ctx, requestID)
    beforeState := captureState(request)

    // 2. Execute actual operation
    err := s.inner.ApproveRequest(ctx, requestID, approverID, approverName, comment)

    // 3. Log audit (even if failed)
    if request != nil {
        afterState := captureState(request)
        s.auditService.CreateAuditLog(ctx, CreateAuditLogParams{
            Action:     coreDomain.AuditActionApprove,
            EntityType: coreDomain.AuditEntityLeaveRequest,
            EntityID:   requestID,
            Before:     beforeState,
            After:      afterState,
            // ... other fields
        })
    }

    return err
}
Pros:
  • ✅ Automatic audit logging
  • ✅ Separation of concerns
  • ✅ Easy to add/remove
Cons:
  • ❌ More complex setup
  • ❌ Requires interface-based design

Approach 3: Event-Driven (Best for Microservices)

Use domain events to trigger audit logging.
// In ApprovalService
func (s *ApprovalService) ApproveRequest(ctx context.Context, ...) error {
    // ... approval logic ...

    // Publish domain event
    s.eventBus.Publish(ctx, &events.LeaveRequestApprovedEvent{
        RequestID:    requestID,
        ApproverID:   approverID,
        ApproverName: approverName,
        BeforeState:  beforeState,
        AfterState:   afterState,
        Timestamp:    time.Now(),
    })

    return nil
}

// Separate audit event handler
type AuditEventHandler struct {
    auditService *AuditService
}

func (h *AuditEventHandler) HandleLeaveRequestApproved(ctx context.Context, event *events.LeaveRequestApprovedEvent) error {
    return h.auditService.CreateAuditLog(ctx, CreateAuditLogParams{
        Action:     coreDomain.AuditActionApprove,
        EntityType: coreDomain.AuditEntityLeaveRequest,
        EntityID:   event.RequestID,
        Before:     event.BeforeState,
        After:      event.AfterState,
        // ...
    })
}
Pros:
  • ✅ Complete decoupling
  • ✅ Async processing possible
  • ✅ Easy to add multiple listeners
  • ✅ Microservice-ready
Cons:
  • ❌ Most complex
  • ❌ Eventual consistency
  • ❌ Requires event bus infrastructure

Approach 4: Middleware/Interceptor (HTTP Level)

Capture audit logs at the HTTP handler level.
// audit_middleware.go
func AuditMiddleware(auditService *AuditService) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // Capture request
            requestBody := captureRequestBody(c)

            // Execute handler
            err := next(c)

            // Log audit
            if shouldAudit(c.Path()) {
                auditService.CreateAuditLog(c.Request().Context(), CreateAuditLogParams{
                    UserID:     getUserID(c),
                    Action:     mapHTTPMethodToAction(c.Request().Method),
                    EntityType: extractEntityType(c.Path()),
                    EntityID:   c.Param("id"),
                    Details:    requestBody,
                    IPAddress:  c.RealIP(),
                    UserAgent:  c.Request().UserAgent(),
                })
            }

            return err
        }
    }
}
Pros:
  • ✅ Centralized
  • ✅ Captures all HTTP operations
  • ✅ Easy to enable/disable
Cons:
  • ❌ No access to domain-level before/after states
  • ❌ HTTP-only (doesn’t capture background jobs)
  • ❌ Less granular

Phase 1: Start with Manual (Quick Win)

Add audit logging to critical operations:
  • Leave request approval/rejection
  • Leave type creation/deletion
  • Policy changes

Phase 2: Add Event-Driven (Scale)

Refactor to use domain events for automatic audit logging.

Phase 3: Add Middleware (Coverage)

Add HTTP middleware for comprehensive coverage of all API calls.

Example: Complete Integration

Here’s how to integrate audit logging into the approval service:
// Step 1: Update ApprovalService constructor
func NewApprovalService(
    leaveRequestRepo ports.LeaveRequestRepository,
    leaveBalanceRepo ports.LeaveBalanceRepository,
    leaveTypeRepo ports.LeaveTypeRepository,
    userRepo ports.UserRepository,
    auditService *AuditService,  // Add this
) *ApprovalService {
    return &ApprovalService{
        leaveRequestRepo: leaveRequestRepo,
        leaveBalanceRepo: leaveBalanceRepo,
        leaveTypeRepo:    leaveTypeRepo,
        userRepo:         userRepo,
        auditService:     auditService,  // Add this
    }
}

// Step 2: Add audit logging to ApproveRequest
func (s *ApprovalService) ApproveRequest(ctx context.Context, requestID, approverID, approverName, comment string) error {
    // Get BEFORE state
    request, err := s.leaveRequestRepo.FindByID(ctx, requestID)
    if err != nil {
        return err
    }

    beforeState := map[string]interface{}{
        "status": string(request.Status),
        "approver_id": request.ApproverID,
    }

    // ... existing approval logic ...

    // Create audit log
    defer func() {
        afterState := map[string]interface{}{
            "status": string(request.Status),
            "approver_id": approverID,
        }

        s.auditService.CreateAuditLog(ctx, CreateAuditLogParams{
            ID:         "audit_" + generateID(),
            UserID:     approverID,
            UserName:   approverName,
            UserRole:   "manager",
            Action:     coreDomain.AuditActionApprove,
            EntityType: coreDomain.AuditEntityLeaveRequest,
            EntityID:   requestID,
            Before:     beforeState,
            After:      afterState,
            Details: map[string]interface{}{
                "comment": comment,
            },
        })
    }()

    return nil
}

What Gets Audited?

Based on the AuditAction enum in audit_log.go:
  • CREATE - New leave requests, leave types
  • UPDATE - Policy changes, leave type modifications
  • DELETE - Leave type deletion
  • APPROVE - Leave request approvals
  • REJECT - Leave request rejections
  • CANCEL - Leave request cancellations
  • EXPORT - Report exports
  • VIEW - Sensitive data access

Next Steps

  • Add audit middleware for HTTP-level logging