Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions ontime-back/docs/logging-redaction-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Logging Redaction Policy

Production logs must only contain operational metadata needed to debug routing,
ownership, status, and latency. Request payloads are not safe log data.

## Request Logs

Controller request logging is centralized in `LoggingAspect` and
`RequestLogPolicy`. Request logs must include only:

- request ID
- route
- method
- actor identifier
- client IP
- response status
- timing in milliseconds

The request logger must not inspect or render `@RequestBody` arguments.

## Sensitive Fields

Never log values or raw key/value payloads for credentials, OAuth material,
profile text, notes, or request bodies. Sensitive key names include:

- `authorization`
- `firebaseToken`
- `password`
- `secret`
- `token`

When field-level logging is genuinely needed, add the field to
`RequestLogPolicy`'s allowlist and keep the log statement metadata-only.

## Regression Guard

`SensitiveLoggingPolicyTest` scans application source for sensitive key names in
logger calls, request-body logging in `LoggingAspect`, and DTO-generated
`toString` methods. Any future exception must update this policy and the
central allowlist in the same change.
108 changes: 27 additions & 81 deletions ontime-back/src/main/java/devkor/ontime_back/LoggingAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,130 +2,71 @@

import devkor.ontime_back.dto.RequestInfoDto;
import devkor.ontime_back.entity.ApiLog;
import devkor.ontime_back.repository.ApiLogRepository;
import devkor.ontime_back.logging.RequestLogPolicy;
import devkor.ontime_back.response.GeneralException;
import devkor.ontime_back.service.ApiLogService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.annotation.Annotation;
import java.util.Map;


@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {

private final ApiLogService apiLogService;
private static final String NO_PARAMS = "No Params";
private static final String NO_BODY = "No Body";

@Pointcut("bean(*Controller)")
private void allRequest() {}

@Around("allRequest()")
public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
RequestInfoDto requestInfoDto = extractRequestInfo();

// requestTime
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String requestId = RequestLogPolicy.resolveRequestId(request);
RequestLogPolicy.exposeRequestId(attributes, requestId);
RequestInfoDto requestInfoDto = extractRequestInfo(request);
long beforeRequest = System.currentTimeMillis();

// pathVariable, requestBody
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Object[] args = joinPoint.getArgs();
Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();

String pathVariable = null;
String requestBody = null;

for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotations = parameterAnnotations[i];
for (Annotation annotation : annotations) {
if (annotation instanceof PathVariable) {
pathVariable = args[i].toString(); // @PathVariable 값 저장
} else if (annotation instanceof RequestBody) {
requestBody = args[i].toString(); // @RequestBody 값 저장
}
}
}

// responseStatus
int responseStatus = 200;
Object result;
try {
// 실제 메서드 실행
result = joinPoint.proceed();
Object result = joinPoint.proceed();
int responseStatus = 200;
if (result instanceof ResponseEntity) {
ResponseEntity<?> responseEntity = (ResponseEntity<?>) result;
responseStatus = responseEntity.getStatusCodeValue(); // 상태 코드 추출
responseStatus = responseEntity.getStatusCode().value();
}

// 정상 요청 로그 저장
long timeTaken = System.currentTimeMillis() - beforeRequest;

ApiLog apiLog = buildApiLog(requestInfoDto, responseStatus, timeTaken);
apiLogService.saveLog(apiLog);

log.info("[Request Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, pathVariable: {}, requestBody: {}, responseStatus: {}, timeTaken: {}",
requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), requestInfoDto.getClientIp(),
pathVariable != null ? pathVariable : NO_PARAMS,
requestBody != null ? requestBody : NO_BODY,
responseStatus, timeTaken);
saveApiLog(requestInfoDto, responseStatus, timeTaken);
log.info("[Request Log] requestId: {}, route: {}, method: {}, actor: {}, clientIp: {}, responseStatus: {}, timeTakenMs: {}",
requestId, requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(),
requestInfoDto.getClientIp(), responseStatus, timeTaken);

return result;

} catch (Exception ex) {
} catch (Throwable ex) {
int responseStatus = mapExceptionToStatusCode(ex);
long timeTaken = System.currentTimeMillis() - beforeRequest;
saveApiLog(requestInfoDto, responseStatus, timeTaken);
log.error("[Error Log] requestId: {}, route: {}, method: {}, actor: {}, clientIp: {}, exception: {}, responseStatus: {}, timeTakenMs: {}",
requestId, requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(),
requestInfoDto.getClientIp(), ex.getClass().getSimpleName(), responseStatus, timeTaken);
throw ex;
}
}

@AfterThrowing(pointcut = "allRequest()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
RequestInfoDto requestInfoDto = extractRequestInfo();

// exceptionName
String exceptionName;
if (ex instanceof GeneralException) {
exceptionName = ((GeneralException) ex).getErrorCode().name();
} else {
exceptionName = ex.getClass().getSimpleName();
};
// exceptionMessage
String exceptionMessage = ex.getMessage();
// responseStatus
int responseStatus = mapExceptionToStatusCode(ex);

log.error("[Error Log] requestUrl: {}, requestMethod: {}, userId: {}, clientIp: {}, exception: {}, message: {}, responseStatus: {}",
requestInfoDto.getRequestUrl(), requestInfoDto.getRequestMethod(), requestInfoDto.getUserId(), requestInfoDto.getClientIp(), exceptionName, exceptionMessage, responseStatus);

ApiLog errorLog = buildApiLog(requestInfoDto, responseStatus, 0);
apiLogService.saveLog(errorLog);
}

// requestinfo 추출
private RequestInfoDto extractRequestInfo() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

private RequestInfoDto extractRequestInfo(HttpServletRequest request) {
String requestUrl = request.getRequestURI();
String requestMethod = request.getMethod();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Expand All @@ -137,6 +78,11 @@ private RequestInfoDto extractRequestInfo() {
return new RequestInfoDto(requestUrl, requestMethod, userId, clientIp);
}

private void saveApiLog(RequestInfoDto requestInfoDto, int responseStatus, long timeTaken) {
ApiLog apiLog = buildApiLog(requestInfoDto, responseStatus, timeTaken);
apiLogService.saveLog(apiLog);
}

// apilog 생성
private ApiLog buildApiLog(RequestInfoDto info, int responseStatus, long timeTaken) {
return ApiLog.builder()
Expand All @@ -149,7 +95,7 @@ private ApiLog buildApiLog(RequestInfoDto info, int responseStatus, long timeTak
.build();
}

private int mapExceptionToStatusCode(Exception e) {
private int mapExceptionToStatusCode(Throwable e) {
if (e instanceof GeneralException ge) {
return ge.getErrorCode().getCode();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AlarmDeviceCurrentRequestDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.Instant;

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AlarmDeviceUnregisterRequestDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.Instant;

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AlarmStatusFailureDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;

@Getter
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class AlarmStatusReportRequestDto {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,12 @@
public class AppleTokenResponseDto {
@JsonProperty("access_token")
private String accessToken;

@JsonProperty("token_type")
private String tokenType;

@JsonProperty("expires_in")
private long expiresIn;

@JsonProperty("refresh_token")
private String refreshToken;

@JsonProperty("id_token")
private String idToken;

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
public class ChangePasswordDto {
private String currentPassword;
private String newPassword;

@Builder
public ChangePasswordDto(String currentPassword, String newPassword) {
this.currentPassword = currentPassword;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package devkor.ontime_back.dto;

import lombok.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.UUID;

@ToString
@Getter
@Builder
@NoArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

import java.util.UUID;

@ToString
@Getter
public class FinishPreparationDto {
private UUID scheduleId;
private Integer latenessTime;

@Builder
public FinishPreparationDto(UUID scheduleId, Integer latenessTime) {
this.scheduleId = scheduleId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package devkor.ontime_back.dto;

import lombok.Getter;
import lombok.ToString;

@ToString
@Getter
public class FirebaseTokenAddDto {
String firebaseToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@ToString
@Getter
@Builder
public class FriendDto {
private Long friendId;
private String friendName;
private String friendEmail;

public FriendDto(Long friendId, String friendName, String friendEmail) {
this.friendId = friendId;
this.friendName = friendName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@

import lombok.Builder;
import lombok.Getter;

import java.util.List;

@Getter
@Builder
public class GetFriendListResponse {

private List<FriendDto> friendsList;

}
Loading
Loading