How to Implement Audit Logging for Compliance Using the AuditAspect in Stirling-PDF

Stirling-PDF provides a declarative audit framework that uses Aspect-Oriented Programming (AOP) to intercept @Audited annotated methods and persist structured compliance records to Spring Actuator's AuditEventRepository with configurable granularity levels.

Stirling-PDF ships with a comprehensive audit framework designed for enterprise compliance requirements such as traceability and tamper-evident logging. The core mechanism relies on the AuditAspect class alongside ControllerAuditAspect, which automatically capture user actions, HTTP requests, and method arguments without requiring manual logging boilerplate. By leveraging the @Audited annotation and AuditConfigurationProperties, developers can implement granular audit trails ranging from basic authentication events to verbose payload logging.

Architecture Overview

The audit framework follows a layered AOP design that minimizes performance overhead while maximizing data capture flexibility.

Configuration via AuditConfigurationProperties

Global audit behavior is controlled by app/proprietary/src/main/java/stirling/software/proprietary/config/AuditConfigurationProperties.java. This bean binds to the audit section in application.yml and holds two critical flags:

  • enabled – Master switch to turn auditing on or off system-wide.
  • auditLevel – Global default granularity (BASIC, STANDARD, or VERBOSE).

Both AuditAspect and ControllerAuditAspect call auditConfig.isEnabled() and auditConfig.getAuditLevel() at the start of every interception to short-circuit processing when auditing is disabled, ensuring zero runtime penalty for inactive configurations.

The @Audited Annotation

The annotation defined in app/proprietary/src/main/java/stirling/software/proprietary/audit/Audited.java declares audit intent on any Spring bean method. Key attributes include:

  • type – The preferred AuditEventType enum value (default HTTP_REQUEST).
  • typeString – Legacy string identifier for backward compatibility or custom events.
  • level – Overrides the global auditLevel for fine-grained control.
  • includeArgs and includeResult – Booleans controlling whether method arguments and return values appear in the audit record (subject to level restrictions).

Core Aspects: AuditAspect vs ControllerAuditAspect

The framework provides two specialized aspects to cover different execution contexts.

AuditAspect (app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditAspect.java) intercepts any method annotated with @Audited across the entire application context. It uses AuditUtils.shouldAudit() for early exit checks, then builds the audit payload via AuditUtils.createBaseAuditData(). If an HTTP request context exists, it enriches the record with web-specific details before resolving the event type and delegating to AuditService.

ControllerAuditAspect (app/proprietary/src/main/java/stirling/software/proprietary/audit/ControllerAuditAspect.java) specifically wraps Spring MVC mappings (@GetMapping, @PostMapping, etc.) and static-resource handlers. It reuses the same AuditUtils methods but adds HTTP latency and status code tracking directly, then calls AuditUtils.addTimingData() with isHttpRequest=true to avoid duplicate measurements.

Data Collection with AuditUtils

The utility class app/proprietary/src/main/java/stirling/software/proprietary/audit/AuditUtils.java centralizes all audit data construction to ensure consistency across aspects. Key methods include:

  • createBaseAuditData() – Generates timestamp, principal name, and optional class/method metadata.
  • addHttpData() – Captures HTTP method, URI, client IP, session ID, request ID, and form parameters based on the current audit level.
  • addFileData() – Records MultipartFile details such as filename, size, and content type.
  • addMethodArguments() – Safely serializes method parameters using safeToString().
  • addTimingData() – Computes latency and HTTP status codes when the level is at least STANDARD.
  • resolveEventType() – Determines the final AuditEventType by analyzing the annotation, controller name, and HTTP verb.
  • safeToString() – Truncates large payloads and sanitizes binary data to prevent repository corruption.

Persistence via AuditService

The app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java wraps the Spring Actuator AuditEventRepository and performs level-aware filtering. It checks auditConfig.isEnabled() && auditConfig.getAuditLevel().includes(level) before writing. The service provides overloaded audit() methods accepting either AuditEventType enums or legacy strings, plus variants that accept a specific principal name.

Step-by-Step Implementation Guide

1. Enable Auditing in application.yml

Configure the global settings in your Spring configuration:

audit:
  enabled: true
  level: VERBOSE      # BASIC | STANDARD | VERBOSE

The AuditConfigurationProperties bean automatically binds these values and makes them available to all aspects.

2. Annotate Target Methods

Apply @Audited to any service or controller method requiring compliance tracking:

package com.example.service;

import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

    /**
     * Generates a PDF report with full audit trail for compliance.
     * Captures document ID, format, and result size at STANDARD level.
     */
    @Audited(
        type = AuditEventType.PDF_PROCESS,
        level = AuditLevel.STANDARD,
        includeArgs = true,
        includeResult = true
    )
    public byte[] generateReport(String documentId, String format) {
        // Business logic producing PDF bytes
        byte[] pdf = createPdf(documentId, format);
        return pdf;
    }
}

The type attribute selects a standardized category from AuditEventType (USER_LOGIN, FILE_OPERATION, SETTINGS_CHANGED, etc.), while level forces STANDARD capture even if the global setting is BASIC. The includeArgs and includeResult flags ensure the audit record contains safe string representations of the input parameters and return value (truncated to 1,000 characters by AuditUtils.safeToString()).

3. Use Legacy String Types for Custom Events

When the standard enum does not cover a domain-specific operation, use typeString for backward compatibility:

@Audited(
    typeString = "CUSTOM_BATCH_EXPORT",
    level = AuditLevel.VERBOSE,
    includeArgs = true
)
public void exportBatch(List<String> ids) {
    // Batch processing logic
}

Both aspects detect when typeString is non-empty and route the event through auditService.audit(String type, ...) instead of the enum variant.

4. Accessing Audit Records

Audit events are stored in the Spring Actuator AuditEventRepository. Access them via the Actuator endpoint /actuator/auditevents (if exposed) or inject the repository directly:

@Autowired
private AuditEventRepository auditRepo;

public List<AuditEvent> recentSecurityEvents() {
    // Query implementation depends on your repository configuration
    return auditRepo.findByPrincipal("user@example.com");
}

The frontend components located in frontend/src/proprietary/components/shared/config/configSections/audit/ (including AuditEventsTable.tsx and AuditFiltersForm.tsx) provide a searchable, filterable interface for compliance officers to review logs without database access.

Complete Code Examples

Auditing a File Upload Service

package com.example.service;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.audit.AuditEventType;
import stirling.software.proprietary.audit.AuditLevel;

@Service
public class FileImportService {

    @Audited(
        type = AuditEventType.FILE_OPERATION,
        level = AuditLevel.VERBOSE,
        includeArgs = true,
        includeResult = false
    )
    public String importFile(MultipartFile file) {
        // Storage logic
        String fileId = storageProvider.save(file.getInputStream());
        return fileId;
    }
}

This configuration captures the MultipartFile metadata (name, size, content type) via AuditUtils.addFileData() and logs the method arguments, but excludes the return value from the audit trail.

Auditing a Custom REST Endpoint

package com.example.web;

import org.springframework.web.bind.annotation.*;
import stirling.software.proprietary.audit.Audited;
import stirling.software.proprietary.audit.AuditLevel;

@RestController
@RequestMapping("/api/v1/jobs")
public class JobController {

    @PostMapping("/execute")
    @Audited(
        typeString = "CUSTOM_JOB_RUN",
        level = AuditLevel.STANDARD,
        includeArgs = true
    )
    public JobStatus runJob(@RequestBody JobRequest request) {
        return jobService.execute(request);
    }
}

ControllerAuditAspect intercepts this mapping, adds HTTP-specific context (URI, method, latency), and records the event using the custom string type.

Manual Audit Logging

For operations outside the AOP context, inject AuditService directly:

@Service
public class ReportingService {

    private final AuditService auditService;

    public ReportingService(AuditService auditService) {
        this.auditService = auditService;
    }

    public void logComplianceNote(String user, String comment) {
        Map<String, Object> data = Map.of(
            "timestamp", java.time.Instant.now().toString(),
            "principal", user,
            "comment", comment,
            "source", "manual_entry"
        );
        auditService.audit("MANUAL_NOTE", data, AuditLevel.BASIC);
    }
}

Summary

  • Enable globally via audit.enabled and audit.level in application.yml using AuditConfigurationProperties.
  • Annotate methods with @Audited, selecting an AuditEventType or custom typeString, and override the level if needed.
  • Rely on aspects – AuditAspect handles general beans while ControllerAuditAspect captures HTTP-specific metadata for web endpoints.
  • Leverage utilities – AuditUtils standardizes data collection including safe string serialization, file metadata, and timing calculations.
  • Persist correctly – AuditService filters by level and writes to AuditEventRepository only when appropriate.
  • Review in UI – Frontend components under frontend/src/proprietary/components/shared/config/configSections/audit/ render searchable compliance reports.

Frequently Asked Questions

What is the difference between AuditAspect and ControllerAuditAspect?

AuditAspect intercepts any Spring bean method annotated with @Audited and is ideal for service-layer operations, while ControllerAuditAspect specifically targets Spring MVC handler methods to capture HTTP-specific data such as request URI, client IP, and response status codes. Both delegate to AuditUtils for consistent data formatting and AuditService for persistence, but ControllerAuditAspect adds web-centric metadata that service-layer aspects cannot access.

How do I exclude sensitive arguments from audit logs?

Set includeArgs = false in the @Audited annotation, or ensure the global audit.level is set to BASIC rather than VERBOSE. At BASIC level, AuditUtils.addMethodArguments() is never invoked, and AuditUtils.safeToString() truncates or excludes large payloads automatically. For fine-grained control over specific fields, you can override the level on a per-method basis using the level annotation attribute.

Where are audit events physically stored?

Events are stored in the Spring Boot Actuator AuditEventRepository bean, which by default uses an in-memory repository but can be configured to persist to databases or external systems via Spring Boot's audit infrastructure. The AuditService class in app/proprietary/src/main/java/stirling/software/proprietary/service/AuditService.java performs the final write operation after checking the configured audit level and enabled status.

Can I use custom event types not defined in AuditEventType?

Yes. Use the typeString attribute of the @Audited annotation instead of type. When typeString is provided (for example, typeString = "CUSTOM_INTEGRATION_SYNC"), the aspect routes the event to auditService.audit(String type, ...) rather than the enum-based overload, allowing arbitrary string identifiers while maintaining full compliance data structure.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →