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
10 changes: 9 additions & 1 deletion api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,31 @@ plugins {
alias(libs.plugins.jib)
}

configurations {
all {
exclude("org.springframework.boot", "spring-boot-starter-logging")
}
}

dependencies {
implementation(project(":domain"))
implementation(project(":common:util"))
implementation(project(":infrastructure:aws"))
implementation(project(":infrastructure:oauth"))
implementation(project(":storage:rdb"))
implementation(project(":storage:redis"))
implementation(project(":common:logging"))

implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.aop)
implementation(libs.spring.boot.starter.validation)
implementation(libs.spring.boot.starter.security)
implementation(libs.spring.boot.starter.log4j2)
implementation(libs.jackson.yaml)
implementation(libs.jakarta.validation)
implementation(libs.jjwt.api)
runtimeOnly(libs.jjwt.jackson)
runtimeOnly(libs.jjwt.impl)
runtimeOnly(project(":common:logging"))

testImplementation(testFixtures(project(":domain")))
testImplementation(libs.bundles.spring.test)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.gotchai.api.global.exception

import com.gotchai.common.util.logger
import com.gotchai.common.util.getLogger
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler
import java.lang.reflect.Method

class AsyncExceptionHandler : AsyncUncaughtExceptionHandler {
private val log by logger()
companion object {
private val log = getLogger()
}

override fun handleUncaughtException(
e: Throwable,
ex: Throwable,
method: Method,
vararg params: Any?
) {
log.error("Exception : {}", e.message, e)
log.error(ex) { "Exception : ${ex.message}" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.gotchai.api.global.exception

import com.gotchai.api.global.dto.ApiResponse
import com.gotchai.api.global.dto.ErrorResponse
import com.gotchai.common.util.logger
import com.gotchai.common.util.getLogger
import com.gotchai.domain.global.exception.ServerException
import jakarta.validation.ConstraintViolationException
import org.springframework.http.HttpHeaders
Expand All @@ -20,7 +20,7 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep
@RestControllerAdvice(basePackages = ["com.gotchai"])
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
companion object {
private val log by logger()
private val log = getLogger()
}

override fun handleMethodArgumentNotValid(
Expand All @@ -29,7 +29,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any> {
log.error("MethodArgumentNotValidException : {}", ex.message, ex)
log.error(ex) { "MethodArgumentNotValidException : ${ex.message}" }

val message =
ex.bindingResult.allErrors
Expand All @@ -50,7 +50,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

@ExceptionHandler(ConstraintViolationException::class)
fun handleConstraintViolationException(ex: ConstraintViolationException): ResponseEntity<ApiResponse<ErrorResponse>> {
log.error("ConstraintViolationException: {}", ex.message, ex)
log.error(ex) { "ConstraintViolationException: ${ex.message}" }
val violations =
ex.constraintViolations.associate {
it.propertyPath.toString().substringAfterLast(".", "unknown") to it.message
Expand All @@ -69,7 +69,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

@ExceptionHandler(MethodArgumentTypeMismatchException::class)
fun handleMethodArgumentTypeMismatchException(ex: MethodArgumentTypeMismatchException): ResponseEntity<ApiResponse<ErrorResponse>> {
log.error("MethodArgumentTypeMismatchException : {}", ex.message, ex)
log.error(ex) { "MethodArgumentTypeMismatchException : ${ex.message}" }

val errorResponse =
ErrorResponse(
Expand All @@ -89,7 +89,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
status: HttpStatusCode,
request: WebRequest
): ResponseEntity<Any> {
log.error("HttpRequestMethodNotSupportedException : {}", ex.message, ex)
log.error(ex) { "HttpRequestMethodNotSupportedException : ${ex.message}" }

val errorResponse =
ErrorResponse(
Expand All @@ -105,7 +105,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

@ExceptionHandler(ServerException::class)
fun handleServerException(ex: ServerException): ResponseEntity<ApiResponse<ErrorResponse>> {
log.error("gotchai CustomException : {}", ex.message, ex)
log.error(ex) { "gotchai CustomException : ${ex.message}" }

val errorResponse =
ErrorResponse(
Expand All @@ -121,7 +121,7 @@ class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

@ExceptionHandler(Exception::class)
protected fun handleException(ex: Exception): ResponseEntity<ApiResponse<ErrorResponse>> {
log.error("Internal Server Error : {}", ex.message, ex)
log.error(ex) { "Internal Server Error : ${ex.message}" }

val errorResponse =
ErrorResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,33 @@ package com.gotchai.api.global.logging
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.gotchai.common.util.getLogger
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.LoggerFactory
import org.slf4j.MDC
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingRequestWrapper
import org.springframework.web.util.WebUtils
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.util.*

@Component
class LoggingFilter(
private val objectMapper: ObjectMapper
) : OncePerRequestFilter() {
companion object {
private val log by lazy { LoggerFactory.getLogger(this::class.java) }
private val log = getLogger()
}

override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
MDC.put("traceId", UUID.randomUUID().toString().substring(0..7))
val isFirstRequest = !this.isAsyncDispatch(request)
var wrapper = request
if (isFirstRequest && request !is ContentCachingRequestWrapper) {
Expand All @@ -49,7 +52,7 @@ class LoggingFilter(
setParameters(request, logData)
setPayload(request, logData)
val json = objectMapper.writeValueAsString(logData)
log.info("REQUEST : $json")
log.info { "REQUEST : $json" }
}

private fun setParameters(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.gotchai.api.global.logging

import com.gotchai.common.util.getLogger
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
Expand All @@ -13,8 +12,8 @@ import org.springframework.web.context.request.ServletRequestAttributes
@Component
class LoggingStopWatchAdvice {
companion object {
val log: Logger = LoggerFactory.getLogger(this::class.java)
const val MAX_AFFORDABLE_TIME: Long = 3000
private val log = getLogger()
const val MAX_AFFORDABLE_TIME = 3000
}

@Around("execution(* com.gotchai.api.presentation..*Controller.*(..))")
Expand All @@ -28,15 +27,12 @@ class LoggingStopWatchAdvice {
val methodName = joinPoint.signature.name

if (timeMs > MAX_AFFORDABLE_TIME) {
log.warn(
"method=${getMethod()}, url=${getRequestURI()}, call: $className - $methodName - timeMs:${timeMs}ms"
)
log.warn { "method=${getMethod()}, url=${getRequestURI()}, call: $className - $methodName - timeMs:${timeMs}ms" }

return proceed
}

log.info(
"method=${getMethod()}, url=${getRequestURI()}, call: $className - $methodName - timeMs:${timeMs}ms"
)
log.info { "method=${getMethod()}, url=${getRequestURI()}, call: $className - $methodName - timeMs:${timeMs}ms" }

return proceed
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import com.gotchai.api.global.annotation.ApiV1Controller
import com.gotchai.api.presentation.v1.admin.request.CreateExamRequest
import com.gotchai.api.presentation.v1.exam.response.ExamResponse
import com.gotchai.domain.admin.port.`in`.AdminCommandUseCase
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping

@ApiV1Controller
Expand All @@ -19,4 +21,14 @@ class AdminController(
adminCommandUseCase
.createExam(request.toCommand())
.let { ExamResponse.from(it) }

@DeleteMapping("/admin/users/{userId}/exams/{examId}/histories")
fun deleteExamHistory(
@PathVariable
userId: Long,
@PathVariable
examId: Long
) {
adminCommandUseCase.deleteExamHistoryByExamIdAndUserId(examId, userId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.gotchai.api.presentation.v1.admin

import com.gotchai.api.common.ControllerTest
import com.gotchai.api.docs.createExamRequestFields
import com.gotchai.api.docs.examResponseFields
import com.gotchai.api.fixture.createCreateExamRequest
import com.gotchai.api.global.dto.ApiResponse
import com.gotchai.api.presentation.v1.exam.response.ExamResponse
import com.gotchai.api.util.bodyForm
import com.gotchai.api.util.desc
import com.gotchai.api.util.document
import com.gotchai.api.util.expectError
import com.gotchai.domain.admin.port.`in`.AdminCommandUseCase
import com.gotchai.domain.exam.exception.ExamHistoryNotFoundException
import com.gotchai.domain.fixture.ID
import com.gotchai.domain.fixture.createExam
import com.ninjasquad.springmockk.MockkBean
import io.mockk.every
import io.mockk.just
import io.mockk.runs
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.reactive.server.expectBody

@WebMvcTest(AdminController::class)
class AdminControllerTest : ControllerTest() {
@MockkBean
private lateinit var adminCommandUseCase: AdminCommandUseCase

init {
describe("createExam()은") {
val request =
createCreateExamRequest()
.also {
every { adminCommandUseCase.createExam(any()) } returns createExam()
}

it("상태 코드 200과 ExamResponse를 반환한다.") {
webClient
.post()
.uri("/api/v1/admin/exams")
.bodyForm(request)
.exchange()
.expectStatus()
.isOk
.expectBody<ApiResponse<ExamResponse>>()
.document("테스트 생성 성공(200)") {
requestForm(createExamRequestFields)
responseBody(examResponseFields)
}
}
}

describe("deleteExamHistory()는") {
context("테스트 기록이 존재하는 경우") {
every { adminCommandUseCase.deleteExamHistoryByExamIdAndUserId(ID, ID) } just runs

it("상태 코드 200을 반환한다.") {
webClient
.delete()
.uri("/api/v1/admin/users/{userId}/exams/{examId}/histories", ID, ID)
.exchange()
.expectStatus()
.isOk
.expectBody<Void>()
.document("테스트 기록 삭제 성공(200)") {
pathParams(
"userId" desc "유저 식별자",
"examId" desc "테스트 식별자"
)
}
}
}

context("테스트 기록이 존재하지 않는 경우") {
every { adminCommandUseCase.deleteExamHistoryByExamIdAndUserId(ID, ID) } throws ExamHistoryNotFoundException()

it("상태 코드 404를 반환한다.") {
webClient
.delete()
.uri("/api/v1/admin/users/{userId}/exams/{examId}/histories", ID, ID)
.exchange()
.expectStatus()
.isNotFound
.expectError()
.document("테스트 기록 삭제 실패(404)") {
pathParams(
"userId" desc "유저 식별자",
"examId" desc "테스트 식별자"
)
}
}
}
}
}
}
1 change: 1 addition & 0 deletions api/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SPRING_PROFILES_ACTIVE: local
4 changes: 1 addition & 3 deletions common/logging/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
dependencies {
implementation(libs.micrometer.tracing.bridge.brave)
}
dependencies {}
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
logging:
config: classpath:logback/logback-${SPRING_PROFILES_ACTIVE:local}.xml
config: classpath:log4j2-${SPRING_PROFILES_ACTIVE}.yml
14 changes: 14 additions & 0 deletions common/logging/src/main/resources/log4j2-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Configuration:
name: Default
status: info
Appenders:
Console:
name: Console_Appender
target: SYSTEM_OUT
PatternLayout:
pattern: "%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{[%p]} %clr{%c{1.}}{cyan}%clr{:}{faint}%equals{ [%X{traceId}]}{ []}{} %m%n%xwEx"
Loggers:
Root:
level: info
AppenderRef:
- ref: Console_Appender
14 changes: 14 additions & 0 deletions common/logging/src/main/resources/log4j2-local.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Configuration:
name: Default
status: info
Appenders:
Console:
name: Console_Appender
target: SYSTEM_OUT
PatternLayout:
pattern: "%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{[%p]} %clr{%c{1.}}{cyan}%clr{:}{faint}%equals{ [%X{traceId}]}{ []}{} %m%n%xwEx"
Loggers:
Root:
level: info
AppenderRef:
- ref: Console_Appender
14 changes: 14 additions & 0 deletions common/logging/src/main/resources/log4j2-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Configuration:
name: Default
status: info
Appenders:
Console:
name: Console_Appender
target: SYSTEM_OUT
PatternLayout:
pattern: "%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{[%p]} %clr{%c{1.}}{cyan}%clr{:}{faint}%equals{ [%X{traceId}]}{ []}{} %m%n%xwEx"
Loggers:
Root:
level: info
AppenderRef:
- ref: Console_Appender
Loading
Loading