Tutorial 9: Audit Trail¶
Pages: 2
📖 Page 1: AuditEvent Implementation¶
Why Audit Trail?¶
Required for compliance: - HIPAA (Healthcare) - GDPR (Privacy) - SOC 2 (Security)
Tracks: Who did what, when, to which resource
AuditEventDTO¶
Create AuditEventDTO.java:
@Data
public class AuditEventDTO {
private String id;
private String action; // C, R, U, D, E
private LocalDateTime recorded;
private String outcome; // 0=success, 4=minor failure, 8=serious failure
private String userName;
private String resourceType; // Patient, Practitioner, etc.
private String resourceId;
private String description;
}
AuditService¶
Create AuditService.java:
@Service
@RequiredArgsConstructor
public class AuditService {
private final IGenericClient fhirClient;
public void createAuditEvent(String action, String resourceType,
String resourceId, String userName) {
AuditEvent audit = new AuditEvent();
// Type - RESTful operation
Coding typeCoding = new Coding();
typeCoding.setSystem("http://terminology.hl7.org/CodeSystem/audit-event-type");
typeCoding.setCode("rest");
typeCoding.setDisplay("RESTful Operation");
audit.setType(typeCoding);
// Action: C=Create, R=Read, U=Update, D=Delete, E=Execute
audit.setAction(AuditEvent.AuditEventAction.valueOf(action));
// Recorded time
audit.setRecorded(new Date());
// Outcome: 0=success
audit.setOutcome(AuditEvent.AuditEventOutcome._0);
// Agent (who performed action)
AuditEvent.AuditEventAgentComponent agent =
new AuditEvent.AuditEventAgentComponent();
Reference agentRef = new Reference();
agentRef.setDisplay(userName != null ? userName : "System");
agent.setWho(agentRef);
agent.setRequestor(true);
audit.addAgent(agent);
// Source (application)
AuditEvent.AuditEventSourceComponent source =
new AuditEvent.AuditEventSourceComponent();
Reference sourceRef = new Reference();
sourceRef.setDisplay("FHIR PMS Application");
source.setObserver(sourceRef);
audit.setSource(source);
// Entity (what was affected)
AuditEvent.AuditEventEntityComponent entity =
new AuditEvent.AuditEventEntityComponent();
Reference entityRef = new Reference(resourceType + "/" + resourceId);
entity.setWhat(entityRef);
Coding entityType = new Coding();
entityType.setCode(resourceType);
entity.setType(entityType);
audit.addEntity(entity);
// Save to FHIR server
try {
fhirClient.create()
.resource(audit)
.execute();
} catch (Exception e) {
// Log but don't fail main operation
System.err.println("Audit failed: " + e.getMessage());
}
}
// Get all audit events
public List<AuditEventDTO> getAllAuditEvents() {
Bundle bundle = fhirClient.search()
.forResource(AuditEvent.class)
.returnBundle(Bundle.class)
.count(100)
.execute();
return bundle.getEntry().stream()
.map(entry -> (AuditEvent) entry.getResource())
.map(this::toDTO)
.collect(Collectors.toList());
}
private AuditEventDTO toDTO(AuditEvent audit) {
AuditEventDTO dto = new AuditEventDTO();
dto.setId(audit.getIdElement().getIdPart());
dto.setAction(audit.getAction().toCode());
dto.setRecorded(audit.getRecorded().toInstant()
.atZone(ZoneId.systemDefault()).toLocalDateTime());
dto.setOutcome(audit.getOutcome().toCode());
if (!audit.getAgent().isEmpty()) {
dto.setUserName(audit.getAgent().get(0).getWho().getDisplay());
}
if (!audit.getEntity().isEmpty()) {
AuditEvent.AuditEventEntityComponent entity = audit.getEntity().get(0);
String ref = entity.getWhat().getReference();
if (ref != null && ref.contains("/")) {
String[] parts = ref.split("/");
dto.setResourceType(parts[0]);
dto.setResourceId(parts[1]);
}
}
return dto;
}
}
📖 Page 2: Auto-Logging & UI¶
Add Audit to All Services¶
Update PatientService methods:
@Service
@RequiredArgsConstructor
public class PatientService {
private final IGenericClient fhirClient;
private final PatientMapper patientMapper;
private final AuditService auditService; // ADD THIS
public PatientDTO createPatient(PatientDTO dto) {
Patient fhirPatient = patientMapper.toFhirResource(dto);
MethodOutcome outcome = fhirClient.create()
.resource(fhirPatient)
.execute();
Patient created = (Patient) outcome.getResource();
// ADD AUDIT
auditService.createAuditEvent("C", "Patient",
created.getIdElement().getIdPart(), "Admin");
return patientMapper.toDTO(created);
}
public PatientDTO getPatientById(String id) {
Patient patient = fhirClient.read()
.resource(Patient.class)
.withId(id)
.execute();
// ADD AUDIT
auditService.createAuditEvent("R", "Patient", id, "Admin");
return patientMapper.toDTO(patient);
}
public PatientDTO updatePatient(String id, PatientDTO dto) {
// ... update logic ...
// ADD AUDIT
auditService.createAuditEvent("U", "Patient", id, "Admin");
return getPatientById(id);
}
public void deletePatient(String id) {
fhirClient.delete()
.resourceById("Patient", id)
.execute();
// ADD AUDIT
auditService.createAuditEvent("D", "Patient", id, "Admin");
}
}
Repeat for Practitioner, Organization, Appointment services!
Audit UI¶
Create AuditController.java:
@Controller
@RequestMapping("/audit")
@RequiredArgsConstructor
public class AuditController {
private final AuditService auditService;
@GetMapping
public String listAuditEvents(
@RequestParam(required = false) String action,
@RequestParam(required = false) String resourceType,
Model model) {
List<AuditEventDTO> events = auditService.getAllAuditEvents();
// Filter
if (action != null && !action.isEmpty()) {
events = events.stream()
.filter(e -> action.equals(e.getAction()))
.collect(Collectors.toList());
}
if (resourceType != null && !resourceType.isEmpty()) {
events = events.stream()
.filter(e -> resourceType.equals(e.getResourceType()))
.collect(Collectors.toList());
}
model.addAttribute("events", events);
model.addAttribute("action", action);
model.addAttribute("resourceType", resourceType);
return "audit/list";
}
}
Create templates/audit/list.html:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Audit Trail</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1>📝 Audit Trail</h1>
<!-- Filters -->
<form class="row mb-3" method="get">
<div class="col-md-3">
<select name="action" class="form-control">
<option value="">All Actions</option>
<option value="C" th:selected="${action == 'C'}">Create</option>
<option value="R" th:selected="${action == 'R'}">Read</option>
<option value="U" th:selected="${action == 'U'}">Update</option>
<option value="D" th:selected="${action == 'D'}">Delete</option>
</select>
</div>
<div class="col-md-3">
<select name="resourceType" class="form-control">
<option value="">All Resources</option>
<option value="Patient" th:selected="${resourceType == 'Patient'}">Patient</option>
<option value="Practitioner" th:selected="${resourceType == 'Practitioner'}">Practitioner</option>
<option value="Organization" th:selected="${resourceType == 'Organization'}">Organization</option>
<option value="Appointment" th:selected="${resourceType == 'Appointment'}">Appointment</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary">Filter</button>
</div>
</form>
<!-- Table -->
<table class="table table-sm table-striped">
<thead>
<tr>
<th>Time</th>
<th>Action</th>
<th>Resource</th>
<th>ID</th>
<th>User</th>
<th>Outcome</th>
</tr>
</thead>
<tbody>
<tr th:each="event : ${events}">
<td th:text="${#temporals.format(event.recorded, 'MMM dd HH:mm')}"></td>
<td>
<span th:switch="${event.action}">
<span th:case="'C'" class="badge bg-success">Create</span>
<span th:case="'R'" class="badge bg-info">Read</span>
<span th:case="'U'" class="badge bg-warning">Update</span>
<span th:case="'D'" class="badge bg-danger">Delete</span>
</span>
</td>
<td th:text="${event.resourceType}"></td>
<td th:text="${event.resourceId}"></td>
<td th:text="${event.userName}"></td>
<td>
<span th:if="${event.outcome == '0'}" class="badge bg-success">✓</span>
<span th:if="${event.outcome != '0'}" class="badge bg-danger">✗</span>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
✅ Audit Complete¶
- AuditEvent DTO and Service
- Automatic logging in all CRUD operations
- Audit UI with filters (action, resource type)
- Compliance ready (HIPAA, GDPR, SOC 2)
🚀 Next¶
System complete! Now test and deploy.
→ Tutorial 10: Testing & Deployment
💡 Quick Tips¶
Non-Blocking
Audit failures shouldn't break main operations (try-catch)
Action Codes
C=Create, R=Read, U=Update, D=Delete, E=Execute
Compliance
Audit trail is required for healthcare applications