31 - Audit Log เบื้องต้น
เป้าหมายของบท
Section titled “เป้าหมายของบท”บทนี้จะเพิ่ม audit log สำหรับ action สำคัญของ admin เช่น เปลี่ยน role หรือเปลี่ยน status
หลังจบบทนี้ผู้อ่านควรเข้าใจ:
- audit log คืออะไร
- ควรบันทึกข้อมูลอะไรบ้าง
- ทำไม audit log ควรอยู่ใน transaction เดียวกับ action หลัก
- เพิ่ม entity/repository/service สำหรับ audit log อย่างไร
- สร้าง endpoint ให้ admin ดู audit log แบบ pagination ได้อย่างไร
Audit log คืออะไร
Section titled “Audit log คืออะไร”Audit log คือบันทึกเหตุการณ์สำคัญของระบบ เช่น:
admin@example.com changed user 5 role from USER to ADMINadmin@example.com changed user 8 status to INACTIVEเมื่อเกิดปัญหา เช่น user ถูกปิดบัญชีผิดคน เราจะย้อนดูได้ว่า:
- ใครเป็นคนทำ
- ทำกับ user คนไหน
- action คืออะไร
- ค่าเดิมคืออะไร
- ค่าใหม่คืออะไร
- เกิดขึ้นเมื่อไร
สร้าง AuditLog entity
Section titled “สร้าง AuditLog entity”สร้างไฟล์:
src/main/java/com/example/backendapi/model/AuditLog.javapackage com.example.backendapi.model;
import jakarta.persistence.Column;import jakarta.persistence.Entity;import jakarta.persistence.GeneratedValue;import jakarta.persistence.GenerationType;import jakarta.persistence.Id;import jakarta.persistence.PrePersist;import jakarta.persistence.Table;import java.time.LocalDateTime;import lombok.Getter;import lombok.NoArgsConstructor;import lombok.Setter;
@Entity@Table(name = "audit_logs")@Getter@Setter@NoArgsConstructorpublic class AuditLog {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Column(nullable = false, length = 100) private String action;
@Column(nullable = false) private Long actorId;
@Column(nullable = false) private Long targetUserId;
@Column(length = 100) private String oldValue;
@Column(length = 100) private String newValue;
@Column(nullable = false) private LocalDateTime createdAt;
@PrePersist void onCreate() { createdAt = LocalDateTime.now(); }}สร้าง AuditLogRepository
Section titled “สร้าง AuditLogRepository”package com.example.backendapi.repository;
import com.example.backendapi.model.AuditLog;import org.springframework.data.jpa.repository.JpaRepository;
public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {}สร้าง AuditLogResponse
Section titled “สร้าง AuditLogResponse”package com.example.backendapi.dto;
import java.time.LocalDateTime;
public record AuditLogResponse( Long id, String action, Long actorId, Long targetUserId, String oldValue, String newValue, LocalDateTime createdAt) {}สร้าง AuditLogService
Section titled “สร้าง AuditLogService”package com.example.backendapi.service;
import com.example.backendapi.common.PageResponse;import com.example.backendapi.dto.AuditLogResponse;import com.example.backendapi.model.AuditLog;import com.example.backendapi.repository.AuditLogRepository;import lombok.RequiredArgsConstructor;import org.springframework.data.domain.PageRequest;import org.springframework.data.domain.Sort;import org.springframework.stereotype.Service;
@Service@RequiredArgsConstructorpublic class AuditLogService {
private final AuditLogRepository auditLogRepository;
public void record( String action, Long actorId, Long targetUserId, String oldValue, String newValue ) { AuditLog log = new AuditLog(); log.setAction(action); log.setActorId(actorId); log.setTargetUserId(targetUserId); log.setOldValue(oldValue); log.setNewValue(newValue); auditLogRepository.save(log); }
public PageResponse<AuditLogResponse> findAll(int page, int size) { int safeSize = Math.min(size, 100); return PageResponse.from( auditLogRepository.findAll( PageRequest.of(page, safeSize, Sort.by("createdAt").descending()) ).map(this::toResponse) ); }
private AuditLogResponse toResponse(AuditLog log) { return new AuditLogResponse( log.getId(), log.getAction(), log.getActorId(), log.getTargetUserId(), log.getOldValue(), log.getNewValue(), log.getCreatedAt() ); }}ผูก audit log กับ change role/status
Section titled “ผูก audit log กับ change role/status”ใน AdminUserService เพิ่ม AuditLogService:
private final AuditLogService auditLogService;ตอนเปลี่ยน role:
User actor = findActor(actorEmail);User target = findUser(id);Role oldRole = target.getRole();target.setRole(request.role());
auditLogService.record( "CHANGE_ROLE", actor.getId(), target.getId(), oldRole.name(), request.role().name());ตอนเปลี่ยน status:
User actor = findActor(actorEmail);User target = findUser(id);UserStatus oldStatus = target.getStatus();target.setStatus(request.status());
auditLogService.record( "CHANGE_STATUS", actor.getId(), target.getId(), oldStatus.name(), request.status().name());เพราะ method อยู่ใน @Transactional ถ้า save audit log error transaction ควร rollback ทั้ง action
จากนั้นปรับ AdminUserController ให้ส่ง email ของ admin ปัจจุบันเข้าไปด้วย ทั้ง endpoint เปลี่ยน role และ status:
@PatchMapping("/{id}/role")public ApiResponse<UserResponse> changeRole( @PathVariable Long id, @Valid @RequestBody ChangeRoleRequest request, Authentication authentication) { return ApiResponse.ok( "User role changed", adminUserService.changeRole(id, request, authentication.getName()) );}
@PatchMapping("/{id}/status")public ApiResponse<UserResponse> changeStatus( @PathVariable Long id, @Valid @RequestBody ChangeStatusRequest request, Authentication authentication) { return ApiResponse.ok( "User status changed", adminUserService.changeStatus(id, request, authentication.getName()) );}import ที่ต้องมี:
import org.springframework.security.core.Authentication;สร้าง endpoint ดู audit log
Section titled “สร้าง endpoint ดู audit log”package com.example.backendapi.controller;
import com.example.backendapi.common.ApiResponse;import com.example.backendapi.common.PageResponse;import com.example.backendapi.dto.AuditLogResponse;import com.example.backendapi.service.AuditLogService;import lombok.RequiredArgsConstructor;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
@RestController@RequestMapping("/api/v1/admin/audit-logs")@RequiredArgsConstructorpublic class AdminAuditLogController {
private final AuditLogService auditLogService;
@GetMapping public ApiResponse<PageResponse<AuditLogResponse>> findAll( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size ) { return ApiResponse.ok("Audit logs found", auditLogService.findAll(page, size)); }}Action ที่ควรบันทึก
Section titled “Action ที่ควรบันทึก”เริ่มจาก:
CHANGE_ROLECHANGE_STATUSต่อไปอาจเพิ่ม:
CREATE_ADMINDISABLE_USERENABLE_USERRESET_PASSWORDDELETE_USERข้อควรระวัง
Section titled “ข้อควรระวัง”- audit log ไม่ควรเก็บ password หรือข้อมูลลับ
- audit log ควรถูกแก้ไขยากกว่าข้อมูลทั่วไป
- endpoint ดู audit log ควรเป็น admin-only
- ในระบบใหญ่ควรมี request id หรือ IP address เพิ่ม
- retention policy ต้องชัด เช่น เก็บกี่เดือนหรือกี่ปี
Checkpoint ปิดภาค Admin System
Section titled “Checkpoint ปิดภาค Admin System”หลังจบบทนี้ ระบบควรทำได้:
- แยก role
USERและADMIN - lock
/api/v1/admin/**ให้ admin เท่านั้น - admin ดูรายชื่อผู้ใช้พร้อม filter ได้
- admin เปลี่ยน role/status ได้
- บันทึก audit log ตอนเปลี่ยน role/status
- admin ดู audit log ได้
แบบฝึกหัดท้ายบท
Section titled “แบบฝึกหัดท้ายบท”- สร้าง
AuditLogentity - สร้าง
AuditLogRepository - สร้าง
AuditLogService - บันทึก log ตอนเปลี่ยน role
- บันทึก log ตอนเปลี่ยน status
- สร้าง endpoint ดู audit log
- ทดสอบ pagination ของ audit log