Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#51 로그 관련 도메인와 로그 API 요청의 필드명 변경 #53

Merged
merged 11 commits into from
Aug 15, 2024
Merged
28 changes: 28 additions & 0 deletions logbat/src/main/java/info/logbat/common/util/UUIDUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package info.logbat.common.util;

import java.nio.ByteBuffer;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class UUIDUtil {

private static final int UUID_BYTE_LENGTH = 16;

public static byte[] uuidStringToBytes(String uuidStr) {
UUID uuid = UUID.fromString(uuidStr);
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[UUID_BYTE_LENGTH]);
byteBuffer.putLong(uuid.getMostSignificantBits());
byteBuffer.putLong(uuid.getLeastSignificantBits());
return byteBuffer.array();
}

public static String bytesToUuidString(byte[] bytes) {
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
long high = byteBuffer.getLong();
long low = byteBuffer.getLong();
UUID uuid = new UUID(high, low);
return uuid.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ public class LogService {
private final LogRepository logRepository;

public long saveLog(CreateLogServiceRequest request) {
Long appId = request.applicationId();
String logLevel = request.logLevel();
String logData = request.logData();
String appKey = request.appKey();
String level = request.level();
String data = request.data();
LocalDateTime timestamp = request.timestamp();
// TODO Log 저장 전 Application ID 체크 로직 추가 필요

Log log = Log.of(appId, logLevel, logData, timestamp);
Log log = Log.of(appKey, level, data, timestamp);

return logRepository.save(log);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@
import java.time.LocalDateTime;

public record CreateLogServiceRequest(
Long applicationId,
String logLevel,
String logData,
String appKey,
String level,
String data,
LocalDateTime timestamp
) {

public static CreateLogServiceRequest of(Long applicationId, CreateLogRequest request) {
public static CreateLogServiceRequest of(String appKey, CreateLogRequest request) {
return new CreateLogServiceRequest(
applicationId,
request.logLevel(),
request.logData(),
appKey,
request.level(),
request.data(),
request.timestamp()
);
}
Expand Down
26 changes: 13 additions & 13 deletions logbat/src/main/java/info/logbat/domain/log/domain/Log.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@
public class Log {

private final Long logId;
private final Long applicationId;
private final String appKey;
private final Level level;
private final LogData logData;
private final LogData data;
private final LocalDateTime timestamp;

public static Log of(Long applicationId, String level, String logData, LocalDateTime timestamp) {
return new Log(applicationId, level, logData, timestamp);
public static Log of(String appKey, String level, String logData, LocalDateTime timestamp) {
return new Log(appKey, level, logData, timestamp);
}

public Log(Long applicationId, String level, String logData, LocalDateTime timestamp) {
this(null, applicationId, level, logData, timestamp);
public Log(String appKey, String level, String data, LocalDateTime timestamp) {
this(null, appKey, level, data, timestamp);
}

public Log(Long logId, Long applicationId, String level, String logData,
public Log(Long logId, String appKey, String level, String data,
LocalDateTime timestamp) {
this.logId = logId;
validateApplicationId(applicationId);
this.applicationId = applicationId;
validateAppKey(appKey);
this.appKey = appKey;
this.level = Level.from(level);
this.logData = LogData.from(logData);
this.data = LogData.from(data);
validateTimestamp(timestamp);
this.timestamp = timestamp;
}

private void validateApplicationId(Long applicationId) {
if (applicationId == null) {
throw new IllegalArgumentException("applicationId는 null이 될 수 없습니다.");
private void validateAppKey(String appKey) {
if (appKey == null || appKey.isBlank()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 항상 고민인 부분인데, 컨트롤러에서도 이것에 대한 검사를 하는것 같아서 두번해도 괜찮나 생각듭니다...!
각자 맡아야 할 책임이 달라서 두군데 모두 있는면이 자연스럽다고 생각합니다!

throw new IllegalArgumentException("appKey는 null이거나 빈 문자열이 될 수 없습니다.");
}
}

Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

성공 실패 응답만 주면 되니까 201로 주는건 어떨까요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영하겠습니다~

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

노션 API 문서도 업데이트 했습니다~

Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
import info.logbat.domain.log.application.payload.request.CreateLogServiceRequest;
import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
Expand All @@ -22,18 +22,15 @@ public class LogController {
private final LogService logService;

@PostMapping
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@PostMapping
@PostMapping

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 응답 status를 정하는건 어떤가요?
해당 API는 성공했을때 201을 보내야 하니 응답 코드에 대한 명시가 함수 내부에 있는것 보다 함수 정의 바로 위에 있는면이 개인적으로 더 보기 좋다고 생각해서 제안드립니다!

Suggested change
@PostMapping
@PostMapping
@ResponseStatus(HttpStatus.CREATED)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 하면 함수 리턴타입을 void로 해야할 것 같아요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했어요!!

public ResponseEntity<Void> saveLog(
@RequestHeader("app-id")
@NotNull(message = "Application ID가 비어있습니다.")
@Positive(message = "Application ID는 양수여야 합니다.") Long applicationId,
@ResponseStatus(HttpStatus.CREATED)
public void saveLog(
@RequestHeader("appKey")
@NotBlank(message = "appKey가 비어있습니다.") String appKey,

@Valid @RequestBody CreateLogRequest request
) {

logService.saveLog(CreateLogServiceRequest.of(applicationId, request));

return ResponseEntity.ok()
.build();
logService.saveLog(CreateLogServiceRequest.of(appKey, request));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

public record CreateLogRequest(
@NotBlank(message = "로그 레벨이 비어있습니다.")
String logLevel,
String level,

@NotBlank(message = "로그 데이터가 비어있습니다.")
String logData,
String data,

@NotNull(message = "타임스탬프가 비어있습니다.")
LocalDateTime timestamp
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package info.logbat.domain.log.repository;

import info.logbat.common.util.UUIDUtil;
import info.logbat.domain.log.domain.Log;
import java.sql.PreparedStatement;
import java.sql.Statement;
Expand All @@ -20,15 +21,15 @@ public class LogRepository {
private final JdbcTemplate jdbcTemplate;

public long save(Log log) {
String sql = "INSERT INTO logs (application_id, level, log_data, timestamp) VALUES (?, ?, ?, ?)";
String sql = "INSERT INTO logs (app_key, level, data, timestamp) VALUES (?, ?, ?, ?)";

KeyHolder keyHolder = new GeneratedKeyHolder();

jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
ps.setLong(1, log.getApplicationId());
ps.setBytes(1, UUIDUtil.uuidStringToBytes(log.getAppKey()));
ps.setString(2, log.getLevel().name());
ps.setString(3, log.getLogData().getValue());
ps.setString(3, log.getData().getValue());
ps.setTimestamp(4, Timestamp.valueOf(log.getTimestamp()));
return ps;
}, keyHolder);
Expand All @@ -55,9 +56,9 @@ public Optional<Log> findById(Long logId) {

private static final RowMapper<Log> LOG_ROW_MAPPER = (rs, rowNum) -> new Log(
rs.getLong("log_id"),
rs.getLong("application_id"),
UUIDUtil.bytesToUuidString(rs.getBytes("app_key")),
rs.getString("level"),
rs.getString("log_data"),
rs.getString("data"),
rs.getTimestamp("timestamp").toLocalDateTime()
);
}
51 changes: 51 additions & 0 deletions logbat/src/test/java/info/logbat/common/util/UUIDUtilTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package info.logbat.common.util;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;

import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

miiiinju1 marked this conversation as resolved.
Show resolved Hide resolved
@DisplayName("UUIDUtil에서 ")
public class UUIDUtilTest {

@ParameterizedTest
@DisplayName("UUID 문자열은 byte 배열로 변환할 수 있다.")
@MethodSource("validUUIDProvider")
void uuidToBytes_ShouldConvertUUIDStringToByteArray(String uuidStr, byte[] expectedBytes) {
// when
byte[] bytes = UUIDUtil.uuidStringToBytes(uuidStr);

assertAll(
() -> assertThat(bytes).isNotNull(),
() -> assertThat(bytes.length).isEqualTo(16),
() -> assertThat(bytes).isEqualTo(expectedBytes)
);
}

@ParameterizedTest
@DisplayName("bytesToUUIDString은 byte 배열을 UUID 문자열로 정확하게 변환해야 한다.")
@MethodSource("validUUIDProvider")
void bytesToUUIDString_ShouldConvertByteArrayToUUIDString(String expectedUUIDStr, byte[] bytes) {
// when
String resultUUIDStr = UUIDUtil.bytesToUuidString(bytes);

// then
assertThat(resultUUIDStr).isEqualTo(expectedUUIDStr);
}


private static Stream<Arguments> validUUIDProvider() {
return Stream.of(
Arguments.of("550e8400-e29b-41d4-a716-446655440000",
new byte[]{85, 14, -124, 0, -30, -101, 65, -44, -89, 22, 68, 102, 85, 68, 0, 0}),
Arguments.of("123e4567-e89b-12d3-a456-426614174000",
new byte[]{18, 62, 69, 103, -24, -101, 18, -45, -92, 86, 66, 102, 20, 23, 64, 0}),
Arguments.of("00000000-0000-0000-0000-000000000000",
new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import info.logbat.domain.log.repository.LogRepository;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -27,16 +28,17 @@ class LogServiceTest {
@Autowired
private LogRepository logRepository;

private static final String 앱_키_문자열 = UUID.randomUUID().toString();

@DisplayName("Log를 저장할 수 있다.")
@Test
void saveLog() {
// given
Long 어플리케이션_ID = 1L;
String 로그_레벨 = "INFO";
String 로그_데이터 = "테스트_로그_데이터";
LocalDateTime 타임스탬프 = LocalDateTime.of(2021, 1, 1, 0, 0, 0);

CreateLogServiceRequest 요청_DTO = new CreateLogServiceRequest(어플리케이션_ID, 로그_레벨, 로그_데이터, 타임스탬프);
CreateLogServiceRequest 요청_DTO = new CreateLogServiceRequest(앱_키_문자열, 로그_레벨, 로그_데이터, 타임스탬프);

// when
long 저장된_ID = logService.saveLog(요청_DTO);
Expand All @@ -46,8 +48,8 @@ void saveLog() {

assertThat(찾은_로그).isPresent()
.get()
.extracting("logId", "applicationId", "level", "logData.value", "timestamp")
.contains(저장된_ID, 1L, Level.INFO, "테스트_로그_데이터", 타임스탬프);
.extracting("logId", "appKey", "level", "data.value", "timestamp")
.contains(저장된_ID, 앱_키_문자열, Level.INFO, "테스트_로그_데이터", 타임스탬프);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import info.logbat.domain.common.ControllerTestSupport;
import info.logbat.domain.log.presentation.payload.request.CreateLogRequest;
import java.time.LocalDateTime;
import java.util.UUID;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand All @@ -19,11 +20,12 @@

class LogControllerTest extends ControllerTestSupport {

private static final String 앱_키_문자열 = UUID.randomUUID().toString();

@DisplayName("[POST] 로그를 정상적으로 생성한다.")
@Test
void createLog() throws Exception {
// given
Long 어플리케이션_ID = 1L;
String 로그_레벨 = "INFO";
String 로그_데이터 = "테스트_로그_데이터";
LocalDateTime 타임스탬프 = LocalDateTime.of(2021, 1, 1, 0, 0, 0);
Expand All @@ -36,11 +38,11 @@ void createLog() throws Exception {
// when
ResultActions perform = mockMvc.perform(post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("app-id", 어플리케이션_ID)
.header("appKey", 앱_키_문자열)
.content(objectMapper.writeValueAsString(요청_DTO)));

// then
perform.andExpect(status().isOk());
perform.andExpect(status().isCreated());
}

@DisplayName("[POST] 어플리케이션_ID가 없으면 400 에러를 반환한다.")
Expand Down Expand Up @@ -68,7 +70,7 @@ void createLogWithoutAppId() throws Exception {
@DisplayName("[POST] 잘못된 입력으로 로그 생성 시 400 에러를 반환한다.")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("invalidLogCreationInputs")
void createLogWithInvalidInput(String testCase, Long appId, String level, String data,
void createLogWithInvalidInput(String testCase, String appKey, String level, String data,
LocalDateTime timestamp) throws Exception {
// given
CreateLogRequest 요청_DTO = new CreateLogRequest(level, data, timestamp);
Expand All @@ -77,7 +79,7 @@ void createLogWithInvalidInput(String testCase, Long appId, String level, String
// when
ResultActions perform = mockMvc.perform(post("/logs")
.contentType(MediaType.APPLICATION_JSON)
.header("app-id", appId)
.header("appKey", 앱_키_문자열)
.content(objectMapper.writeValueAsString(요청_DTO)));

// then
Expand All @@ -86,17 +88,15 @@ void createLogWithInvalidInput(String testCase, Long appId, String level, String

private static Stream<Arguments> invalidLogCreationInputs() {
return Stream.of(
Arguments.of("어플리케이션_ID가 음수인 경우", -1L, "INFO", "테스트_로그_데이터",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_레벨이 null인 경우", 1L, null, "테스트_로그_데이터",
Arguments.of("로그_레벨이 null인 경우", 앱_키_문자열, null, "테스트_로그_데이터",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_레벨이 빈 문자열인 경우", 1L, " ", "테스트_로그_데이터",
Arguments.of("로그_레벨이 빈 문자열인 경우", 앱_키_문자열, " ", "테스트_로그_데이터",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_데이터가 null인 경우", 1L, "INFO", null,
Arguments.of("로그_데이터가 null인 경우", 앱_키_문자열, "INFO", null,
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("로그_데이터가 빈 문자열인 경우", 1L, "INFO", " ",
Arguments.of("로그_데이터가 빈 문자열인 경우", 앱_키_문자열, "INFO", " ",
LocalDateTime.of(2021, 1, 1, 0, 0, 0)),
Arguments.of("타임스탬프가 null인 경우", 1L, "INFO", "테스트_로그_데이터", null)
Arguments.of("타임스탬프가 null인 경우", 앱_키_문자열, "INFO", "테스트_로그_데이터", null)
);
}
}
Loading
Loading